跳转到内容
OmniRoute source

MITM TPROXY Transparent Decrypt (中文 (简体))

其他四种捕获模式各有一项局限:

模式 流量引导方式 局限
AgentBridge 通过 /etc/hosts 对固定主机集进行 DNS 欺骗 仅支持已注册的 IDE 代理主机
自定义主机 通过 /etc/hosts 对每个主机进行 DNS 欺骗 每个主机需要一个条目;编辑 hosts 需要 sudo
HTTP_PROXY HTTP_PROXY/HTTPS_PROXY 环境变量 仅支持遵循该环境变量的应用
系统级代理 操作系统代理设置 会修改全局状态;需要还原

TPROXY 透明解密改为在内核层引导流量。它在 mangle OUTPUT 链中标记发往目标端口(默认为 443)的新建本地出站 TCP 连接, ip rule 将已标记的数据包重新路由至本地交付, 重新进入后,mangle PREROUTING 中的 TPROXY 目标会将其交给 IP_TRANSPARENT 监听器——该监听器随后终止 TLS 并捕获明文。

当你希望捕获并解密来自符合以下条件的进程的流量时,请使用此模式:

  • 连接到 AgentBridge 未注册的主机,并且
  • 不遵循 HTTP_PROXY,并且
  • 你不希望通过更改系统级代理来干扰该进程。

由于拦截发生在内核中,发起连接的进程无需进行任何配置更改 ——但该进程必须信任 OmniRoute 安装的动态 CA (请参阅§4)。


要求 详情
操作系统 仅限 Linux — IP_TRANSPARENT 是 Linux 独有的套接字选项。在其他所有平台上,加载器均返回“不可用”。
权限 需要 CAP_NET_ADMIN 能力才能创建透明套接字并应用 iptables/ip 规则 — 实际使用中应以 root 身份运行。
原生插件 必须构建一个小型 N-API 插件 (src/mitm/tproxy/native/transparent.c),或将其作为预构建版本提供。请参阅 §3。
内核模块 iptables 需支持 TPROXY、mangle 和 mark 匹配(已针对内核 6.8.0 验证)。

优雅降级:如果缺少任何要求(非 Linux、没有工具链、插件未构建), 插件加载器 (src/mitm/tproxy/transparentSocket.ts::loadTransparentAddon) 会返回 null,而不是抛出异常。随后,捕获模式状态会报告 available: false,仪表板开关将被禁用,并显示工具提示 “TPROXY 解密需要 Linux + root + 原生插件”,OmniRoute 的其余功能则会 继续正常运行。


Node 的 net 模块无法在 bind() 之前 调用 setsockopt(IP_TRANSPARENT), 而这是 TPROXY 的要求(否则内核会丢弃重定向的数据包)。该插件 (src/mitm/tproxy/native/transparent.c,通过 binding.gyp 构建)是一个小型 N-API 模块,公开三个函数,并通过 transparentSocket.ts 使用:

插件函数 套接字操作 用途
createTransparentListener(ip, port) socket() + SO_REUSEADDR + IP_TRANSPARENT + bind() + listen(),返回原始 fd 透明捕获监听器(Node 通过 server.listen({ fd }) 接管该 fd)
setSocketMark(fd, mark) 在现有 fd 上调用 setsockopt 设置 SO_MARK 防止循环(为代理自身的套接字设置标记)
connectMarked(ip, port, mark) 在非阻塞 connect() 之前 执行 socket() + SO_MARK,返回 fd 重新加密后的上游转发(SYN 携带该标记)

原始目标地址从 socket.localAddress/localPort 读取 — TPROXY 会保留它,因此无需执行 SO_ORIGINAL_DST/NAT 查找。

终端窗口
npm run build:native:tproxy # 进入 src/mitm/tproxy/native 并执行 node-gyp rebuild
# -> native/build/Release/transparent.node
  • 执行 npm run build 期间,scripts/build/build-tproxy-native.mjs 会运行 node-gyp rebuild。此过程仅限 Linux 且失败不会中止构建 — 缺少工具链只会使 捕获模式不可用。
  • assembleStandalone.mjs 会将 build/Release/transparent.node 复制到 独立运行包中;transparentSocket.ts 会同时通过相对于模块和相对于 cwd 的路径 (<cwd>/src/mitm/tproxy/native/...) 解析该文件。
  • build/ 和 prebuilds/ 已被 git 忽略 — 二进制文件会被构建,但绝不 提交。

