use std::io::Write;
use tempfile::NamedTempFile;
fn write_config(content: &str) -> NamedTempFile {
let mut f = NamedTempFile::new().unwrap();
f.write_all(content.as_bytes()).unwrap();
f.flush().unwrap();
f
}
#[test]
fn redacted_uri_never_includes_credentials() {
let spec = eggress_uri::ProxyChainSpec {
hops: vec![eggress_uri::ProxyHopSpec {
protocols: vec![eggress_uri::ProtocolSpec::Http],
endpoint: eggress_uri::EndpointSpec {
host: "proxy.example".to_string(),
port: 8080,
},
credentials: Some(eggress_uri::CredentialSpec {
username: "admin".to_string(),
password: "supersecret123".to_string(),
}),
rule: None,
local_bind: None,
plugins: Vec::new(),
auth_prefix: None,
tls: false,
server_name: None,
insecure: false,
}],
};
let redacted = eggress_uri::RedactedUri::new(&spec);
let display = format!("{}", redacted);
assert!(
!display.contains("admin"),
"redacted URI must not contain username, got: {}",
display
);
assert!(
!display.contains("supersecret123"),
"redacted URI must not contain password, got: {}",
display
);
assert!(
display.contains("****:****@"),
"redacted URI should contain masked credentials, got: {}",
display
);
}
#[test]
fn redacted_uri_multi_hop_all_credentials_hidden() {
let spec = eggress_uri::ProxyChainSpec {
hops: vec![
eggress_uri::ProxyHopSpec {
protocols: vec![eggress_uri::ProtocolSpec::Socks5],
endpoint: eggress_uri::EndpointSpec {
host: "hop1".to_string(),
port: 1080,
},
credentials: Some(eggress_uri::CredentialSpec {
username: "user1".to_string(),
password: "pass1".to_string(),
}),
rule: None,
local_bind: None,
plugins: Vec::new(),
auth_prefix: None,
tls: false,
server_name: None,
insecure: false,
},
eggress_uri::ProxyHopSpec {
protocols: vec![eggress_uri::ProtocolSpec::Http],
endpoint: eggress_uri::EndpointSpec {
host: "hop2".to_string(),
port: 8080,
},
credentials: Some(eggress_uri::CredentialSpec {
username: "user2".to_string(),
password: "pass2".to_string(),
}),
rule: None,
local_bind: None,
plugins: Vec::new(),
auth_prefix: None,
tls: false,
server_name: None,
insecure: false,
},
],
};
let display = format!("{}", eggress_uri::RedactedUri::new(&spec));
assert!(!display.contains("user1"));
assert!(!display.contains("pass1"));
assert!(!display.contains("user2"));
assert!(!display.contains("pass2"));
assert_eq!(display.matches("****:****@").count(), 2);
}
#[test]
fn admin_endpoints_never_expose_credentials() {
let config = r#"
version = 1
[[listeners]]
name = "http-in"
bind = "127.0.0.1:8080"
protocols = ["http"]
[listeners.auth]
type = "password"
username = "admin"
password = "s3cret"
[[upstreams]]
id = "proxy1"
uri = "socks5://user:pass@proxy.example:1080"
[routing]
default = "direct"
"#;
let f = write_config(config);
let path = f.path().to_str().unwrap();
let rt = eggress_config::load_and_validate(path).unwrap();
let router = std::sync::Arc::new(eggress_routing::Router::new(
rt.rules.clone(),
rt.default_action.clone(),
));
let snap = eggress_admin::AdminSnapshot {
generation: 1,
router,
pac: None,
static_routes: Vec::new(),
listeners: Vec::new(),
};
let status_json = serde_json::json!({
"version": "1.0.1",
"generation": snap.generation,
});
let status_str = status_json.to_string();
assert!(
!status_str.contains("user"),
"status JSON must not contain upstream username"
);
assert!(
!status_str.contains("pass"),
"status JSON must not contain upstream password"
);
}
#[test]
fn http_connect_credentials_with_control_chars_rejected() {
let bad_usernames = vec![
"user\x00name",
"user\x1fname",
"user\x7fname",
"\x09username",
"\x0d\x0ausername",
];
let bad_passwords = vec![
"pass\x00word",
"pass\x1fword",
"pass\x7fword",
"password\x01",
];
for user in &bad_usernames {
let spec = eggress_uri::ProxyChainSpec {
hops: vec![eggress_uri::ProxyHopSpec {
protocols: vec![eggress_uri::ProtocolSpec::Http],
endpoint: eggress_uri::EndpointSpec {
host: "proxy".to_string(),
port: 8080,
},
credentials: Some(eggress_uri::CredentialSpec {
username: user.to_string(),
password: "password".to_string(),
}),
rule: None,
local_bind: None,
plugins: Vec::new(),
auth_prefix: None,
tls: false,
server_name: None,
insecure: false,
}],
};
let display = format!("{}", eggress_uri::RedactedUri::new(&spec));
assert!(
!display.contains(user),
"redacted URI must not contain raw username with control chars, got: {}",
display
);
}
for pass in &bad_passwords {
let spec = eggress_uri::ProxyChainSpec {
hops: vec![eggress_uri::ProxyHopSpec {
protocols: vec![eggress_uri::ProtocolSpec::Http],
endpoint: eggress_uri::EndpointSpec {
host: "proxy".to_string(),
port: 8080,
},
credentials: Some(eggress_uri::CredentialSpec {
username: "user".to_string(),
password: pass.to_string(),
}),
rule: None,
local_bind: None,
plugins: Vec::new(),
auth_prefix: None,
tls: false,
server_name: None,
insecure: false,
}],
};
let display = format!("{}", eggress_uri::RedactedUri::new(&spec));
assert!(
!display.contains(pass),
"redacted URI must not contain raw password with control chars, got: {}",
display
);
}
}
#[test]
fn udp_dangerous_targets_rejected() {
use eggress_core::TargetAddr;
use std::str::FromStr;
let dangerous_targets = vec![
"224.0.0.1:80",
"239.255.255.250:1900",
"255.255.255.255:80",
"0.0.0.0:80",
];
for target_str in &dangerous_targets {
let target = TargetAddr::from_str(target_str).unwrap();
match target.host {
eggress_core::TargetHost::Ip(ip) => {
if let std::net::IpAddr::V4(v4) = ip {
assert!(
v4.is_multicast() || v4.is_broadcast() || v4.is_unspecified(),
"expected dangerous IPv4 address: {}",
target_str
);
}
}
_ => panic!("expected IP address for: {}", target_str),
}
}
let valid_targets = vec!["192.168.1.1:8080", "127.0.0.1:443", "10.0.0.1:80"];
for target_str in &valid_targets {
let target = TargetAddr::from_str(target_str).unwrap();
match target.host {
eggress_core::TargetHost::Ip(ip) => {
if let std::net::IpAddr::V4(v4) = ip {
assert!(
!v4.is_multicast() && !v4.is_broadcast() && !v4.is_unspecified(),
"valid target should not be dangerous: {}",
target_str
);
}
}
_ => panic!("expected IP address for: {}", target_str),
}
}
}
#[test]
fn unsupported_protocol_combinations_not_silent() {
use eggress_core::capability::{classify_upstream_chain, CapabilityResult};
use eggress_uri::*;
let chain = ProxyChainSpec {
hops: vec![ProxyHopSpec {
protocols: vec![ProtocolSpec::Shadowsocks],
endpoint: EndpointSpec {
host: "proxy".to_string(),
port: 8388,
},
credentials: None,
rule: None,
local_bind: None,
plugins: Vec::new(),
auth_prefix: None,
tls: false,
server_name: None,
insecure: false,
}],
};
let caps = classify_upstream_chain(&chain);
assert!(caps.is_tcp_supported());
assert!(caps.is_udp_supported());
assert_eq!(caps.tcp_connect, CapabilityResult::Supported);
assert_eq!(caps.udp_associate, CapabilityResult::Supported);
let chain = ProxyChainSpec {
hops: vec![ProxyHopSpec {
protocols: vec![ProtocolSpec::Http],
endpoint: EndpointSpec {
host: "proxy".to_string(),
port: 8080,
},
credentials: None,
rule: None,
local_bind: None,
plugins: Vec::new(),
auth_prefix: None,
tls: false,
server_name: None,
insecure: false,
}],
};
let caps = classify_upstream_chain(&chain);
assert!(caps.is_tcp_supported());
assert!(!caps.is_udp_supported());
assert!(matches!(
caps.udp_associate,
CapabilityResult::UnsupportedProtocol { .. }
));
let chain = ProxyChainSpec {
hops: vec![ProxyHopSpec {
protocols: vec![ProtocolSpec::Socks4],
endpoint: EndpointSpec {
host: "proxy".to_string(),
port: 1080,
},
credentials: None,
rule: None,
local_bind: None,
plugins: Vec::new(),
auth_prefix: None,
tls: false,
server_name: None,
insecure: false,
}],
};
let caps = classify_upstream_chain(&chain);
assert!(caps.is_tcp_supported());
assert!(!caps.is_udp_supported());
let chain = ProxyChainSpec {
hops: vec![
ProxyHopSpec {
protocols: vec![ProtocolSpec::Socks5],
endpoint: EndpointSpec {
host: "hop1".to_string(),
port: 1080,
},
credentials: None,
rule: None,
local_bind: None,
plugins: Vec::new(),
auth_prefix: None,
tls: false,
server_name: None,
insecure: false,
},
ProxyHopSpec {
protocols: vec![ProtocolSpec::Http],
endpoint: EndpointSpec {
host: "hop2".to_string(),
port: 8080,
},
credentials: None,
rule: None,
local_bind: None,
plugins: Vec::new(),
auth_prefix: None,
tls: false,
server_name: None,
insecure: false,
},
],
};
let caps = classify_upstream_chain(&chain);
assert!(caps.is_tcp_supported());
assert!(!caps.is_udp_supported());
assert!(matches!(
caps.udp_associate,
CapabilityResult::UnsupportedChain { reason }
if reason == "multi-hop contains a non-UDP protocol"
));
let chain = ProxyChainSpec {
hops: vec![ProxyHopSpec {
protocols: vec![ProtocolSpec::Http, ProtocolSpec::Socks5],
endpoint: EndpointSpec {
host: "proxy".to_string(),
port: 8080,
},
credentials: None,
rule: None,
local_bind: None,
plugins: Vec::new(),
auth_prefix: None,
tls: false,
server_name: None,
insecure: false,
}],
};
let caps = classify_upstream_chain(&chain);
assert!(!caps.is_tcp_supported());
assert!(!caps.is_udp_supported());
assert!(matches!(
caps.tcp_connect,
CapabilityResult::UnsupportedChain { .. }
));
}
#[test]
fn config_rejects_udp_without_socks5_upstream() {
let config = r#"
version = 1
[[listeners]]
name = "socks-in"
bind = "127.0.0.1:1080"
protocols = ["socks5"]
udp_enabled = true
[[upstreams]]
id = "http-proxy"
uri = "http://proxy.example:8080"
[[upstream_groups]]
id = "main"
members = ["http-proxy"]
[[rules]]
id = "route-all"
upstream_group = "main"
"#;
let f = write_config(config);
let path = f.path().to_str().unwrap();
let result = eggress_config::load_and_validate(path);
assert!(
result.is_err(),
"UDP listener with HTTP-only upstreams should be rejected"
);
let err_msg = format!("{}", result.unwrap_err());
assert!(
err_msg.contains("no UDP-capable upstreams"),
"Error should mention UDP capability: {}",
err_msg
);
}
#[test]
fn config_rejects_udp_with_multi_hop_chain() {
let config = r#"
version = 1
[[listeners]]
name = "socks-in"
bind = "127.0.0.1:1080"
protocols = ["socks5"]
udp_enabled = true
[[upstreams]]
id = "multi-hop"
uri = "socks5://hop1:1080__http://hop2:8080"
[[upstream_groups]]
id = "main"
members = ["multi-hop"]
[[rules]]
id = "route-all"
upstream_group = "main"
"#;
let f = write_config(config);
let path = f.path().to_str().unwrap();
let result = eggress_config::load_and_validate(path);
assert!(
result.is_err(),
"Multi-hop chain with UDP listener should be rejected"
);
}