Codex Handbook
network-proxy/src/socks5.rs 1016 lines
use crate::config::NetworkMode;use crate::connect_policy::TargetCheckedTcpConnector;use crate::mitm;use crate::network_policy::BlockDecisionAuditEventArgs;use crate::network_policy::NetworkDecision;use crate::network_policy::NetworkDecisionSource;use crate::network_policy::NetworkPolicyDecider;use crate::network_policy::NetworkPolicyDecision;use crate::network_policy::NetworkPolicyRequest;use crate::network_policy::NetworkPolicyRequestArgs;use crate::network_policy::NetworkProtocol;use crate::network_policy::emit_block_decision_audit_event;use crate::network_policy::evaluate_host_policy;use crate::policy::normalize_host;use crate::reasons::REASON_METHOD_NOT_ALLOWED;use crate::reasons::REASON_MITM_REQUIRED;use crate::reasons::REASON_PROXY_DISABLED;use crate::responses::PolicyDecisionDetails;use crate::responses::blocked_message_with_policy;use crate::state::BlockedRequest;use crate::state::BlockedRequestArgs;use crate::state::NetworkProxyState;use anyhow::Context as _;use anyhow::Result;use rama_core::Layer;use rama_core::Service;use rama_core::error::BoxError;use rama_core::extensions::Extensions;use rama_core::extensions::ExtensionsMut;use rama_core::extensions::ExtensionsRef;use rama_core::layer::AddInputExtensionLayer;use rama_core::service::service_fn;use rama_net::address::HostWithPort;use rama_net::client::EstablishedClientConnection;use rama_net::proxy::ProxyRequest;use rama_net::proxy::ProxyTarget;use rama_net::proxy::StreamForwardService;use rama_net::stream::Socket;use rama_net::stream::SocketInfo;use rama_socks5::Socks5Acceptor;use rama_socks5::server::DefaultConnector;use rama_socks5::server::DefaultUdpRelay;use rama_socks5::server::udp::RelayRequest;use rama_socks5::server::udp::RelayResponse;use rama_tcp::TcpStream;use rama_tcp::client::Request as TcpRequest;use rama_tcp::server::TcpListener;use std::io;use std::net::SocketAddr;use std::net::TcpListener as StdTcpListener;use std::pin::Pin;use std::sync::Arc;use std::task::Context as TaskContext;use std::task::Poll;use std::time::Instant;use tokio::io::AsyncRead;use tokio::io::AsyncWrite;use tokio::io::ReadBuf;use tracing::error;use tracing::info;use tracing::warn;pub async fn run_socks5(    state: Arc<NetworkProxyState>,    addr: SocketAddr,    policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,    enable_socks5_udp: bool,) -> Result<()> {    let listener = TcpListener::build()        .bind(addr)        .await        // See `http_proxy.rs` for details on why we wrap `BoxError` before converting to anyhow.        .map_err(rama_core::error::OpaqueError::from)        .map_err(anyhow::Error::from)        .with_context(|| format!("bind SOCKS5 proxy: {addr}"))?;    run_socks5_with_listener(state, listener, policy_decider, enable_socks5_udp).await}pub async fn run_socks5_with_std_listener(    state: Arc<NetworkProxyState>,    listener: StdTcpListener,    policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,    enable_socks5_udp: bool,) -> Result<()> {    let listener =        TcpListener::try_from(listener).context("convert std listener to SOCKS5 proxy listener")?;    run_socks5_with_listener(state, listener, policy_decider, enable_socks5_udp).await}async fn run_socks5_with_listener(    state: Arc<NetworkProxyState>,    listener: TcpListener,    policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,    enable_socks5_udp: bool,) -> Result<()> {    let addr = listener        .local_addr()        .context("read SOCKS5 listener local addr")?;    info!("SOCKS5 proxy listening on {addr}");    match state.network_mode().await {        Ok(NetworkMode::Limited) => {            info!(                "SOCKS5 UDP and non-HTTPS SOCKS5 TCP are blocked in limited mode; HTTPS SOCKS5 TCP requires MITM inspection"            );        }        Ok(NetworkMode::Full) => {}        Err(err) => {            warn!("failed to read network mode: {err}");        }    }    let tcp_connector = TargetCheckedTcpConnector::new(state.clone());    let policy_tcp_connector = service_fn({        let policy_decider = policy_decider.clone();        move |req: TcpRequest| {            let tcp_connector = tcp_connector.clone();            let policy_decider = policy_decider.clone();            async move { handle_socks5_tcp(req, tcp_connector, policy_decider).await }        }    });    let socks_proxy = service_fn(|request| async move { proxy_socks5_tcp(request).await });    let socks_connector = DefaultConnector::default()        .with_connector(policy_tcp_connector)        .with_service(socks_proxy);    let base = Socks5Acceptor::new().with_connector(socks_connector);    if enable_socks5_udp {        let udp_state = state.clone();        let udp_decider = policy_decider.clone();        let udp_relay = DefaultUdpRelay::default().with_async_inspector(service_fn({            move |request: RelayRequest| {                let udp_state = udp_state.clone();                let udp_decider = udp_decider.clone();                async move { inspect_socks5_udp(request, udp_state, udp_decider).await }            }        }));        let socks_acceptor = base.with_udp_associator(udp_relay);        listener            .serve(AddInputExtensionLayer::new(state).into_layer(socks_acceptor))            .await;    } else {        listener            .serve(AddInputExtensionLayer::new(state).into_layer(base))            .await;    }    Ok(())}async fn handle_socks5_tcp(    req: TcpRequest,    tcp_connector: TargetCheckedTcpConnector,    policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,) -> Result<EstablishedClientConnection<Socks5TcpConnection, TcpRequest>, BoxError> {    let app_state = req        .extensions()        .get::<Arc<NetworkProxyState>>()        .cloned()        .ok_or_else(|| io::Error::other("missing state"))?;    let host = normalize_host(&req.authority.host.to_string());    let port = req.authority.port;    let target = req.authority.clone();    if host.is_empty() {        return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid host").into());    }    let client = req        .extensions()        .get::<SocketInfo>()        .map(|info| info.peer_addr().to_string());    match app_state.enabled().await {        Ok(true) => {}        Ok(false) => {            emit_socks_block_decision_audit_event(                &app_state,                NetworkDecisionSource::ProxyState,                REASON_PROXY_DISABLED,                NetworkProtocol::Socks5Tcp,                host.as_str(),                port,                client.as_deref(),            );            let details = PolicyDecisionDetails {                decision: NetworkPolicyDecision::Deny,                reason: REASON_PROXY_DISABLED,                source: NetworkDecisionSource::ProxyState,                protocol: NetworkProtocol::Socks5Tcp,                host: &host,                port,            };            let _ = app_state                .record_blocked(BlockedRequest::new(BlockedRequestArgs {                    host: host.clone(),                    reason: REASON_PROXY_DISABLED.to_string(),                    client: client.clone(),                    method: None,                    mode: None,                    protocol: "socks5".to_string(),                    decision: Some(details.decision.as_str().to_string()),                    source: Some(details.source.as_str().to_string()),                    port: Some(port),                }))                .await;            let client = client.as_deref().unwrap_or_default();            warn!("SOCKS blocked; proxy disabled (client={client}, host={host})");            return Err(policy_denied_error(REASON_PROXY_DISABLED, &details).into());        }        Err(err) => {            error!("failed to read enabled state: {err}");            return Err(io::Error::other("proxy error").into());        }    }    let mode = match app_state.network_mode().await {        Ok(mode) => mode,        Err(err) => {            error!("failed to evaluate method policy: {err}");            return Err(io::Error::other("proxy error").into());        }    };    // SOCKS5 only exposes host and port, so only the default HTTPS port is identifiable as a    // TLS stream that the HTTPS MITM path can safely terminate.    let socks5_tcp_target_is_https = port == 443;    if mode == NetworkMode::Limited && !socks5_tcp_target_is_https {        emit_socks_block_decision_audit_event(            &app_state,            NetworkDecisionSource::ModeGuard,            REASON_METHOD_NOT_ALLOWED,            NetworkProtocol::Socks5Tcp,            host.as_str(),            port,            client.as_deref(),        );        let details = PolicyDecisionDetails {            decision: NetworkPolicyDecision::Deny,            reason: REASON_METHOD_NOT_ALLOWED,            source: NetworkDecisionSource::ModeGuard,            protocol: NetworkProtocol::Socks5Tcp,            host: &host,            port,        };        let _ = app_state            .record_blocked(BlockedRequest::new(BlockedRequestArgs {                host: host.clone(),                reason: REASON_METHOD_NOT_ALLOWED.to_string(),                client: client.clone(),                method: None,                mode: Some(NetworkMode::Limited),                protocol: "socks5".to_string(),                decision: Some(details.decision.as_str().to_string()),                source: Some(details.source.as_str().to_string()),                port: Some(port),            }))            .await;        let client = client.as_deref().unwrap_or_default();        warn!(            "SOCKS blocked; limited mode only supports HTTPS MITM (client={client}, host={host}, port={port})"        );        return Err(policy_denied_error(REASON_METHOD_NOT_ALLOWED, &details).into());    }    let request = NetworkPolicyRequest::new(NetworkPolicyRequestArgs {        protocol: NetworkProtocol::Socks5Tcp,        host: host.clone(),        port,        client_addr: client.clone(),        method: None,        command: None,        exec_policy_hint: None,    });    match evaluate_host_policy(&app_state, policy_decider.as_ref(), &request).await {        Ok(NetworkDecision::Deny {            reason,            source,            decision,        }) => {            let details = PolicyDecisionDetails {                decision,                reason: &reason,                source,                protocol: NetworkProtocol::Socks5Tcp,                host: &host,                port,            };            let _ = app_state                .record_blocked(BlockedRequest::new(BlockedRequestArgs {                    host: host.clone(),                    reason: reason.clone(),                    client: client.clone(),                    method: None,                    mode: None,                    protocol: "socks5".to_string(),                    decision: Some(details.decision.as_str().to_string()),                    source: Some(details.source.as_str().to_string()),                    port: Some(port),                }))                .await;            let client = client.as_deref().unwrap_or_default();            warn!("SOCKS blocked (client={client}, host={host}, reason={reason})");            return Err(policy_denied_error(&reason, &details).into());        }        Ok(NetworkDecision::Allow) => {            let client = client.as_deref().unwrap_or_default();            info!("SOCKS allowed (client={client}, host={host}, port={port})");        }        Err(err) => {            error!("failed to evaluate host: {err}");            return Err(io::Error::other("proxy error").into());        }    }    let host_has_mitm_hooks = match app_state.host_has_mitm_hooks(&host).await {        Ok(has_hooks) => has_hooks,        Err(err) => {            error!("failed to inspect MITM hooks for {host}: {err}");            return Err(io::Error::other("proxy error").into());        }    };    let mitm_state = match app_state.mitm_state().await {        Ok(state) => state,        Err(err) => {            error!("failed to load MITM state: {err}");            return Err(io::Error::other("proxy error").into());        }    };    let socks_needs_mitm =        socks5_tcp_target_is_https && (mode == NetworkMode::Limited || host_has_mitm_hooks);    if (host_has_mitm_hooks && !socks5_tcp_target_is_https)        || (socks_needs_mitm && mitm_state.is_none())    {        emit_socks_block_decision_audit_event(            &app_state,            NetworkDecisionSource::ModeGuard,            REASON_MITM_REQUIRED,            NetworkProtocol::Socks5Tcp,            host.as_str(),            port,            client.as_deref(),        );        let details = PolicyDecisionDetails {            decision: NetworkPolicyDecision::Deny,            reason: REASON_MITM_REQUIRED,            source: NetworkDecisionSource::ModeGuard,            protocol: NetworkProtocol::Socks5Tcp,            host: &host,            port,        };        let _ = app_state            .record_blocked(BlockedRequest::new(BlockedRequestArgs {                host: host.clone(),                reason: REASON_MITM_REQUIRED.to_string(),                client: client.clone(),                method: None,                mode: Some(mode),                protocol: "socks5".to_string(),                decision: Some(details.decision.as_str().to_string()),                source: Some(details.source.as_str().to_string()),                port: Some(port),            }))            .await;        let client = client.as_deref().unwrap_or_default();        warn!(            "SOCKS blocked; MITM required to enforce HTTPS policy (client={client}, host={host}, mode={mode:?}, hooked_host={host_has_mitm_hooks}, https_target={socks5_tcp_target_is_https})"        );        return Err(policy_denied_error(REASON_MITM_REQUIRED, &details).into());    }    if socks_needs_mitm && let Some(mitm_state) = mitm_state {        let client = client.as_deref().unwrap_or_default();        info!("SOCKS MITM enabled (client={client}, host={host}, port={port}, mode={mode:?})");        return Ok(EstablishedClientConnection {            input: req,            conn: Socks5TcpConnection::Mitm {                target,                mode,                mitm: mitm_state,                extensions: Extensions::new(),            },        });    }    info!("SOCKS upstream dial started (host={host}, port={port})");    let connect_started_at = Instant::now();    let result = tcp_connector.serve(req).await.map(|connection| {        let EstablishedClientConnection { input, conn } = connection;        EstablishedClientConnection {            input,            conn: Socks5TcpConnection::Direct(conn),        }    });    match &result {        Ok(_) => info!(            "SOCKS upstream dial established (host={host}, port={port}, elapsed_ms={})",            connect_started_at.elapsed().as_millis()        ),        Err(_) => warn!(            "SOCKS upstream dial failed (host={host}, port={port}, elapsed_ms={})",            connect_started_at.elapsed().as_millis()        ),    }    result}/// Internal connector output for SOCKS5 TCP. MITM requests do not dial upstream before the/// inner HTTPS request is inspected, so they carry the target metadata instead of a socket.#[derive(Debug)]enum Socks5TcpConnection {    Direct(TcpStream),    Mitm {        target: HostWithPort,        mode: NetworkMode,        mitm: Arc<mitm::MitmState>,        extensions: Extensions,    },}impl AsyncRead for Socks5TcpConnection {    fn poll_read(        self: Pin<&mut Self>,        cx: &mut TaskContext<'_>,        buf: &mut ReadBuf<'_>,    ) -> Poll<io::Result<()>> {        match self.get_mut() {            Self::Direct(stream) => Pin::new(stream).poll_read(cx, buf),            Self::Mitm { .. } => Poll::Ready(Ok(())),        }    }}impl AsyncWrite for Socks5TcpConnection {    fn poll_write(        self: Pin<&mut Self>,        cx: &mut TaskContext<'_>,        buf: &[u8],    ) -> Poll<io::Result<usize>> {        match self.get_mut() {            Self::Direct(stream) => Pin::new(stream).poll_write(cx, buf),            Self::Mitm { .. } => Poll::Ready(Ok(buf.len())),        }    }    fn poll_flush(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<io::Result<()>> {        match self.get_mut() {            Self::Direct(stream) => Pin::new(stream).poll_flush(cx),            Self::Mitm { .. } => Poll::Ready(Ok(())),        }    }    fn poll_shutdown(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<io::Result<()>> {        match self.get_mut() {            Self::Direct(stream) => Pin::new(stream).poll_shutdown(cx),            Self::Mitm { .. } => Poll::Ready(Ok(())),        }    }}impl Socket for Socks5TcpConnection {    fn local_addr(&self) -> io::Result<SocketAddr> {        match self {            Self::Direct(stream) => stream.local_addr(),            Self::Mitm { .. } => Ok(SocketAddr::from(([0, 0, 0, 0], 0))),        }    }    fn peer_addr(&self) -> io::Result<SocketAddr> {        match self {            Self::Direct(stream) => stream.peer_addr(),            Self::Mitm { .. } => Ok(SocketAddr::from(([0, 0, 0, 0], 0))),        }    }}impl ExtensionsRef for Socks5TcpConnection {    fn extensions(&self) -> &Extensions {        match self {            Self::Direct(stream) => stream.extensions(),            Self::Mitm { extensions, .. } => extensions,        }    }}impl ExtensionsMut for Socks5TcpConnection {    fn extensions_mut(&mut self) -> &mut Extensions {        match self {            Self::Direct(stream) => stream.extensions_mut(),            Self::Mitm { extensions, .. } => extensions,        }    }}async fn proxy_socks5_tcp(    request: ProxyRequest<TcpStream, Socks5TcpConnection>,) -> Result<(), BoxError> {    let ProxyRequest { mut source, target } = request;    match target {        Socks5TcpConnection::Direct(target) => StreamForwardService::default()            .serve(ProxyRequest { source, target })            .await            .map_err(Into::into),        Socks5TcpConnection::Mitm {            target, mode, mitm, ..        } => {            source.extensions_mut().insert(ProxyTarget(target));            source.extensions_mut().insert(mode);            source.extensions_mut().insert(mitm);            mitm::mitm_stream(source).await.map_err(Into::into)        }    }}async fn inspect_socks5_udp(    request: RelayRequest,    state: Arc<NetworkProxyState>,    policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,) -> io::Result<RelayResponse> {    let RelayRequest {        server_address,        payload,        extensions,        ..    } = request;    let host = normalize_host(&server_address.ip_addr.to_string());    let port = server_address.port;    if host.is_empty() {        return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid host"));    }    let client = extensions        .get::<SocketInfo>()        .map(|info| info.peer_addr().to_string());    match state.enabled().await {        Ok(true) => {}        Ok(false) => {            emit_socks_block_decision_audit_event(                &state,                NetworkDecisionSource::ProxyState,                REASON_PROXY_DISABLED,                NetworkProtocol::Socks5Udp,                host.as_str(),                port,                client.as_deref(),            );            let details = PolicyDecisionDetails {                decision: NetworkPolicyDecision::Deny,                reason: REASON_PROXY_DISABLED,                source: NetworkDecisionSource::ProxyState,                protocol: NetworkProtocol::Socks5Udp,                host: &host,                port,            };            let _ = state                .record_blocked(BlockedRequest::new(BlockedRequestArgs {                    host: host.clone(),                    reason: REASON_PROXY_DISABLED.to_string(),                    client: client.clone(),                    method: None,                    mode: None,                    protocol: "socks5-udp".to_string(),                    decision: Some(details.decision.as_str().to_string()),                    source: Some(details.source.as_str().to_string()),                    port: Some(port),                }))                .await;            let client = client.as_deref().unwrap_or_default();            warn!("SOCKS UDP blocked; proxy disabled (client={client}, host={host})");            return Err(policy_denied_error(REASON_PROXY_DISABLED, &details));        }        Err(err) => {            error!("failed to read enabled state: {err}");            return Err(io::Error::other("proxy error"));        }    }    match state.network_mode().await {        Ok(NetworkMode::Limited) => {            emit_socks_block_decision_audit_event(                &state,                NetworkDecisionSource::ModeGuard,                REASON_METHOD_NOT_ALLOWED,                NetworkProtocol::Socks5Udp,                host.as_str(),                port,                client.as_deref(),            );            let details = PolicyDecisionDetails {                decision: NetworkPolicyDecision::Deny,                reason: REASON_METHOD_NOT_ALLOWED,                source: NetworkDecisionSource::ModeGuard,                protocol: NetworkProtocol::Socks5Udp,                host: &host,                port,            };            let _ = state                .record_blocked(BlockedRequest::new(BlockedRequestArgs {                    host: host.clone(),                    reason: REASON_METHOD_NOT_ALLOWED.to_string(),                    client: client.clone(),                    method: None,                    mode: Some(NetworkMode::Limited),                    protocol: "socks5-udp".to_string(),                    decision: Some(details.decision.as_str().to_string()),                    source: Some(details.source.as_str().to_string()),                    port: Some(port),                }))                .await;            return Err(policy_denied_error(REASON_METHOD_NOT_ALLOWED, &details));        }        Ok(NetworkMode::Full) => {}        Err(err) => {            error!("failed to evaluate method policy: {err}");            return Err(io::Error::other("proxy error"));        }    }    let request = NetworkPolicyRequest::new(NetworkPolicyRequestArgs {        protocol: NetworkProtocol::Socks5Udp,        host: host.clone(),        port,        client_addr: client.clone(),        method: None,        command: None,        exec_policy_hint: None,    });    match evaluate_host_policy(&state, policy_decider.as_ref(), &request).await {        Ok(NetworkDecision::Deny {            reason,            source,            decision,        }) => {            let details = PolicyDecisionDetails {                decision,                reason: &reason,                source,                protocol: NetworkProtocol::Socks5Udp,                host: &host,                port,            };            let _ = state                .record_blocked(BlockedRequest::new(BlockedRequestArgs {                    host: host.clone(),                    reason: reason.clone(),                    client: client.clone(),                    method: None,                    mode: None,                    protocol: "socks5-udp".to_string(),                    decision: Some(details.decision.as_str().to_string()),                    source: Some(details.source.as_str().to_string()),                    port: Some(port),                }))                .await;            let client = client.as_deref().unwrap_or_default();            warn!("SOCKS UDP blocked (client={client}, host={host}, reason={reason})");            Err(policy_denied_error(&reason, &details))        }        Ok(NetworkDecision::Allow) => Ok(RelayResponse {            maybe_payload: Some(payload),            extensions,        }),        Err(err) => {            error!("failed to evaluate UDP host: {err}");            Err(io::Error::other("proxy error"))        }    }}fn emit_socks_block_decision_audit_event(    state: &NetworkProxyState,    source: NetworkDecisionSource,    reason: &str,    protocol: NetworkProtocol,    host: &str,    port: u16,    client_addr: Option<&str>,) {    emit_block_decision_audit_event(        state,        BlockDecisionAuditEventArgs {            source,            reason,            protocol,            server_address: host,            server_port: port,            method: None,            client_addr,        },    );}fn policy_denied_error(reason: &str, details: &PolicyDecisionDetails<'_>) -> io::Error {    io::Error::new(        io::ErrorKind::PermissionDenied,        blocked_message_with_policy(reason, details),    )}#[cfg(test)]mod tests {    use super::*;    use crate::config::NetworkMode;    use crate::config::NetworkProxyConfig;    use crate::config::NetworkProxySettings;    use crate::mitm_hook::MitmHookConfig;    use crate::mitm_hook::MitmHookMatchConfig;    use crate::network_policy::test_support::POLICY_DECISION_EVENT_NAME;    use crate::network_policy::test_support::capture_events;    use crate::network_policy::test_support::find_event_by_name;    use crate::runtime::ConfigReloader;    use crate::runtime::ConfigReloaderFuture;    use crate::runtime::ConfigState;    use crate::state::NetworkProxyConstraints;    use crate::state::build_config_state;    use pretty_assertions::assert_eq;    use rama_core::extensions::Extensions;    use rama_core::extensions::ExtensionsMut;    use rama_net::address::HostWithPort;    use rama_net::address::SocketAddress;    use rama_socks5::server::udp::RelayDirection;    use std::net::IpAddr;    use std::net::Ipv4Addr;    use std::sync::Arc;    use std::sync::Mutex;    // Managed MITM CA files live under the shared test CODEX_HOME, so MITM-enabled config state    // must be materialized one test at a time.    static MITM_CONFIG_STATE_LOCK: Mutex<()> = Mutex::new(());    #[derive(Clone)]    struct StaticReloader {        state: ConfigState,    }    impl ConfigReloader for StaticReloader {        fn maybe_reload(&self) -> ConfigReloaderFuture<'_, Option<ConfigState>> {            Box::pin(async { Ok(None) })        }        fn reload_now(&self) -> ConfigReloaderFuture<'_, ConfigState> {            Box::pin(async { Ok(self.state.clone()) })        }        fn source_label(&self) -> String {            "static test reloader".to_string()        }    }    fn state_for_settings(network: NetworkProxySettings) -> Arc<NetworkProxyState> {        let config = NetworkProxyConfig { network };        let _mitm_config_state_guard = config            .network            .mitm            .then(|| MITM_CONFIG_STATE_LOCK.lock().unwrap());        let state = build_config_state(config, NetworkProxyConstraints::default()).unwrap();        let reloader = Arc::new(StaticReloader {            state: state.clone(),        });        Arc::new(NetworkProxyState::with_reloader(state, reloader))    }    #[tokio::test(flavor = "current_thread")]    async fn handle_socks5_tcp_emits_block_decision_for_proxy_disabled() {        let state = state_for_settings(NetworkProxySettings {            enabled: false,            mode: NetworkMode::Full,            ..NetworkProxySettings::default()        });        let mut request =            TcpRequest::new(HostWithPort::try_from("example.com:443").expect("valid authority"));        request.extensions_mut().insert(state.clone());        let (result, events) = capture_events(|| async {            handle_socks5_tcp(                request,                TargetCheckedTcpConnector::new(state.clone()),                /*policy_decider*/ None,            )            .await        })        .await;        assert!(result.is_err(), "proxy-disabled request should be denied");        let event = find_event_by_name(&events, POLICY_DECISION_EVENT_NAME)            .expect("expected policy decision event");        assert_eq!(event.field("network.policy.scope"), Some("non_domain"));        assert_eq!(event.field("network.policy.decision"), Some("deny"));        assert_eq!(event.field("network.policy.source"), Some("proxy_state"));        assert_eq!(            event.field("network.policy.reason"),            Some(REASON_PROXY_DISABLED)        );        assert_eq!(            event.field("network.transport.protocol"),            Some("socks5_tcp")        );        assert_eq!(event.field("server.address"), Some("example.com"));        assert_eq!(event.field("server.port"), Some("443"));        assert_eq!(event.field("http.request.method"), Some("none"));        assert_eq!(event.field("client.address"), Some("unknown"));    }    #[tokio::test(flavor = "current_thread")]    async fn handle_socks5_tcp_uses_mitm_in_limited_mode() {        let mut settings = NetworkProxySettings {            enabled: true,            mode: NetworkMode::Limited,            mitm: true,            ..NetworkProxySettings::default()        };        settings.set_allowed_domains(vec!["example.com".to_string()]);        let state = state_for_settings(settings);        let mut request =            TcpRequest::new(HostWithPort::try_from("example.com:443").expect("valid authority"));        request.extensions_mut().insert(state.clone());        let result = handle_socks5_tcp(            request,            TargetCheckedTcpConnector::new(state),            /*policy_decider*/ None,        )        .await        .expect("limited-mode HTTPS should use MITM");        assert!(matches!(result.conn, Socks5TcpConnection::Mitm { .. }));    }    #[tokio::test(flavor = "current_thread")]    async fn handle_socks5_tcp_blocks_non_https_in_limited_mode() {        let mut settings = NetworkProxySettings {            enabled: true,            mode: NetworkMode::Limited,            ..NetworkProxySettings::default()        };        settings.set_allowed_domains(vec!["example.com".to_string()]);        let state = state_for_settings(settings);        let mut request =            TcpRequest::new(HostWithPort::try_from("example.com:80").expect("valid authority"));        request.extensions_mut().insert(state.clone());        let (result, events) = capture_events(|| async {            handle_socks5_tcp(                request,                TargetCheckedTcpConnector::new(state),                /*policy_decider*/ None,            )            .await        })        .await;        assert!(            result.is_err(),            "limited-mode non-HTTPS SOCKS should be denied"        );        let event = find_event_by_name(&events, POLICY_DECISION_EVENT_NAME)            .expect("expected policy decision event");        assert_eq!(event.field("network.policy.scope"), Some("non_domain"));        assert_eq!(event.field("network.policy.decision"), Some("deny"));        assert_eq!(event.field("network.policy.source"), Some("mode_guard"));        assert_eq!(            event.field("network.policy.reason"),            Some(REASON_METHOD_NOT_ALLOWED)        );        assert_eq!(            event.field("network.transport.protocol"),            Some("socks5_tcp")        );        assert_eq!(event.field("server.address"), Some("example.com"));        assert_eq!(event.field("server.port"), Some("80"));        assert_eq!(event.field("http.request.method"), Some("none"));        assert_eq!(event.field("client.address"), Some("unknown"));    }    #[tokio::test(flavor = "current_thread")]    async fn handle_socks5_tcp_blocks_limited_mode_without_mitm_state() {        let mut settings = NetworkProxySettings {            enabled: true,            mode: NetworkMode::Limited,            ..NetworkProxySettings::default()        };        settings.set_allowed_domains(vec!["example.com".to_string()]);        let state = state_for_settings(settings);        let mut request =            TcpRequest::new(HostWithPort::try_from("example.com:443").expect("valid authority"));        request.extensions_mut().insert(state.clone());        let err = handle_socks5_tcp(            request,            TargetCheckedTcpConnector::new(state),            /*policy_decider*/ None,        )        .await        .expect_err("limited-mode HTTPS requires MITM");        assert!(            format!("{err:?}").contains("MITM required"),            "unexpected error: {err:?}"        );    }    #[tokio::test(flavor = "current_thread")]    async fn handle_socks5_tcp_uses_mitm_for_hooked_host_in_full_mode() {        let mut settings = NetworkProxySettings {            enabled: true,            mode: NetworkMode::Full,            mitm: true,            mitm_hooks: vec![MitmHookConfig {                host: "api.github.com".to_string(),                matcher: MitmHookMatchConfig {                    methods: vec!["POST".to_string()],                    path_prefixes: vec!["/repos/openai/".to_string()],                    ..MitmHookMatchConfig::default()                },                ..MitmHookConfig::default()            }],            ..NetworkProxySettings::default()        };        settings.set_allowed_domains(vec!["api.github.com".to_string()]);        let state = state_for_settings(settings);        let mut request =            TcpRequest::new(HostWithPort::try_from("api.github.com:443").expect("valid authority"));        request.extensions_mut().insert(state.clone());        let result = handle_socks5_tcp(            request,            TargetCheckedTcpConnector::new(state),            /*policy_decider*/ None,        )        .await        .expect("hooked HTTPS should use MITM");        assert!(matches!(result.conn, Socks5TcpConnection::Mitm { .. }));    }    #[tokio::test(flavor = "current_thread")]    async fn handle_socks5_tcp_blocks_hooked_non_https_host_in_full_mode() {        let mut settings = NetworkProxySettings {            enabled: true,            mode: NetworkMode::Full,            mitm: true,            mitm_hooks: vec![MitmHookConfig {                host: "api.github.com".to_string(),                matcher: MitmHookMatchConfig {                    methods: vec!["POST".to_string()],                    path_prefixes: vec!["/repos/openai/".to_string()],                    ..MitmHookMatchConfig::default()                },                ..MitmHookConfig::default()            }],            ..NetworkProxySettings::default()        };        settings.set_allowed_domains(vec!["api.github.com".to_string()]);        let state = state_for_settings(settings);        let mut request =            TcpRequest::new(HostWithPort::try_from("api.github.com:80").expect("valid authority"));        request.extensions_mut().insert(state.clone());        let err = handle_socks5_tcp(            request,            TargetCheckedTcpConnector::new(state),            /*policy_decider*/ None,        )        .await        .expect_err("hooked non-HTTPS SOCKS should require MITM");        assert!(            format!("{err:?}").contains("MITM required"),            "unexpected error: {err:?}"        );    }    #[tokio::test(flavor = "current_thread")]    async fn inspect_socks5_udp_emits_block_decision_for_mode_guard_deny() {        let state = state_for_settings(NetworkProxySettings {            enabled: true,            mode: NetworkMode::Limited,            ..NetworkProxySettings::default()        });        let request = RelayRequest {            direction: RelayDirection::South,            server_address: SocketAddress::new(IpAddr::V4(Ipv4Addr::new(93, 184, 216, 34)), 53),            payload: Default::default(),            extensions: Extensions::new(),        };        let (result, events) = capture_events(|| async {            inspect_socks5_udp(request, state, /*policy_decider*/ None).await        })        .await;        assert!(result.is_err(), "limited-mode UDP request should be denied");        let event = find_event_by_name(&events, POLICY_DECISION_EVENT_NAME)            .expect("expected policy decision event");        assert_eq!(event.field("network.policy.scope"), Some("non_domain"));        assert_eq!(event.field("network.policy.decision"), Some("deny"));        assert_eq!(event.field("network.policy.source"), Some("mode_guard"));        assert_eq!(            event.field("network.policy.reason"),            Some(REASON_METHOD_NOT_ALLOWED)        );        assert_eq!(            event.field("network.transport.protocol"),            Some("socks5_udp")        );        assert_eq!(event.field("server.address"), Some("93.184.216.34"));        assert_eq!(event.field("server.port"), Some("53"));        assert_eq!(event.field("http.request.method"), Some("none"));        assert_eq!(event.field("client.address"), Some("unknown"));    }}