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

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

- 作者：David（道雾轩）
- 专栏：Codex 源码解读（https://daiw.net/manual/codex-source.md）
- 最后更新：2026-09-29
- 原文：https://daiw.net/manual/codex-source/network-proxy
- 转载与引用：请注明出处并附原文链接（https://daiw.net/about/copyright）

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

前三篇的平台沙箱对网络只有两档：`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` 等选项。写法与通配规则见手册的[沙箱、审批与安全](https://daiw.net/manual/codex/sandbox-approvals)。

## 什么时候起代理

```rust
        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` 表只提供备料，不会自己拉起代理（见[权限模型](https://daiw.net/manual/codex-source/permissions-model)）。管理员在 `requirements.toml` 里写了 `[network]` 时例外：这时总会生成代理规格，管理员的名单与限制压在用户配置之上。会话启动时 `start_managed_network_proxy` 先把规则文件里的 `network_rule`（见[执行策略](https://daiw.net/manual/codex-source/execpolicy)）并进域名名单，再拉起代理：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` 标记。守规矩的程序会走代理；不守规矩的，由操作系统沙箱兜底：

| 平台 | 怎么保证“只能连代理” |
| --- | --- |
| macOS | Seatbelt 网络段只放行到 `localhost` 上代理端口的出站连接（见 [macOS 沙箱](https://daiw.net/manual/codex-source/seatbelt)） |
| Linux | 新的网络命名空间里只有一座通往代理的桥，seccomp 只许 IP 套接字（见 [Linux 沙箱](https://daiw.net/manual/codex-source/linux-sandbox)） |
| Windows | 离线沙箱用户的防火墙规则只放行代理端口的回环连接，要求 elevated 后端（见 [Windows 沙箱](https://daiw.net/manual/codex-source/windows-sandbox)） |

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

## 放行、拒绝、询问

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

```rust
        // 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。第三步：允许名单为空时一律拒绝，不在名单里就拒绝。名单的写法由这段注释定义：

```rust
        // 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 直接拦下。

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

```rust
    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`）；没有决定者时同样按名单拒绝。

## 询问落到谁头上

```mermaid
sequenceDiagram
  participant P as 沙箱里的命令
  participant X as 本地代理
  participant N as NetworkApprovalService
  participant R as 钩子 审查者 或用户
  P->>X: CONNECT 到名单外的主机
  X->>X: host_blocked 判定为 NotAllowed
  X->>N: decider.decide
  N->>R: request_approval NetworkAccess
  R-->>N: 决定
  N-->>X: Allow 或 Deny
  X-->>P: 建立隧道或返回 403
```

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

- 先查会话级的缓存：同一环境、主机、协议、端口之前被放行过或拒绝过，直接返回。同一条命令对同一主机的并发请求只弹一次，其余等同一个结果。
- 审批策略是 `never`，或配置档不是 `Managed`，直接拒绝。
- 否则构造 `ApprovalAction::NetworkAccess` 交给 `Session::request_approval`，照样是钩子、自动审查、用户的顺序（见[审批流程](https://daiw.net/manual/codex-source/approvals-flow)）。选“本次会话允许”记进会话缓存；选“以后都允许”或“以后都拦截”，会同时更新运行中代理的名单，并往 `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》对照

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

---

上一篇：[Windows 沙箱 · 沙箱用户、ACL 与防火墙](https://daiw.net/manual/codex-source/windows-sandbox) · 下一篇：[执行策略 · Starlark 规则与提权](https://daiw.net/manual/codex-source/execpolicy)
