背景
我想基于 Managed Agents 做一个 MR review 的 agent,整体流程大致如下:
git push触发 GitLab CI Runner 运行一个 job;- job 向内网的 code review service 发送一个 POST 请求;
- code review service 收到请求后,调用 Managed Agents API 拉起一个公网沙箱,在沙箱中运行 code review agent;
- code review agent 拉取代码,进行 review。

问题出在第 4 步:沙箱在公网环境,无法访问企业内网的 GitLab,代码拉不下来。
我又一次撞上了网络这堵空气墙。
Managed Agents API 提供的能力我都评估过,但没有一个能很好地解决这个问题:
| 方案 | 说明 | 为什么不行 |
|---|---|---|
| Files API | 把文件上传到对象存储,对象存储会直接挂载到沙箱上 | 每次都要把整个 repo 上传一遍,不优雅 |
| Custom Tools | 订阅 event stream 监听 custom tool call,调用内部服务后把结果回传给 agent | 没办法把整个 repo 塞进上下文 |
| MCP Tunnel | 把内网 MCP Server 通过 Cloudflare Tunnel 连到 Anthropic 的 VPC,避免暴露到公网 | GitLab 的 MCP 服务器没有一个方法能方便地 clone 整个 repo |
MCP Tunnel 给了我一个思路:利用反向隧道,把内网的特定服务定向暴露给沙箱。
要打通这条路,需要先理解两个问题:为什么公网访问不了内网(NAT),以及怎么打洞(内网穿透 / 隧道)。
为什么公网访问不了内网:NAT / NAPT
由于 IPv4 地址资源紧缺,内网机器通常没有公网 IP,只有私有 IP。它们访问公网服务时,路由器会把数据包的源 IP 和源端口改写成自己的公网 IP + 一个临时分配的端口,并在 NAT 转发表中记录这条映射(内网 IP:端口 ↔ 公网 IP:端口)。这种"地址 + 端口"一起转换的做法,严格来说叫 NAPT(Network Address Port Translation),也就是日常生活中常说的 NAT。
之后公网服务器返回响应时,路由器查表找到对应的内网机器,再把包转发回去。整个过程对内网机器是透明的,它感觉不到自己被"改过地址"。
但这也意味着:公网无法主动连接内网。NAT 转发表里只有内网主动发起的连接记录;公网主动发来的数据包查不到映射条目,路由器不知道该转发给谁,只能丢弃。
这就是部署在内网的服务没有公网入口的根本原因——它缺的不是服务本身,而是一条"从外往里"的路。

内网穿透:打一个"公网可见"的洞
内网穿透(也叫反向隧道)的思路是:让内网服务主动去够公网,而不是等公网来访问。
内网中的服务主动向公网的一个 relay server 发起连接,并保持这条 TCP 长连接;
relay server 把它映射到自己的一个端口上。
之后任何 client 访问 relay server 的这个端口时,relay server 都会把流量顺着这条长连接转发给内网服务。

这个方案的代价也很明显:内网服务被完整暴露到了公网——任何知道 relay server 地址和端口的人都能访问,存在被扫描和攻击的风险。
而我想要的效果是:内网 GitLab 只允许我的沙箱访问,对公网其他任何人不可见。
普通内网穿透做不到这一点,需要更"定向"的隧道。
定向打洞:frp 的 stcp 模式
我用的工具是 frp,一个开源的内网穿透工具。frp 由 frps(服务端)和 frpc(客户端)组成:
- frps 运行在有公网 IP 的 relay server 上,负责接收注册和转发流量;
- frpc 有两种角色:
- 服务侧(proxy):运行在内网,把内网服务注册到 frps;
- 访问侧(visitor):运行在需要访问的一方(这里是沙箱),把远端服务折射到本地端口。
普通 frp 代理(type = "tcp")会在 frps 上开放一个公网端口,效果等同于前面说的内网穿透;而 stcp(Secret TCP)模式下,frps 只为双方牵线,不在公网开放任何服务端口——只有持有配对密钥的 visitor 才能通过 frps 找到服务侧的代理。这正好满足"只给沙箱开一条缝"的需求。
最终拓扑

