mirror of
https://github.com/openai/codex.git
synced 2026-04-28 08:34:54 +00:00
## Why `argument-comment-lint` was green in CI even though the repo still had many uncommented literal arguments. The main gap was target coverage: the repo wrapper did not force Cargo to inspect test-only call sites, so examples like the `latest_session_lookup_params(true, ...)` tests in `codex-rs/tui_app_server/src/lib.rs` never entered the blocking CI path. This change cleans up the existing backlog, makes the default repo lint path cover all Cargo targets, and starts rolling that stricter CI enforcement out on the platform where it is currently validated. ## What changed - mechanically fixed existing `argument-comment-lint` violations across the `codex-rs` workspace, including tests, examples, and benches - updated `tools/argument-comment-lint/run-prebuilt-linter.sh` and `tools/argument-comment-lint/run.sh` so non-`--fix` runs default to `--all-targets` unless the caller explicitly narrows the target set - fixed both wrappers so forwarded cargo arguments after `--` are preserved with a single separator - documented the new default behavior in `tools/argument-comment-lint/README.md` - updated `rust-ci` so the macOS lint lane keeps the plain wrapper invocation and therefore enforces `--all-targets`, while Linux and Windows temporarily pass `-- --lib --bins` That temporary CI split keeps the stricter all-targets check where it is already cleaned up, while leaving room to finish the remaining Linux- and Windows-specific target-gated cleanup before enabling `--all-targets` on those runners. The Linux and Windows failures on the intermediate revision were caused by the wrapper forwarding bug, not by additional lint findings in those lanes. ## Validation - `bash -n tools/argument-comment-lint/run.sh` - `bash -n tools/argument-comment-lint/run-prebuilt-linter.sh` - shell-level wrapper forwarding check for `-- --lib --bins` - shell-level wrapper forwarding check for `-- --tests` - `just argument-comment-lint` - `cargo test` in `tools/argument-comment-lint` - `cargo test -p codex-terminal-detection` ## Follow-up - Clean up remaining Linux-only target-gated callsites, then switch the Linux lint lane back to the plain wrapper invocation. - Clean up remaining Windows-only target-gated callsites, then switch the Windows lint lane back to the plain wrapper invocation.
870 lines
27 KiB
Rust
870 lines
27 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;
|
|
|
|
#[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,
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|
|
}
|
|
|
|
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, and HTTPS CONNECTs are tunneled without
|
|
/// MITM interception.
|
|
#[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,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[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,
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
#[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"
|
|
);
|
|
}
|
|
}
|