加载器按以下优先顺序探测: native/build/Release/transparent.node,然后是 native/prebuilds/transparent.node (同时检查相对于模块的路径和 <cwd>/src/mitm/tproxy/ 下的路径)。


§4 按 SNI 动态生成的 CA 与信任库安装程序

Section titled “§4 按 SNI 动态生成的 CA 与信任库安装程序”

#6684 更新: AgentBridge 静态服务器(src/mitm/server.cjs)现在 也采用相同的 CA/叶证书架构模式,而不再使用单个静态 自签名叶证书。它使用一个独立的 CA 实例 (src/mitm/cert/rootCa.ts,持久化于 <DATA_DIR>/mitm/ca.key/ca.crt) 并安装到已有的 omniroute-mitm.crt 信任库槽位中 (替换其中旧的单一叶证书——无需清理双重信任), 与下文 TPROXY 自己的 omniroute-tproxy-ca.crt 槽位完全隔离。 全新安装的 AgentBridge 会自动使用 CA 模型;已经 信任旧静态叶证书的安装则会继续使用它,直到操作员通过 MITM_ROOT_CA_ENABLED=true 主动选择启用(参见 src/mitm/cert/migration.ts)—— 一个能够为任何主机签发叶证书的可信 MITM CA,其能力实质上 远强于旧的固定 SAN 叶证书,因此对于已建立信任的安装, 绝不会静默切换。

过去,静态 AgentBridge MITM 证书之所以可用,只是因为 AgentBridge 仅对一组固定的主机进行 DNS 欺骗(现在已与下文模型统一)。TPROXY 会拦截任意主机,因此其监听器必须针对客户端请求的任意 SNI 提供有效的叶证书——AgentBridge 现在也有同样的要求:需覆盖完整的 MITM_TOOL_HOSTS 集合(9 个工具条目),而不再只是 4 个 antigravity 主机。

动态 CA(src/mitm/tproxy/dynamicCert.ts)

Section titled “动态 CA(src/mitm/tproxy/dynamicCert.ts)”

DynamicCertStore 运行一个本地 CA(基于 selfsigned 依赖项构建),它:

  • 通过 generateMitmCa() 生成一个长期有效的 CA(CN "OmniRoute MITM CA", 有效期 10 年,basicConstraints CA=true + keyUsage keyCertSign,cRLSign, 2048 位 RSA / SHA-256)。
  • 通过 issueLeafCert() 按需为每个 SNI 主机名签发叶证书(有效期 1 年, subjectAltName = SNI 主机),并为每个主机名缓存一个 tls.SecureContext。
  • 为终止 TLS 的服务器提供 createSNICallback()(参见 §5)。
  • 可通过 existingCa 构造,以确保重启后 CA 保持不变 (因此无需重新安装到信任库)。

CA 私钥绝不会离开本机。

信任库安装程序(src/mitm/tproxy/caTrust.ts)

Section titled “信任库安装程序(src/mitm/tproxy/caTrust.ts)”

被拦截的客户端必须信任动态 CA,因此启动捕获模式时, 会将 CA 证书安装到操作系统信任库中的一个专用槽位—— omniroute-tproxy-ca.crt(常量 TPROXY_CA_CERT_NAME)——该槽位与 静态 MITM 证书的槽位(omniroute-mitm.crt)保持独立,确保两者 绝不会相互覆盖。

installTproxyCa(caPem, sudoPassword?) 会检测发行版的锚点目录 (按顺序检测:优先 Debian 风格),并运行对应的刷新命令:

锚点目录 刷新命令
/usr/local/share/ca-certificates update-ca-certificates
/etc/ca-certificates/trust-source/anchors update-ca-trust
/etc/pki/ca-trust/source/anchors update-ca-trust
/etc/pki/trust/anchors update-ca-certificates

安装时会先将 PEM 暂存到临时文件,然后以特权方式对锚点目录执行 mkdir -p,使用 cp 将暂存文件复制到其中,并运行刷新命令。 uninstallTproxyCa() 只会移除专用槽位(不影响静态 MITM 证书) 并刷新信任库——在非 Linux 系统上不执行任何操作。

