Files
codex/codex-rs/network-proxy/src/config.rs
evawong-oai 3d94e24a3d Add MITM hook config model (#18868)
## Stack
1. This PR adds MITM hook config and model only.
2. Runtime follow up: #20659 wires hook enforcement into the proxy
request path.
3. User facing config follow up: #18240 moves MITM policy into the
PermissionProfile network tree.

## Why
1. Viyat asked for the original parent PR to be split so reviewers can
inspect the policy model before request behavior changes.
2. This PR gives the proxy a typed MITM hook model, validation, matcher
compilation, permissions TOML plumbing, schema support, and config
tests.
3. This PR deliberately does not change CONNECT or MITM request
handling.
4. Keeping runtime behavior out of this PR makes the review boundary
simple: does the policy model parse, validate, compile, and lower
correctly.

## Summary
1. Add the MITM hook config model and matcher compilation.
2. Validate hosts, methods, paths, query matchers, header matchers,
secret sources, and reserved body matching.
3. Add wildcard matcher support for path, query value, and header value
matching.
4. Add permissions TOML and schema support for flat runtime hook config.
5. Add config loader tests for MITM hook overlay behavior.

## Validation
1. Regenerated the config schema.
2. Ran the network proxy MITM hook unit tests.
3. Ran the core permission profile MITM hook parsing tests.
4. Ran the core config schema fixture test.
5. Ran the scoped Clippy fixer for the network proxy crate.
6. Ran the scoped Clippy fixer for the core crate.

## Notes
1. Runtime enforcement moved to #20659.
2. User facing PermissionProfile TOML shape remains in #18240.
2026-05-20 12:51:12 -07:00

877 lines
28 KiB
Rust

use anyhow::Context;
use anyhow::Result;
use anyhow::bail;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde::Serializer;
use std::collections::BTreeMap;
use std::net::IpAddr;
use std::net::SocketAddr;
use std::path::Path;
use tracing::warn;
use url::Url;
use crate::mitm_hook::MitmHookConfig;
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct NetworkProxyConfig {
#[serde(default)]
pub network: NetworkProxySettings,
}
/// Variant order encodes effective precedence for duplicate patterns:
/// `None < Allow < Deny`, so deny wins over allow when entries conflict.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "lowercase")]
pub enum NetworkDomainPermission {
None,
Allow,
Deny,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NetworkDomainPermissionEntry {
pub pattern: String,
pub permission: NetworkDomainPermission,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct NetworkDomainPermissions {
pub entries: Vec<NetworkDomainPermissionEntry>,
}
impl Serialize for NetworkDomainPermissions {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
self.effective_entries()
.into_iter()
.map(|entry| (entry.pattern, entry.permission))
.collect::<BTreeMap<_, _>>()
.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for NetworkDomainPermissions {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let entries = BTreeMap::<String, NetworkDomainPermission>::deserialize(deserializer)?
.into_iter()
.map(|(pattern, permission)| NetworkDomainPermissionEntry {
pattern,
permission,
})
.collect();
Ok(Self { entries })
}
}
impl NetworkDomainPermissions {
fn effective_entries(&self) -> Vec<NetworkDomainPermissionEntry> {
let mut order = Vec::new();
let mut effective_permissions = BTreeMap::new();
for entry in &self.entries {
if !effective_permissions.contains_key(&entry.pattern) {
order.push(entry.pattern.clone());
}
let permission = effective_permissions
.entry(entry.pattern.clone())
.or_insert(entry.permission);
if entry.permission > *permission {
*permission = entry.permission;
}
}
order
.into_iter()
.filter_map(|pattern| {
effective_permissions.remove(&pattern).map(|permission| {
NetworkDomainPermissionEntry {
pattern,
permission,
}
})
})
.collect()
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum NetworkUnixSocketPermission {
Allow,
None,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct NetworkUnixSocketPermissions {
#[serde(flatten)]
pub entries: BTreeMap<String, NetworkUnixSocketPermission>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(default)]
pub struct NetworkProxySettings {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_proxy_url")]
pub proxy_url: String,
pub enable_socks5: bool,
#[serde(default = "default_socks_url")]
pub socks_url: String,
pub enable_socks5_udp: bool,
pub allow_upstream_proxy: bool,
#[serde(default)]
pub dangerously_allow_non_loopback_proxy: bool,
#[serde(default)]
pub dangerously_allow_all_unix_sockets: bool,
#[serde(default)]
pub mode: NetworkMode,
#[serde(default)]
pub domains: Option<NetworkDomainPermissions>,
#[serde(default)]
pub unix_sockets: Option<NetworkUnixSocketPermissions>,
pub allow_local_binding: bool,
#[serde(default)]
pub mitm: bool,
#[serde(default)]
pub mitm_hooks: Vec<MitmHookConfig>,
}
impl Default for NetworkProxySettings {
fn default() -> Self {
Self {
enabled: false,
proxy_url: default_proxy_url(),
enable_socks5: true,
socks_url: default_socks_url(),
enable_socks5_udp: true,
allow_upstream_proxy: true,
dangerously_allow_non_loopback_proxy: false,
dangerously_allow_all_unix_sockets: false,
mode: NetworkMode::default(),
domains: None,
unix_sockets: None,
allow_local_binding: false,
mitm: false,
mitm_hooks: Vec::new(),
}
}
}
impl NetworkProxySettings {
pub fn allowed_domains(&self) -> Option<Vec<String>> {
self.domain_entries(NetworkDomainPermission::Allow)
}
pub fn denied_domains(&self) -> Option<Vec<String>> {
self.domain_entries(NetworkDomainPermission::Deny)
}
fn domain_entries(&self, permission: NetworkDomainPermission) -> Option<Vec<String>> {
self.domains
.as_ref()
.map(|domains| {
domains
.effective_entries()
.iter()
.filter(|entry| entry.permission == permission)
.map(|entry| entry.pattern.clone())
.collect()
})
.filter(|entries: &Vec<String>| !entries.is_empty())
}
pub fn allow_unix_sockets(&self) -> Vec<String> {
self.unix_sockets
.as_ref()
.map(|unix_sockets| {
unix_sockets
.entries
.iter()
.filter(|(_, permission)| {
matches!(permission, NetworkUnixSocketPermission::Allow)
})
.map(|(path, _)| path.clone())
.collect()
})
.unwrap_or_default()
}
pub fn set_allowed_domains(&mut self, allowed_domains: Vec<String>) {
self.set_domain_entries(allowed_domains, NetworkDomainPermission::Allow);
}
pub fn set_denied_domains(&mut self, denied_domains: Vec<String>) {
self.set_domain_entries(denied_domains, NetworkDomainPermission::Deny);
}
pub fn upsert_domain_permission(
&mut self,
host: String,
permission: NetworkDomainPermission,
normalize: impl Fn(&str) -> String,
) {
let mut domains = self.domains.take().unwrap_or_default();
let normalized_host = normalize(&host);
domains
.entries
.retain(|entry| normalize(&entry.pattern) != normalized_host);
domains.entries.push(NetworkDomainPermissionEntry {
pattern: host,
permission,
});
self.domains = (!domains.entries.is_empty()).then_some(domains);
}
pub fn set_allow_unix_sockets(&mut self, allow_unix_sockets: Vec<String>) {
self.set_unix_socket_entries(allow_unix_sockets, NetworkUnixSocketPermission::Allow);
}
fn set_domain_entries(&mut self, entries: Vec<String>, permission: NetworkDomainPermission) {
let mut domains = self.domains.take().unwrap_or_default();
domains
.entries
.retain(|entry| entry.permission != permission);
for entry in entries {
if !domains
.entries
.iter()
.any(|existing| existing.pattern == entry && existing.permission == permission)
{
domains.entries.push(NetworkDomainPermissionEntry {
pattern: entry,
permission,
});
}
}
self.domains = (!domains.entries.is_empty()).then_some(domains);
}
fn set_unix_socket_entries(
&mut self,
entries: Vec<String>,
permission: NetworkUnixSocketPermission,
) {
let mut unix_sockets = self.unix_sockets.take().unwrap_or_default();
unix_sockets
.entries
.retain(|_, existing| *existing != permission);
for entry in entries {
unix_sockets.entries.insert(entry, permission);
}
self.unix_sockets = (!unix_sockets.entries.is_empty()).then_some(unix_sockets);
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum NetworkMode {
/// Limited (read-only) access: only GET/HEAD/OPTIONS are allowed for HTTP. HTTPS CONNECT is
/// blocked unless MITM is enabled so the proxy can enforce method policy on inner requests.
/// SOCKS5 remains blocked in limited mode.
Limited,
/// Full network access: all HTTP methods are allowed. HTTPS CONNECTs are tunneled directly.
/// MITM hooks do not currently make full mode enter MITM.
#[default]
Full,
}
impl NetworkMode {
pub fn allows_method(self, method: &str) -> bool {
match self {
Self::Full => true,
Self::Limited => matches!(method, "GET" | "HEAD" | "OPTIONS"),
}
}
}
fn default_proxy_url() -> String {
"http://127.0.0.1:3128".to_string()
}
fn default_socks_url() -> String {
"http://127.0.0.1:8081".to_string()
}
/// Clamp non-loopback bind addresses to loopback unless explicitly allowed.
fn clamp_non_loopback(
addr: SocketAddr,
allow_non_loopback: bool,
name: &str,
override_setting_name: &str,
) -> SocketAddr {
if addr.ip().is_loopback() {
return addr;
}
if allow_non_loopback {
warn!("DANGEROUS: {name} listening on non-loopback address {addr}");
return addr;
}
warn!(
"{name} requested non-loopback bind ({addr}); clamping to 127.0.0.1:{port} (set {override_setting_name} to override)",
port = addr.port()
);
SocketAddr::from(([127, 0, 0, 1], addr.port()))
}
pub(crate) fn clamp_bind_addrs(
http_addr: SocketAddr,
socks_addr: SocketAddr,
cfg: &NetworkProxySettings,
) -> (SocketAddr, SocketAddr) {
let http_addr = clamp_non_loopback(
http_addr,
cfg.dangerously_allow_non_loopback_proxy,
"HTTP proxy",
"dangerously_allow_non_loopback_proxy",
);
let socks_addr = clamp_non_loopback(
socks_addr,
cfg.dangerously_allow_non_loopback_proxy,
"SOCKS5 proxy",
"dangerously_allow_non_loopback_proxy",
);
if cfg.allow_unix_sockets().is_empty() && !cfg.dangerously_allow_all_unix_sockets {
return (http_addr, socks_addr);
}
// `x-unix-socket` is intentionally a local escape hatch. If the proxy is reachable from
// outside the machine, it can become a remote bridge into local daemons
// (e.g. docker.sock). To avoid footguns, enforce loopback binding whenever unix sockets
// are enabled.
if cfg.dangerously_allow_non_loopback_proxy && !http_addr.ip().is_loopback() {
warn!(
"unix socket proxying is enabled; ignoring dangerously_allow_non_loopback_proxy and clamping HTTP proxy to loopback"
);
}
if cfg.dangerously_allow_non_loopback_proxy && !socks_addr.ip().is_loopback() {
warn!(
"unix socket proxying is enabled; ignoring dangerously_allow_non_loopback_proxy and clamping SOCKS5 proxy to loopback"
);
}
(
SocketAddr::from(([127, 0, 0, 1], http_addr.port())),
SocketAddr::from(([127, 0, 0, 1], socks_addr.port())),
)
}
pub struct RuntimeConfig {
pub http_addr: SocketAddr,
pub socks_addr: SocketAddr,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct UnixStyleAbsolutePath(String);
impl UnixStyleAbsolutePath {
fn parse(value: &str) -> Option<Self> {
value.starts_with('/').then(|| Self(value.to_string()))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ValidatedUnixSocketPath {
Native(AbsolutePathBuf),
UnixStyleAbsolute(UnixStyleAbsolutePath),
}
impl ValidatedUnixSocketPath {
pub(crate) fn parse(socket_path: &str) -> Result<Self> {
let path = Path::new(socket_path);
if path.is_absolute() {
let path = AbsolutePathBuf::from_absolute_path(path)
.with_context(|| format!("failed to normalize unix socket path {socket_path:?}"))?;
return Ok(Self::Native(path));
}
if let Some(path) = UnixStyleAbsolutePath::parse(socket_path) {
return Ok(Self::UnixStyleAbsolute(path));
}
bail!("expected an absolute path, got {socket_path:?}");
}
}
pub(crate) fn validate_unix_socket_allowlist_paths(cfg: &NetworkProxyConfig) -> Result<()> {
for (index, socket_path) in cfg.network.allow_unix_sockets().iter().enumerate() {
ValidatedUnixSocketPath::parse(socket_path)
.with_context(|| format!("invalid network.allow_unix_sockets[{index}]"))?;
}
Ok(())
}
pub fn resolve_runtime(cfg: &NetworkProxyConfig) -> Result<RuntimeConfig> {
validate_unix_socket_allowlist_paths(cfg)?;
let http_addr = resolve_addr(&cfg.network.proxy_url, /*default_port*/ 3128)
.with_context(|| format!("invalid network.proxy_url: {}", cfg.network.proxy_url))?;
let socks_addr = resolve_addr(&cfg.network.socks_url, /*default_port*/ 8081)
.with_context(|| format!("invalid network.socks_url: {}", cfg.network.socks_url))?;
let (http_addr, socks_addr) = clamp_bind_addrs(http_addr, socks_addr, &cfg.network);
Ok(RuntimeConfig {
http_addr,
socks_addr,
})
}
fn resolve_addr(url: &str, default_port: u16) -> Result<SocketAddr> {
let addr_parts = parse_host_port(url, default_port)?;
let host = if addr_parts.host.eq_ignore_ascii_case("localhost") {
"127.0.0.1".to_string()
} else {
addr_parts.host
};
match host.parse::<IpAddr>() {
Ok(ip) => Ok(SocketAddr::new(ip, addr_parts.port)),
Err(_) => Ok(SocketAddr::from(([127, 0, 0, 1], addr_parts.port))),
}
}
pub fn host_and_port_from_network_addr(value: &str, default_port: u16) -> String {
let trimmed = value.trim();
if trimmed.is_empty() {
return "<missing>".to_string();
}
let parts = match parse_host_port(trimmed, default_port) {
Ok(parts) => parts,
Err(_) => {
return format_host_and_port(trimmed, default_port);
}
};
format_host_and_port(&parts.host, parts.port)
}
fn format_host_and_port(host: &str, port: u16) -> String {
if host.contains(':') {
format!("[{host}]:{port}")
} else {
format!("{host}:{port}")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct SocketAddressParts {
host: String,
port: u16,
}
fn parse_host_port(url: &str, default_port: u16) -> Result<SocketAddressParts> {
let trimmed = url.trim();
if trimmed.is_empty() {
bail!("missing host in network proxy address: {url}");
}
// Avoid treating unbracketed IPv6 literals like "2001:db8::1" as scheme-prefixed URLs.
if matches!(trimmed.parse::<IpAddr>(), Ok(IpAddr::V6(_))) && !trimmed.starts_with('[') {
return Ok(SocketAddressParts {
host: trimmed.to_string(),
port: default_port,
});
}
// Prefer the standard URL parser when the input is URL-like. Prefix a scheme when absent so
// we still accept loose host:port inputs.
let candidate = if trimmed.contains("://") {
trimmed.to_string()
} else {
format!("http://{trimmed}")
};
if let Ok(parsed) = Url::parse(&candidate)
&& let Some(host) = parsed.host_str()
{
let host = host.trim_matches(|c| c == '[' || c == ']');
if host.is_empty() {
bail!("missing host in network proxy address: {url}");
}
return Ok(SocketAddressParts {
host: host.to_string(),
port: parsed.port().unwrap_or(default_port),
});
}
parse_host_port_fallback(trimmed, default_port)
}
fn parse_host_port_fallback(input: &str, default_port: u16) -> Result<SocketAddressParts> {
let without_scheme = input
.split_once("://")
.map(|(_, rest)| rest)
.unwrap_or(input);
let host_port = without_scheme.split('/').next().unwrap_or(without_scheme);
let host_port = host_port
.rsplit_once('@')
.map(|(_, rest)| rest)
.unwrap_or(host_port);
if host_port.starts_with('[')
&& let Some(end) = host_port.find(']')
{
let host = &host_port[1..end];
let port = host_port[end + 1..]
.strip_prefix(':')
.and_then(|port| port.parse::<u16>().ok())
.unwrap_or(default_port);
if host.is_empty() {
bail!("missing host in network proxy address: {input}");
}
return Ok(SocketAddressParts {
host: host.to_string(),
port,
});
}
// Only treat `host:port` as such when there's a single `:`. This avoids
// accidentally interpreting unbracketed IPv6 addresses as `host:port`.
if host_port.bytes().filter(|b| *b == b':').count() == 1
&& let Some((host, port)) = host_port.rsplit_once(':')
{
if host.is_empty() {
bail!("missing host in network proxy address: {input}");
}
return Ok(SocketAddressParts {
host: host.to_string(),
port: port.parse::<u16>().ok().unwrap_or(default_port),
});
}
if host_port.is_empty() {
bail!("missing host in network proxy address: {input}");
}
Ok(SocketAddressParts {
host: host_port.to_string(),
port: default_port,
})
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
fn settings_with_unix_sockets(unix_sockets: &[&str]) -> NetworkProxySettings {
let mut settings = NetworkProxySettings::default();
if !unix_sockets.is_empty() {
settings.set_allow_unix_sockets(
unix_sockets
.iter()
.map(|path| (*path).to_string())
.collect(),
);
}
settings
}
#[test]
fn network_proxy_settings_default_matches_local_use_baseline() {
assert_eq!(
NetworkProxySettings::default(),
NetworkProxySettings {
enabled: false,
proxy_url: "http://127.0.0.1:3128".to_string(),
enable_socks5: true,
socks_url: "http://127.0.0.1:8081".to_string(),
enable_socks5_udp: true,
allow_upstream_proxy: true,
dangerously_allow_non_loopback_proxy: false,
dangerously_allow_all_unix_sockets: false,
mode: NetworkMode::Full,
domains: None,
unix_sockets: None,
allow_local_binding: false,
mitm: false,
mitm_hooks: Vec::new(),
}
);
}
#[test]
fn partial_network_config_uses_struct_defaults_for_missing_fields() {
let config: NetworkProxyConfig = serde_json::from_str(
r#"{
"network": {
"enabled": true
}
}"#,
)
.unwrap();
let expected = NetworkProxySettings {
enabled: true,
..NetworkProxySettings::default()
};
assert_eq!(config.network, expected);
}
#[test]
fn set_allowed_domains_preserves_existing_deny_for_same_pattern() {
let mut settings = NetworkProxySettings::default();
settings.set_denied_domains(vec!["example.com".to_string()]);
settings.set_allowed_domains(vec!["example.com".to_string()]);
assert_eq!(settings.allowed_domains(), None);
assert_eq!(
settings.denied_domains(),
Some(vec!["example.com".to_string()])
);
}
#[test]
fn network_domain_permissions_serialize_to_effective_map_shape() {
let mut settings = NetworkProxySettings::default();
settings.set_denied_domains(vec!["example.com".to_string()]);
settings.set_allowed_domains(vec!["example.com".to_string()]);
let config = NetworkProxyConfig { network: settings };
let value = serde_json::to_value(&config).unwrap();
assert_eq!(
value,
serde_json::json!({
"network": {
"enabled": false,
"proxy_url": "http://127.0.0.1:3128",
"enable_socks5": true,
"socks_url": "http://127.0.0.1:8081",
"enable_socks5_udp": true,
"allow_upstream_proxy": true,
"dangerously_allow_non_loopback_proxy": false,
"dangerously_allow_all_unix_sockets": false,
"mode": "full",
"domains": {
"example.com": "deny",
},
"unix_sockets": null,
"allow_local_binding": false,
"mitm": false,
"mitm_hooks": [],
}
})
);
}
#[test]
fn parse_host_port_defaults_for_empty_string() {
assert!(parse_host_port("", /*default_port*/ 1234).is_err());
}
#[test]
fn parse_host_port_defaults_for_whitespace() {
assert!(parse_host_port(" ", /*default_port*/ 5555).is_err());
}
#[test]
fn parse_host_port_parses_host_port_without_scheme() {
assert_eq!(
parse_host_port("127.0.0.1:8080", /*default_port*/ 3128).unwrap(),
SocketAddressParts {
host: "127.0.0.1".to_string(),
port: 8080,
}
);
}
#[test]
fn parse_host_port_parses_host_port_with_scheme_and_path() {
assert_eq!(
parse_host_port(
"http://example.com:8080/some/path",
/*default_port*/ 3128
)
.unwrap(),
SocketAddressParts {
host: "example.com".to_string(),
port: 8080,
}
);
}
#[test]
fn parse_host_port_strips_userinfo() {
assert_eq!(
parse_host_port(
"http://user:pass@host.example:5555",
/*default_port*/ 3128
)
.unwrap(),
SocketAddressParts {
host: "host.example".to_string(),
port: 5555,
}
);
}
#[test]
fn parse_host_port_parses_ipv6_with_brackets() {
assert_eq!(
parse_host_port("http://[::1]:9999", /*default_port*/ 3128).unwrap(),
SocketAddressParts {
host: "::1".to_string(),
port: 9999,
}
);
}
#[test]
fn parse_host_port_does_not_treat_unbracketed_ipv6_as_host_port() {
assert_eq!(
parse_host_port("2001:db8::1", /*default_port*/ 3128).unwrap(),
SocketAddressParts {
host: "2001:db8::1".to_string(),
port: 3128,
}
);
}
#[test]
fn parse_host_port_falls_back_to_default_port_when_port_is_invalid() {
assert_eq!(
parse_host_port("example.com:notaport", /*default_port*/ 3128).unwrap(),
SocketAddressParts {
host: "example.com".to_string(),
port: 3128,
}
);
}
#[test]
fn host_and_port_from_network_addr_defaults_for_empty_string() {
assert_eq!(
host_and_port_from_network_addr("", /*default_port*/ 1234),
"<missing>"
);
}
#[test]
fn host_and_port_from_network_addr_formats_ipv6() {
assert_eq!(
host_and_port_from_network_addr("http://[::1]:8080", /*default_port*/ 3128),
"[::1]:8080"
);
}
#[test]
fn resolve_addr_maps_localhost_to_loopback() {
assert_eq!(
resolve_addr("localhost", /*default_port*/ 3128).unwrap(),
"127.0.0.1:3128".parse::<SocketAddr>().unwrap()
);
}
#[test]
fn resolve_addr_parses_ip_literals() {
assert_eq!(
resolve_addr("1.2.3.4", /*default_port*/ 80).unwrap(),
"1.2.3.4:80".parse::<SocketAddr>().unwrap()
);
}
#[test]
fn resolve_addr_parses_ipv6_literals() {
assert_eq!(
resolve_addr("http://[::1]:8080", /*default_port*/ 3128).unwrap(),
"[::1]:8080".parse::<SocketAddr>().unwrap()
);
}
#[test]
fn resolve_addr_falls_back_to_loopback_for_hostnames() {
assert_eq!(
resolve_addr("http://example.com:5555", /*default_port*/ 3128).unwrap(),
"127.0.0.1:5555".parse::<SocketAddr>().unwrap()
);
}
#[test]
fn clamp_bind_addrs_allows_non_loopback_when_enabled() {
let cfg = NetworkProxySettings {
dangerously_allow_non_loopback_proxy: true,
..Default::default()
};
let http_addr = "0.0.0.0:3128".parse::<SocketAddr>().unwrap();
let socks_addr = "0.0.0.0:8081".parse::<SocketAddr>().unwrap();
let (http_addr, socks_addr) = clamp_bind_addrs(http_addr, socks_addr, &cfg);
assert_eq!(http_addr, "0.0.0.0:3128".parse::<SocketAddr>().unwrap());
assert_eq!(socks_addr, "0.0.0.0:8081".parse::<SocketAddr>().unwrap());
}
#[test]
fn clamp_bind_addrs_forces_loopback_when_unix_sockets_enabled() {
let cfg = {
let mut settings = settings_with_unix_sockets(&["/tmp/docker.sock"]);
settings.dangerously_allow_non_loopback_proxy = true;
settings
};
let http_addr = "0.0.0.0:3128".parse::<SocketAddr>().unwrap();
let socks_addr = "0.0.0.0:8081".parse::<SocketAddr>().unwrap();
let (http_addr, socks_addr) = clamp_bind_addrs(http_addr, socks_addr, &cfg);
assert_eq!(http_addr, "127.0.0.1:3128".parse::<SocketAddr>().unwrap());
assert_eq!(socks_addr, "127.0.0.1:8081".parse::<SocketAddr>().unwrap());
}
#[test]
fn clamp_bind_addrs_forces_loopback_when_all_unix_sockets_enabled() {
let cfg = NetworkProxySettings {
dangerously_allow_non_loopback_proxy: true,
dangerously_allow_all_unix_sockets: true,
..Default::default()
};
let http_addr = "0.0.0.0:3128".parse::<SocketAddr>().unwrap();
let socks_addr = "0.0.0.0:8081".parse::<SocketAddr>().unwrap();
let (http_addr, socks_addr) = clamp_bind_addrs(http_addr, socks_addr, &cfg);
assert_eq!(http_addr, "127.0.0.1:3128".parse::<SocketAddr>().unwrap());
assert_eq!(socks_addr, "127.0.0.1:8081".parse::<SocketAddr>().unwrap());
}
#[test]
fn resolve_runtime_rejects_relative_allow_unix_sockets_entries() {
let cfg = NetworkProxyConfig {
network: settings_with_unix_sockets(&["relative.sock"]),
};
let err = match resolve_runtime(&cfg) {
Ok(runtime) => panic!(
"relative allow_unix_sockets should fail, but resolve_runtime succeeded: {:?}",
runtime.http_addr
),
Err(err) => err,
};
assert!(
err.to_string().contains("network.allow_unix_sockets[0]"),
"error should point at the invalid allow_unix_sockets entry: {err:#}"
);
}
#[test]
fn resolve_runtime_accepts_unix_style_absolute_allow_unix_sockets_entries() {
let cfg = NetworkProxyConfig {
network: settings_with_unix_sockets(&["/private/tmp/example.sock"]),
};
assert!(
resolve_runtime(&cfg).is_ok(),
"unix-style absolute allow_unix_sockets entry should be accepted"
);
}
}