尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Gemini CLI 沙箱网络出口隔离:GEMINI_SANDBOX_PROXY_COMMAND 代理脚本原理与完整示例

Gemini CLI 沙箱网络出口隔离:GEMINI_SANDBOX_PROXY_COMMAND 代理脚本原理与完整示例 Gemini CLI 沙箱网络出口隔离GEMINI_SANDBOX_PROXY_COMMAND 代理脚本原理与完整示例【免费下载链接】gemini-cliAn open-source AI agent that brings the power of Gemini directly into your terminal.项目地址: https://gitcode.com/GitHub_Trending/gemi/gemini-cli本文以仓库中的示例代理脚本 docs/examples/proxy-script.md 为主体讲透 Gemini CLI 沙箱的代理化网络出口机制如何用GEMINI_SANDBOX_PROXY_COMMAND指定一个只放行特定域名 HTTPS 流量的自研代理以及该代理在 macOS Seatbelt 与 Docker/Podman 两条沙箱路径下是如何被自动拉起、注入环境变量、与沙箱生命周期联动的。读完后你可以复制并改造示例脚本为 CLI 的沙箱命令实现域名级的出站网络白名单。1. 为什么需要代理脚本沙箱的出口网络控制Gemini CLI 的沙箱sandboxing用于把 shell 命令、文件写入等高风险操作与宿主系统隔离开完整的沙箱方案Seatbelt、Docker/Podman、gVisor、LXC见 docs/cli/sandbox.md。其中有一类 profile 是专门的代理化变体通过SEATBELT_PROFILE环境变量选择permissive-proxied写操作受限网络流量经由代理restrictive-proxied严格限制网络流量经由代理strict-proxied读写都受限网络流量经由代理从 Seatbelt 策略文件源码可以看到*-proxied变体之所以经由代理是因为策略里只放开了对localhost:8877的出站 TCP 连接其余网络流量一律默认拒绝。以 sandbox-macos-strict-proxied.sb 为例;; allow outbound network traffic through proxy on localhost:8877 ;; set GEMINI_SANDBOX_PROXY_COMMANDcommand to run proxy alongside sandbox ;; proxy must listen on :::8877 (see docs/examples/proxy-script.md) (allow network-outbound (remote tcp localhost:8877))这就引出了示例代理脚本存在的意义GEMINI_SANDBOX_PROXY_COMMAND环境变量的值是一个命令CLI 会在启动沙箱的同时用它拉起一个监听:::8877的代理服务器:::表示所有网络接口含 IPv4 映射地址。沙箱内所有出站流量被迫先打到这个代理而代理内部再做域名/端口级的二次过滤——这是仓库文档 CONTRIBUTING.md 中Proxied networking一节描述的能力且所有沙箱方法包括 Seatbelt 的*-proxiedprofile都支持。2. 示例代理脚本完整解析下面完整继承 docs/examples/proxy-script.md 给出的示例脚本。该脚本用 Node.js 实现一个 CONNECT 隧道代理只允许HTTPS连接到example.com:443以及googleapis.com拒绝一切其他请求。#!/usr/bin/env node /** * license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ // Example proxy server that listens on :::8877 and only allows HTTPS connections to example.com. // Set GEMINI_SANDBOX_PROXY_COMMANDscripts/example-proxy.js to run proxy alongside sandbox // Test via curl https://example.com inside sandbox (in shell mode or via shell tool) import http from node:http; import net from node:net; import { URL } from node:url; import console from node:console; const PROXY_PORT 8877; const ALLOWED_DOMAINS [example.com, googleapis.com]; const ALLOWED_PORT 443; const server http.createServer((req, res) { // Deny all requests other than CONNECT for HTTPS console.log( [PROXY] Denying non-CONNECT request for: ${req.method} ${req.url}, ); res.writeHead(405, { Content-Type: text/plain }); res.end(Method Not Allowed); }); server.on(connect, (req, clientSocket, head) { // req.url will be in the format hostname:port for a CONNECT request. const { port, hostname } new URL(http://${req.url}); console.log([PROXY] Intercepted CONNECT request for: ${hostname}:${port}); if ( ALLOWED_DOMAINS.some( (domain) hostname domain || hostname.endsWith(.${domain}), ) port ALLOWED_PORT ) { console.log([PROXY] Allowing connection to ${hostname}:${port}); // Establish a TCP connection to the original destination. const serverSocket net.connect(port, hostname, () { clientSocket.write(HTTP/1.1 200 Connection Established\r\n\r\n); // Create a tunnel by piping data between the client and the destination server. serverSocket.write(head); serverSocket.pipe(clientSocket); clientSocket.pipe(serverSocket); }); serverSocket.on(error, (err) { console.error([PROXY] Error connecting to destination: ${err.message}); clientSocket.end(HTTP/1.1 502 Bad Gateway\r\n\r\n); }); } else { console.log([PROXY] Denying connection to ${hostname}:${port}); clientSocket.end(HTTP/1.1 403 Forbidden\r\n\r\n); } clientSocket.on(error, (err) { // This can happen if the client hangs up. console.error([PROXY] Client socket error: ${err.message}); }); }); server.listen(PROXY_PORT, () { const address server.address(); console.log([PROXY] Proxy listening on ${address.address}:${address.port}); console.log( [PROXY] Allowing HTTPS connections to domains: ${ALLOWED_DOMAINS.join(, )}, ); });2.1 关键设计点逐段说明白名单常量第 25–27 行const PROXY_PORT 8877; const ALLOWED_DOMAINS [example.com, googleapis.com]; const ALLOWED_PORT 443;PROXY_PORT 8877不是随意选的沙箱策略只放行localhost:8877CLI 内部默认的代理地址也是http://localhost:8877见 sandbox.ts 的默认值端口必须与这两处对齐。ALLOWED_DOMAINS是出站白名单注意第 46 行的匹配逻辑hostname domain || hostname.endsWith(.${domain})——它同时允许example.com本体与其任意子域如api.example.com但不会误放notexample.com因为endsWith(.example.com)带点号前缀。ALLOWED_PORT 443与端口做字符串严格比较只放行 TLS 默认端口。普通 HTTP 请求一律 405第 29–36 行const server http.createServer((req, res) { // Deny all requests other than CONNECT for HTTPS res.writeHead(405, { Content-Type: text/plain }); res.end(Method Not Allowed); });这个处理器捕获所有非 CONNECT的 HTTP 请求例如沙箱内curl http://...的明文访问统一以405 Method Not Allowed拒绝。它同时还有一个副产品作用CLI 用它做就绪探测——CLI 启动后会反复向http://localhost:8877发 curl 探测只要端口能应答哪怕返回 405就认为代理已就绪见 sandbox.ts 的等待循环。CONNECT 隧道放行逻辑第 38–74 行HTTPS 客户端访问代理时会发送CONNECT host:443请求server.on(connect, ...)回调中的req.url即hostname:port形式。脚本的处理链路是用new URL(http://${req.url})安全地拆出hostname与port域名命中白名单且端口为 443 → 调用net.connect(port, hostname, ...)向真实目标建立 TCP 连接成功后向客户端写HTTP/1.1 200 Connection Established并pipe双向转发完成隧道。注意serverSocket.write(head)head是客户端已随 CONNECT 一起发出的首段数据不能丢上游连接失败 → 回502 Bad Gateway域名/端口不匹配 → 直接clientSocket.end(HTTP/1.1 403 Forbidden)掐断客户端主动挂断等 socket 错误只打日志、不做处理避免异常打断其他连接。由于代理只做域名匹配后盲目建 TCP 隧道它属于连接建立前的准入控制一旦隧道建立代理看不到 TLS 加密后的具体内容。这是设计权衡——保持实现极简同时把出口收敛到白名单域名的 443 端口。监听方式第 76 行server.listen(PROXY_PORT)未指定 hostNode.js 会监听所有接口等价于:::8877正好满足 CONTRIBUTING.md 对代理命令必须监听:::8877的要求——因为在容器路径下代理容器与沙箱容器通过独立的 Docker 网络互通代理必须对容器网络可达而不是只对宿主回环可达。3. CLI 如何驱动代理两条沙箱路径的源码证据GEMINI_SANDBOX_PROXY_COMMAND的完整生命周期都在 packages/cli/src/utils/sandbox.ts 中实现。从源码结构看macOS Seatbelt 路径和容器路径的行为差异很大值得分别说明。3.1 macOS Seatbelt 路径代理作为宿主进程运行Seatbelt 只限制子进程沙箱内的 Gemini CLI 进程代理本身运行在宿主侧。源码 sandbox.ts#L229-L286 的流程读取命令const proxyCommand process.env[GEMINI_SANDBOX_PROXY_COMMAND]注入代理环境变量沙箱子进程的环境会被写入HTTPS_PROXY/https_proxy/HTTP_PROXY/http_proxy四个大小写变体小写变体是curl等工具要求的取值优先级为已有的HTTPS_PROXY→https_proxy→HTTP_PROXY→http_proxy都没有时默认http://localhost:8877若宿主设置了NO_PROXY/no_proxy也会一并透传拉起代理spawn(proxyCommand, { shell: true, detached: true })detached: true使代理处于独立进程组退出时对整个进程组发SIGTERM干净地停掉等待就绪until timeout 0.25 curl -s http://localhost:8877; do sleep 0.25; done每 0.25 秒探测一次端口故障联动代理进程意外退出时CLI 会向沙箱进程发SIGTERM并抛出FatalSandboxErrorProxy command ... exited with code ...即代理挂掉则沙箱整体终止避免沙箱在无出口约束下继续运行日志代理的 stderr 以[PROXY STDERR]前缀写入 debugLoggerstdout 的转发被有意注释掉源码注释说明原因是干扰 ink 的渲染。3.2 容器路径Docker/Podman代理跑在独立容器里容器场景下沙箱容器处于一个--internal的 Docker 网络中无宿主机出口代理则运行在自己的容器里同时连接两个网络一个可访问宿主的外部网络和一个--internal的沙箱网络。源码链路网络常量定义在 sandboxUtils.ts#L14-L15export const SANDBOX_NETWORK_NAME gemini-cli-sandbox; export const SANDBOX_PROXY_NAME gemini-cli-sandbox-proxy;环境变量重写sandbox.ts#L520-L544把代理地址中的localhost替换为SANDBOX_PROXY_NAME即gemini-cli-sandbox-proxy再以--env HTTPS_PROXY...等形式注入沙箱容器——因为从沙箱容器视角看代理的 DNS 名字就是那个容器名双网络编排sandbox.ts#L546-L565只要设置了GEMINI_SANDBOX_PROXY_COMMAND沙箱网络就以--internal创建同时额外创建gemini-cli-sandbox-proxy网络供代理容器使用。源码注释说明这样做的动机让代理在 macOS 上 rootless podmanhost ↔ VM ↔ container 的隔离拓扑下也能工作代理容器启动sandbox.ts#L792-L864以--name gemini-cli-sandbox-proxy、-p 8877:8877、挂载当前工作目录运行复用沙箱同一镜像保证代理脚本可执行、无需额外依赖用parse(proxyCommand, process.env)把命令字符串安全地分词成参数数组后spawn源码注释明确这是防止命令注入与 Seatbelt 路径的shell: true不同容器路径是shell: false就绪探测同样是轮询http://localhost:8877最后network connect gemini-cli-sandbox gemini-cli-sandbox-proxy把代理容器挂入沙箱内部网络兼容不支持多个--network参数的旧版 Docker退出清理通过docker rm -f gemini-cli-sandbox-proxy完成。3.3 使用方式小结结合文档 docs/examples/proxy-script.md 的头部注释与 docs/cli/sandbox.md 的 profile 说明典型用法是# 1. 把示例脚本保存为可执行命令如 scripts/example-proxy.js # 2. 选择代理化 profileSeatbelt 示例 export SEATBELT_PROFILEstrict-proxied # 3. 指定代理命令并启动带沙箱的 CLI export GEMINI_SANDBOX_PROXY_COMMANDnode scripts/example-proxy.js gemini -s -p curl https://example.com # 沙箱内测试应放行要点与限制代理必须监听:::8877所有接口、8877 端口CLI 的就绪探测和默认代理地址都锚定在这里代理随沙箱自动启动/停止无需手动管理进程想让沙箱内客户端curl、npm 等真正走代理前提是它们遵循HTTPS_PROXY/HTTP_PROXY环境变量——代理只约束愿意走代理的流量Seatbelt*-proxiedprofile 通过策略层强制把出站收敛到localhost:8877这是最强的组合若宿主已设置HTTPS_PROXY等变量CLI 会沿用其值Seatbelt 路径或在替换localhost后注入容器路径默认值http://localhost:8877仅在完全未设置时生效。4. 改造示例脚本的实战方向以仓库示例为骨架最常见的定制是把ALLOWED_DOMAINS换成真实业务域名例如需要让沙箱内npm install工作的场景可以放行 npm 官方源与镜像的域名具体域名以实际使用为准并保持ALLOWED_PORT 443不变。若需要放行非 HTTPS 端口则需同时扩展端口白名单逻辑。调试时建议先在宿主直接运行代理命令如node example-proxy.js观察[PROXY] Intercepted CONNECT request for: ...日志确认放行/拒绝分支配合DEBUG1启动 CLI查看 debugLogger 中的[PROXY STDERR]输出与waiting for proxy to start ...进度调试模式参考 docs/cli/sandbox.md 的 Debug mode 一节在沙箱内用curl https://example.com白名单内与curl https://other.com白名单外对照验证 200 与 403 行为。5. 相关路径索引内容路径本文主体示例代理脚本docs/examples/proxy-script.md沙箱总览与 profile 列表docs/cli/sandbox.md代理生命周期与双路径实现packages/cli/src/utils/sandbox.ts沙箱/代理网络与容器名常量packages/cli/src/utils/sandboxUtils.tsSeatbelt 严格代理 profile端口 8877 放行点packages/cli/src/utils/sandbox-macos-strict-proxied.sbProxied networking 官方说明CONTRIBUTING.md代理环境变量相关测试packages/cli/src/utils/sandbox.test.ts综上GEMINI_SANDBOX_PROXY_COMMAND是 Gemini CLI 沙箱体系中网络出口可审计化的抓手Seatbelt 策略或容器内部网络把流量强制导向:::8877示例代理脚本再在 CONNECT 层做域名白名单过滤两者配合使沙箱内命令的出站访问从全放收敛到仅白名单域名的 443 端口且代理的启动、就绪探测、故障联动与退出清理全部由 CLI 自动完成。【免费下载链接】gemini-cliAn open-source AI agent that brings the power of Gemini directly into your terminal.项目地址: https://gitcode.com/GitHub_Trending/gemi/gemini-cli创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表