- install.sh: install from a checkout or via curl|bash, tag-pinned download, --skill-only / --uninstall / --dry-run / --yes, ends with verify --deep - set-exit: follow a rotating exit, probe first, rewrite policy only when the proxy serves the candidate address, then re-verify - LOG_REFUSALS=1: refusals go to journald under claude-egress-guard - AGENTS.md, llms.txt, Makefile: machine entry points and make check - README/README.zh-CN rewritten for two audiences (agents, humans) with a real verify sample, compatibility table, per-start cost, rotation, logging and an explicit out-of-scope list (npx, second installs, containers, root, SOCKS) - default allow list is 127.0.0.0/8 so systemd-resolved keeps working - 11 new self-tests, 42 total
13 KiB
claude-egress-guard(中文说明)
给 agent 用的部署 skill,也是给人用的一键安装脚本:把 Linux 上的 Claude Code 钉死在一个出口上,出口不对就拒绝启动。
在 VPS 上跑 Claude Code,最容易出问题的不是安装,而是出口泄漏:写在
/etc/profile.d 里的 claude() 函数只覆盖交互式 shell,Python、sh -c、
cron、systemd 和任何直接调用 /usr/bin/claude 的路径都会绕过去;代理中途挂掉时,
客户端还会悄悄从机房 IP 直连出去。
这个项目在二进制层补上这个洞:claude 命令本身变成闸门,启动前核验代理的真实
出口地址,注入代理变量,并可选地把整个进程树关进内核级 IP 白名单。任何异常都拒绝
启动,没有「继续执行」的分支。
给 agent:从这里开始
这个仓库是给 agent(Hermes、Codex、Claude Code 或任何支持 skill 的运行器)消费的, 不只是给人读的 README。
# 把 skill 装进 agent 的 skills 目录(不需要 root,不改主机)
curl -fsSL https://raw.githubusercontent.com/yinyues2/claude-egress-guard/main/install.sh \
| bash -s -- --skill-only --dest ~/.hermes/skills
然后对 agent 说:
用
claude-egress-guard这个 skill:先问我代理入口、预期出口地址、国家、额外网段, 然后安装、跑verify --deep、做一次故障演练,最后把 PASS/FAIL 行贴给我。
skill 里写清的约束(SKILL.md):
- 只问 4 个事实——代理入口、预期出口地址、国家、额外白名单;其余从主机自己读,不重复追问;
- 三档授权——只读诊断直接做;安装/验证/演练/卸载整计划批一次;可能切断操作者自身访问的操作当场确认;
- 证据而非形容词——每条结论都要贴对应的 PASS 行,演练结果要记录,没输出不算通过;
- 硬性禁止——不停 ssh/tailscale/docker 单元,不把
ALLOW_IPS放大到0.0.0.0/0,不为通过检查而关闭某一层,不碰账号/付款/身份核验。
配套文件:AGENTS.md(机器入口)、
references/agent-playbook.md
(带判断点的分步手册)、llms.txt(给 LLM 工具用的链接索引)、
references/troubleshooting.md。
一键安装
curl -fsSL https://raw.githubusercontent.com/yinyues2/claude-egress-guard/main/install.sh \
| sudo bash -s -- --proxy http://127.0.0.1:1080 --exit-ip 203.0.113.10 --country JP
这条命令会下载固定版本的发布包、打印安装计划、安装闸门、备份被替换的文件,最后自动跑
verify --deep。任何一层失败就停下并指出是哪一层,不会把装了一半的闸门当成功。
| 参数 | 含义 |
|---|---|
--proxy URL |
本地 HTTP/mixed 代理入口(必填) |
--exit-ip ADDR |
代理出口的公网 IPv4(必填) |
--country CC |
要强制的两位国家码(可选) |
--allow LIST |
内核白名单里额外放行的网段,逗号分隔 |
--strict-net on|off |
是否给进程树加内核白名单(默认 on) |
--dry-run |
只打印计划,不写任何东西 |
--yes |
跳过确认提示(自动化用) |
--ref REF |
指定 tag 或分支(默认固定 tag) |
--uninstall |
移除闸门并从备份恢复 |
--skill-only |
只把 skill 复制到 skills 目录 |
203.0.113.10 是文档专用地址(RFC 5737),请换成你代理实际报告的地址。回环和主机
IPv4 DNS 解析器会自动放行;代理上游、Tailscale 网段、内网 API 用 --allow 显式加。
给 root 之前先读一遍。 install.sh 是不混淆的纯 shell 脚本,下载的是固定 tag 的
压缩包,不是移动的分支。也可以先 clone 再 sudo ./install.sh ...。
卸载走同一条通道:
curl -fsSL https://raw.githubusercontent.com/yinyues2/claude-egress-guard/main/install.sh \
| sudo bash -s -- --uninstall
装好之后是什么样
$ sudo claude-egress-guard verify --deep
PASS config present: /etc/claude-egress-guard.conf
PASS real binary executable
PASS gate installed: /usr/local/bin/claude
PASS gate mirrored: /usr/bin/claude
PASS egress check: ok exit=203.0.113.10 country=JP
PASS kernel jail enabled, allow list: 127.0.0.0/8 ::1/128 192.0.2.53/32
PASS systemd-run available
PASS IPv6 disabled system-wide
PASS jail blocks direct egress
PASS jail still allows the proxy path
---- 0 check(s) failed
每一行都是实测观察,不是「配置里写了」:最后两行是真的在白名单里发请求验证的。
claude(任意入口:shell、Python、cron、systemd、绝对路径)
└─ /usr/local/bin/claude 闸门:核验出口 -> 注入代理 -> 内核白名单
├─ /usr/bin/claude 同一份闸门(npm 更新会改回符号链接,闸门自愈)
└─ /opt/claude-real/claude 真身(符号链接,跟随包更新)
└─ 你的代理 127.0.0.1:1080 -> 预期出口地址
每次启动的顺序:
- 读
/etc/claude-egress-guard.conf,缺失或不可读 → 拒绝(77); - 如果包更新把
/usr/bin/claude改回符号链接,先补回闸门; - 问代理要真实出口地址和国家,不符/超时/HTTP 错误 → 拒绝(77),不重试、不回落直连;
- 把
ALL_PROXY/http_proxy/https_proxy(大小写)导出给客户端和所有子进程,NO_PROXY只留回环; STRICT_NET=1时,在临时 systemd scope 里以IPAddressDeny=any+ 白名单 exec, 子进程继承的是内核级限制,不是口头承诺。
诊断路径不碰网络:--version、--help、--ceg-check 在探测之前就短路返回。
保证什么
| 层 | 行为 |
|---|---|
| 出口核验 | 每次启动前核验地址和国家;不符、超时、报错一律退出码 77 |
| 代理环境 | 六个代理变量导出给客户端和全部子进程 |
| 内核白名单 | 整个进程树 IPAddressDeny=any,只放行回环、白名单和显式允许的网段 |
| 入口覆盖 | /usr/local/bin/claude 和 /usr/bin/claude 都是闸门,真身放在 /opt/claude-real |
| 自愈 | 包自更新改写的 /usr/bin/claude 会在下一次调用时补回 |
| fail-closed | 配置缺失、检查器缺失、代理不通、出口不符、国家不符、没有 systemd-run、非 root 且 STRICT_NET=1:全部拒绝 |
| 审计线索 | LOG_REFUSALS=1 时每次拒绝写入 journald,tag claude-egress-guard |
| 可回滚 | 被替换的文件都在 /var/lib/claude-egress-guard/backup/<时间戳>/,uninstall 恢复 |
环境要求与兼容性
| 项目 | 要求 |
|---|---|
| 系统 | Linux + systemd 235+(IPAddressDeny= 需要) |
| Shell | bash 4.3+ |
| 工具 | curl、install、readlink、mktemp、awk、sed |
| 权限 | 正式安装要 root;--prefix 可做无特权沙箱 |
| Claude Code | 已安装,npm 全局或官方安装器均可 |
| 代理 | 本地 HTTP 或 mixed 入口(只有 SOCKS 的话,客户端代理变量用不了) |
已实测:Debian 13 / systemd 257 / Claude Code 2.1.296(npm 全局),代理在回环上的 住宅 HTTP 入口。CI 在 Ubuntu 24.04 上跑全套。
未验证、不支持:macOS/BSD、WSL、systemd 235 以下、非 root 用户 scope 下开
STRICT_NET=1、容器内的 Claude。
每次启动成本:一次经代理的 HTTPS 往返(视距离约 100-400 ms);--version/--help 不探测。
命令
| 命令 | 用途 |
|---|---|
install ... |
打印计划、备份、安装(支持 --dry-run) |
verify |
静态检查 + 实时出口核验 |
verify --deep |
额外证明「白名单挡住直连、仍放行代理」 |
status |
查看当前策略(代理凭据打码) |
set-exit --ip ADDR [--country CC] |
跟随变化的出口:先探测,代理确实在该地址才改策略,再自动 verify |
drill --service UNIT |
停代理单元,证明闸门拒绝,再拉起并等待恢复 |
show <guard|check|config> |
打印已安装的闸门/检查器/策略,便于审计 |
uninstall |
从最新备份恢复并移除闸门 |
退出码:0 成功,1 校验失败,2 用法错误,77 闸门拒绝启动。
出口变了怎么办
住宅出口会漂。别手改配置,让闸门跟着走:
sudo claude-egress-guard set-exit --ip 198.51.100.23 --country JP
它先用候选地址探测代理,代理确实在该地址才原子改写 EXPECTED_IP/EXPECTED_COUNTRY
(权限保持 0600),然后自动跑 verify;不爆料就拒绝,不会把没验证过的地址写进策略。
不知道新地址时先看一眼:
curl -4 -fsS --max-time 8 -x http://127.0.0.1:1080 https://www.cloudflare.com/cdn-cgi/trace
取输出里的 ip= 和 loc=。
拒绝日志
拒绝默认只出现在终端,没人盯着就看不见。开一次 journald 记录:
sudo sed -i 's/^LOG_REFUSALS=0/LOG_REFUSALS=1/' /etc/claude-egress-guard.conf
之后:
journalctl -t claude-egress-guard --since -24h
每条是 refused: <原因>。这决定了你是「我觉得没拒绝过」还是「它拒绝过 11 次,都在这」。
日常维护
| 事件 | 动作 |
|---|---|
npm i -g 或 Claude Code 自更新 |
sudo claude-egress-guard verify --deep(备用路径会自动补回) |
| 代理出口变了 | sudo claude-egress-guard set-exit --ip 新地址 --country CC |
| 代理改走远程中转 | 用 --allow 加中转地址,再 verify --deep |
| 不要内核白名单 | 设 STRICT_NET=0 再 verify(会出现 WARN),进程树出口不再强制 |
| 怀疑有绕过 | status,然后看 /proc/<pid>/cgroup 里有没有 run-r...scope |
不做什么
- 不创建、不管理代理。 入口要你自己提供并保持存活。
- 只闸一个二进制。
npx @anthropic-ai/claude-code不经过/usr/local/bin/claude, 因此不受保护;要么别这么用,要么自己再包一层。另一份装在别的前缀里的 Claude 同样不受保护。 - 不是 VPN。 主机上其他服务、包管理器、其他用户不受影响,白名单只管 Claude 进程树。
- 挡不住 root。 有 root 就能设
STRICT_NET=0、改配置或直接跑/opt/claude-real/claude。 这是护栏,不是强制访问控制边界。 - 不覆盖容器。 Docker/Kubernetes 的流量不经过主机的出口路径。
- 不保证账号安全。 注册、付款、身份核验和平台判定都由账号本人承担;这个项目不用于规避服务商条款。
完整威胁模型见 SECURITY.md。
从源码手动安装
git clone https://github.com/yinyues2/claude-egress-guard.git
cd claude-egress-guard
# 一条命令,参数和 curl 版一致
sudo ./install.sh --proxy http://127.0.0.1:1080 --exit-ip 203.0.113.10 --country JP
# 或者直接驱动 CLI
sudo skills/claude-egress-guard/scripts/claude-egress-guard install \
--proxy http://127.0.0.1:1080 --exit-ip 203.0.113.10 --country JP --dry-run
开发与测试
make check # shellcheck + 42 项自测 + 泄漏扫描
make test # 只跑沙箱自测(不需要 root,不联网)
make lint
make scan
自测在临时前缀里用假的 curl/id/logger/systemd-run 跑,不碰主机。
仓库结构
install.sh 一键安装(下载固定 tag)
AGENTS.md agent 运行器的机器入口
llms.txt 给 LLM 工具的链接索引
skills/claude-egress-guard/
SKILL.md 部署 skill 本体
references/ playbook 与故障排查
scripts/claude-egress-guard 安装/验证/演练/卸载 CLI
scripts/guard-claude.sh 闸门本体(安装为 `claude`)
scripts/egress-check.sh 出口检查器(可单独运行)
scripts/selftest.sh 42 项沙箱断言
templates/ 策略文件示例
tools/leak-scan.py 拦截凭据、个人标识和非文档网段
许可、安全、隐私
Apache-2.0,见 LICENSE。安全模型与漏洞报告见 SECURITY.md, 数据收集见 PRIVACY.md(简短版:不收集任何东西,只探测你自己的代理)。