网络代理 · 联网权限怎么落地

沙箱只能把网络整个打开或整个关掉;codex-network-proxy 补上中间档:会话启动一个本地 HTTP 与 SOCKS5 代理,命令经环境变量被引向它,操作系统沙箱再保证命令只能连到代理端口。代理按“拒绝名单优先、本地地址默认挡、允许名单兜底”裁决,名单外的主机交给内核的审批服务内联询问;需要检查 HTTPS 内容时它自签 CA 做中间人,还能替命令注入真实凭据。

作者 David更新于 第 32 篇(共 57 篇)

网络代理 · 联网权限怎么落地

前三篇的平台沙箱对网络只有两档:NetworkSandboxPolicy 是 Restricted 就断网,是 Enabled 就全通。“只许访问这几个域名”这种中间档,由 codex-network-proxy 实现,这个 crate 去掉测试约 1.7 万行,在这一部分涉及的 crate 里仅次于 Windows 沙箱。

用户看到的样子

代理是实验特性 network_proxy(默认关闭),而且只约束已经开放的网络:先让沙箱允许联网([sandbox_workspace_write] 的 network_access,或配置档的 network.enabled),再在 [features.network_proxy] 或配置档的 [permissions.<name>.network] 里列 domains,值为 allow 或 deny,还可以设 mode = "limited" 只许只读请求。被拦的请求收到 403,响应头 x-proxy-error 写着原因,如 blocked-by-allowlist、blocked-by-denylist、blocked-by-method-policy。遇到需要审批的主机,TUI 给出 Yes, just this once、Yes, and allow this host for this conversation、Yes, and allow this host in the future 等选项。写法与通配规则见手册的沙箱、审批与安全。

什么时候起代理

        if feature_enabled
            && candidate_permission_profile
                .network_sandbox_policy()
                .is_enabled()
        {
            if let Some(FeatureToml::Config(config)) =
                features.and_then(|features| features.network_proxy.as_ref())
            {
                apply_network_proxy_feature_config(&mut configured_proxy, config);
            }
            configured_proxy.set_credential_broker_openai_base_url(credential_broker_base_url);
            configured_proxy.enabled = true;
        }

(codex-rs/core/src/config/network_config.rs:98)

特性打开、配置档本身允许联网,代理才启用;配置档的 network 表只提供备料,不会自己拉起代理(见权限模型)。管理员在 requirements.toml 里写了 [network] 时例外:这时总会生成代理规格,管理员的名单与限制压在用户配置之上。会话启动时 start_managed_network_proxy 先把规则文件里的 network_rule(见执行策略)并进域名名单,再拉起代理:HTTP 监听默认 127.0.0.1:3128,SOCKS5 默认 127.0.0.1:8081 且默认开启;非回环的监听地址会被压回回环,除非显式设置 dangerously_allow_non_loopback_proxy。

命令怎么被引到代理上

代理本身不拦截任何流量,它只是一个等着被连的服务。起命令时,apply_proxy_env_overrides(codex-rs/network-proxy/src/proxy.rs:769)往子进程环境里写一组变量:HTTP_PROXY、HTTPS_PROXY 及其小写形式,yarn、npm、bundler、pip、docker 各自的代理变量,WebSocket 用的 WS_PROXY、WSS_PROXY,都指向 HTTP 代理;SOCKS5 开着时 ALL_PROXY 与 FTP_PROXY 指向 socks5h:// 地址;NODE_USE_ENV_PROXY=1 让 Node.js 内置的 HTTP 客户端也认代理变量;NO_PROXY 只在允许本地绑定时列出本地地址,否则置空,好让本地目标也经过代理检查;最后是一个 CODEX_NETWORK_PROXY_ACTIVE=1 标记。守规矩的程序会走代理;不守规矩的,由操作系统沙箱兜底:

平台怎么保证“只能连代理”
macOSSeatbelt 网络段只放行到 localhost 上代理端口的出站连接(见 macOS 沙箱)
Linux新的网络命名空间里只有一座通往代理的桥,seccomp 只许 IP 套接字(见 Linux 沙箱)
Windows离线沙箱用户的防火墙规则只放行代理端口的回环连接,要求 elevated 后端(见 Windows 沙箱)

受管网络下,每次执行还会拿到一个执行级的代理视图(NetworkProxy::for_execution),带一个归属令牌。Linux 的桥在转发前先写一帧令牌,代理据此知道这个连接属于哪条命令,审批和拦截记录都能落到具体的工具调用上。

放行、拒绝、询问

代理收到 CONNECT 或普通 HTTP 请求后,先规范化主机名(去空白、去端口、去方括号、转小写),然后按固定顺序判定:

        // Decision order matters:
        //  1) explicit deny always wins
        //  2) local/private networking is opt-in (defense-in-depth)
        //  3) allowlist is enforced when configured
        if globset_matches_host_or_unscoped(&deny_set, host_str) {
            return Ok(HostBlockDecision::Blocked(HostBlockReason::Denied));
        }

        let is_allowlisted = globset_matches_host_or_unscoped(&allow_set, host_str);

(codex-rs/network-proxy/src/runtime.rs:721)