- 内网(我自己的 Mac)上用 Docker 跑了一个 frpc(服务侧),注册服务
corp-gitlab,指向内网 GitLab; - GCP 上起了一台有公网 IP 的 VM 跑 frps(203.0.113.10:7000),只开控制通道;
- 另一台 GCP VM 模拟沙箱,跑 frpc 的 visitor 角色,把
corp-gitlab折射到本地 127.0.0.1:80,再用 hosts 把gitlab.internal.example.com指向 127.0.0.1——沙箱里git clone http://gitlab.internal.example.com/...就能走了。
三份配置
Mac 上(frpc,服务侧):
serverAddr = "203.0.113.10" # frps 的公网地址(端口单独配置)
serverPort = 7000 # 控制通道端口
auth.token = "<your-frp-auth-token>" # 连接 frps 的认证 token
[[proxies]]
name = "corp-gitlab" # 服务名,visitor 靠它找到本代理
type = "stcp" # ★ 关键:stcp 模式,frps 不开放公网服务端口
secretKey = "<your-stcp-secret-key>" # 配对密钥,与 visitor 必须一字不差
localIP = "gitlab.internal.example.com" # ★ 有人来访问时,我去连谁
localPort = 80
GCP VM 上(frps):
bindPort = 7000 # 只开控制通道(stcp 模式下连服务端口都没有)
auth.token = "<your-frp-auth-token>"
模拟沙箱的 GCP VM 上(frpc,访问侧 / visitor):
serverAddr = "203.0.113.10"
serverPort = 7000
auth.token = "<your-frp-auth-token>"
[[visitors]]
name = "gitlab-visitor"
type = "stcp"
serverName = "corp-gitlab" # 我要消费服务侧注册的哪个服务
secretKey = "<your-stcp-secret-key>" # 配对凭证(必须和服务侧一字不差)
bindAddr = "127.0.0.1" # 监听口只开在本地,不对外
bindPort = 80
另外在沙箱 VM 上配置 hosts 映射:
127.0.0.1 gitlab.internal.example.com
流量是怎么走的
沙箱里执行 git clone http://gitlab.internal.example.com/... 时:
- hosts 把
gitlab.internal.example.com解析到 127.0.0.1; - 连接打到 visitor 监听的 127.0.0.1:80;
- visitor 通过 frps 找到注册了
corp-gitlab且密钥匹配的服务侧 frpc; - frps 在两者之间中转流量,Mac 上的 frpc 再去连接内网 GitLab(gitlab.internal.example.com:80);
- 数据原路返回。对沙箱来说,GitLab 就像"长在本地"一样。
为什么这样就不算"暴露到公网"
- frps 的 7000 端口是控制通道,只接受持有 token 的 frpc 注册,且不对外开放任何服务端口;
- 服务侧注册的
corp-gitlab不会被分配公网端口,公网扫描扫不到; - 即使有人能连上 frps,没有与
corp-gitlab匹配的secretKey,也拿不到服务; - 沙箱侧 visitor 只监听 127.0.0.1,不会额外暴露任何东西。
实践中的注意点
- GCP 防火墙:VPC 默认拒绝入站流量,需要为 frps 所在的 VM 放行 7000 端口的入站 TCP 规则,否则 frpc 连不上 frps。
- 流量加密:frp 默认不加密隧道流量,内网 GitLab 的请求会明文经过公网上的 frps。建议开启 frp 的 TLS(
transport.tls.enable = true),GitLab 侧尽量走 HTTPS。 - 沙箱侧初始化:沙箱是临时的,每次拉起都要安装 frpc、写入 visitor 配置、改 hosts。建议把这几步做成初始化脚本,由 code review service 在创建沙箱时注入;visitor 配置里的 token / secretKey 不要写死在公共镜像里。
- 端口权限:Linux 上监听 1024 以下端口需要 root。若 frpc 不是以 root 运行,要么把 visitor 的
bindPort换成 8080 之类的高位端口,要么调整非特权端口范围(sysctl net.ipv4.ip_unprivileged_port_start=0)。 - 密钥管理:配置里有两层凭据(连接 frps 的
auth.token、stcp 配对的secretKey),均为明文,注意保管与定期轮换。
效果
在模拟沙箱的 VM 上执行 git clone http://gitlab.internal.example.com/example-repo.git,可以正常拉取代码:

整个链路(沙箱 → frps → Mac → 内网 GitLab)一次打通,沙箱里感知不到隧道的存在,体验上就像 GitLab 部署在沙箱本地一样。
总结
| 需求 | 方案 |
|---|---|
| 沙箱能访问内网 GitLab | frp stcp 隧道:内网 frpc(proxy)+ 公网 frps + 沙箱 frpc(visitor) |
| GitLab 不暴露给公网 | stcp 模式不在 frps 上开放服务端口,访问需要配对密钥 |
回顾最初对三种 Managed Agents 能力的评估:Files API 太重、Custom Tools 塞不下 repo、MCP Tunnel 只适用于 MCP 协议——而 frp stcp 在协议层面无侵入,沙箱里就是一次普通的 git clone,因此是最贴合"让沙箱拉代码"这个场景的方案。
后续可以考虑的方向:把沙箱侧初始化封装成镜像或启动脚本;开启 frp TLS 加密;使用短期密钥;以及评估 frp 的 xtcp 模式(P2P 直连,流量不经过 frps,但受双方 NAT 类型限制)。