use super::listeners::is_loopback_bind;
use super::rules::MAX_MATCH_EXPR_DEPTH;
use crate::error::ConfigWarning;
use crate::model::{ConfigFile, MatchExprConfig};
pub fn validate_config_security(config: &ConfigFile) -> Vec<ConfigWarning> {
let mut warnings = Vec::new();
if let Some(ref listeners) = config.listeners {
for (i, listener) in listeners.iter().enumerate() {
let path = format!("listeners[{}].bind", i);
if !is_loopback_bind(&listener.bind) {
let has_auth = listener.auth.is_some();
let has_shadowsocks = listener.shadowsocks.is_some();
let has_ssr = listener.ssr.is_some();
let has_trojan = listener.trojan.is_some();
if !has_auth && !has_shadowsocks && !has_ssr && !has_trojan {
warnings.push(ConfigWarning {
path,
message: format!(
"listener '{}' binds to {} without authentication — \
this may expose the proxy to untrusted networks",
listener.name, listener.bind,
),
});
}
}
}
}
if let Some(ref admin) = config.admin {
if let Some(ref bind) = admin.bind {
if !is_loopback_bind(bind) && admin.auth.is_none() {
warnings.push(ConfigWarning {
path: "admin.bind".to_string(),
message: format!(
"admin server binds to {} without authentication — \
this may expose admin endpoints to untrusted networks",
bind,
),
});
}
}
}
if let Some(ref servers) = config.reverse_servers {
for (i, server) in servers.iter().enumerate() {
let path = format!("reverse_servers[{}].control_bind", i);
if !is_loopback_bind(&server.control_bind) {
let has_auth = server.auth_username.is_some()
&& (server.auth_password.is_some() || server.auth_password_env.is_some());
if !has_auth {
warnings.push(ConfigWarning {
path,
message: format!(
"reverse server '{}' control channel binds to {} without authentication — \
any client can connect and request proxying",
server.id, server.control_bind,
),
});
}
}
}
}
warn_protocol_aliases(config, &mut warnings);
warnings
}
pub(crate) fn warn_protocol_aliases(config: &ConfigFile, warnings: &mut Vec<ConfigWarning>) {
if let Some(ref rules) = config.rules {
for (i, rule) in rules.iter().enumerate() {
let base = format!("rules[{i}]");
if let Some(ref expr) = rule.match_expr {
walk_match_expr_for_alias(expr, &format!("{base}.match"), warnings, 0);
}
}
}
}
pub(crate) fn walk_match_expr_for_alias(
expr: &MatchExprConfig,
path: &str,
warnings: &mut Vec<ConfigWarning>,
depth: usize,
) {
if depth >= MAX_MATCH_EXPR_DEPTH {
return;
}
match expr {
MatchExprConfig::Composite(composite) => {
if let Some(ref all) = composite.all {
for (i, child) in all.iter().enumerate() {
walk_match_expr_for_alias(
child,
&format!("{path}.all[{i}]"),
warnings,
depth + 1,
);
}
}
if let Some(ref any) = composite.any_of {
for (i, child) in any.iter().enumerate() {
walk_match_expr_for_alias(
child,
&format!("{path}.any_of[{i}]"),
warnings,
depth + 1,
);
}
}
if let Some(ref not) = composite.not {
walk_match_expr_for_alias(not, &format!("{path}.not"), warnings, depth + 1);
}
}
MatchExprConfig::Leaf(leaf) => {
if leaf.protocol.as_deref() == Some("httponly") {
warnings.push(ConfigWarning {
path: format!("{path}.protocol"),
message: "'httponly' is a pproxy compatibility alias for 'http' \
and does not select distinct protocol semantics"
.to_string(),
});
}
}
}
}