第二步挡的是回环、私有网段、链路本地、CGNAT、测试网段等非公网地址,除非打开 allow_local_binding。字面量的本地地址要显式列进允许名单才放行;主机名则先做一次 DNS 解析,解析到非公网地址就拦,哪怕它在允许名单里,注释点名防的是 DNS rebinding。第三步:允许名单为空时一律拒绝,不在名单里就拒绝。名单的写法由这段注释定义:

        // Supported domain patterns:
        // - "example.com": match the exact host
        // - "*.example.com": match any subdomain (not the apex)
        // - "**.example.com": match the apex and any subdomain
        // - "api?.example.com": match exactly one character after "api"
        // - "*": match every host when explicitly enabled for allowlist compilation

(codex-rs/network-proxy/src/policy.rs:219)

全局 * 只能用在允许名单里,出现在拒绝名单会报错。同一个模式同时写了 allow 与 deny 时,按 None < Allow < Deny 的枚举顺序取 deny。mode = "limited" 再加一道方法检查:只许 GET、HEAD、OPTIONS。HTTPS 的 CONNECT 隧道里看不到方法,所以 limited 模式会自动打开中间人(下文),在解开的请求上检查方法;没有中间人状态时,CONNECT 直接拦下。

三种拦截里,只有“不在允许名单”这一种可以商量:

    let host_decision = state.host_blocked(&request.host, request.port).await?;
    let (decision, policy_override) = match host_decision {
        HostBlockDecision::Allowed => (NetworkDecision::Allow, false),
        HostBlockDecision::Blocked(HostBlockReason::NotAllowed) => {
            if let Some(decider) = decider {
                // ...
                let decider_decision = map_decider_decision(decider.decide(request).await);
                let policy_override = matches!(decider_decision, NetworkDecision::Allow);
                (decider_decision, policy_override)
            } else {
                // ...
            }
        }
        HostBlockDecision::Blocked(reason) => (

(codex-rs/network-proxy/src/network_policy.rs:369)

显式拒绝和本地地址直接 403,只有名单外的主机才去问“决定者”(NetworkPolicyDecider);没有决定者时同样按名单拒绝。

询问落到谁头上

图表加载中…

决定者在内核里就是 NetworkApprovalService::handle_inline_policy_request(codex-rs/core/src/tools/network_approval.rs:625)。它是“内联”的:命令的请求挂在代理上等着,审批走完再放行或拒绝,命令不必失败重跑。几条规则:

  • 先查会话级的缓存:同一环境、主机、协议、端口之前被放行过或拒绝过,直接返回。同一条命令对同一主机的并发请求只弹一次,其余等同一个结果。
  • 审批策略是 never,或配置档不是 Managed,直接拒绝。
  • 否则构造 ApprovalAction::NetworkAccess 交给 Session::request_approval,照样是钩子、自动审查、用户的顺序(见审批流程)。选“本次会话允许”记进会话缓存;选“以后都允许”或“以后都拦截”,会同时更新运行中代理的名单,并往 default.rules 追加一条 network_rule。

决定者不是总在:会话级代理只在 requirements.toml 有 [network] 时装上它;工作环境带网络策略时,每次执行会拿到一个兜底的决定者。管理员设了 managed_allowed_domains_only,决定者被整个拿掉,名单外一律硬拒。

中间人证书与凭据代理

有三种情况需要看清 HTTPS 的内容:limited 模式、配置了 MITM 钩子(按主机、方法、路径前缀匹配请求,删除或注入请求头),以及凭据代理。这时代理为进程生成一个 CA,私钥只留在内存里,证书写进 $CODEX_HOME/proxy/,并把证书与原有信任链合成一个信任包,经 SSL_CERT_FILE、REQUESTS_CA_BUNDLE、NODE_EXTRA_CA_CERTS、GIT_SSL_CAINFO、CARGO_HTTP_CAINFO 等 11 个环境变量(CUSTOM_CA_ENV_KEYS,codex-rs/network-proxy/src/certs.rs:260)告诉常见的 HTTPS 客户端。

凭据代理(credential_broker)把“密钥不进沙箱”做成了机制:子进程环境里的 GH_TOKEN、GITHUB_TOKEN、OPENAI_API_KEY 等被替换成形状相同的假值;请求真正发往绑定的主机(如 api.github.com、api.openai.com)时,代理在中间人层把 Authorization 头换回真实凭据。明文 HTTP 请求默认不注入,除非显式打开 dangerously_allow_plaintext_credential_injection。沙箱里的命令即使把环境变量打印出来,也拿不到真密钥。

和《从 LLM 到 Coding Agent》对照

那本书的权限系统在开篇就担心过“一个发请求的工具加一段 API key”。Codex 的回答分两层:操作系统沙箱保证命令只能连到代理,代理按名单与审批决定去哪,凭据代理再让真密钥根本不出现在沙箱里。Grok Build 的做法(见其沙箱与权限)是限网档下用 seccomp 把子进程网络整个封死;Codex 多出来的这一层代理,让“允许联网,但只许去这些地方”成为可能。


上一篇:Windows 沙箱 · 沙箱用户、ACL 与防火墙 · 下一篇:执行策略 · Starlark 规则与提权

本页目录