所有特权命令都通过 execFileWithPassword(src/mitm/systemCommands.ts) 运行——使用带有参数数组、无 shell、无字符串插值的 spawn (硬性规则 #13)。当进程以 root 身份运行时(例如在 VPS 上), 目标命令会直接运行,无需密码;在非 root 桌面环境中,sudoPassword 会通过 stdin 传递给 sudo -S。

桌面端的 sudoPassword 通过 POST 请求正文提供,用于授权安装 信任库;当进程以 root 身份运行时,会完全忽略该参数。


处理管线(全部位于 src/mitm/tproxy/ 下):

本地应用 ──TCP/443──▶ mangle OUTPUT 标记连接 (fwmark)
ip rule → 本地路由表 → lo
mangle PREROUTING TPROXY → IP_TRANSPARENT 监听器(端口 8443)
│ captureMode.ts:从 socket.localAddress 读取原始目标地址
▼
tlsCapture.ts:
1. 使用按 SNI 生成的叶证书 (dynamicCert) 终止客户端的 TLS
2. 内部 http.Server 解析解密后的明文
3. 捕获 → globalTrafficBuffer.push(),source: "tproxy"
(应用 sanitizeHeaders + maskSecret)
4. 通过带绕过标记的套接字重新加密并转发到原始目标
(connectMarked,防循环)
│
▼
原始上游 (api.example.com)
  • TLS 终止 (createTlsCaptureServer):使用动态 CA 的 SNI 回调,将原始拦截套接字封装到服务端 tls.TLSSocket 中,然后将解密后的流交给内部 http.Server(标准的 MITM 终止技巧)。套接字生命周期受 MITM_IDLE_TIMEOUT_MS 限制,因此挂起的隧道无法耗尽文件描述符。
  • 捕获 (handleDecryptedRequest):推送一个 InterceptedRequest,其中 source: "tproxy",状态初始为 "in-flight";标头先经过 sanitizeHeaders(),正文先经过 maskSecret(),然后才进入缓冲区。随后使用响应、大小和延迟信息更新该条目。
  • 重新加密转发 (createForward / realForward):重新加密并转发到原始目标。rejectUnauthorized 默认为 true(默认安全)——根据客户端请求的 SNI/Host 验证上游证书,因此原始客户端会拒绝的内容,代理也会同样拒绝。

由于规则会标记新的本地出站连接,代理自身重新加密后的转发通常也会被再次拦截,从而形成无限循环。转发路径使用绕过套接字标记 (SO_MARK) 来防止这种情况:

  • realForward 通过 connectMarked(ip, port, DEFAULT_BYPASS_MARK) 打开其上游套接字——DEFAULT_BYPASS_MARK = 0x539——它会在 connect() 之前设置 SO_MARK,因此转发连接的 SYN 会携带绕过标记。
  • mangle OUTPUT 规则排除已经携带绕过标记的连接 (-m mark ! --mark &lt;bypassMark&gt;),因此代理的转发连接不会被重新标记,也不会再次进入 TPROXY。

实现说明:必须在代理的 createConnection 上安装带绕过标记的套接字(存在代理时,https.request({ createConnection }) 会被静默忽略),否则转发过程将打开一个未标记的套接字,循环便会再次出现。这是经过 e2e 验证的防循环修复。


控制措施 详细说明
仅限环回的 API /api/tools/agent-bridge/tproxy 由 LOCAL_ONLY_API_PREFIXES(src/server/authz/routeGuard.ts)中的 /api/tools/agent-bridge/ 前缀覆盖。环回限制在身份验证之前执行(硬性规则 #15 + #17)——通过隧道泄露的 JWT 无法启动 TPROXY 捕获;该捕获会应用 iptables 规则,并通过子进程安装信任存储区 CA。
专用 CA 位置 动态 CA 安装到 omniroute-tproxy-ca.crt,绝不会覆盖静态 MITM 证书。
CA 密钥绝不离开主机 DynamicCertStore 将 CA 密钥保存在内存中;不会将其导出。
敏感信息掩码 在调用 globalTrafficBuffer.push() 之前,对请求/响应正文运行 maskSecret(),并对标头运行 sanitizeHeaders()。
无 shell 插值 所有 iptables/ip/信任存储区命令均通过带有参数数组的 execFile/execFileWithPassword 运行(硬性规则 #13)。
上游证书验证 重新加密的转发默认验证上游证书(rejectUnauthorized: true)。
错误信息净化 该路由的错误响应会经过 sanitizeErrorMessage() 处理(硬性规则 #12)。

MITM CA 是一项强大的能力。 受操作系统信任且能够为任意 主机签名的 CA,意味着 OmniRoute 拦截的任何内容都可以被解密。它被限制在 显式启用且仅限本地的 TPROXY 捕获模式之后,默认关闭,并且在停止该模式时会从信任存储区中 移除相应条目。


崩溃绝不能遗留 mangle 规则或陈旧路由。命令构建器 (src/mitm/tproxy/commands.ts)和执行器(src/mitm/tproxy/setup.ts)保证 回滚操作严格按照相反顺序执行应用操作的精确逆操作。

applyTproxy(cfg) 按顺序执行应用命令;发生任何失败时,它都会尽力执行完整的 revertTproxy(cfg),然后重新抛出异常——因此,防火墙要么完全应用, 要么完全回滚,绝不会处于部分应用状态。revertTproxy(cfg) 按相反顺序执行 逆向命令,并忽略失败(具有幂等性——可安全地无条件调用, 例如在 AgentBridge 的 repairMitm() 清理流程中调用)。

validateTproxyConfig(cfg) 会在任何命令执行前运行:端口必须为 1–65535, mark/routeTable/bypassMark 必须为正整数,并且 bypassMark 必须 不同于 mark(防止循环)。

终端窗口
ip rule add fwmark &lt;mark&gt; lookup &lt;routeTable&gt;
ip route add local 0.0.0.0/0 dev lo table &lt;routeTable&gt;
iptables -t mangle -A OUTPUT -p tcp --dport &lt;dport&gt; -m mark ! --mark &lt;bypassMark&gt; -j MARK --set-mark &lt;mark&gt;
iptables -t mangle -A PREROUTING -p tcp --dport &lt;dport&gt; -m mark --mark &lt;mark&gt; -j TPROXY --on-port &lt;onPort&gt; --tproxy-mark &lt;mark&gt;

回滚操作按相反顺序删除它们:PREROUTING -D、OUTPUT -D、ip route del、ip rule del。

此方案基于 OUTPUT,因为 MITM 的使用场景是_本机_出站 流量(同一主机上的应用),而仅使用 PREROUTING 中的 TPROXY 无法 看到这些流量——PREROUTING 只能看到转发流量。OUTPUT 链会标记新的 本地连接,ip rule 将其重新路由到本地交付(lo),随后 PREROUTING 将其分配给透明监听器。


启动请求(POST /api/tools/agent-bridge/tproxy)接受以下字段, 这些字段由 StartTproxyBodySchema(tproxy/route.ts)验证。所有字段均为可选, 未提供时将回退到其默认值:

字段 类型 默认值 说明
dport int (1–65535) 443 要透明拦截的目标 TCP 端口
mark int (≥1) 0x2333 在 OUTPUT 中设置的防火墙标记,由 ip rule + PREROUTING 匹配
onPort int (1–65535) 8443 透明(IP_TRANSPARENT)监听器绑定的端口
routeTable int (≥1) 233 保存 local 0.0.0.0/0 路由的策略路由表 id
bypassMark int (≥1, ≠ mark) 0x539 代理为其自身上游连接设置的绕过套接字标记(SO_MARK);在 OUTPUT 中排除(防止循环)
sudoPassword string — 仅适用于非 root 桌面环境:授权安装信任存储;以 root 身份运行时忽略

TPROXY 没有环境变量——所有配置均通过 POST 请求体或上述默认值提供。


  1. 打开流量检查器(/dashboard/tools/traffic-inspector)。
  2. 在捕获模式工具栏中,找到 “TPROXY 解密” ⚠ 按钮 (src/app/(dashboard)/dashboard/tools/traffic-inspector/components/CaptureModesToolbar.tsx)。
    • 如果该按钮处于禁用状态,并显示工具提示“TPROXY 解密需要 Linux + root + 原生插件”,则表示此主机上无法使用原生插件(非 Linux、 缺少工具链或插件尚未构建)。请参阅§2和§3。
  3. 点击该按钮。它通过 startTproxyCaptureMode()(src/lib/inspector/tproxyCaptureApi.ts) 调用 POST /api/tools/agent-bridge/tproxy,该调用会: 构建动态 CA、打开透明监听器、应用防火墙规则, 并将 CA 安装到操作系统信任存储区。
  4. 运行时,该切换按钮会变为琥珀色并显示实时拦截计数 (· &lt;interceptCount&gt;)。被拦截的请求会出现在请求列表中, 并带有 source: "tproxy"。
  5. 再次点击可停止——通过 stopTproxyCaptureMode() 调用 DELETE /api/tools/agent-bridge/tproxy,关闭监听器、卸载 CA, 并还原防火墙规则。

捕获模式状态(运行中 / 可用 / 拦截计数 / 监听器端口)来自 GET /api/tools/agent-bridge/tproxy(src/mitm/tproxy/captureManager.ts 中的 getCaptureStatus())。同一时间只能运行一个 TPROXY 会话—— 启动第二个会话时将被拒绝,并返回“TPROXY 捕获模式已在运行”。


无法加载原生插件。请确认:当前系统为 Linux、已构建插件 (npm run build:native:tproxy),且进程可以加载 transparent.node。 isTransparentSocketAvailable() 控制该切换按钮;缺少插件时, GET /api/tools/agent-bridge/tproxy 返回 available: false。

  • 确认被拦截的进程确实连接到配置的 dport (默认为 443)。
  • 确认该进程信任动态 CA。CA 以 omniroute-tproxy-ca.crt 的名称安装;使用自身信任存储区的应用(Firefox/Chrome NSS) 可能还需要将证书添加到其信任存储区中。
  • 运行 AgentBridge 诊断自检(请参阅 AGENTBRIDGE.md),以执行证书信任状态 / 服务器 健康检查。

revertTproxy() 与应用操作完全相反,并且具有幂等性。停止该模式会还原 规则;如果 OmniRoute 在会话期间被终止,请使用 AgentBridge 修复操作(POST /api/tools/agent-bridge/repair)撤销残留的系统 状态(DNS 欺骗、根 CA、系统代理)。TPROXY mangle 规则和路由也会 在重启时自动清除。

无限循环 / 代理拦截自身的转发

Section titled “无限循环 / 代理拦截自身的转发”

这是反循环场景。确认 bypassMark 与 mark 不同(验证逻辑会强制执行 此要求),并确认转发使用 connectMarked(realForward 中确实如此)。 请参阅§5 反循环。


文件 职责
src/mitm/tproxy/commands.ts 纯 iptables/ip 应用及还原命令构建器;validateTproxyConfig
src/mitm/tproxy/setup.ts 事务式 applyTproxy / revertTproxy 运行器(失败时回滚)
src/mitm/tproxy/transparentSocket.ts 原生插件加载器(loadTransparentAddon)、createTransparentListenerFd、connectMarked、setSocketMark、isTransparentSocketAvailable
src/mitm/tproxy/native/transparent.c N-API 插件:createTransparentListener(IP_TRANSPARENT)、setSocketMark、connectMarked
src/mitm/tproxy/native/binding.gyp node-gyp 构建清单
src/mitm/tproxy/dynamicCert.ts DynamicCertStore——按 SNI 生成动态 CA 并缓存叶证书
src/mitm/tproxy/caTrust.ts 操作系统信任存储区安装/卸载(installTproxyCa / uninstallTproxyCa,专用槽位)
src/mitm/tproxy/tlsCapture.ts TLS 终止解密引擎及重新加密的反循环转发
src/mitm/tproxy/captureMode.ts 透明监听器编排;从 socket.localAddress 读取原始目标地址
src/mitm/tproxy/captureManager.ts 单例生命周期:startCaptureMode / stopCaptureMode / getCaptureStatus
src/app/api/tools/agent-bridge/tproxy/route.ts GET / POST / DELETE 路由(LOCAL_ONLY)
src/lib/inspector/tproxyCaptureApi.ts 客户端 fetch 辅助函数(fetchTproxyStatus / startTproxyCaptureMode / stopTproxyCaptureMode)

OmniRoute 源码 (a58000c7685f)

HagiCode

HagiCode 是一套智能体编码工作台:结构化工作流、多 Agent 并行执行与 Hero Dungeon 视图,把想法变成真正交付的软件。

让想法更快变成好用的软件,让智能编码更聪明、更高效,也更有趣。

HagiCode 浅色主题主界面截图
  • Smart结构化工作流将意图转化为从想法到交付的可执行路径。
  • Efficient多 Agent 工作流让调研、实现与审阅并行推进。
  • FunHero Dungeon 让长时间编码协作更直观、更有参与感。
访问 HagiCode