sing-box 在 Arch / CachyOS 笔记本上的透明代理、规则分流和本地面板实践。这份长文把审计、验证、面板、测试、优化、历史方案、踩坑、脚本、试跑套件和脱敏配置合并成一份文档,按实际使用顺序排列。
上游来自 emoeem/Singborg,每小时自动同步;原来的单篇链接会自动跳转到对应章节。项目状态概览见 Singborg 项目页。
本文由 13 篇文档自动合并,内容最后同步于 2026-10-29。每章末尾可跳转上游原文。
Singborg 01:sing-box 现状审计
sing-box 现状审计(2026-10-02,真流量实测)
本章目录(13 节)
- 0. 结论速览
- 1. DNS 泄露(实测证据)
- 2. 广告拦截(覆盖率实测)
- 3. 分流正确性(Clash API 证据)
- 4. 问题清单与改法(按优先级)
- 5. 这个 fork 实测支持、上游 1.14 没有的能力
- 6. 建议落地顺序(含当前状态)
- 7. 外部规则源评估:HenryChiao/mihomo_yamls(用户提供)
- 8. 复测结果(2026-10-02 10:12 应用后实测)
- 9. 收尾状态(2026-10-02 10:35,三项全部就绪/已验证)
- 10. eBPF 模式切换完成 + 测量方法更正(2026-10-02 10:38)
- 11. eBPF 数据面实测(2026-10-02 13:0x 重要更正)
- 12. 决策:切回 TUN 模式(2026-10-02 11:1x)
对象:正在运行的
sing-box v1.14.2-reF1nd(审计当时是 TUN 模式,/etc/sing-box/config.json,22 出站 / 9 规则集)
方法:全部为实测——DNS 回显、DoH 绕行基准、Clash APIchains+rule、规则集反编译、真实 IP 连接归因。追注(2026-10-02 23:01 全篇复核,实测):本文按时间分段保留当天记录,但运行状态已经不是 TUN——以本追注为准:
inbounds[0].type = "ebpf"(local.data_plane=cgroup+shared.data_plane=packet_rewrite,interface:["virbr0"],共享附件以 TCX 挂载);
规则集 15 份(其中 6 份来自自建包/usr/share/sing-box-rule-sets/);出站 22 未变。
各节中已过时/被推翻的结论,均在该处加了带日期的追注(原记录保留,不改写历史)。
0. 结论速览
| 检查项 | 结果 |
|---|---|
| DNS 泄露 | ✅ 干净:53 端口劫持彻底(8.8.8.8/路由器/223.5.5.5/1.1.1.1 全被接管);无任何明文 53 外发;海外解析走 Cloudflare DoH(回显 172.64.212.11)、国内走 阿里 DoH(223.5.5.5:443);IPv6 无黑洞规则 |
| 广告拦截 | 🟡 子域层面拦得不错,但有 10 个追踪家族真漏(criteo / outbrain / scorecardresearch / quantserve / adsrvr / rubiconproject / doubleverify / appsflyer / branch / CN SDK) |
| 分流正确性 | ✅ Google/GitHub/OpenAI/Reddit/Telegram/Steam → 代理;百度/B站/腾讯/Aliyun → 直连;规则命中可溯源 |
| geoip/cn 数据 | ❌ 过期:本地 8,045 条 CIDR(9/12),上游已 9,648 条;实测阿里云 8.130.222.175 因缺段被判成走代理 |
| 节点健康 | ✅ 无失败节点,延迟 146–313 ms(Auto → 新加坡Z02) |
| 配置卫生 | 🟡 2 条冗余路由规则;3 个 DNS 服务器无人引用;1 条 1.14 弃用告警(1.16 移除) |
追注(2026-10-02 23:01 复核):本表是当天上午审计当时的结论,下列三行已过时,均已处理:
- 广告拦截 🟡(10 个追踪家族真漏) → 已补:新增
anti-AD(99,506 条)、adblockfilters(215,248 条域名后缀)、ads-extra(12 条),
DNS 与连接层两条规则都接上(见 §2.3 追注、§8.1 追注);现只剩tanx.com/uc.cn两个 apex 刻意不拦。- geoip/cn 过期 → 已修:
route.rules现在用["geoip/cn","geoip/cn-fresh","cncidr-mihomo"]三份表。
实测8.130.222.175:geoip/cn(8,045 条)不命中;geoip/cn-fresh(9,648 条)命中8.130.0.0/15;cncidr-mihomo(17,958 条)命中8.128.0.0/10→ 现已 direct(见 §8.2)。- 配置卫生三项全部处理完:2 条冗余路由规则已删(现
route.rules共 11 条,无protocol quic + port 443、无无条件→ Proxy);
闲置的 DNS 服务器已改成故障转移组并启用(dns-proxy/dns-direct,且dns-local已删除);external_ui*已移除,experimental.clash_api只剩cache_file/clash_api——实测 journal 里那条弃用 WARN 只出现在 09:32(修复前)的一次启动。- (”DNS 泄露”那行的 ✅ 结论仍然成立,但两条证据细节已变:那时”返回空”其实是 REFUSED、现在是真的 NXDOMAIN;出口回显现在是 IPv6 的 Cloudflare 地址。详见 §8.4 / §10.2 追注。)
1. DNS 泄露(实测证据)
① 53 端口劫持:dig @8.8.8.8 / @192.168.31.1 / @223.5.5.5 / @1.1.1.1 doubleclick.net
→ 四个全部返回空(说明查询都进了 sing-box 的 DNS 规则,而不是真的发到那些解析器)
② 出口回显:dig TXT o-o.myaddr.l.google.com
→ 系统解析器与 @1.1.1.1 都返回 172.64.212.11(Cloudflare 的解析出口)
→ 海外域名由 Cloudflare DoH 经代理解析,ISP 看不到
③ 明文 DNS:ss -un / -tn 里没有任何指向外部 :53 的套接字
→ 没有 UDP/53 明文外发(sing-box 走 DoH over 443)
现存 53 监听者只有本机服务:127.0.0.53/54(systemd-resolved)、192.168.122.1(libvirt)
④ 国内解析:ss 显示到 223.5.5.5:443 的 DoH 连接
→ 国内域名走阿里 DoH(不是明文 UDP,比之前那版更强)
⑤ resolvectl:tun0 = 172.19.0.2 / fdfe:dcba:9876::2,DNS Domain ~.
→ 系统解析器 100% 指向隧道
(**追注 2026-10-02 23:01:这是 TUN 模式下的状态,现已不适用**——当前 eBPF 模式没有 tun0,
`resolvectl` 里 wlan0 用 `192.168.31.1` 与宿主路由器的 ULA 网关(`fd00:6868:…::1`),解析器不再指向隧道;
劫持改由 eBPF 在 53 端口完成,实测 `dig @8.8.8.8 doubleclick.net`、`dig @223.5.5.5 doubleclick.net` 都立刻返回 NXDOMAIN,拦截仍有效。)
唯一残留面:dns.servers 里那个 dns-local(type: local)现在没人引用;一旦将来被误引用,就会绕过 DoH 走系统解析器 → 建议直接删掉。
(追注 2026-10-02 23:01:已删除——当前 dns.servers 共 7 条,无 dns-local。)
一个天然限制(你已规避):hijack-dns 只覆盖 53 端口/dns 协议,浏览器自作主张的 DoH(443)不经过 sing-box 的 DNS 规则。
好在你的广告拦截同时写在 route.rules 里(连接层按 sniff 出的域名 reject),所以即使用户开浏览器 DoH,
广告连接照样被 RST —— 这个”双写”设计是对的,别只留 DNS 那一条。
2. 广告拦截(覆盖率实测)
方法:dig(走 sing-box 的 DNS 规则)对比 绕开 sing-box 的 Cloudflare DoH(基准真相,因为 443 不在 hijack-dns 范围内)。
2.1 apex 域名层:36 个测试,拦 19 / 漏 13
漏拦:criteo.com、outbrain.com、scorecardresearch.com、quantserve.com、adsrvr.org、rubiconproject.com、doubleverify.com、branch.io、appsflyer.com、admaster.com.cn、tanx.com、umeng.com、miaozhen.com
2.2 但 apex 不等于真实端点——子域层复测
| 类型 | 结果 |
|---|---|
| ✅ 拦得住 | *.umeng.com(ulogs/alogs/ulog/log/r)、atm.youku.com、ad.m.iqiyi.com、t7z.cupid.iqiyi.com、dsp.miaozhen.com、app.adjust.com、cm.admaster.com.cn、jp.tanx.com、acs.tanx.com、cm.branch.io、cnzz.com.edgekey.net |
| ❌ 真漏 | dis/static/gum.criteo.com、amplify/tr/log.outbrain.com、sb/b.scorecardresearch.com、pixel/secure.quantserve.com、match/js.adsrvr.org、pixel/token.rubiconproject.com、tps/cdn.doubleverify.com、app/t/api2.appsflyer.com、api2/app.branch.io、sdkmob.com、ad.duowan.com、uc.cn、aly.admaster.co |
2.3 修复方案(已验证)
现有两条规则集 geosite/category-ads-all(910 条)+ Ads_AWAvenue(合法 SRS\x02,反编译失败只因它是 AdGuard 语法编译来的)覆盖不到这些追踪家族。
候选补充表的实测覆盖(14 个漏拦域名):
| 候选表 | 规则数 | 覆盖 | 体积 |
|---|---|---|---|
| anti-AD | 99,506 | 13/14 | 2.2 MB |
| AdGuard DNS filter | 177,432 | 12/14 | 4.3 MB |
| hagezi Pro | 227,618 | 13/14 | 5.1 MB |
推荐 anti-AD(国内维护、体积最小、覆盖最全)。这个 fork 自带转换器,一条命令离线生成:
curl -sL -o /tmp/antiad.txt https://anti-ad.net/adguard.txt
sing-box rule-set convert /tmp/antiad.txt -t adguard -o anti-ad.srs # 0.3 秒,产出 770 KB
已实测:源文件 anti-ad.txt 有 99,506 条非注释行,生成的 anti-ad.srs 为 770,834 B,加入配置后 sing-box check 通过 ✅。(口径提醒:99,506 是源文件的条数;.srs 是 AdGuard 二进制格式,sing-box rule-set decompile 会直接 FATAL,所以编译后的条数无法核对 —— 别把源文件条数当成 .srs 里的条数。)
追注(2026-10-02 23:01:本方案已落地,但落地方式与下文不同):anti-AD 不是手工
sudo cp到/etc/sing-box/rule-set/,
而是随自建包sing-box-rule-sets(现装20261002-2)装到/usr/share/sing-box-rule-sets/anti-ad.srs(770,834 B)。
实测源文件/home/emo/.cache/sing-box-rule-sets/anti-ad.txt(2,199,016 B)= 99,506 条非注释行,其中 99,457 条||域名^规则 ⇒ 99,506 这个数字可复现。
实际还多接了两份表:adblockfilters(217heidai,反编译得 215,248 条域名后缀;与 anti-AD 精确交集 75,382 条 = 占 adblockfilters 的 35%,独有 139,866 条)
和ads-extra(自制,12 条:ad.duowan.com/ads.wps.cn/sdkmob.com)。
两条拦截规则的rule_set现为["geosite/category-ads-all","Ads_AWAvenue","anti-AD","ads-extra","adblockfilters"],
其中 DNS 那条用的是predefined+rcode: NXDOMAIN,不是reject(见 §10.2)。
(另:geosite/category-ads-all反编译实测 910 条 = 162 domain + 748 suffix + 66 regex,与 §2.3 一致。)
⚠️
branch.io/appsflyer.com是移动归因 SDK,拦了可能影响某些 App 的深链跳转;不想影响就从表里排除。
3. 分流正确性(Clash API 证据)
每条连接都带 chains(出口链)与 rule/rulePayload(命中的规则):
| 目标(sniff 出的真实 host) | 出口 | 命中规则 |
|---|---|---|
www.google.com / chat.openai.com |
Proxy → Auto → 日本Z01 | [geosite/google geosite/category-ai-!cn] => route(Proxy) |
github.com / www.reddit.com / t.me / www.microsoft.com / shared.valve.map.fastly.net |
Proxy → Auto | geosite/geolocation-!cn => route(Proxy) |
www.a.shifen.com(百度) / a.w.bilicdn1.com(B站) / ins-r23tsuuf.ias.tencent-cloud.net(腾讯) |
direct | geosite/cn => route(direct) |
123.57.116.48 / 183.2.172.177 / 121.14.77.201 / 223.5.5.5 / 47.98.147.100 |
direct | geoip/cn => route(direct) |
8.8.8.8 / 104.16.132.229 |
Proxy | route(Proxy)(final) |
8.130.222.175(阿里云杭州) |
❌ Proxy | final(geoip/cn 缺这个段) |
追注(2026-10-02 23:01 复核):上表是审计当时的抓样,其中两处已变:
① 最后一行8.130.222.175已修(现在命中geoip/cn-fresh/cncidr-mihomo→ direct,见 §8.2);
② 第 4 行现在命中的规则集是["geoip/cn","geoip/cn-fresh","cncidr-mihomo"](route.rules第 11 条,实测确认)。
另外Proxy → Auto的落地节点是动态的(当前Auto选中的是🇭🇰 香港Z02,不是表里的日本Z01/新加坡Z02)——
表格里的节点名只代表当时那一刻,规则命中部分(geosite/google、geosite/geolocation-!cn、geosite/cn、final=Proxy)与当前配置一致。
4. 问题清单与改法(按优先级)
P1 —— 广告覆盖缺口
加 anti-AD 规则集(~/.cache/sing-box-rule-sets/anti-ad.srs 已生成,待 sudo cp 到 /etc/sing-box/rule-set/):
(追注 2026-10-02 23:01:已落地,但路径/清单与此处不同——实际随自建包装到 /usr/share/sing-box-rule-sets/anti-ad.srs,
且 rule_set 列表还多了 ads-extra、adblockfilters;DNS 侧用的是 predefined/NXDOMAIN,不是 reject。见 §2.3 追注。)
// route.rule_set 追加
{ "type": "local", "tag": "anti-AD", "format": "binary", "path": "/etc/sing-box/rule-set/anti-ad.srs" }
// dns.rules 第 1 条 与 route.rules 第 4 条,rule_set 都追加 "anti-AD"
{ "rule_set": ["geosite/category-ads-all", "Ads_AWAvenue", "anti-AD"], "action": "reject" }
P1 —— geoip/cn 过期(实测 8.130.222.0/24 走了代理)
上游最新 geoip/cn.srs 含 9,648 条 CIDR 且包含 8.130.0.0/15(本地那份 8,045 条缺)。两条路:
- 刷新系统包:
sudo pacman -Syu sing-geoip-rule-set(当前 20260912,落后 3 周); - 本地双保险:把上游最新文件放
/etc/sing-box/rule-set/geoip-cn-fresh.srs,加第二个 local 规则集,路由里改成"rule_set": ["geoip/cn", "geoip/cn-fresh"]。
长期方案(很契合你已有的仓库):在
~/pkgbuild-source里加一个sing-box-rule-sets包,
用 CI 定时拉 MetaCubeX geosite/geoip + anti-AD,转成.srs打包安装 —— 规则集就跟系统更新一起滚动,不用手工维护。
追注(2026-10-02 23:01:以上两条都已落地)
- 方案 2(本地双保险):
/usr/share/sing-box-rule-sets/geoip-cn-fresh.srs实测 9,648 条且含8.130.0.0/15;路由里已用["geoip/cn","geoip/cn-fresh","cncidr-mihomo"]。geoip/cn本体未变:sing-geoip-rule-set 20260912-1,8,045 条,实测不含8.130.222.175。- 方案 1(刷系统包)未做:
pacman -Q sing-geoip-rule-set仍是20260912-1。- 长期方案也已实现:
~/pkgbuild-source/packages/sing-box-rule-sets(见 §9、§6 第 6 项)。
P2 —— 1.14 弃用告警(1.16 会移除)
WARN legacy `external_ui_download_detour` Clash API option is deprecated in sing-box 1.14.0 and will be removed in 1.16.0.
正好和「去掉 MetaCubeXD」一起处理(见 singbox-panel-guide.md §9)。
追注(2026-10-02 23:01:已解决):
experimental.clash_api现在只有cache_file(=/var/lib/sing-box/cache.db)与clash_api两项,
不再有external_ui*;curl http://127.0.0.1:9090/ui/实测 404。
journal 里该 WARN 只在 09:32:44(修复前的那次启动)出现过一次,此后无。
P2 —— 3 个 DNS 服务器无人引用 + 可做故障转移
dns-proxy-google、dns-direct-tencent、dns-local 都没被任何规则引用。
(追注 2026-10-02 23:01:此句已过时,本方案已落地 —— 见下方代码块后的追注。)
这个 fork 实测支持 type: group(上游 1.14 不支持,fork 有),所以可以把它们变成故障转移组:
{ "type": "group", "tag": "dns-proxy", "servers": ["dns-proxy-cf", "dns-proxy-google"] },
{ "type": "group", "tag": "dns-direct", "servers": ["dns-direct-ali", "dns-direct-tencent"] }
// dns.final 与相关 dns.rules 改指向这两个组;dns-local 直接删除(潜在泄露面)
追注(2026-10-02 23:01:已启用):配置里已有
{"type":"group","tag":"dns-proxy","servers":["dns-proxy-cf","dns-proxy-google"]}与{"type":"group","tag":"dns-direct","servers":["dns-direct-ali","dns-direct-tencent"]};dns.final = "dns-proxy",dns.rules里国内走dns-direct、海外走dns-proxy;dns-local已删除(dns.servers共 7 条:hosts-bootstrap/dns-proxy-cf/dns-proxy-google/dns-direct-ali/dns-direct-tencent/dns-proxy/dns-direct)。
P3 —— 两条冗余路由规则
route.rules 第 11 条(protocol quic + port 443 → Proxy)与第 12 条(无条件 → Proxy)都与route.final = Proxy 重复,可删(少两条规则匹配)。
(追注 2026-10-02 23:01:已删除 —— 当前 route.rules 共 11 条,实测无 protocol quic + port 443、也无无条件 → Proxy 的规则。)
P3 —— 可选性能优化
store.steampowered.com / cdn.steamstatic.com / www.microsoft.com 现在都走代理(geolocation-!cn)。
本地规则集里已有 geosite-steam@cn.srs(还有 microsoft 相关),若你下载 Steam 游戏/Office 觉得慢,
可加一条 geosite/steam@cn → direct(CDN 直连通常快数倍)。这是偏好项,不是问题。
5. 这个 fork 实测支持、上游 1.14 没有的能力
| 特性 | 结果 | 用途 |
|---|---|---|
dns.servers[].type = "group" |
✅ | DNS 故障转移组(上游 1.14 无) |
outbound.type = "anytls" |
✅ | 新协议 |
rule_set.type = "remote" |
✅ | 规则集自动更新(配合 cache_file,不必每次启动联网) |
dns.servers[].type = "hosts" |
✅ | DoH 域名预解析(你已在用) |
sing-box rule-set convert -t adguard |
✅ | AdGuard 语法 → .srs(本报告 P1 就靠它) |
sing-box rule-set decompile |
✅(AdGuard 源除外) | 规则集内容审计 |
6. 建议落地顺序(含当前状态)
已打包成脚本:
apply-audit-fixes.sh(P1+P2 默认执行;--with-cncidr/--with-direct-list/--with-dns-groups为可选增强)。
流程与安全管线已在本机干跑验证:四种开关组合产出的配置sing-box check全部通过。
| # | 动作 | 状态 |
|---|---|---|
| 1 | anti-AD 规则集(补 10 个追踪家族):装文件 → DNS 与路由两条 reject 规则都加 anti-AD |
✅ 已完成(走自建包 sing-box-rule-sets 装到 /usr/share/sing-box-rule-sets/,并多加 adblockfilters、ads-extra;DNS 侧为 predefined/NXDOMAIN) |
| 2 | 刷新 geoip/cn:优先 pacman -Syu sing-geoip-rule-set;脚本默认用上游最新 .srs 做双保险(含 8.130.0.0/15) |
✅ 已完成(geoip/cn-fresh + cncidr-mihomo 已生效,8.130.222.175 复测为 direct,见 §8.2;系统包仍未刷,仍是 20260912-1) |
| 3 | 清理:删 dns-local、删与 final 重复的 2 条路由规则、去掉 external_ui* |
✅ 全部完成(实测 dns.servers 无 dns-local、route.rules 11 条无冗余项、clash_api 无 external_ui*、/ui/ → 404) |
| 4 | DNS 故障转移组(可选):把闲置的 dns-proxy-google / dns-direct-tencent 变成组 |
✅ 已启用(dns-proxy / dns-direct 组已在配置里并被 dns.final/dns.rules 引用) |
| 5 | mihomo 增强(可选):cncidr 第三份国内 IP 表 + STUN/主机/LAN cache 直连清单 |
✅ 已启用(cncidr-mihomo 17,958 条 + must-direct 33 条,均在配置中) |
| 6 | 长期:pkgbuild 仓库加 sing-box-rule-sets 包,CI 定时拉 MetaCubeX + anti-AD + mihomo .srs |
✅ 已完成(包已发布安装:sing-box-rule-sets 20261002-2;CI 工作流 refresh-rule-sets.yml 已在仓库中,见 §9) |
追注(2026-10-02 23:01 复核):上表 6 项状态由”待执行/待规划”更新为实测结果(本表标题即”含当前状态”,故直接改写单元格;原始结论见 §8/§9/§12 的时间线)。
第 5 项的--with-cncidr --with-direct-list两个产物都能在配置里找到对应文件;第 6 项的每日刷新已用手动触发验证过一次(§9)。
7. 外部规则源评估:HenryChiao/mihomo_yamls(用户提供)
实测结论:这个仓库能直接供 sing-box 用(不是只有 mihomo 格式),而且有两点很有价值。
7.1 它发布现成的 sing-box 规则集
ruleset 分支下 singbox/version1..version4/ 各 104 个分类(adrules / ai / cn / cncidr / gfw / games / microsoft-cn / apple-cn / private / proxy / netflix / spotify …),
每个分类同时给 .json(源)与 .srs(二进制),每天 07:30(UTC+8) 自动重建。
实测:四个 version 在你这台 sing-box 1.14.2-reF1nd 上全部能加载(version1 是较啰嗦的老格式 1821 KB,v2–v4 均 1671 KB)。
7.2 广告表对比:它的 adrules 略逊于 anti-AD
拿之前实测的真漏端点逐个比对:
| 目标 | mihomo adrules(197,019 条) |
anti-AD(99,506 条) |
|---|---|---|
| criteo / outbrain / scorecardresearch / quantserve / adsrvr / rubicon / doubleverify / appsflyer / branch | ✅ | ✅ |
sdkmob.com / ad.duowan.com / uc.cn(国内 SDK) |
❌ | ✅ |
| 小计 | 11/14 | 14/14 |
→ 主广告表仍用 anti-AD;adrules 的互补价值在于它含 IP 类广告规则(anti-AD 只有域名),可作为第二阶段补充。
7.3 它的 cncidr 正好补上你 geoip 的洞
cncidr(version4, 79 KB)有 17,958 条 CIDR,且包含 8.128.0.0/10(覆盖你实测漏掉的 8.130.222.0/24)。
→ 已采纳为脚本的 --with-cncidr:给国内 IP 匹配加第三份独立来源(本地 geoip/cn + 上游最新 + mihomo cncidr)。
7.4 从它的 178 份 YAML 里提炼出的可借鉴点
| 它的做法 | 对你这套的意义 |
|---|---|
| 域名规则在前、IP 规则在后 | ✅ 你已满足(geosite 规则都在 geoip 之前) |
fake-ip-filter 里塞了 lancache.steamcontent.com、*.stun.*、xbox.*、srv.nintendo.net、dns.msftncsi.com |
⚠️ 能改进:这些走代理会坏功能(LAN 缓存失效、主机 NAT 类型变差)→ 已提炼成 must-direct.srs,脚本 --with-direct-list 一条直连规则搞定 |
proxy-server-nameserver(解析节点域名用国内 DoH) |
✅ 你等价物是 route.default_domain_resolver = dns-direct-ali(且节点是 IP,本就不受影响) |
nameserver-policy 按域名分流解析器 |
✅ 你等价物是 dns.rules(且你用的是真 IP + DoH,比 fake-ip 更干净) |
走 no-resolve 减少无谓解析 |
sing-box 里由 sniff + 规则顺序天然覆盖 |
7.5 长期怎么吃这个仓库(两条路)
- remote rule_set +
update_interval:sing-box 支持{"type":"remote","url":"https://raw.githubusercontent.com/HenryChiao/mihomo_yamls/ruleset/singbox/version4/xxx.srs","download_detour":"Proxy"},
配合cache_file只在过期时联网(你配置里的http_clients/route.default_http_client正好能复用为下载出口); - (更契合你)在自己的 pkgbuild 仓库里加包:CI 定时拉这些
.srs+ anti-AD,转好并安装到/etc/sing-box/rule-set/,
规则集就跟系统更新一起滚动 —— 比你手工下载可靠,也留痕。
8. 复测结果(2026-10-02 10:12 应用后实测)
应用脚本执行成功:备份 /etc/sing-box/backups/config.20261002-101233.pre-audit.json,check 通过,健康检查通过(经代理出口 <proxy-exit-ip>、4 个测试域名全拦)。以下为独立复测。
8.1 广告拦截:真漏追踪端点已拦住
| 目标 | 应用前 | 应用后 |
|---|---|---|
dis.criteo.com / tr.outbrain.com / sb.scorecardresearch.com / pixel.quantserve.com |
❌ | ✅ |
match.adsrvr.org / pixel.rubiconproject.com / tps.doubleverify.com |
❌ | ✅ |
app.appsflyer.com / api2.branch.io |
❌ | ✅ |
| apex:criteo / outbrain / scorecardresearch / quantserve / adsrvr / rubiconproject / doubleverify / branch / appsflyer / admaster / umeng / miaozhen | ❌ | ✅ |
| 对照组 google / github / baidu / bilibili / qq / taobao / zhihu / weixin | 正常 | ✅ 仍正常(无误伤) |
仍剩 4 个(性质已查清):
| 域名 | 结论 |
|---|---|
tanx.com |
anti-AD 只有子域规则(` |
sdkmob.com |
anti-AD 0 条规则 → 真漏(国内 SDK 域名,可选补)——追注 2026-10-02 23:01:已补,ads-extra.srs 的 domain_suffix 含 sdkmob.com,实测 dig sdkmob.com → NXDOMAIN |
ad.duowan.com |
anti-AD 不含 ad.duowan.com(它有的是 a.v./gt./market./other.v./playstats.v./stats.v./vupload.duowan.com 共 7 条)→ 真漏(可选补)——追注 2026-10-02 23:01:已补,ads-extra.srs 的 domain 含 ad.duowan.com,实测 → NXDOMAIN |
uc.cn |
我之前测量有误:源文本子串匹配把 nnuc.cn/open-uc.cn 当成了 uc.cn;按规则语义它其实从未被覆盖。而且 uc.cn 是 UC 浏览器整个 apex,拦了会误伤 → 建议只拦其广告子域,不拦 apex(追注 2026-10-02 23:01 复测:dig uc.cn → NOERROR,确实未被拦;adblockfilters 215,248 条后缀里没有** apex uc.cn,只有 19 条 *.uc.cn 子域——与”只拦广告子域、不拦 apex”的建议一致**) |
更正:本报告 §2.3 里”anti-AD 覆盖 13/14”是基于源文本子串匹配的宽松估计;
按规则语义精确复测,anti-AD 覆盖的是上面列出的追踪端点集合(21/25 端点),并非全部。追注(2026-10-02 23:01):上面”仍剩 4 个”里
sdkmob.com/ad.duowan.com已由ads-extra.srs补齐(实测两者都返回 NXDOMAIN);
现在刻意不拦的只剩tanx.com/uc.cn两个 apex。adblockfilters(215,248 条)也已接入,但同样不含这两个 apex。
8.2 geoip 洞已修
修复前: 8.130.222.175 → Proxy(final)
修复后: 8.130.222.175 → direct rule_set=[geoip/cn geoip/cn-fresh cncidr-mihomo] => route(direct) ✅
对照 : 8.8.8.8 → Proxy ✅(没有把国外 IP 误判成国内)
8.3 must-direct 规则生效
lancache.steamcontent.com → direct rule_set=must-direct => route(direct) ✅
store.steampowered.com → Proxy rule_set=geosite/geolocation-!cn ✅(对照,未被误伤)
8.4 DNS 仍无泄露(复测)
@8.8.8.8/@192.168.31.1/@223.5.5.5查doubleclick.net全部返回空(53 仍被完全劫持);- 出口回显
o-o.myaddr.l.google.com→172.64.212.11(Cloudflare,仍走代理解析); dig +short doubleclick.net→ 空。
追注(2026-10-02 23:01 复测):拦截仍然有效,但两个细节已变:
① 那时”返回空”其实是 REFUSED(§10.2 更正过),现在 DNS 广告规则已改成predefined/NXDOMAIN,dig @8.8.8.8 doubleclick.net→status: NXDOMAIN(合法应答,不再是拒绝/超时);
② 出口回显现在返回的是 IPv6 地址2400:cb00:1176:1024::a29f:610b(仍是 Cloudflare 段,结论不变)。
8.5 IPv6 与健康
curl -6 https://ipv6.google.com→ 200,出口<proxy-exit-ip>(走代理)✅;ip -6 rule无unreachable黑洞 ✅。注:脚本里原先那条复测命令误写成
curl -4(AAAA-only 站点永远打不开),已修正为-6。- 服务
active,systemd计内存 42.9M → 50.3M(+7.4 MB,来自 4 份新增规则集),无规则集加载告警。
追注(2026-10-02 23:01 复测):内存已远高于当时。实测
ActiveState=active、NRestarts=0、已运行约 1h18m:systemctl show sing-box -p MemoryCurrent→ 104,726,528 B ≈ 99.8 MiB(MemoryPeak108,077,056 B),/proc/<pid>/status的VmRSS→ 118,944 kB ≈ 116 MiB。
也就是说”50 MB 级 / 75 MB 级”都已不成立,随规则集与运行时长增长。ip -6 rule仍只有0 / 32766,无黑洞 ✅;ip rule只有0 / 32766 / 32767、tun0不存在(当前是 eBPF 模式)。
8.6 一个未解释的观察(待留意,非配置 bug)
重启后日志有两条 74.125.250.129:443/:80 using outbound/direct: i/o timeout。
已排查:三份 CN 规则集(geoip/cn、geoip/cn-fresh、cncidr-mihomo)都不含该 IP,google.cn / gstatic.com 也都正确走 Proxy。即不是新数据的误判;不可复现,先记录观察。
9. 收尾状态(2026-10-02 10:35,三项全部就绪/已验证)
| 项 | 状态 | 证据 |
|---|---|---|
| 补充规则集打包 | ✅ 已推送并发布 | 91f52ee;CI 全绿;私有仓库已含 sing-box-rule-sets-20261002-1 |
| 规则集日更自动化 | ✅ 已实测(提前用手动触发验证) | 触发 refresh-rule-sets.yml → 11 秒完成 → 下载三源哈希一致 → No rule-set source changed → updated=false → 「Commit and push」步骤 skipped → 远端 main 仍是 91f52ee(无多余提交) |
| DNS 故障转移组(P3) | ✅ 已启用(原记”🟡 待执行”) | apply-audit-fixes.sh --with-dns-groups 的产物已在 /etc/sing-box/config.json:dns-proxy/dns-direct 两个组 + dns.final=dns-proxy(该 fork 支持 type: group,上游 1.14 不支持) |
| eBPF 切换 | ✅ 已执行(原记”🟡 待执行”) | switch-to-ebpf.sh;10:37 切到 eBPF,当天夜间再加挂 shared(packet_rewrite on virbr0)。当前 inbounds[0].type=ebpf,tun0 不存在 |
| 广告端点覆盖 | ✅ 24/26 | 剩 tanx.com/uc.cn 两个 apex 刻意不拦(sdkmob.com/ad.duowan.com 后由 ads-extra 补齐,见 §8.1) |
| 规则集加载 | ✅ 15 份全通 | 6 份来自 /usr/share/sing-box-rule-sets(包):anti-ad / geoip-cn-fresh / cncidr-mihomo / must-direct / ads-extra / adblockfilters |
追注(2026-10-02 23:01 复核):本节是 10:35 时的状态快照,上表已按实测更新三处(DNS 组、eBPF 切换、规则集计数 14→15 份、包内 5→6 份)。
另:包版本也已前进——本节写的sing-box-rule-sets-20261002-1,当前装的是20261002-2(pacman -Q sing-box-rule-sets);91f52ee这个 commit 在~/pkgbuild-source里存在(git log可见,CI 工作流refresh-rule-sets.yml也在)。
9.1 eBPF 切换脚本的健康判据(与 TUN 模式不同)
switch-to-ebpf.sh 不使用「等 tun0」这种 TUN 判据,而是:
systemctl is-activesing-box api ebpf --url http://127.0.0.1:9091 --secret <clash密钥>→inbounds非空(附件真的挂上了)- 不设任何代理的
curl https://api.ipify.org必须等于经本地代理口的出口(这是”内核接管成功”的铁证) curl https://myip.ipip.net仍返回国内电信 IP(分流没坏)dig doubleclick.net/tr.outbrain.com仍为空(DNS 劫持没坏)- 任一失败 → 自动回滚到备份的 TUN 配置并重启
同理 apply-audit-fixes.sh 的健康检查也已改为模式感知:配置里是 tun 才等 tun0,是 ebpf 就改用第 3 条做判据。
10. eBPF 模式切换完成 + 测量方法更正(2026-10-02 10:38)
sudo ./switch-to-ebpf.sh 于 10:37:51 切换成功(第一次尝试因我的 DNS 判据读了过渡期缓存而误判回滚,
判据已改成”随机子域 + 直连解析器 + 对照域名 + 重试 20s”,第二次全部通过)。
10.1 eBPF 模式实测结果(全部通过)
| 检查项 | 结果 |
|---|---|
| TUN 设备 | tun0 已消失 ✅ |
| 路由残留 | ip rule 只剩 0/32766/32767,无 9000/9001/9002 残留;table 2022 空 ✅ |
| IPv6 黑洞 | 0 条 ✅ |
| eBPF 附件 | api ebpf → 1 个活动入站,attachments: cgroup @ /sys/fs/cgroup;内核程序 sb_ebpf_conn4(CGroupSockAddr)、sb_self_create/release(CGroupSock) ✅ |
| 内核接管 | 不设任何代理的 curl api.ipify.org == 经本地代理口的出口 ✅(铁证) |
| 分流 | 国内 → 电信 IPv6 直连;海外 → 代理出口 <proxy-exit-ip> ✅ |
| IPv6 | curl -6 ipv6.google.com → 200 ✅ |
| DNS 劫持 | 直连 3 个解析器(8.8.8.8 / 路由器 / 223.5.5.5)查随机广告子域 → 全部被拒 ✅;出口回显仍是 Cloudflare(172.64.214.12)✅ |
| 计数器 | 所有 eBPF 计数器为 0、无失败项、日志无告警 ✅ |
| 资源 | 内存 50→63 MB(eBPF 程序/map),可接受 |
| 面板 | mode=ebpf;/api/ebpf/policy 现在 present=true(分应用可用了),应用列表 10 条 ✅ |
追注(2026-10-02 23:01 实测):
- “eBPF 附件”这一行已变化:现在
sing-box api ebpf返回 2 个附件 ——role=local mechanism=cgroup @ /sys/fs/cgroup
与role=shared mechanism=tcx @ virbr0(localDataPlane=cgroup、sharedDataPlane=packet_rewrite、sharedEnabled=true、state=normal、lastError="")。
本表只记了 local 那一个,是因为加挂shared是当天夜间的事(见 §11、§12 追注)。- 计数器仍全 0 ✅(
assignmentLookupFailures/tcSocketLookupFailures/rewriteFailures/recovery*等实测均为 0)。- “资源 50→63 MB”已过时:现在
MemoryCurrent ≈ 99.8 MiB、VmRSS ≈ 116 MiB(见 §8.5 追注)。- “面板”这一行本次未能验证:面板 API 在
127.0.0.1:9096,无令牌访问/api/ebpf/policy返回 401;9091上同名路径是 302 跳到/dashboard/。所以present=true与”应用列表 10 条”未复核。
10.2 ⚠️ 测量方法更正(重要)
本文档前面所有”DNS 拦截 = ✅”的判定,用的是”dig 无输出”,而我把 dig 的 stderr 丢掉了 ——
于是”REFUSED“和”超时“看起来一模一样。带完整输出复测后真相是:
| 查询路径 | 实际结果 |
|---|---|
直连任意解析器(dig @8.8.8.8 / @223.5.5.5 / @192.168.31.1 doubleclick.net) |
status: REFUSED(立刻返回,是合法 DNS 响应) |
经 systemd-resolved(dig doubleclick.net) |
communications error to 127.0.0.53#53: timed out;resolvectl 自己说 server or network returned error: REFUSED |
即:sing-box 的 DNS reject 语义是 REFUSED,而 systemd-resolved 不会把 REFUSED 转告客户端
(它当作”上游故障”去重试其他上游,最终放弃),于是走系统解析器的应用每个广告域名要等约 5 秒才失败。
这不影响”拦截有效”的结论(广告域名确实拿不到解析),但影响体验,且 TUN 模式同样存在(只是以前没测出来)。
修法:DNS 广告规则由 reject(REFUSED) 改为 predefined + rcode: NXDOMAIN ——
NXDOMAIN 是正常应答,resolved 会如实转告,应用立刻失败并做负缓存。该 fork 已实测支持该写法:apply-audit-fixes.sh --nxdomain-ads(干跑 check 通过)。
追注(2026-10-02 23:01 实测:本节问题已修复,表内 REFUSED/超时已不复现):
修法已应用 —— 当前dns.rules[0]就是{"rule_set":["geosite/category-ads-all","Ads_AWAvenue","anti-AD","ads-extra","adblockfilters"],"action":"predefined","rcode":"NXDOMAIN"}。
复测(带完整输出):
dig @8.8.8.8 doubleclick.net→status: NXDOMAIN(@223.5.5.5、@192.168.31.1同为 NXDOMAIN),立刻返回;- 经
systemd-resolved(dig doubleclick.net)→ 直接status: NXDOMAIN,不再有 5 秒超时;- 对照
dig @8.8.8.8 example.com→NOERROR(劫持只命中广告规则里的域名,不是把 53 全拒)。
本节记录的”REFUSED 被 resolved 吞掉”是修复前的历史现象,保留作记录。
11. eBPF 数据面实测(2026-10-02 13:0x 重要更正)
⚠️ 本节原标题写的是「本机只能用 local
cgroup(tc / shared 均不可用)」—— 那是错的。
当时只实测了local tc(失败),然后推断「shared 也是 TC 路径所以不可用」,
而且 shared 从未真跑过(脚本卡在「找不到 podman 网桥」就退出了)。
官方文档写明:四条数据面互相独立,一条不可用不能推断另一条。
以 root 权限重新逐条探测后的真实结果:
| 数据面 | 探测结果(sudo sing-box tools ebpf status … --json) |
|---|---|
local cgroup |
preflight_passed(PASS 37 / FAIL 0 / UNKNOWN 0) |
local tc |
❌ 实测启动失败:register TC eBPF TCP listener: operation not supported |
shared packet_rewrite |
✅ preflight_passed(PASS 33 / FAIL 0 / UNKNOWN 0) |
shared socket_assign |
✅ preflight_passed(PASS 31 / FAIL 0 / UNKNOWN 0) |
更正后的结论:本机只是 local tc 这一条不可用,两条 shared 都可用
→ eBPF 模式下容器/虚拟机可以被接管(这才是「接管本机与共享网络流量」的完整形态),
不必为了容器/VM 覆盖切回 TUN。探测必须用与服务相同的权限(普通用户跑只会得到一堆 unknown)。
11.0 原始记录(保留:怎么错的比结论更值得记)
追注(2026-10-02 晚):下面这段的前提有误 —— 「pasta 不创建宿主 socket」已被实测推翻
(ss -tnp能看到passt.avx2的 ESTAB socket);当时以为「不支持的 TC 路径」后来也证明
共享数据面(shared)可用,只有 local 的 tc 模式不行。原始推理保留在此当反面教材。
为覆盖 rootless 容器(当时认为 pasta 把包 splice 进宿主栈、不创建宿主 socket,cgroup socket 钩子看不到),
试了两条路,都撞在同一处内核能力上:
FATAL start service: start inbound/ebpf[ebpf-in]:
register TC eBPF TCP listener: operation not supported
| 试验 | 结果 |
|---|---|
local.data_plane: tc |
❌ sing-box 启动即 FATAL(上面那行),API 9091 与本地代理口 7892 都连不上;脚本自动回滚成功,服务已恢复 |
shared(data_plane: packet_rewrite) |
❌ 同一条 TC 路径,不必再试(我在 enable-container-proxy.sh 里加了守卫直接拦下) |
11.1 预检为什么没提前发现(重要)
| 预检模式 | 结果 | 含义 |
|---|---|---|
--mode local(我脚本原先只跑这个) |
preflight_passed,PASS 37 / FAIL 0 / UNKNOWN 0 |
local cgroup 路径确实全支持 ✓(所以这个结论没错) |
--mode all(含 shared/tc 那 28 项) |
inconclusive:pass 13 / warn 2 / unknown 36,required_issues 28 |
TC 支持无法用预检确认,只能实跑才知道 |
即:--mode local 全绿 不代表 TC 可用。两个脚本现在都会在请求 tc/shared 时先跑--mode all,非 passed 就拦下并给出替代方案(--force 可强行尝试)。
11.2 容器的最终结论(已实测)
| 容器网络 | 出口 | 结论 |
|---|---|---|
| rootless 默认(pasta) | 失败(境外直连被墙) | ❌ 无法被接管 —— 原解释「pasta 不创建宿主 socket」已被实测推翻(ss -tnp 可见 passt.avx2 的 ESTAB socket),真正卡点未确证,见 docs/09 §6 |
--network=host |
<proxy-exit-ip> = 代理出口 |
✅ 可行(容器直接用宿主 socket) |
| rootful 网桥 + shared | <proxy-exit-ip> |
✅ 可行(2026-10-02 晚复测:shared=packet_rewrite on virbr0,直连与代理两条路都通过) |
修正后的结论:本机(kernel 7.2.8-1-cachyos-bore-lto)上,桥接进 VM/容器的流量可以靠 eBPF shared 接管(实测通过);
只有 pasta 这种用户态转发仍要靠 --network=host。
(TUN 模式没有这个限制,因为它靠 auto_route 管转发流量;这是切到 eBPF 的唯一实际回退项。)
12. 决策:切回 TUN 模式(2026-10-02 11:1x)
追注(2026-10-02 23:01 复核:本节的决策与理由都已被推翻,结论不再成立)
- 本节的前提是错的:当时以为”本机内核不支持 TC 路径”,实际只有 local 的
tc模式不可用,
两条shared数据面实测都可用(§11,shared=packet_rewrite以 TCX 挂在virbr0上);- “pasta 不创建宿主 socket”这个机制解释也是错的(
ss -tnp可见passt.avx2的 ESTAB 宿主 socket,真正卡点未确证,见 §11.0 追注与docs/09§6);- 因此当天夜间又切回了 eBPF:当前
inbounds[0].type="ebpf",local.data_plane=cgroup+shared.data_plane=packet_rewrite(interface:["virbr0"])。
所以下表把 eBPF 写成”只有 localcgroup一个数据面”已不符合现状(当前是cgroup+shared两个);scripts/switch-to-tun.sh保留作回退手段。
§11 一度被读成”本机内核不支持 TC 路径”,据此推断 eBPF 的 local cgroup 数据面覆盖不到 rootless 容器
(当时以为是”pasta 不创建宿主 socket”)。这两个前提现已被推翻(见上追注),
而 TUN 靠 auto_route 覆盖转发流量,容器/虚拟机天然被代理。
权衡”eBPF 的新特性”(分应用 UID 策略)与”容器+VM 的完整覆盖”后,当时选择回到 TUN:
| 能力 | TUN | eBPF (cgroup) |
|---|---|---|
| 本机流量接管 | ✅ | ✅ |
| 容器(rootless pasta) | ✅ auto_route 覆盖(本次未复测) | ❌ 看不到(除非 --network=host;2026-10-02 晚复测仍是境外失败、国内正常) |
| 虚拟机 / 共享网络 | ✅ | ✅ 实测通过(用 shared=packet_rewrite,不是 local 的 tc 模式;见 docs/09 §6) |
| 分应用 UID 策略(面板) | ❌ | ✅ |
| 内核依赖 | 需 TUN 设备 | 需 BPF/cgroup(本机满足) |
切换工具:scripts/switch-to-tun.sh(只替换 inbound,保留 DNS 故障转移组、predefined/NXDOMAIN
广告规则、15 份规则集与包路径;check → 备份 → 替换 → 重启 → 健康检查 → 失败自动回滚)。
干跑已验证:生成的配置 check 通过、7 个 DNS 服务器 / 15 份规则集 / 11 条路由规则全部保留。
(追注 2026-10-02 23:01:这两行的”14 份”当时是对的,现已改成实测的 15 份;7 个 DNS 服务器、11 条路由规则两项实测一致。)
备份链完整:
config.*.pre-ebpf.json(TUN 定义来源)、config.*.pre-dataplane.json、config.*.pre-tun.json(切回 TUN 时自动生成),任何一步都能回退。追注(2026-10-02 23:01 实测):
/etc/sing-box/backups/里现有pre-audit(8 个)、pre-ebpf(2 个)、pre-dataplane(3 个),
没有config.*.pre-tun.json(最新一个是config.20261002-214131.pre-audit.json,21:41)。
也就是说”切回 TUN”那一步要么没走这个脚本、要么没留下这个备份,”备份链完整”这句只对前两类成立。
Singborg 02:eBPF 可行性验证与 A/B 试跑
sing-box 能不能用 eBPF(本机 CachyOS 现状实测)
本章目录(11 节)
结论时间:2026-10-02 方法:全部为本机真实二进制 / 真实内核实测,不是推断。
对象:当时正在跑的 sing-box 1.14.2(/etc/sing-box/config.json,mixed-in + tun-in)。
📌 2026-10-02 晚复核(现状,先读这条):本文开头几节的”用不了 eBPF”是自建包之前的状态,已被推翻。
现在/usr/bin/sing-box就是 fork 构建(sing-box-ebpf 1.14.2.ref1nd-2,Tags: …,with_ebpf),
配置是mixed-in+ebpf-in(tun-in已移除),服务正跑在 eBPF 模式下:local=cgroup✅、shared=packet_rewriteonvirbr0✅(共享附件以 TCX 挂载,api ebpf实测role=shared mechanism=tcx framing=ethernet)。
下面凡说”现在用不了 eBPF / 现在的二进制不行”的段落,都按这条读。
0. 一句话结论
你现在装的 sing-box 用不了 eBPF;不是配置问题,是二进制里没有这个入站。
❌ 已被推翻(2026-10-02 晚):上面这句说的是当时装的 Arch 官方包。现在装的是自建
sing-box-ebpf
包(fork 构建,Tags含with_ebpf),eBPF 入站正在用。本节其余事实性描述仍然成立。
eBPF 入站是reF1nd/sing-box分支的实验特性(编译标签with_ebpf,仅 Linux / Android),
上游SagerNet/sing-box没有这个功能。Android 上的 NetProxy 模块正是内置了这个分支内核。你这台机器的内核完全够用——我已经现场编译出带
with_ebpf的 Linux 版二进制,
它能直接读你现有的/etc/sing-box配置(checkexit=0),并对本机跑了内核能力预检。而且不必用 alpha:分支在 stable 基线
v1.14.2-reF1nd(与你现装的 1.14.2 同基线)上同样带
eBPF,我也编好并验证过了(check -C /etc/sing-boxexit=0,且没有 1.15 的tun.stack弃用告警)。
| 目标 | 能否用 eBPF | 依据 |
|---|---|---|
| 当时的 sing-box 1.14.2(Arch 官方包,已卸) | ❌ 不能 | 实测 FATAL: unknown inbound type: ebpf(exit=1) |
现装的 sing-box-ebpf 1.14.2.ref1nd-2(自建包,fork 构建) |
✅ 已在用 | sing-box version 的 Tags 含 with_ebpf;api ebpf 显示 local(cgroup) + shared(tcx) 均已挂载(2026-10-02 晚实测) |
| 本机换用 reF1nd 分支二进制 | ✅ 可以(内核已具备,二进制已编好,见第 4 节) | 见第 4、5 节 |
| Android + NetProxy 模块 | ✅ 设计如此(eBPF 是唯一数据面) | 模块无 TPROXY / REDIRECT 回退,内核不行就起不来 |
1. 为什么当时的二进制不行(实测)
📌 2026-10-02 晚追注(本节保留原文,属历史记录):下面是 Arch 官方包的实测。现在装的
sing-box-ebpf
包Tags里有with_ebpf(sing-box version实测尾部…,badlinkname,tfogo_checklinkname0,with_ebpf),
所以”check报 unknown inbound type”这个故障在当前二进制上不会复现。
$ cat > /tmp/ebpf-test.json # 官方样例的 ebpf 入站
$ sing-box check -c /tmp/ebpf-test.json
FATAL[0000] decode config at /tmp/ebpf-test.json: inbounds[0]: unknown inbound type: ebpf
exit=1
当时(Arch 官方包)二进制的编译标签里没有 with_ebpf:
Tags: with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,
with_tailscale,with_ccm,with_ocm,with_cloudflared,with_naive_outbound,with_usbip,
with_openvpn,with_openconnect,badlinkname,tfogo_checklinkname0
2. eBPF 入站到底是谁的功能
| 项目 | 情况 |
|---|---|
| 上游 SagerNet/sing-box | ❌ 没有:docs/configuration/inbound/ 下无 ebpf.md(404),入站列表只有 tun / redirect / tproxy / mixed / socks / http … |
reF1nd/sing-box(分支 reF1nd-testing) |
✅ 有:eBPF 入站文档,标注”sing-box 1.15.0 中的更改 / 实验功能”,//go:build with_ebpf && (linux || android) |
| eBPF 程序本体 | 在独立依赖 CHIZI-0618/sing-ebpf 里(internal/bpfgen/*_bpfel.o、*_bpfeb.o 预编译内嵌)→ 普通构建不需要 NDK / clang,只要 Go |
| Fanju6/NetProxy-Magisk(Android) | 内置的”sing-box 内核”就是上面这个分支,构建标签含 with_ebpf(见其 .github/resources.json) |
| 分支发布产物 | GitHub Releases 为空(只有 tag;2026-10-02 晚复核仍为 0)→ Linux / Android 二进制都要自己编,Arch 仓库更没有(本机已用自建包 ~/pkgbuild-source/packages/sing-box-ebpf 解决,见 4.1) |
分支自述:“本分支现已加入实验性的 eBPF 入站支持,可在 Linux 与 Root Android 上通过内核 eBPF 程序接管网络连接,
无需创建 TUN 设备,也不依赖传统的 iptables/nftables REDIRECT 或 TPROXY 规则”。
2.1 “alpha 才有?有没有编好的二进制?”——逐条实测
| 问题 | 答案 | 证据 |
|---|---|---|
| eBPF 只在 alpha? | ❌ 不是。1.14 stable 基线的分支 tag 一样有 | v1.14.0-reF1nd / v1.14.1-reF1nd / v1.14.2-reF1nd 的 include/ebpf.go 全部 HTTP 200;v1.14.2-reF1nd 的 option/ebpf.go 里 local/shared/cgroup/packet_rewrite/include_uid 字段与新版一致,protocol/ebpf/ 有 54 个文件 |
| 上游 alpha 有预编译二进制? | ✅ 有,但装了也没 eBPF | 上游 v1.15.0-alpha.9 是 prerelease,带 167 个资产(linux/darwin/windows 各架构、SFA-*.apk、deb/rpm…);但 eBPF 入站源码在上游根本不存在(docs/configuration/inbound/ebpf.md → 404) |
| 分支(reF1nd)有预编译二进制? | ❌ 一个都没有 | GET /repos/reF1nd/sing-box/releases?per_page=100 → count: 0。只有 tag,无任何 asset |
| NetProxy 模块的核是哪来的? | 它自己编的 | 其 .github/workflows/update-resources.yml 用 TAGS=${{ … }} + make -C .sing-box-source build,然后 go version -m .sing-box-source/sing-box | grep -q 'with_ebpf' 自检 |
| 分支自己的 Docker 镜像? | ❌ 不能用 | docker.yml 用的标签集是 release/DEFAULT_BUILD_TAGS*(不含 with_ebpf),且只在 release 事件触发——而分支没有 release |
第三方 Docker Hub eyaoyi/sing-box-ref1nd-stable |
⚠️ 别用 | 最后更新 2026-08,停在 v1.13.19;非官方,且 eBPF 在容器里还要额外处理 cgroup/权限 |
结论:想要 eBPF,二进制只能自己编(或自己打包)。 幸运的是构建只要 Go(BPF 对象是CHIZI-0618/sing-ebpf 模块里预编译内嵌的 *_bpfel.o),
不需要 NDK / clang / CGO。这正好适合你 ~/pkgbuild-source 那套 CI。
3. 你这台机器的内核体检(全部实测通过)
Linux 7.2.8-1-cachyos-bore-lto #1 SMP PREEMPT_DYNAMIC x86_64
| 要求(来自分支的 eBPF 内核要求文档) | 你的机器 | 结果 |
|---|---|---|
CONFIG_BPF / CONFIG_BPF_SYSCALL / CONFIG_BPF_JIT |
y / y / y | ✅ |
CONFIG_CGROUP_BPF(local cgroup 数据面默认) |
y | ✅ |
CONFIG_NET_CLS_BPF / CONFIG_NET_SCH_INGRESS / CONFIG_NET_CLS_ACT |
=m / =m / y |
✅(ko 在 /lib/modules/…/net/sched/,按需加载) |
CONFIG_VETH(local tc 数据面才需要) |
=m |
✅ |
CONFIG_BPF_STREAM_PARSER(SOCKMAP 加速) |
y | ✅ |
| cgroup v2 统一挂载 | cgroup2 /sys/fs/cgroup cgroup2 rw,… |
✅ |
| bpffs | bpf /sys/fs/bpf bpf rw,… |
✅ |
| LPM trie 内核缺陷(6.6.0–6.6.46) | 7.2.8,不在受影响区间 | ✅ |
| 现有 tc 钩子冲突 | tc filter show 所有网卡 无任何 filter(dae 残留早已清净) |
✅(2026-10-02 晚复核:virbr0 上有 libvirt 自己的 htb qdisc + 一条 u32 filter(csum 动作),不是 eBPF 钩子;shared 走的是 TCX,与这类 filter 不冲突) |
3.1 root 预检正式结果:全绿通过 ✅
sudo ~/.cache/sing-box-bin/sing-box-v1.14.2-reF1nd-with_ebpf \
tools ebpf status --mode all --interface virbr0 --json
"result": "preflight_passed",
"preflight": true, "exact_object_load": true,
"summary": { "pass": 51, "warn": 0, "fail": 0, "unknown": 0,
"required_failures": 0, "required_unknowns": 0, "required_issues": 0 }
含义(不是”参数看着对”,是真的把对象喂给 verifier 并加载了):
| 关键 PASS 项 | 说明 |
|---|---|
selected cgroup eBPF object |
真加载了 cgroup 程序与其 map 规格(未挂 hook);coarse-time UDP 变体可用 |
selected packet-rewrite eBPF object |
真加载了 shared 报文改写程序(未挂 classifier) |
BPF map type Array / Hash / LRUHash / LPMTrie / PerCPUArray |
五类 map 全部可建 |
CGroupSockAddr + bpf_get_current_uid_gid |
本机 UID 级(分应用)接管能力就位 |
| IPv4/IPv6 透明 TCP/UDP、original-dst、packet-info | 透明 socket 全套通过 |
BPF JIT |
bpf_jit_enable=1 |
locked-memory limit |
RLIMIT_MEMLOCK 自动调成 unlimited |
TC interface framing virbr0 |
ether 帧 ✅ → 虚拟机共享路径可用(2026-10-02 晚已端到端实测通过,见 3.2 追注) |
LPM trie policy updates |
7.2.8 不在缺陷区间,PASS |
⚠️ 一个必须注意的细节:探测里那条
cgroup path报的是/sys/fs/cgroup/user.slice/…/kitty-30065-0.scope—— 那是”探测进程自己“所在的 cgroup
(我从你终端里跑的),不是接管范围。真正全局接管要么省略local.cgroup_path(挂 cgroup v2 根层级),
要么显式写/sys/fs/cgroup;写成上面那个 scope 就只会代理你终端里跑的程序。
官方也明说”attach permission and exclusivity are still checked during startup“——
挂载权限只在真实启动时校验,所以第一次起服务才是真考验(见 5.2 / 5.4 的兜底方案)。
之前非 root 探测时看到的
UNKNOWN/FATAL: map create: operation not permitted不是内核不支持,是权限不够:/proc/sys/kernel/unprivileged_bpf_disabled = 2,非 root 连一个 map 都建不了。
即 eBPF 相关的check、探测、启动都必须 root。
3.2 A/B 试跑结果(2026-10-02 08:54,同一套判据)
用 trial/run-ebpf-trial.sh 实测(脚本先停 TUN 服务、前台起 eBPF 核心、跑完恢复;脚本在仓库 trial/ 下):
| 判据 | TUN 基线 | eBPF 阶段 | 结论 |
|---|---|---|---|
tun0_state |
present | absent | 真的没有 TUN 设备 |
proxy_exit_ip |
无 TUN 仍在走代理 ✅ | ||
ipv6_http_code |
200 | 200 | IPv6 正常 ✅ |
ipv6_blackhole |
0 | 0 | 无黑洞规则 ✅ |
direct_isp_ip |
国内仍直连 ✅ | ||
resolve_github |
ok | ok | DNS 通 ✅ |
ads_dns_answer |
[](被拦) |
⚠️ 127.0.0.53 timed out(瞬时) |
见下 |
结论:eBPF 数据面在这台机器上完整可用。 尤其重要的是,3.1 里唯一”没被预验证”的点
——root 挂 cgroup v2 根层级的权限/独占性——实际启动时通过了(日志 sing-box started (0.13s),
且流量确实被接管)。分应用所需的 UID hook 也已在预检中确认就位。
两处尚需一次干净重跑才能定论:
ads_dns_answer那次超时:切换瞬间 systemd-resolved 的上游(TUN 的172.19.0.2)刚消失,
属于瞬时;且该判据当时还受 systemd-resolved 自身缓存干扰(脚本 v2 已改为dig @1.1.1.1+
每轮flush-caches)。- 第二轮
SHARED_IFACE=virbr0的”基线”其实是无代理直连(因为第一轮恢复失败),
所以那份 diff 无参考价值;虚拟机共享路径仍未端到端验证(当时virbr0是 DOWN,没有虚拟机在跑)。
✅ 2026-10-02 晚追注:这一条已经补上了。
shared(packet_rewriteonvirbr0)已做端到端实测
(netns + veth 接到virbr0):直连与走代理两条路都通过——Clash API 命中链路=Proxy → Auto → 新加坡Z02,virbr0抓包 11 SYN / 11 SYN-ACK,curl200。
另外,local的tc数据面实测不可用(register TC eBPF TCP listener: operation not supported),
所以 5.2 里”cgroup 被拒就换 tc”的兜底不成立(见 5.2 / 6 的追注)。
🔒 本节表格里 TUN 基线的出口 IP 原文是真实代理出口地址,公开仓库已脱敏为<proxy-exit-ip>。
4. 我现场编译出来的二进制(已放好)
用 podman 里的 Go 1.26.8 编的分支 reF1nd-testing(HEAD bccceeb):
podman run --rm -v /tmp/sb-fork:/src -v gomodcache:/go/pkg/mod -w /src \
-e CGO_ENABLED=0 -e GOOS=linux -e GOARCH=amd64 \
docker.io/library/golang:1.26 sh -c "go build -trimpath -tags '$TAGS' \
-ldflags \"-s -w -X 'github.com/sagernet/sing-box/constant.Version=v1.15.0-alpha.9-reF1nd'\" \
-o /src/sing-box-ebpf ./cmd/sing-box"
产物(两个版本都编好并验证过,可任选;推荐 stable 基线的那个):
| 路径 | 版本 | sha256 | 大小 |
|---|---|---|---|
~/.cache/sing-box-bin/sing-box-v1.14.2-reF1nd-with_ebpf |
v1.14.2-reF1nd(与你现在同基线) |
87583b8695369f6a340fb1a4fe27dc652b59e02ff59d3a50d658fc2e632164f3 |
86.8 MB |
~/.cache/sing-box-bin/sing-box-v1.15.0-alpha.9-reF1nd-with_ebpf |
v1.15.0-alpha.9-reF1nd |
8a55dfe9125164de26afb7a889bcf6458922fbfd76d34046f51a9695d2dee1e4 |
88.5 MB |
实测结果(都在你本机跑的):
| 实测项 | v1.14.2-reF1nd |
v1.15.0-alpha.9-reF1nd |
|---|---|---|
version |
go1.26.8 linux/amd64 ✅ |
go1.26.8 linux/amd64 ✅ |
编译标签含 with_ebpf |
✅ | ✅ |
check -C /etc/sing-box(你现在的 TUN 配置) |
exit=0,零告警 ✅ | exit=0,唯一告警:tun.stack 在 1.15 弃用 |
check -c <tun 换成 ebpf 入站的配置> |
非 root 下 exit=1,原因是 map create: operation not permitted —— 权限门,不是配置错误(官方样例配置在该权限下同样失败) |
同上 |
tools ebpf status 内核预检 |
命令可用,能逐项报能力 | 同左 |
注意一个反直觉点:这个分支的
sing-box check遇到 ebpf 入站会真的去建 eBPF map,
所以”非 root 时check失败”≠ 配置写错。字段名我已对齐option/ebpf.go,无 unknown field。
4.1 用你自己的 ~/pkgbuild-source 打包(已完成)
✅ 已落地:
~/pkgbuild-source/packages/sing-box-ebpf/(PKGBUILD + .SRCINFO)。
容器内真实makepkg构建通过(产物 25.5 MB,含二进制 + 两个 systemd 单元 + license),
namcap 仅两条 Go 静态二进制常态告警(lacks FULL RELRO/lacks PIE,退出码 0,不会中断 CI)。
配套的管理界面见docs/03-面板指南.md(源码在~/pkgbuild-source/packages/sing-box-panel/)。
你那个仓库(CI 从 packages/<name>/{PKGBUILD,.SRCINFO} 构建 → 发到 repo 分支 → pacman 直接装)
正好解决”没有预编译二进制”这件事:一次打包,以后 pacman 升级,不用手工编。packages/daed-emo/ 就是现成模板(同样是 Go 构建 + 自带 systemd 单元)。
建议新增 packages/sing-box-ebpf/,要点:
source=('git+https://github.com/reF1nd/sing-box.git#tag=v1.14.2-reF1nd') # stable 基线;要 1.15 特性再换 tag
makedepends=('base-devel' 'git' 'go')
provides=('sing-box') # 装的是 /usr/bin/sing-box,沿用发行版单元
conflicts=('sing-box' 'sing-box-git')
# 构建标签务必含 with_ebpf(否则就是普通的 sing-box,白装):
# with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,
# with_tailscale,with_ccm,with_ocm,with_cloudflared,with_usbip,with_openvpn,
# with_openconnect,badlinkname,tfogo_checklinkname0,with_ebpf
with_naive_outbound不要带(Linux +CGO_ENABLED=0编不过,见第 8 节)。conflicts=('sing-box')是刻意的:两个透明代理不能共存,装这个就等于顶掉官方包。- 打包时顺手把发行版单元的 capability 限制补上
CAP_BPF CAP_PERFMON(见 5.2),否则服务起不来。
(2026-10-02 晚实测:自建包最终用的是User=root+ 不设CapabilityBoundingSet+LimitMEMLOCK=infinity,
这样最省事,不需要逐个补 CAP。)
我没有直接改你的仓库——你说一声我就按仓库约定加上
PKGBUILD+.SRCINFO(并用makepkg --printsrcinfo自检)。
5. 真要切到 eBPF,需要改什么
5.1 配置:tun-in → ebpf-in(mixed-in 保留)
{
"type": "ebpf",
"tag": "ebpf-in",
"network": ["tcp", "udp"],
"local": {
"enabled": true,
"data_plane": "cgroup",
"dns_mode": "respect_policy",
"bypass_private_address": true,
"ipv6": true,
"exclude_uid": [ "<可选:不需要走代理的 UID>" ]
},
"shared": { "enabled": false }
}
端口 53:由
dns_mode在内核侧接管(hijack先接管再筛 UID,respect_policy先筛再接管),
替代 TUN 时代的hijack-dns路由动作;原规则留着无害,可删。IPv6:由
local.ipv6显式开关,不再需要为了 v6 去给 TUN 配地址,也不会再出现ip -6 rule里那条unreachable黑洞(那是 TUN 模式的防泄露设计)。分应用(Linux 上 TUN 做不到的事):
include_uid/exclude_uid/*_uid_range;include_package/exclude_package是 Android 专用,桌面别用。局域网 / 虚拟机共享:
shared填的是下游接口,不是上游!
你这台机器上默认出口是wlan0,所以不要填 wlan0;要让 libvirt 的虚拟机也走代理,应填virbr0:"shared": { "enabled": true, "data_plane": "packet_rewrite", "interface": ["virbr0"] }且 shared 模式自己不做 NAT / DHCP / IPv6 RA(那些仍由 libvirt dnsmasq 负责),
只支持以太网帧接口(raw-IP / PPP / 隧道链路要换socket_assign)。一个实例只能有一个 ebpf 入站做 local 接管;绝对不要 TUN 与 eBPF 同时开(等于两个透明代理)。
5.2 systemd:当时的发行版单元跑不了 eBPF
✅ 2026-10-02 晚复核:这条建议已经落地了。 现在用的是自建包自带的单元
(/usr/lib/systemd/system/sing-box.service,来自sing-box-ebpf),实测就是User=root+ 完全不限制CapabilityBoundingSet+LimitMEMLOCK=infinity;systemctl show sing-box显示ActiveState=active、NRestarts=0,不需要逐个补CAP_BPF/CAP_PERFMON。
下面这段是旧单元的体检记录(保留原文)。
发行版单元是 User=sing-box + CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
——没有 CAP_BPF/CAP_PERFMON,而且 local cgroup 数据面要挂到 cgroup v2 根层级(特权操作)。
建议照 daed 的老路:用 root 跑(User=root 或干脆去掉 User=),capability 限制放宽或至少补CAP_BPF CAP_PERFMON CAP_NET_ADMIN CAP_SYS_ADMIN;ExecStart 的 -D /var/lib/sing-box -C /etc/sing-box 不用动。
若坚持非 root,cgroup_path 指到自己被 systemd 委派的子树,但那样只能接管该子树内的进程,不是全局透明。
兜底(root 挂根 cgroup 被拒时):把 local.data_plane 从 cgroup 换成 tc
(跟随默认接口,预检里 veth / 策略路由 / socket lookup 能力都已 PASS),代价是依赖默认接口、需要建 veth。
❌ 2026-10-02 晚推翻:这条兜底不成立。 本机
local的tc数据面实测起不来:FATAL start service: start inbound/ebpf[ebpf-in]: register TC eBPF TCP listener: operation not supported。
教训:预检(不挂 hook 的加载探测)PASS 不等于能挂上并跑通;cgroup挂载若被拒,没有tc这条退路。
5.3 预检(已跑过:preflight_passed,51 PASS / 0 fail)
sudo ~/.cache/sing-box-bin/sing-box-v1.14.2-reF1nd-with_ebpf \
tools ebpf status --mode all --interface virbr0 --json
结果见 3.1。FAIL 才是真缺能力;UNKNOWN 表示探测被安全策略挡住,需要换权限重跑。
注意删掉 --interface virbr0 就没探测虚拟机共享路径,反之不想要共享路径可只测 --mode local。
5.4 切换顺序与回滚(已于 2026-10-02 10:37 执行成功,脚本:switch-to-ebpf.sh)
✅ 现成脚本(仓库
scripts/目录):sudo scripts/switch-to-ebpf.sh(可选--shared virbr0、--shared-plane packet_rewrite|socket_assign)。
它做完整安全管线:内核预检 → tun→ebpf 改配置 →check→ 备份 → 原子替换 → 重启 →
eBPF 专属健康检查(api ebpf附件状态 + 不经代理的请求是否走代理 + 国内是否仍直连 + DNS 是否仍拦)
→ 任一失败自动回滚到 TUN。下面是不用脚本时的等价手工步骤。
sudo cp -a /etc/sing-box/config.json /etc/sing-box/config.json.pre-ebpf.$(date +%Y%m%d-%H%M%S) # 1 备份
sudo scripts/switch-to-local-rules.sh --dry-run # 2 仍用旧的 TUN 脚本预览
sudo systemctl stop sing-box # 3 停服务
# 4 用新二进制 + ebpf 配置在前台手跑,盯日志;出问题 Ctrl-C 即回到无代理状态
sudo ~/.cache/sing-box-bin/sing-box-v1.15.0-alpha.9-reF1nd-with_ebpf -D /var/lib/sing-box -C /etc/sing-box run
# 5 没问题再换 systemd 单元(第 5.2 节)并 systemctl start sing-box
验证四条(与现有审计报告同一套判据):
curl -s https://myip.ipip.net # 期望:你的电信 IP(直连)
curl -s https://api.ipify.org # 期望:代理节点 IP
curl -6 -s -o /dev/null -w '%{http_code}\n' https://ipv6.google.com # 期望 200
ip -6 rule show | grep -c unreachable # 期望 0
回滚:systemctl stop sing-box → 把 config.json.pre-ebpf.* 覆盖回 /etc/sing-box/config.json →
换回 /usr/bin/sing-box(1.14.2 + TUN)与发行版单元 → systemctl start sing-box。
6. 代价 / 什么时候值得
| 说明 | |
|---|---|
| ⚠️ 版本 | 分支基线的 tag 可选 stable(v1.14.2-reF1nd,与你现装同基线)或 alpha(v1.15.0-alpha.9-reF1nd);eBPF 本身不是 alpha 专属 |
| ⚠️ 维护 | 无 Linux release 产物、无 Arch 包 → 每次升级都要自己 podman/Go 重编;建议用 ~/pkgbuild-source 打包解决(见 4.1) |
| ⚠️ 覆盖面 | local cgroup 只管本机 socket;转发流量(libvirt VM)不在内,那要 shared + virbr0(已端到端实测 ✅)。❌ 原文”含 rootless podman”是错的:2026-10-02 晚实测 rootless podman(pasta)容器仍走不了代理——境外失败、国内直连与 DNS 劫持正常、--network=host 可用;pasta 属用户态转发,local 与 shared 两条路都不覆盖它。而 TUN 的 auto_route 是一把全包住 |
| ⚠️ 首次启动 | root 挂根 cgroup 的权限/独占性只在真启动时校验(3.1)——实际启动通过了(api ebpf 可见 role=local mechanism=cgroup,服务 NRestarts=0);tc 数据面tc 不可用(见 5.2 追注),没有这条退路 |
| ✅ 收益 1 | 分应用分流(include_uid/exclude_uid)——这是 TUN 在 Linux 上给不了的,且预检确认 bpf_get_current_uid_gid 可用 |
| ✅ 收益 2 | 不建 TUN 设备、无 TUN 拷贝,理论上更省;不用 iptables/nftables REDIRECT/TPROXY |
| ✅ 收益 3 | type: "group" 故障转移组用上type: "group" 在 1.14.2-reF1nd 上就能用——线上配置的 dns-proxy / dns-direct 正是 type: "group",不需要 1.15 |
| ✅ 现状对比 | 你这套 TUN 已经过完整审计:DNS 劫持完整、v6 已修、podman 透明、并发/大文件都过。没有刚需就别换;当年 daed 的 eBPF 正是把 DNS 和 IPv6 TCP 弄坏才被换掉的 |
建议(预检通过后的版本):
能力门已经过了(51 PASS / 0 fail,连 cgroup 与 packet_rewrite 对象都真加载过),所以剩下的是行为风险,不是”能不能跑”。
顺序:先打包(4.1)→ 再备份/前台试跑/回滚(5.4);只在你要”分应用”或”去掉 TUN”时才真正切。
只是想尝鲜的话,成本最低的试法是:不开机自启,前台跑一次、跑完立刻 Ctrl-C,然后用审计报告里那套判据(DNS 四源一致、
国内直连 IP、curl -6200、ip -6 rule无 unreachable、podman 出口 IP)打一遍分。✅ 2026-10-02 晚复核:切换已经做了(见 5.4),当日晚又补完了 shared 的端到端验证;服务至今
ActiveState=active、NRestarts=0。唯一没被”审计判据”覆盖的是 rootless podman(pasta):
它在 eBPF 下仍走不了代理(见上表”覆盖面”行)。
7. Android(NetProxy 模块)怎么判断能不能用
- 模块只提供 eBPF 透明代理入站,没有 TPROXY/REDIRECT 回退:内核能力不足 → 透明代理根本起不来。
- 厂商内核可能关掉/回移部分 BPF 能力,不能只看 Android 或内核版本号,要用模块自带探测:
su -c '/data/adb/modules/netproxy/netproxyctl ebpf status configured'
su -c '/data/adb/modules/netproxy/netproxyctl ebpf status all --raw'
- 默认数据面:本机
cgroup(EBPF_LOCAL_DATA_PLANE=cgroup),共享网络packet_rewrite(EBPF_SHARED_ENABLED=0默认关)。 - 需要:Magisk / KernelSU / APatch + Root;内核支持 BPF、TC classifier、透明 socket、socket lookup;本机路径还要 veth 与策略路由(未复核:本机实测
tc数据面在 7.2.8 上不可用、而cgroup路径不依赖 veth,这句是照模块文档转述的,没有实机验证)。 - 它的”sing-box 配置”与你的
/etc/sing-box/config.json不是一份东西:它用runtime/现生成的 Provider + eBPF 配置,
静态主配置在config/singbox/config.json。你的 TUN 配置思路(规则集、DoH、selector)可以照搬,但tun入站整段要去掉。
8. 复现 / 清理
# 重编(缓存已在 podman 卷里,所以很快;需要 podman,不需要装 Go)
TAGS="with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,with_cloudflared,with_usbip,with_openvpn,with_openconnect,badlinkname,tfogo_checklinkname0,with_ebpf"
# stable 基线(推荐;/tmp/sb-fork-1142 已存在,可直接复用)
git clone --depth 1 --branch v1.14.2-reF1nd https://github.com/reF1nd/sing-box.git /tmp/sb-fork-1142
# 或者最新分支:--branch reF1nd-testing
cd /tmp/sb-fork-1142 && podman run --rm -v "$PWD":/src -v gomodcache:/go/pkg/mod \
-v gomodcache-build:/root/.cache/go-build -w /src \
-e CGO_ENABLED=0 -e GOOS=linux -e GOARCH=amd64 -e GOTOOLCHAIN=local \
docker.io/library/golang:1.26 sh -c "go build -trimpath -ldflags '-s -w' -tags '$TAGS' -o /src/sing-box-ebpf ./cmd/sing-box"
# 清理我留下的构建缓存(约 2 GB,放着你下次重编会快很多)
podman volume rm gomodcache gomodcache-build
podman rmi docker.io/library/golang:1.26
坑:
with_naive_outbound在 Linux 上CGO_ENABLED=0编不过
(cronet-go/lib/linux_amd64: build constraints exclude all Go files),所以标签里没带它——
你的订阅是 Shadowsocks,用不到 naive。
9. 踩坑记录:cache.db 属主(试跑时踩到,已修)
📌 2026-10-02 晚追注(本节保留原文,属历史记录):现行单元是
User=root(自建sing-box-ebpf包),/var/lib/sing-box/cache.db现为root:root 644,所以”被改成 emo 属主导致服务写不了”这个具体故障
在当前单元下不会复现;但下面两条铁律(别拿线上配置跑非 rootcheck、active≠ 就绪)仍然成立。
⚠️ 另:本次没能复现”check初始化 cache-file”这一步——把cache_file.path指到可写临时目录后sing-box checkexit=0 且没有生成文件;带ebpf入站时则更早地在map create: operation not permitted
失败。所以”check走到服务初始化”是真的(会去建 eBPF map),但”它顺手重建了cache.db“这一环存疑。
症状:systemctl restart sing-box 后服务 active 一秒即挂,无限重启循环,tun0 消失:
FATAL[0000] start service: initialize cache-file: open /var/lib/sing-box/cache.db: permission denied
根因:/etc/sing-box/config.json 里 experimental.cache_file.path 是绝对路径/var/lib/sing-box/cache.db,而非 root 身份跑 sing-box check -c/-C <线上配置> 也会真的初始化
cache-file(这个 fork 的 check 会走到服务初始化)。于是该文件被重建为 emo:emo 644,
而服务用户是 sing-box(目录 755,只有属主可写)→ 服务再也写不了自己的缓存。
当时老进程还握着旧 inode,所以表面无恙,直到重启才炸。
修复(一条命令,缓存可丢弃):
sudo chown sing-box:sing-box /var/lib/sing-box/cache.db && sudo systemctl restart sing-box
# 或者干脆:sudo rm -f /var/lib/sing-box/cache.db && sudo systemctl restart sing-box
两条铁律:
- 不要用非 root 对线上配置跑
sing-box check/run。要验证就先复制一份,并把experimental.cache_file.path改到/tmp(试跑脚本 v2 已内置这个隔离)。 systemctl报active≠ TUN 已就绪:实测有 1 秒级竞态,is-active为真时tun0
可能还没建出来。判断健康要is-active+ip link show tun0一起看(脚本 v2 已改成轮询 10s)。
(eBPF 模式下没有tun0,判据要换成api ebpf的附件状态 + “不设代理的出口 == 经代理的出口”,见 09 文档 §9。)
Singborg 03:sing-box Panel 面板指南
sing-box 控制台(sing-box-panel)使用与设计
本章目录(9 节)
~/pkgbuild-source/packages/sing-box-panel/,包名sing-box-panel,与sing-box-ebpf解耦(TUN / eBPF 两种模式都能用)。
后端 Python3 标准库,前端 单页无构建,运行状态页 内嵌 zashboard(MIT,随包分发,不联网加载)。
1. 它解决什么问题
MetaCubeXD / zashboard 这类面板只能读 Clash API,也就是”看”运行状态。你自己的需求里有三件事它们做不到:
| 需求 | Clash 类面板 | 本面板 |
|---|---|---|
| 运行状态(连接/流量/延迟/日志) | ✅ | 内嵌 zashboard + 自己的服务卡片与 eBPF 探针 |
| 节点 / 订阅 | ❌ | 抓取 → 解析 → 写入 outbounds → 校验 → 重启 |
| 分应用策略 | ❌ | 写 eBPF 入站的 include_uid / exclude_uid |
| 服务控制 / eBPF 能力预检 | ❌ | systemd + sing-box tools ebpf status |
| 配置安全应用 | ❌ | 备份 → check → 原子替换 → 重启 → 健康检查 → 失败自动回滚 |
2. 安装与启动
# 从私有仓库(CI 构建产物)
sudo pacman -S sing-box-panel
# 或者用本地构建的包
sudo pacman -U sing-box-panel-0.1.0-5-any.pkg.tar.zst
sudo systemctl enable --now sing-box-panel
sudo cat /var/lib/sing-box-panel/token # 访问令牌(0600;服务首次启动时自动生成,PANEL_TOKEN 可覆盖)
然后浏览器打开 http://127.0.0.1:9096/,把上面的令牌粘进去。令牌存在浏览器 localStorage,点右上角「锁定」可清除。
面板只监听 127.0.0.1;所有
/api/*请求都要求X-Panel-Token。静态页面本身不含任何机密。
3. 四个页签
3.1 运行状态
- 服务卡片:active/enabled、PID、启动时间;启动/重启/重载/停止按钮(
systemctl)。 - 核心卡片:sing-box 版本、入站模式(eBPF / TUN)、配置文件路径与修改时间。
- 实时流量:连接数、累计上/下行(轮询 Clash API
/connections,3s 一次)。 - eBPF 内核能力预检:点一下就跑
sing-box tools ebpf status --mode all --json,
逐条列出 PASS / FAIL / UNKNOWN。这是”改配置前先确认内核吃不吃得下”的按钮,
它不挂载程序、不改动任何状态。要测虚拟机共享路径就在输入框填下游接口(例如virbr0)。 - 日志:
journalctl -u sing-box最近 200 行。 - 集成卡片:Clash API 地址、密钥(一键复制),以及三个入口 ——
zashboard(内嵌,深链#/setup?...&secret=…自动带密钥,点开即配好)、官方 dashboard(127.0.0.1:9091/dashboard/,讲 sing-box 原生 API,排障用)、
MetaCubeXD(只在 sing-box 配置里启用clash_api.external_ui时才出现,见 §9)。
3.2 节点与订阅
- 订阅:填 URL → 添加并拉取。支持
- base64 编码的链接列表 / 纯链接列表:
ss://(SIP002 与 legacy)、vmess://、vless://、trojan://、hysteria2:///hy2://、tuic:// - Clash YAML(
proxies:);装了python-yaml用它,没装则用内置子集解析器 - 暂不支持的类型(如
snell)会被跳过并在提示里列出,不会污染配置
- base64 编码的链接列表 / 纯链接列表:
- 写入配置:把启用订阅的节点写入
outbounds,并把它们加进托管策略组。 - 策略组与节点:从 Clash API 实时读;点节点即切换,点「测」做单点延迟测试,「整组测速」批量测。
- 订阅抓取默认走本机代理口(
subscription_proxy,默认http://127.0.0.1:7892),
这样机场订阅在国内也能拉到;不需要就把它设成空串。
托管契约(很重要):面板只碰自己上次写进去的东西——managed.json 记录 outbound_tags(自己加的节点)与 group_members(自己往组里加的成员)。
重新写入时先删掉这些、再加新的,不会动你手工维护的节点和组成员(例如你的 Download 组、
地区组里的手写成员)。默认托管组是 Proxy(selector)与 Auto(urltest),可在面板配置里改。
3.3 分应用策略
- 模式:关闭 / 黑名单(选中的不走代理)/ 白名单(只有选中的走代理)。
- 应用列表来自
.desktop条目 +/proc扫描,按 UID 聚合(显示进程数与可执行文件)。 - 也可以手工加 UID。点「应用策略」写回 eBPF 入站的
local.include_uid/exclude_uid,
然后走安全管线重启核心。 - Linux 上策略粒度就是 UID(同一 UID 的多个进程一起生效);
include_package是 Android 专用字段,桌面不要用。
3.4 配置与服务
- 配置编辑器:整份
config.json,可「仅校验」(sing-box check)或「应用并重启」。 - 应用流程:JSON 解析 →
sing-box check→ 备份 → 原子替换 →systemctl restart→ 健康检查
(is-active,TUN 模式再等tun0,可选再经代理口探一个 URL)→ 任何一步失败就写回旧配置并重启。 - 备份与回滚:每次应用前自动备份到
/etc/sing-box/backups/(保留最近backup_keep份,默认 30),
列表里可一键恢复。恢复本身也走同一条安全管线。
4. 配置文件:/etc/sing-box/panel.json(可选)
面板在缺少该文件时用内置默认值。示例见包里的 /usr/share/sing-box-panel/panel.json.example:
| 键 | 默认 | 说明 |
|---|---|---|
listen / port |
127.0.0.1 / 9096 |
监听地址与端口 |
sing_box_config |
/etc/sing-box/config.json |
被管理的 sing-box 配置 |
service |
sing-box |
systemd 单元名 |
sing_box_bin |
/usr/bin/sing-box |
用于 check / tools ebpf |
clash_api |
http://127.0.0.1:9090 |
Clash API 地址(密钥自动从 sing-box 配置的 experimental.clash_api.secret 读取) |
state_dir |
/var/lib/sing-box-panel |
令牌、subscriptions.json、managed.json |
subscription_proxy |
http://127.0.0.1:7892 |
拉订阅走的代理;空串=直连 |
managed_group / managed_urltest |
Proxy / Auto |
托管组名;设空串则不动组 |
restart_timeout |
20 |
重启后等健康检查的秒数 |
health_probe_url |
空 | 设了就用它做端到端探测(经本机代理口 curl);空=只查服务状态 |
backup_keep |
30 |
保留的备份份数 |
5. 安全模型
- 只监听本机 + 令牌鉴权(
secrets.compare_digest常量时间比较);令牌文件 0600。 - 以 root 运行(systemd 单元里写明原因):写
/etc/sing-box/、systemctl、以及sing-box check遇到 eBPF 入站时会真的创建 BPF map(非 root 必然失败)。 - 没有任意命令执行接口:只有固定的几类操作(服务控制、配置读写、订阅、策略、探针、日志)。
- 静态目录做了路径穿越防护(必须是
ui_dir/zashboard_dir的子路径)。 - 写配置永远先校验、先备份,失败自动回滚——这条是本面板存在的主要理由。
6. 排障
| 现象 | 原因 / 处理 |
|---|---|
面板起不来,日志 Address already in use |
端口被占;改 panel.json 的 port |
| 打开面板一直要求令牌 | 令牌在 /var/lib/sing-box-panel/token;sudo cat 查看 |
「应用并重启」提示 check 未通过 … operation not permitted |
面板不是以 root 运行(eBPF 配置的 check 需要建 BPF map)。用 systemd 单元启动,别用普通用户前台跑 |
| 订阅更新失败 | 检查 subscription_proxy 是否可达(默认走 127.0.0.1:7892),或把机场域名加进直连规则 |
| zashboard 里没有数据 | 面板里的入口现在会自动带上密钥(#/setup?…&secret=…)。若你手动输地址,密钥是 experimental.clash_api.secret(你这里是 <clash-api-secret>) |
| 想让面板管理别的组名 | 改 managed_group / managed_urltest,再点一次「写入配置」 |
| 手工改过配置后想同步 | 「配置与服务」里点「重新载入文件」 |
7. 已经做过的验证(本机实测)
| 验证项 | 结果 |
|---|---|
| 后端语法 + dry-run 启动 | ✅ py_compile;--dry-run 下不碰 systemd,配置写 /tmp |
| 令牌门 | ✅ 无令牌 /api/state → 401;/api/ping 免鉴权仅报告版本 |
| 静态服务 | ✅ /、/app.js、/style.css 200;/dash/ 与 /dash/assets/*.js 200(zashboard 相对路径直接用) |
| 订阅解析 | ✅ ss(SIP002/legacy)、vmess、vless(reality+grpc)、trojan(ws)、hysteria2(obfs)、tuic、Clash YAML 全部解析;snell 正确跳过 |
| 发现并修复的解析 bug | ✅ trojan 链接缺 security= 时丢失 SNI → 现在保留 |
| 订阅写入 + 幂等 | ✅ 出站 24→24(不重复堆叠),Proxy 17→19、Auto 11→13,managed.json 只记自己加的东西 |
| 分应用写回 | ✅ 白名单 → include_uid(去重)、黑名单 → exclude_uid、关闭 → 清空 |
| 打包 | ✅ makepkg --printsrcinfo 与 .SRCINFO 一致;仓库自带 check-package.sh 通过;容器内 makepkg 构建 + 安装 + 运行 --version 全通 |
| namcap | ⚠️ 只剩 2 条误报(退出码 0,不会中断 CI):python3 未声明(Arch 的 python 本身就 Provides: python3);sing-box 可能不需要(面板通过 sing-box check / tools ebpf status 调用它,namcap 看不到) |
构建产物(2026-10-02 晚复核并更新版本号):~/.cache/pkgbuild-out/sing-box-panel-0.1.0-5-any.pkg.tar.zst、~/.cache/pkgbuild-out/sing-box-ebpf-1.14.2.ref1nd-2-x86_64.pkg.tar.zst。
8. 第一次装上后建议这样验证
sudo pacman -U ~/.cache/pkgbuild-out/sing-box-ebpf-*.pkg.tar.zst # 会用 eBPF 版顶掉官方 sing-box
sudo pacman -U ~/.cache/pkgbuild-out/sing-box-panel-*.pkg.tar.zst
sudo systemctl daemon-reload
sudo systemctl restart sing-box # 先仍用 TUN 也没关系,面板两种模式都能管
sudo systemctl enable --now sing-box-panel
sudo cat /var/lib/sing-box-panel/token # 浏览器打开 http://127.0.0.1:9096/ 后粘贴
确认面板三个只读动作正常:运行状态卡片有数据 → eBPF 预检 能跑出 PASS → zashboard ↗ 能打开并显示节点。
然后才是把 config.json 里的 tun 入站换成 ebpf(面板「配置与服务」里直接改,校验+回滚都有)。
追注(2026-10-02 晚):本节写于 TUN 时代,上面这条”把 tun 入站换成 ebpf”的迁移已经完成 ——
当前/etc/sing-box/config.json只有mixed+ebpf两个入站(local.data_plane: cgroup、shared.data_plane: packet_rewriteonvirbr0,TCX 挂载),没有tun入站,tun0设备也不存在。
首次装机的验证流程本身仍然适用,只是”先仍用 TUN”这句已成为历史。
9. 运行时面板:只留 zashboard
决定(2026-10-02):保留 zashboard,去掉 MetaCubeXD。
| zashboard | MetaCubeXD | |
|---|---|---|
| 活跃度 | ★3.5k,2026-09-30,v3.29.1 | ★4.4k,2026-09-30 |
| 观感 | 更现代(PWA、连接 GeoIP/地球视图、多后端 Clash+dae) | 成熟但界面偏旧 |
| 在你这套里的形态 | 随面板离线内嵌(2.9 MB,dist-no-fonts),深链自动带密钥,启动不依赖网络 | 由 sing-box 启动时联网下载到 /var/lib/sing-box/ui,多一个动态件 |
| API | Clash(+ dae) | Clash |
面板里的 MetaCubeXD 入口是数据驱动的:只有配置里存在 clash_api.external_ui 才显示,
所以摘掉配置后按钮自动消失,不用改面板代码。
摘掉 MetaCubeXD(先备份配置、check 通过才重启):
sudo python3 - <<'INNER'
import json, shutil, time
p = '/etc/sing-box/config.json'
shutil.copy2(p, f'/etc/sing-box/config.json.bak.{time.strftime("%Y%m%d-%H%M%S")}')
d = json.load(open(p))
ca = d.get('experimental', {}).get('clash_api', {}) or {}
gone = {k: ca.pop(k) for k in ('external_ui', 'external_ui_download_url', 'external_ui_download_detour') if k in ca}
json.dump(d, open(p, 'w'), ensure_ascii=False, indent=2)
print('已从 clash_api 移除:', gone)
INNER
sudo /usr/bin/sing-box check -C /etc/sing-box && sudo systemctl restart sing-box
sudo rm -rf /var/lib/sing-box/ui # 删掉已下载的 MetaCubeXD 静态文件
验证:curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:9090/ui/ 不再是 200;
面板刷新后 MetaCubeXD 按钮消失;zashboard 仍正常(密钥由面板深链自动带上)。
顺带:摘掉后
http_clients与route.default_http_client就没有使用者了(当初只为下载面板而生),
留着无害,想干净也可以一并删掉。更正(2026-10-02 晚实测):上面这句不成立 —— 摘掉
external_ui后这两个仍然有使用者:
当前配置里route.default_http_client = "proxy-rules",且services[api].dashboard.http_client = "proxy-rules"
(官方 dashboard 用它取静态资源)。所以不要跟着删,删了 dashboard 会失去出站。
可核对:curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:9091/dashboard/→200;
而/var/lib/sing-box/ui目录确实已不存在(ls /var/lib/sing-box/ui→ 没有那个文件或目录)。
Singborg 04:sing-box 功能测试报告
sing-box 功能实测报告(DNS 泄露 / 广告拦截 / 分流 / Podman)
本章目录(6 节)
测试时间:2026-10-01 20:4x 环境:CachyOS,sing-box 1.14.2,TUN(stack: system),规则集全部本地化
测试方式:全部为真实流量实测(dig / curl / podman 容器),非推断。
追注(2026-10-02 晚):本文全部在 TUN 模式下完成,属于当时快照。当前运行模式已是 eBPF
(local.data_plane: cgroup+shared.data_plane: packet_rewriteonvirbr0,TCX 挂载;本机没有tun0)。
下文凡依赖tun0/TUN 接管路径的结论都只在 TUN 模式下成立,各节末尾的”追注”给出 eBPF 模式复测结果。
一、DNS 泄露 —— 53 端口已完全劫持 ✅
判据:同一个域名问 4 个不同 DNS 服务器,看答案是否被统一。
| 域名 | @192.168.31.1(路由器/电信) | @223.5.5.5(阿里) | @1.1.1.1(CF) | 系统 resolved |
|---|---|---|---|---|
www.google.com |
142.251.152.119 | 142.251.150.119 | 142.251.153.119 | 142.251.151.119 |
四个来源返回的都是真实 Google IP(142.251.x)。若未劫持,路由器/电信 DNS 对 google 会返回被污染的国内 IP。
→ 所有 53 端口查询都被 sing-box 接管,系统 DNS、路由器 DNS 都不再被直接使用。
resolvectl query github.com→-- link: tun0,查询确实进入隧道- 本地 53 监听者:
127.0.0.53/127.0.0.54(systemd-resolved)、192.168.122.1(libvirt dnsmasq,供虚拟机用)—— 这三条最终都会经过主机 → TUN → 被劫持
追注(2026-10-02 晚,eBPF 模式):劫持结论仍然成立,但路径不是 TUN:
resolvectl query github.com现在显示-- link: wlan0,/etc/resolv.conf走127.0.0.53存根,
wlan0 的上游是路由器(fd00:6868:6868::1/192.168.31.1);本机没有tun0,ip rule/ip -6 rule里也没有 TUN 规则。
劫持发生在 eBPF local cgroup 数据面(本机进程的 53 流量在 cgroup 层被接走)。
复测口径与本文一致:@192.168.31.1、@223.5.5.5、@1.1.1.1、系统 resolved 问www.google.com
四个来源返回的都是同一批真实 Google IP(142.251.x)。
⚠️ 一个需要你知道的设计取舍(不是 bug)
| 域名类型 | DNS 走向 | 运营商能看到吗 |
|---|---|---|
海外域名(geolocation-!cn、google、AI) |
DoH(Cloudflare / Google)经代理 | ❌ 看不到 |
国内域名(geosite/cn、apple@cn) |
明文 UDP 发往 223.5.5.5(阿里公共 DNS,直连) | ✅ 能看到你查了哪些国内域名 |
这是国内分流的标准取舍(为了拿到准确的国内 CDN 解析)。想彻底不留痕可以改成 DoH —— 这正是
NetProxy-Magisk 的做法(它的 ali/tencent 都是type: "https",并用 hosts 服务预解析 DoH 域名以打破自举循环):
"servers": [
{ "type": "hosts", "tag": "hosts-bootstrap",
"predefined": { "dns.alidns.com": ["223.5.5.5", "223.6.6.6"], "doh.pub": ["120.53.53.53", "1.12.12.12"] } },
{ "type": "https", "tag": "dns-direct-ali", "server": "dns.alidns.com",
"domain_resolver": "hosts-bootstrap", "detour": "direct" },
{ "type": "https", "tag": "dns-direct-tencent", "server": "doh.pub",
"domain_resolver": "hosts-bootstrap", "detour": "direct" }
]
代价:多一次 TLS 握手(有缓存影响很小);若阿里 DoH 不可用,国内解析会失败。要不要上,你决定,我可以直接改好并校验。
更正(2026-10-02 晚实测):上面”国内域名 = 明文 UDP 发往 223.5.5.5 → 运营商能看到”这一行
已不适用于当前配置,这段建议已经落地(不再是可选项):/etc/sing-box/config.json里dns-direct-ali(type: https→dns.alidns.com)与dns-direct-tencent(doh.pub)都用hosts-bootstrap预解析、直连 443,dns-direct组也被apple@cn/cn规则引用。
也就是说国内解析走 DoH,运营商看不到你查了哪些国内域名(只剩到 DoH 端点的 SNI/连接元数据)。
eBPF 模式下结论相同(dig复测结果与 TUN 时代一致)。
二、广告拦截 —— 连接层 ✅ / DNS 层曾有 bug(已修)
规则集 geosite/category-ads-all 实测内容:162 个精确域名 + 748 个后缀,共 940 条。
连接层:全部拦截 ✅
| 广告域名 | DNS | HTTPS |
|---|---|---|
| doubleclick.net | 142.250.73.78 | ❌ Recv failure: 连接被对方重置(TCP RST) |
| pagead2.googlesyndication.com | — | ❌ RST |
| ad.doubleclick.net | — | ❌ RST |
| 对照:github.com / www.google.com | ✅ | ✅ 200 |
reject 动作对 TCP 直接回 RST,表现就是”连接被对方重置”。
DNS 层:发现规则顺序 bug 并已修复 🐛
原配置里 DNS 规则顺序是:apple@cn → cn → google/AI → !cn → 广告 reject。
广告 reject 排在最后,于是同时属于其他分类的广告域名会先被路由出去,照样解析成功:
| 广告域名 | 为什么漏 |
|---|---|
pagead.l.google.com |
命中 geosite/google(第 3 条)→ 解析成 142.251.169.155 |
adwords.com |
同上 → 172.217.221.113 |
guanggaoad.youku.com |
命中 geosite/cn(第 2 条)→ 182.242.82.251 |
而路由规则里 reject 排在第 4 条(在 google/cn 之前),所以连接层拦得住 —— 两边不一致。
修复:把广告 reject 提到 DNS 规则第 1 条。现在顺序为广告 reject → apple@cn → cn → google/AI → !cn,已通过 sing-box check(exit=0)。
修复后这类域名在 DNS 阶段直接 NXDOMAIN,浏览器不会再去尝试连接。
追注(2026-10-02 晚实测):顺序与”第 1 条”仍然对,但动作已不是
reject:现配置第 1 条是action: predefined+rcode: NXDOMAIN。原因:DNS 层的reject回的是 REFUSED,systemd-resolved收到上游 REFUSED 后不转告客户端而是去重试其他上游,应用会一直等到超时(约 5 秒);predefined+NXDOMAIN 是正常应答,应用立刻失败并做负缓存(详见docs/09§3)。
另外广告拦截的规则集已从 1 份扩到 5 份(geosite/category-ads-all+Ads_AWAvenue+anti-AD+ads-extra+adblockfilters),DNS 规则与路由规则两边同步引用。
复测:dig +short doubleclick.net→ 空;dig random-zzq.doubleclick.net→status: NXDOMAIN;
对照域名github.com正常解析;dig +tcp同样被拦。
注:
criteo.com、outbrain.com不在该规则集内,不会被拦 —— 需要的话可以加Ads_AWAvenue规则集。更正(2026-10-02 晚实测):加上另外 4 份规则集后,这两个域名现在已经被拦:
dig +short criteo.com、dig +short outbrain.com都返回空;sing-box rule-set match -f binary显示criteo.com命中anti-ad.srs与adblockfilters.srs,outbrain.com同样命中这两份。
三、分流 —— 完全正确 ✅
| 目标 | 实测出口 | 结论 |
|---|---|---|
api.ipify.org(海外) |
<proxy-exit-ip> |
走代理节点 ✅ |
myip.ipip.net(国内) |
直连(真实 ISP IP)✅ | |
ip.3322.net(国内) |
直连 ✅ |
geosite/cn + geoip/cn → direct、geolocation-!cn → Proxy 都按预期命中。
追注(2026-10-02 晚,eBPF 模式):分流结论不变(复测
api.ipify.org与curl -x http://127.0.0.1:7892都返回代理出口 IP,走 socks5 也一致;myip.ipip.net返回国内电信地址),
但机制多了一层:eBPF 入站的local.bypass_rule_set(geoip/cn、geoip/cn-fresh、cncidr-mihomo)
让命中这三份 CN IP 表的流量在 eBPF 层就绕过 sing-box 直接出网。
所以”所有流量都经 sing-box”这个绝对化说法是错的:国内 IP 直连是有意绕过,
分流由”eBPF 绕过表 + 路由规则 3~11 条”共同决定。
另外国内回显现在优先给 IPv6(240e:…电信地址),因为 eBPF 入站开了ipv6: true。
四、Podman 容器代理 —— TUN 下完全透明 ✅(eBPF 下不成立,先读追注)
追注(2026-10-02 晚,eBPF 模式,模式限定):本节结论只在 TUN 模式下成立。
当前模式是 eBPF,实测 rootless podman(pasta)容器内境外走不了代理(国内直连与 DNS 劫持仍正常):
| 检查项(eBPF 模式复测,podman 6.1.3 rootless / pasta) | 结果 |
|---|---|
容器内访问境外 https://api.ipify.org |
❌ can't connect to remote host: Connection refused(没有走代理) |
| Clash API 连接列表 | 容器那条境外连接不出现(确认没被接管) |
容器内访问国内 https://myip.ipip.net |
✅ <home-ip>(电信,直连正常) |
容器内广告域名 DNS(nslookup doubleclick.net) |
✅ NXDOMAIN(DNS 劫持生效) |
podman run --network=host |
✅ 出口 = 代理出口 IP(可用处置) |
容器 /etc/resolv.conf |
3 条:169.254.1.1、192.168.31.1、fd00:6868:6868::1;前两条 Connection refused,实际生效的是第 3 条 |
旧解释已被推翻:「pasta 不创建宿主 socket,所以 cgroup 钩子看不见」是错的 ——
ss -tnp能看到passt.avx2(进程名是passt.avx2,不是pasta)的宿主 socket,
源地址是宿主 ULAfd00:6868:6868::71b,DNS 转发与 TCP 转发都走宿主 socket。
所以”现象”(境外不通)成立,但”机制”未知;shared能接管的是桥接进来的包,pasta 属用户态转发,两条路都不覆盖它。
处置:容器用--network=host,或切回 TUN。eBPF 下对应的旋钮是bypass_private_address与bypass_rule_set(不是exclude_interface,下面那句注意用的是 TUN 时代的说法),
具体卡在哪一环尚未确证(见docs/09§6)。以下 TUN 时代的原文保留
(注意其中slirp是过时说法,podman 现在用 pasta)。
沙箱内 podman 6.1.2(rootless)实测:
| 检查项 | 结果 |
|---|---|
| 拉取镜像 | ✅ 成功(走代理) |
容器内 /etc/resolv.conf |
nameserver 169.254.1.1(podman 网关)→ 转发到主机 → 被 sing-box 劫持 |
| 容器出口 IP | <proxy-exit-ip>(= 宿主机的代理出口) ✅ |
| 容器访问国内回显 |
结论:TUN 的 auto_route 已经把容器(含 podman 的 NAT / slirp 路径)的转发流量一并接管,
容器内不需要设 HTTP_PROXY、不需要 --network=host,分流规则也照样生效。
注意:如果以后在配置里加
exclude_interface(例如排除podman0),容器就会绕过代理;
目前没有这类排除项,所以是接管状态。
五、其它功能
| 功能 | 结果 |
|---|---|
mixed 本地代理口 7892 |
✅ curl -x http://127.0.0.1:7892 出口 = 代理 IP |
| UDP / QUIC | ✅ 早前实测 HTTP/3 正常(cloudflare-quic 0.30s) |
| 规则集本地化 | ✅ 15 份全部 local,启动零网络依赖(2026-10-02 晚复核:原为 8 组;其中 6 份来自自建包 /usr/share/sing-box-rule-sets/) |
| 面板 | 官方 dashboard :9091/dashboard/ ✅(复测 200);MetaCubeXD 已摘除(clash_api.external_ui 已删 → :9090/ui/ 复测 404,/var/lib/sing-box/ui 不存在) |
六、结论与待办
没有发现 DNS 泄露到运营商的情况(海外域名全程 DoH 经代理)。
更正(2026-10-02 晚):原文下面这句”唯一’可见’的是国内域名的明文 UDP 查询,属设计取舍,
可按第一节改为 DoH”已经过时 —— 国内解析现在同样走 DoH(dns-direct-ali/dns-direct-tencent),
见第一节的更正块。
发现并修复的问题:DNS 广告拦截规则顺序错误(已移到第 1 条)。
待你执行(应用最新配置,含 12 项改动):
cd ~/Documents/deepseek-harness/default-workspace
sudo ./switch-to-local-rules.sh
追注(2026-10-02 晚):这份”待执行”已经完成 —— 配置早已部署,运行模式也从 TUN 迁到了 eBPF。
现在切模式用仓库里的scripts/switch-to-ebpf.sh/scripts/switch-to-tun.sh;switch-to-local-rules.sh是 TUN 时代的脚本,只作历史留档。
应用后建议复测:
dig +short doubleclick.net # 期望:空(修复后 DNS 层也拦了)
dig +short pagead.l.google.com # 期望:空
curl -s https://api.ipify.org # 期望:代理 IP
curl -s https://myip.ipip.net # 期望:你的电信 IP
Singborg 05:网络层审计记录
sing-box 网络全面审计报告
审计时间:2026-10-01 20:5x 对象:正在运行的 sing-box 1.14.2(TUN stack: system,规则集全本地化)
方法:全部为真实流量实测(dig / curl / ss / ip / podman 容器),非推断。
追注(2026-10-02 晚):本文审计在 TUN 模式下完成,属当时快照。当前模式已是 eBPF
(local: cgroup+shared: packet_rewriteonvirbr0,TCX 挂载),本机没有tun0,
所以下文所有tun0/TUN 地址/MTU 类证据只描述当时状态;逐条复测见”结论速览”后的追注与各节追注。
结论速览
| 检查项 | 结果 |
|---|---|
| daed 残留 | ⚠️ dae0 设备与 tc 钩子已清,但 daens netns 仍在 → 已加入脚本自动清理 |
| 遗留系统代理 | ✅ 干净(gsettings mode=none,无 *_proxy 环境变量) |
| DNS 劫持完整性 | ✅ UDP 与 TCP 都被劫持 |
| IPv6 处理 | ❌ TUN 无 v6 地址 → 所有 IPv6 被黑洞,v6-only 站点不可达 → 已修 |
| IPv6 泄露 | ✅ 无泄露(宁可不可达,也不绕过代理) |
| 局域网 | ✅ 路由器 / 本机 / libvirt 网关都通,strict_route 没吃掉 LAN |
| 私有反查 | ✅ dig -x 192.168.31.1 → XiaoQiang.(resolved 按链路送到路由器) |
| NTP / 时钟 | ✅ systemd-timesyncd active,System clock synchronized: yes |
| 并发稳定性 | ✅ 20 个并发请求 20/20 成功 |
| 大文件传输 | ✅ 10 MB 完整下载无中断(1.68 MB/s,proof.ovh.net) |
| MSS / MTU | ✅ TUN 侧 8948、远端 1400/1489 协商,由 sing-box 正确处理,重传率 0.27% |
| SOCKS5 | ✅ curl --socks5-hostname 127.0.0.1:7892 可用 |
| 应用自带 DoH | ✅ curl --doh-url https://dns.google/dns-query → 200 |
| Podman | ✅ 出口=代理 IP、国内=直连、DNS 经 pasta→宿主→劫持、端口发布正常(本节是 TUN 模式下的审计;eBPF 模式下这条不成立,见 docs/09 §6) |
| 订阅节点质量 | ⚠️ 存在失效节点(5s i/o timeout),urltest 会自动绕开 |
逐条复测(2026-10-02 晚,eBPF 模式)
检查项 TUN 时代结论 eBPF 模式现状 daed 残留 ⚠️ daensnetns 仍在✅ 已清理: ip netns list输出为空、dae0不存在、tc filter show dev wlan0 ingress无输出遗留系统代理 ✅ 干净 ✅ 仍干净: gsettings get org.gnome.system.proxy mode→'none',无*_proxy环境变量IPv6 处理 ❌ TUN 无 v6 地址 → 全部黑洞 ✅ 已可用: ip -6 rule只有local/main(无9000: from all unreachable)、eBPF 入站ipv6: true、curl -6 https://ipv6.google.com→200DNS 劫持完整性 ✅ UDP 与 TCP 都被劫持 ✅ 仍成立,改由 eBPF local cgroup 数据面完成: dig +tcp doubleclick.net @192.168.31.1→ 空、dig +tcp www.google.com @192.168.31.1→ 真实 Google IP私有反查 ✅ dig -x 192.168.31.1→XiaoQiang.⚠️ 现在返回 _gateway.(路由器 PTR 变了),链路仍正常局域网 ✅ 都通 ✅ ping -c2 192.168.31.1→ 2/2,0% lossNTP / 时钟 ✅ ✅ systemd-timesyncdactive、System clock synchronized: yesMSS / MTU ✅ TUN 侧 8948 ➖ 不适用:没有 TUN 设备,MTU 不再是 TUN 的事 运行配置 旧版(第 12/13 项未应用) 已是最新:DNS 广告顺序修复、IPv6、MetaCubeXD 摘除都已落地;当前 15 份规则集全 local系统 DNS resolvectl→tun0(172.19.0.2)resolvectl→wlan0(fd00:6868:6868::1/192.168.31.1),但查询仍被劫持(见第 4 条的追注)
Bug 1:daens 网络命名空间残留
$ ip netns list
daens (id: 0) ← 残留
$ ip -brief link show dae0
(已不存在)✅
$ tc filter show dev wlan0 ingress | grep daed
(无)✅
- 影响:一个空 netns,几乎无害;但说明当初 daed 并未干净卸载,若以后有程序误用该 netns 会出怪问题。
- 修复:部署脚本新增
1b/6 清理 dae 残留步骤(ip netns delete daens,并顺带复查dae0与 tc 钩子)。
Bug 2:IPv6 在 TUN 模式下完全不可用(结构性)
根因:TUN 只配了 172.19.0.1/30(无 v6 地址),于是 sing-box 装了这条规则主动黑洞全部 v6:
$ ip -6 rule show
9000: from all unreachable ← sing-box 的防泄露黑洞
这是 sing-box 的有意设计(没有 v6 出口就别让 v6 偷偷绕出去),所以不会泄露;代价是 v6 目标一律不可达。
实测证据(ipv6.google.com 是 AAAA-only:2404:6800:400b:c00e::8b):
| 访问方式 | 结果 |
|---|---|
| TUN 模式直连 | ❌ 000(失败) |
经本地代理口 127.0.0.1:7892 |
✅ 200 |
→ 你的代理节点本身能到 IPv6,只是客户端用不上。任何 AAAA-only 的站点(部分 Google 服务、v6-only CDN/自建服务)在 TUN 模式下都打不开。
修复:给 TUN 加上 v6 地址(已写入待部署配置并 check 通过):
"inbounds": [{ "type": "tun", "address": ["172.19.0.1/30", "fdfe:dcba:9876::1/126"], ... }]
修复后:v6 流量同样进入 TUN → 经代理出去;ip_is_private 规则保证 LAN 的 ULA v6 仍直连;strict_route: true 继续防止绕过。
追注(2026-10-02 晚,eBPF 模式):本节只描述 TUN 模式,上面的 TUN v6 地址修法仅作历史记录。
切到 eBPF 后不存在 v6 黑洞:ip -6 rule show只有local/main两张表(没有unreachable规则),curl -6 https://ipv6.google.com实测200;国内回显也已经是电信 IPv6 地址。
应用后请这样验证:
curl -6 -s -o /dev/null -w '%{http_code}\n' https://ipv6.google.com # 期望 200(现在是 000)
ip -6 rule show | grep -c unreachable # 期望 0
若你不想要 v6(例如担心某些应用优先走 v6 变慢),把这行
"fdfe:dcba:9876::1/126"删掉即可回到现在的
“v6 不可达但绝不泄露” 状态。两种都安全。
次要观察
- 订阅里有失效节点:日志反复出现
connection upload handshake: dial tcp <node-server-ip>:<node-server-port>: i/o timeout(5 秒超时)。
该节点已死。urltest 会自动切走,但探测/回退时会带来偶发 5 秒卡顿。
已优化:Auto的interval已从10m收紧到5m(死节点最迟 5 分钟被换掉);
另新增地区级 urltest(Auto-Japan / Auto-HongKong / Auto-Singapore)。
(IP 已脱敏:它是订阅节点的 server 地址;端口与🇭🇰 香港Z02 | IEPL的server_port一致,
但节点域名与解析结果都可能变,别把 IP 当身份。)追注(2026-10-02 晚):
interval: 5m确实已生效(Auto/Auto-Japan/Auto-HongKong/Auto-Singapore都是5m✓)。
最近日志里的失效节点换成了🇯🇵 日本Z05 | 下载专用(connection refused,同样已脱敏)。
失效节点会持续更替,不要把某个具体 IP 当结论。 - 一个国内直连目标超时:
182.140.248.155:443(无 PTR、直连也000)→ 目标本身不可达,
很可能是某个广告/追踪域名解析到的死 IP,不是配置问题。 - 运行中的配置仍是旧版(20:31 部署):所以现在还不包含
DNS 广告拦截顺序修复(第 12 项)、MetaCubeXD 面板(第 11 项)、IPv6 修复(第 13 项)。追注(2026-10-02 晚):这条已过期 —— 配置早已更新,上述三项都已落地(其中 MetaCubeXD 反过来是被摘除),
运行模式也从 TUN 换成了 eBPF。 resolvectl显示tun0已成为系统当前 DNS(172.19.0.2,DNS Domain: ~.)→ 系统解析直连 sing-box ✅。追注(2026-10-02 晚,eBPF 模式):现在
resolvectl指向wlan0(DNSfd00:6868:6868::1、192.168.31.1),
没有tun0。系统解析器虽然直连路由器,但它的 53 查询在 eBPF local cgroup 层被劫持
(dig www.google.com返回真实 Google IP、dig random-zzq.doubleclick.net→NXDOMAIN),
所以”系统解析实际走 sing-box”这个结论仍然成立。
待执行
cd ~/Documents/deepseek-harness/default-workspace
sudo ./switch-to-local-rules.sh --dry-run # 共 13 项 + dae 残留清理
sudo ./switch-to-local-rules.sh
追注(2026-10-02 晚):这份”待执行”已完成(12/13 项都已应用),切模式脚本现在是
scripts/switch-to-ebpf.sh/scripts/switch-to-tun.sh;switch-to-local-rules.sh只作历史留档。
应用后再跑一次这份审计里的关键三条:
curl -6 -s -o /dev/null -w '%{http_code}\n' https://ipv6.google.com # 期望 200
dig +short doubleclick.net # 期望 空
curl -s https://myip.ipip.net # 期望 你的电信 IP
Singborg 06:透明代理优化评审
sing-box 配置优化评审 + 参考项目分析 + GUI 选型
本章目录(9 节)
本文所有改动都用 sing-box 1.14.2 真二进制 验证过(
sing-box checkexit=0,无弃用告警)。
参考项目:Fanju6/NetProxy-Magisk(Magisk 模块,内置
sing-box 1.15.0-alpha.8 + eBPF 透明代理 + Android App + WebUI,维护很积极)。
🕓 2026-10-02 复核追注
本文写于 TUN 时代。当天中午起本机已切到 eBPF 模式:现役
sing-box-ebpf 1.14.2.ref1nd-2
(fork 构建),入站是local.cgroup+shared: packet_rewrite(挂virbr0),
配置里已没有 TUN 入站(sing-box api ebpf实测role=local/mechanism=cgroup+role=shared/mechanism=tcx)。
所以文中凡以「你用 TUN」为前提的表述(第二节末行、第四节tun.stack一项、第六节)都已过时;
另有两处结论后来被实测推翻(type: "group"的支持情况、/ui/自托管面板)。
下面都在原处加了带日期的追注,原文一律保留 —— 记录”当时是怎么判的”才是本文的价值。
一、已应用的改动(累计 17 项,逐项经真二进制校验)
| # | 改动 | 依据 |
|---|---|---|
| 1 | 7 组规则集 remote → local |
启动不再依赖被污染的 raw.githubusercontent.com |
| 2 | log.level info → warn |
日志里逐条打印 dns: exchanged AAAA …,纯噪音 |
| 3 | cache_file.store_dns: true |
DNS 缓存跨重启保留 |
| 4 | route.find_process true → false |
无任何进程分流规则,却在每条连接做 /proc 查找(日志可见 router: found process path) |
| 5 | 删除冗余 socks:7893 |
mixed:7892 已含 SOCKS5 |
| 6-7 | Proxy/Auto 的 interrupt_exist_connections → false |
原来切节点会掐断所有已建连接 → pacman 下载中断 |
| 8 | Proxy 成员 13 → 16(当天又补入 Download,现为 17) |
Japan/HongKong/Singapore 原来不是 Proxy 成员,面板里选不到 |
| 9 | geosite/apple@cn → 直连(路由 + DNS) |
借鉴自该项目:Apple 服务走国内 CDN 更快,避免绕道海外 |
| 10 | experimental.clash_api → services:[api + 内置 dashboard] |
借鉴自该项目:sing-box 自己下载并伺服官方面板,浏览器开 http://127.0.0.1:9091 就是 GUI |
第 10 项用到的默认面板源是 sing-box 二进制内置的https://github.com/SagerNet/sing-box-dashboard/archive/refs/heads/gh-pages.zip
(实测可下载,6.7 MB、2026-09-28 更新),经 http_client: proxy-rules 走代理下载到/var/lib/sing-box/dashboard。
二、参考项目里值得抄但我没采纳的
| 它的写法 | 为什么没采纳 |
|---|---|
{"type":"group","servers":[...]} DNS 服务器组(多上游故障转移) |
❌ FATAL: unknown transport type: group。这是 1.15 的新特性(它内置 1.15.0-alpha.8)。等你升级到 1.15,这正好能把那 3 个没用的 DNS 服务器改成故障转移组 —— 记下来⚠️ 2026-10-02 追注:此结论已被推翻。 现役 fork 构建 sing-box-ebpf 1.14.2.ref1nd 支持 type: "group"(实测:含 group 的最小配置 sing-box check exit=0),线上配置已在用 dns-proxy = [cf, google] / dns-direct = [ali, tencent] 两个故障转移组(当天 10:35 启用),不必等 1.15。(”上游原版 1.14 不支持”这次未再复核) |
{"type":"hosts","predefined":{...}} 预解析 DoH 域名 |
你的 DoH 直接用 IP + tls.server_name,不存在”解析 DoH 自己的域名”的循环,不需要 |
{"action":"resolve","match_only":true} 放在 geoip 规则前 |
主要为 FakeIP 模式准备;你是 real-ip,TUN 收到的是真实目的 IP,geoip 直接可匹配 |
clash_mode: Global/Direct/AllowAds 规则 |
让面板的模式开关真正生效(可选增强,非必需;你的 selector 已能直接切节点) |
Ads_AWAvenue 广告规则集 |
比 geosite/category-ads-all 更激进的国内广告列表,可选;你现在已有 reject 规则 |
log.output 写日志文件 |
你有 journald,不需要 |
只有 mixed 入站 + eBPF 做透明代理 |
那是 dae 的路子(它内部有 ebpf/ 代码);local.cgroup + shared packet_rewrite),TUN 不再使用 |
值得注意:它的规则集也是本地 .srs 优先(随模块打包),只有广告列表走远程 —— 印证了我们
“规则集本地化”的方向是对的。
三、GUI 选型(你问的重点)
结论:官方面板偏工程化,社区主流是 metacubexd / zashboard;两者协议不同,所以现在两套并存。
⚠️ 2026-10-02 收尾复核:实际落地的 GUI 不是下表的 MetaCubeXD,而是自研的
sing-box-panel(127.0.0.1:9096,内嵌 zashboard,ss -ltnp实测在听、HTTP 200、
页面含MetaCubeXD/zashboard字样)。现役clash_api没有external_ui/external_ui_download_url字段,所以下表第一行的http://127.0.0.1:9090/ui/现在 404
(Clash API 根路径无密钥仍是 401);官方 dashboard 那一行的http://127.0.0.1:9091/dashboard/仍 200(根路径 302 →/dashboard/)。
下面 9099 端口的实测是当时的一次性试跑,9099 现在没有监听。
| 面板 | 地址 | 协议 | 说明 |
|---|---|---|---|
| MetaCubeXD(推荐) | http://127.0.0.1:9090/ui/(自托管) 或 https://metacubex.github.io/metacubexd/ + 后端 http://127.0.0.1:9090、密钥 <clash-api-secret> |
Clash REST | 观感现代;节点延迟测试、连接实时流量、规则、日志齐全。密钥 <clash-api-secret> |
| 官方 dashboard | http://127.0.0.1:9091/dashboard/ | gRPC-Web(daemon.StartedService) |
官方出品、功能全但界面朴素;密钥同样 <clash-api-secret> |
| zashboard | https://board.zash.run.place/ | Clash REST | 更现代的另一选择;把 external_ui_download_url 换成它的 gh-pages zip 即可自托管 |
| Throne | sudo pacman -S throne(archlinuxcn 1.3.2-1) |
Qt 桌面程序,后端 sing-box | 真正的桌面 GUI,发布活跃(2026-09-29)。⚠️ 它自己管理内核与配置,不要和 systemd 服务同时开 |
| sing-box for Desktop | — | — | 官方仓库自述是 “Experimental Windows client”,Linux 暂无产物 |
实测验证(用你的二进制在 9099 端口真跑了一遍)
| 实测项 | 结果 |
|---|---|
external_ui 自托管路径 |
/ui/ → 200,标题 MetaCubeXD(zip 顶层目录被自动剥掉;/ui/metacubexd-gh-pages/ 会 404) |
Access-Control-Allow-Origin |
* |
| 私网访问预检 | Access-Control-Allow-Private-Network: true(Chrome 的 PNA 不会拦托管面板) |
Clash API 根路径 / 无密钥 |
401 {"message":"Unauthorized"} ← 你之前看到的报错就是它(旧配置的 Clash API) |
三之二、其余 GUI 说明
四、我验证过、明确不建议做的(避免被网上教程带偏)
| 项目 | 结论 | 证据 |
|---|---|---|
| 阻断 QUIC(UDP 443) | ❌ 不要 | 实测你节点 UDP 中继正常:cloudflare-quic.com HTTP/3 0.300s vs HTTP/2 0.310s |
dns.independent_cache |
❌ 不要 | 真二进制告警:1.14 弃用、1.16 移除(你配置里本来就没有) |
把 dns_mode 写进 dns 段 |
❌ 无效 | 实测 json: unknown field "dns_mode";它是入站字段、不在 dns 段(TUN 入站的取值是 disabled/native/hijack;2026-10-02 追注:现役 eBPF 入站的 local/shared 也有同名 dns_mode,线上取 hijack),你用 hijack-dns 规则动作已实现且工作正常 |
cache_file.store_rdrc |
❌ 不要 | 1.14 已弃用,且规则集本地化后无意义 |
tun.stack: system / mtu: 9000 |
⚠️ 已不适用(2026-10-02 追注) | 现役配置没有 TUN 入站(当天起走 eBPF:local.cgroup + shared packet_rewrite),这两项只存在于备份的 TUN 配置里 |
dns.strategy: prefer_ipv4 / 规则顺序 |
✅ 保持 | 都是当前场景的最优选择(你的 IPv6 只有 ULA,必须偏 IPv4 —— 实测线上 dns.strategy = "prefer_ipv4";DNS 劫持必须在私网直连之前 —— 实测 hijack-dns 仍是第 2 条规则、ip_is_private 第 3 条) |
五、死配置清单(可选整洁化)
以下条目没有被任何规则/选择器引用,留着不影响运行(⚠️ 2026-10-02 复核:其中两条已被实测推翻,
见下方删除线;原文保留以记录当时的判断):
dns.servers里的dns-proxy-google、dns-direct-tencent、dns-local(5 个里 3 个没用)
→ 升级到 1.15 后可以用type: "group"把它们变成故障转移组(见第二节)
→ ⚠️ 2026-10-02 复核:此条已推翻。 实测现役dns.servers共 7 个,dns-proxy-google与dns-direct-tencent都在被group引用(dns-proxy = [dns-proxy-cf, dns-proxy-google]、dns-direct = [dns-direct-ali, dns-direct-tencent]),配置里根本没有dns-local。
故障转移组也已在当天 10:35 落地,不需要等 1.15。- 出站
block(现代写法是reject规则动作,你已在用)—— ✅ 2026-10-02 实测仍成立:outbounds里确有block,而 11 条路由规则只引用了direct/Proxy 路由规则 10(quic/443 → Proxy)和规则 11(无条件→ Proxy)—— 与route.final = Proxy重复,两条都空转
→ ⚠️ 2026-10-02 复核:此条已推翻(配置早被重排)。现役共 11 条规则:第 10 条是geoip/telegram → Proxy(仍与route.final = Proxy重复,属空转),第 11 条是geoip/cn + geoip/cn-fresh + cncidr-mihomo → direct(不空转,是 CN 直连兜底,不能删)。
「quic/443」和「无条件 → Proxy」这两条规则现在都不存在。http_clients/route.default_http_client—— 现在只被第 10 项的面板下载用到,仍有价值,保留
(✅ 2026-10-02 实测:http_clients = [proxy-rules]、route.default_http_client = "proxy-rules",services[api].dashboard.http_client = "proxy-rules"确实还在引用)
六、前瞻:升级到 1.15 时
官方 migration 目录里 1.15.0 有一节 Migrate TUN stack。等 CachyOS 把 sing-box 升到
1.15 时,先跑一次 sing-box check -c /etc/sing-box/config.json 看告警;
顺带可以把 type: "group" 的 DNS 故障转移组用上。
⚠️ 2026-10-02 追注:这一节的两项都已不成立。
①type: "group"的 DNS 故障转移组当天已经启用(见第二节追注),不用等 1.15;
② 本机没有 TUN 入站(当天起走 eBPF),Migrate TUN stack对本机没有对象。
另外现役跑的是 fork 构建sing-box-ebpf 1.14.2.ref1nd-2(pacman -Q sing-box-ebpf实测),
升级节奏跟 reF1nd fork 的 tag 走,不是 CachyOS 的官方sing-box包。
七、怎么应用
cd ~/Documents/deepseek-harness/default-workspace
sudo ./switch-to-local-rules.sh --dry-run # 预览(含上面 10 项)
sudo ./switch-to-local-rules.sh # 应用
脚本流程:放好规则集 → 写配置 → sing-box check 通过才重启 → 验证出口 IP;
失败自动还原(/etc/sing-box/config.json.bak.<时间戳>,.bak 结尾不会被 -C 目录模式误读)。
改动前的配置备份在 ~/.config/sing-box/config.tun-perf.json.pre-opt.<时间戳>。
Singborg 07:daed DNS 故障诊断(历史记录)
pacman -Syy 大面积失败:诊断与修复
本章目录(6 节)
🕓 历史文档(2026-10-02 追注)
daed 已经不在用了。 现役透明代理是 sing-box —— fork 构建
sing-box-ebpf 1.14.2.ref1nd-2,
走 eBPF(local.cgroup+shared: packet_rewrite挂virbr0),不是 TUN、更不是 dae。
实测:pacman -Q daed dae→ 两个包都**”未找到”**;systemctl status daed→ 无此 unit;tc qdisc show dev wlan0→ 只剩noqueue(没有clsact,tc filter show为空)→ dae 的
eBPF 钩子已清干净;ip -brief link无dae0、ip netns list为空。所以本文是 daed 时代的一次故障定位记录,保留它的价值在于”当时是怎么判的”:
文中的结论、命令、面板地址(127.0.0.1:2023)都已不是现状。第 2 节的”立刻验证”
(sudo systemctl stop daed)现在已无从执行 —— 实际的处置是整体弃用 daed、迁到 sing-box
(见 08-daed-迁移计划,那份计划已执行完毕,文首有待办核对表)。
结论:是 daed(
daed-emo1.27.0.r19.gb3043aa)的 DNS 劫持链路在失败,不是镜像源、也不是 IPv6 单独造成的。
你新配的 emoeem 多代理是好的 —— 本次emoeem 4.0 KiB 100%已成功下载。
1. 证据链
| 观测 | 数据 | 含义 |
|---|---|---|
| daed 服务 | active (running),PID 1344,/usr/bin/daed run -c /etc/daed/ |
透明代理在运行 |
| eBPF 钩子 | wlan0 ingress/egress 挂 daed_wan_ingress_l2 / daed_wan_egress_l2;dae0(netkit L3,netns daens) |
dae 在网络层拦截所有流量 |
| DNS 被劫持 | 日志中 dst="192.168.31.1:53" 19 次、dst="[fd00:6868:6868::1]:53" 17 次 |
连路由器 DNS 都被 dae 接管 |
| DNS 直接失败 | DNS ingress fast path failed; sending SERVFAIL response(36 次/约 1 分钟) |
客户端收到 SERVFAIL → Could not resolve host |
| 根因 | bootstrap resolver returned no usable address for "<节点域名>"(183 次) |
dae 解析不了代理节点域名 |
| 节点不可达 | [StickyIP] All proxy IPs failed(95 次)、dial ... i/o timeout |
节点拨号超时 → 上游全挂 |
| 抖动实测 | 连续 10 次 curl -4 https://mirror.krfoss.org/ → 7 成功 / 3 失败 |
间歇性失败,符合拦截层丢弃/回绝 |
| 本地立即失败 | curl -6 ... total=0.000004s code=000 |
被本地组件瞬时回绝,而非网络慢 |
| 版本来源 | daed-emo 1.27.0.r19.gb3043aa,打包者 Unknown Packager,URL 指向 daeuniverse/daed |
这是第三方打包的变体(来自 emoeem 仓库),非官方二进制 |
为什么症状看起来是”TLS 错误”
- pacman → DNS 查询被 dae 拦下;
- dae 要把查询转发到
tcp+udp://8.8.8.8:53,但经代理节点转发(outbound=proxy); - 节点域名的 bootstrap 解析失败 / 节点拨号超时;
- dae 回
SERVFAIL→ pacman 报Resolving timed out/Could not resolve host; - 已建连的会话在等待上游时被打断 → OpenSSL 报
unexpected eof while reading。
Group re-selects dialer ... min_moving_avg=1.94s→2.498s 显示 daed 正在节点之间反复抖动,说明当前订阅节点大面积不可用。
2. 立刻验证(决定性实验)
sudo systemctl stop daed # 临时停掉
sudo pacman -Syy # 若全部变绿 → 100% 确认是 daed
sudo systemctl start daed # 验证完再启回来
应急时也可以用这个办法先把系统更新做完。
3. 修复 daed 侧(根因)
在 daed 面板(http://127.0.0.1:2023)按顺序检查:
DNS 上游不要走代理:把 DNS upstream 从
8.8.8.8(当前经代理转发)改成国内可直连的223.5.5.5/119.29.29.29,并设为 direct / 直连;顺带配置 bootstrap resolver 为直连 IP,
避免”解析节点域名又依赖节点”的循环。刷新/更换订阅:183 次节点域名解析失败 + 95 次拨号超时说明节点大面积失效,更新订阅或换机场。
换掉
daed-emo:它是 emoeem 仓库里的第三方打包变体(Unknown Packager),
与上游daed行为可能有差异。可换官方发布版对比,能立刻排除”打包/版本 bug”。让系统别用被劫持的 DNS(减少影响面):
nmcli con mod <wifi-ssid> ipv4.ignore-auto-dns yes ipv4.dns 223.5.5.5 nmcli con mod <wifi-ssid> ipv6.ignore-auto-dns yes nmcli con up <wifi-ssid>
4. 顺带修 IPv6(独立问题,会叠加放大故障)
wlan0 只有 ULA fd00:6868:6868::/64 + 一条 RA 默认路由,没有可用全局 IPv6;
而 IPv6 DNS fd00:6868:6868::1 响应要 1261 ms(IPv4 路由器 DNS 仅 11 ms),
但 AAAA 记录让程序优先走 IPv6。二选一:
# 方案 A:全局让 glibc 优先 IPv4(推荐,最省事)
echo 'precedence ::ffff:0:0/96 100' | sudo tee -a /etc/gai.conf
# 方案 B:直接关掉这张卡的 IPv6
nmcli con mod <wifi-ssid> ipv6.method disabled && nmcli con up <wifi-ssid>
5. pacman 侧加固(抗抖动,可选)
/etc/pacman.conf 里启用带重试的下载器:
XferCommand = /usr/bin/curl -4 --retry 3 --retry-delay 2 --connect-timeout 10 -L -C - -f -o %o %u
再配合已有的 emoeem 多代理段,单点故障就不会让 -Syy 整片报错。
Singborg 08:daed → sing-box 迁移计划(历史记录)
从 daed 迁移:选型结论 + 落地方案(CachyOS / 单 wlan0 / 机场订阅)
本章目录(7 节)
🕓 历史文档 + 待办核对(2026-10-02 追注)
这是 daed 时代的迁移方案,已执行完毕 —— 但最终落地方式与文中不同:没有用 TUN,用的是 eBPF
(现役sing-box-ebpf 1.14.2.ref1nd-2,local.cgroup+shared: packet_rewrite挂virbr0)。
逐条核对本文的待办(2026-10-02 实测):
本文的待办 现状 证据 停用 / 卸载 daed ✅ 已完成 pacman -Q daed dae→ 两个包都”未找到”;systemctl status daed→ 无此 unit;systemctl list-unit-files | grep -i dae→ 只有accounts-daemon等同名干扰项清 wlan0上的daed_*tc 钩子(第 3 节)✅ 已完成 tc qdisc show dev wlan0→ 只有noqueue(无clsact);tc filter show dev wlan0 ingress/egress→ 空删 dae0网卡 /daensnetns✅ 已完成 ip -brief link只有lo/eno1/wlan0/virbr0,无dae0;ip netns list为空systemd-resolved恢复✅ 正常 dig @8.8.8.8 doubleclick.net→NXDOMAIN(被 sing-box 正常劫持,非 SERVFAIL)/etc/daed配置目录⚠️ 仍残留 stat /etc/daed→drwxr-x--- root:root,2026-10-01 18:55(本文写作时没把它列进清单;内容是空的旧配置,可安全删除,但未删)装 sing-box 并常驻(第 4.2 节) ✅ 已完成 sing-box-ebpf 1.14.2.ref1nd-2;sing-box.serviceactive、NRestarts=0用文中那份 TUN 配置 / config.tun-perf.json(第 4 节)❌ 未采用 现役配置没有任何 TUN 入站,走 eBPF; ~/.config/sing-box/config.tun-perf.json只作备份保留另:第 1 节的内核对比表已过时两处 —— ① sing-box 现役的是 reF1nd fork 构建
(with_ebpf,上游没有 eBPF 入站);② 全局接管方式除 TUN / redirect / tproxy 外,fork 还提供 eBPF。
第 4.3 节的调优表以 TUN 为前提,TUN 那几行已不适用。
0. 一句话结论
功能最全 + 性能最强的活跃内核 =
sing-box。
你手上那份 436 行配置本来就是按它的最优实践写的(.srs二进制规则集、DoH、prefer_ipv4),
我已用真二进制(sing-box 1.14.2,与cachyos-extra-v3版本一致)校验通过,并额外生成了一份
带 TUN 入站的性能取向版本,随时可用。
1. 内核对比(功能 + 性能)
| 维度 | sing-box | mihomo (Clash.Meta) | Xray-core (+v2rayA) |
|---|---|---|---|
| 协议原生度 | 最全:SS2022 / VMess / VLESS / Trojan / Hysteria2 / TUIC v5 / ShadowTLS / Reality / WireGuard 均原生 | SS/VMess/VLESS/Trojan/Reality 稳;Hysteria2、TUIC、WireGuard 标注实验性 | VLESS+Reality/XTLS 实现方,协议性能最强 |
| 吞吐(同协议基准) | 高约 10–20% | 基准 | 极高(Reality 场景) |
| 内存占用 | 约 40–80 MB | 约 80–150 MB | 约 60–120 MB |
| DNS 模块 | 最现代:独立 dns.rules、原生 DoT/DoH/DoQ、FakeDNS |
Fake-IP + Redir-Host,成熟但基于 Clash legacy(无 DoQ) | 依赖前端 |
| 规则生态 | .srs 二进制规则集,MetaCubeX 规则可直接用 |
最丰富:ACL4SSR / Loyalsoldier / 数百 YAML rule-provider | geosite/geoip |
| 全局接管方式 | TUN / redirect / tproxy 三种 | TUN / tproxy / redir | 无内置,需 v2rayA |
| 面板生态 | 官方文档为主,中文教程偏少 | 最多:metacubexd / zashboard / yacd | v2rayA Web UI |
| 订阅格式 | JSON(Clash YAML 不能直接用) | Clash YAML 原生直用 | V2Ray/JSON |
| 上游状态 | 活跃,v1.14.x | 活跃,v1.19.x,另引入 Smart 自适应选路 | 活跃 |
性能数据的来源是第三方内核对比评测(sing-box vs Mihomo 内核对比、Mihomo vs Sing-Box 选型),外部数据、未经我复现,仅供参考。
另外同一批资料里也自相矛盾(把 mihomo 的主语言写成 Python,实际是 Go),所以别把数字当精确值。
关键判断:你的 11 个节点全是 Shadowsocks,瓶颈在机场出口带宽,内核差异(<20%)在家用宽带上基本感知不到。
所以”性能最强”这件事上真正拉开差距的,是下面第 4 节的几项调优,而不是选谁。
为什么不是别的
- mihomo:如果你更看重生态(Clash YAML 订阅直接用、规则集最多、面板最多)就选它 —— 但要在内存、吞吐、
以及 Hysteria2/TUIC 稳定性上让步。它不是”功能/性能最强”,而是”生态最强”。 - Xray-core:只有当你订阅是 VLESS+Reality 且要榨干协议性能时才值得,代价是路由/TUN 得靠 v2rayA 补。
- dae(现状):eBPF 进内核分流,性能天花板其实最高(无 TUN 拷贝),但它正是被放弃的那个 ——
而且我们已经实测到它把 DNS 和 IPv6 TCP 都弄坏了。
2. 现状盘点
| 项目 | 状态 |
|---|---|
| 当前代理 | daed-emo 1.27.0.r19.gb3043aa-1(emoeem 仓库,Unknown Packager) |
| 发行版官方包 | archlinuxcn/daed **2.1.1-1** —— 比你现在这个新一个大版本 |
| 已装未用 | flclash 0.8.96-1(无配置目录、无 TUN 设备,从未启动) |
| 现成资产 | ~/.config/sing-box/config.json(436 行):18 出站(11 SS + 4 selector + 1 urltest)、DNS prefer_ipv4、route 11 规则 + 2 组 .srs 二进制规则集、DoH 上游 |
| 唯一缺口 | 没有 TUN 入站,只有 mixed:7892 / socks:7893 → 只能当本地代理,不能全局接管 |
| TUN 可用性 | 宿主机内核 7.2.8-1-cachyos-bore-lto,tun.ko.zst 在 /lib/modules/,按需加载 ✅ |
一个更正(我先前说错了)
我最初以为 config.json 第 331/341 行的尾随逗号会让 sing-box 起不来 —— 这是错的。
实测:sing-box 自带的宽松 JSON 解码器容忍尾随逗号(原始未修改的文件直接 check 通过);
只有标准 JSON 工具(jq、Python json)会拒绝。所以:
- 尾随逗号不是它没被用起来的原因;真正原因是
sing-box二进制从来没装(配置 9 月 21 日生成后原封未动)。 - 但仍建议清掉,否则编辑器、
jq、订阅转换脚本会报错。
3. 迁移前必做:清掉 dae 的 eBPF 残留 ⚠️(✅ 2026-10-02 实测已清干净)
dae 靠 tc clsact + eBPF 挂在网卡上(wlan0 上现有 daed_wan_ingress_l2 / daed_wan_egress_l2)。
只删包不停服务,钩子会残留,换任何客户端都会继续坏网。
sudo systemctl stop daed && sudo systemctl disable daed
sudo pacman -Rns daed-emo
tc filter show dev wlan0 ingress | grep daed # 确认残留
tc filter show dev wlan0 egress | grep daed
sudo tc qdisc del dev wlan0 clsact # 有残留才清(会移除 wlan0 上所有 clsact 规则)
ip -brief link show dae0 2>/dev/null && sudo ip link del dae0
ip netns list # 看 daens 是否残留
sudo systemctl restart systemd-resolved
resolvectl query github.com # 应立刻返回
验证:连续 10 次 curl -4 -s -o /dev/null -w '%{http_code}\n' https://mirror.krfoss.org/ 全绿。
4. 落地:sing-box(已为你准备好文件)
4.1 我已放好的东西
| 路径 | 说明 |
|---|---|
~/.config/sing-box/config.json |
你的原文件,我没动(md5 2dfb01a9fc10f90135747f38b18c2f2b) |
~/.config/sing-box/config.tun-perf.json |
我生成的性能取向版本:已加 TUN + stack: system + tcp_fast_open: true,已用真二进制 sing-box check 通过 ✅ |
~/.cache/sing-box-bin/sing-box |
校验用的 1.14.2 静态二进制,以后不装包也能 check 配置 |
4.2 装包与启用(需要你在终端跑 sudo)
sudo pacman -S sing-box
sudo mkdir -p /etc/sing-box /var/lib/sing-box
sudo cp ~/.config/sing-box/config.tun-perf.json /etc/sing-box/config.json
# cache_file 路径改到 /var/lib/sing-box/cache.db(服务以 root 跑)
sudo sed -i 's#/home/emo/.cache/sing-box/cache.db#/var/lib/sing-box/cache.db#' /etc/sing-box/config.json
~/.cache/sing-box-bin/sing-box check -c /etc/sing-box/config.json # 复核
# /etc/systemd/system/sing-box.service
[Unit]
Description=sing-box service
After=network-online.target nss-lookup.target
Wants=network-online.target
[Service]
Type=simple
ExecStartPre=/usr/bin/sing-box check -c /etc/sing-box/config.json
ExecStart=/usr/bin/sing-box run -c /etc/sing-box/config.json
Restart=on-failure
RestartSec=3
LimitNOFILE=infinity
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload && sudo systemctl enable --now sing-box
resolvectl query github.com
curl -s -o /dev/null -w '%{http_code}\n' https://www.google.com
4.3 那几项真正影响性能的设置(我都已验证可用)
⚠️ 2026-10-02 追注:本表以 TUN 为前提。现役没有 TUN 入站(走 eBPF),所以
TUN stack与「入站只留 TUN」两行已不适用;其余四项仍在生效 ——
实测线上outbounds[].tcp_fast_open = true、rule_set.format = "binary"、experimental.cache_file.enabled = true、dns.strategy = "prefer_ipv4"。
而mixed:7892也没有被删掉(仍作为面板/命令行的代理口在用)。
| 设置 | 取值 | 作用 |
|---|---|---|
TUN stack |
system |
走内核 TCP/IP 栈,吞吐最高;gvisor 最兼容但最慢,mixed 折中 |
出站 tcp_fast_open |
true |
建连少一个 RTT(sing-box check 接受 ✅) |
rule_set.format |
binary(你已在用) |
.srs 二进制匹配远快于文本 geosite |
experimental.cache_file |
已启用 | 规则/节点缓存,启动快、内存稳 |
| 入站 | 只留 TUN | 全局接管后 mixed/socks 是多余暴露面 |
dns.strategy |
prefer_ipv4(你已有) |
你这条线路 IPv6 只有 ULA、无全局出口,必须偏 IPv4 |
4.4 最省事的过渡方案(不装 TUN、不要 root)
你配置里的 mixed 监听 127.0.0.1:7892,普通用户前台跑起来即可让 pacman 走它:
# /etc/pacman.conf
XferCommand = /usr/bin/curl -4 -x http://127.0.0.1:7892 --retry 3 --retry-delay 2 -L -C - -f -o %o %u
5. 三条铁律
- 永远不要同时开两个透明代理(daed + FlClash/sing-box 同时 TUN = 之前那种 DNS/TLS 全乱的局面)。
- 换客户端前先确认 dae 的
tc钩子已清干净(第 3 节)。(✅ 2026-10-02 实测:wlan0上已只剩noqueue,dae0与daens都不存在)⚠️ 但注意:/etc/daed目录还残留着(见文首核对表) - DNS 一律偏 IPv4:这条线路 IPv6 只有 ULA、没有全局出口,任何
prefer_ipv6都会拖垮体验。
Singborg 09:踩坑与经验
踩坑与经验
本章目录(6 节)
这篇是整份记录里最该先看的:结论会过时,踩过的方式不会。分四类 ——
sing-box 与内核、工具链、测量方法(我在这里犯过错)、以及流程上的经验。
一、sing-box / 内核
1. ebpf 入站只存在于某个 fork,上游没有
上游 SagerNet 的 sing-box 没有 ebpf 入站(文档 404):这个入站在 reF1nd/sing-box 分支里
(with_ebpf 构建标签,Linux/Android)。而那个 fork 0 个 release → 想要就得自己编。
判断依据不是”文档说有”,而是:拿一个带 ebpf 入站的配置去 check,上游二进制直接报unknown inbound type: ebpf。
2. 内核数据面要逐条探测,不能互相推断(我在这里推断错过一次)
先看结论:官方文档写得很清楚 ——
这四条路径(local
cgroup、localtc、sharedpacket_rewrite、sharedsocket_assign)
不是按内核版本递进的能力层级。厂商内核可能支持其中一条,却在另一条的对象加载或挂载阶段拒绝。
实测结果(root 权限,逐条单独探测):local cgroup ✅ / local tc ❌ /
shared packet_rewrite ✅(PASS 33)/ shared socket_assign ✅(PASS 31)。
即本机只是 local tc 这一条不可用,两条 shared 都可用 —— 容器/虚拟机在 eBPF 下能被接管。
我犯的错:local tc 实测失败(register TC eBPF TCP listener: operation not supported)之后,
我推断“shared 也是 TC 路径,所以同样不可用”,甚至据此给脚本加了守卫把这个方案挡掉。
但事实上 —— shared 我从来没真跑过(脚本卡在”找不到 podman 网桥”就退出了),
而且按上面的文档,这个推断本身就不成立。
正确的做法(也是文档给的做法):用要用的那条路径单独做只读探测,并且
用与服务相同的权限(普通用户跑会得到一堆 unknown,因为建不了 BPF map):
sudo sing-box tools ebpf status --shared-data-plane socket_assign --interface wlan0 --json
sudo sing-box tools ebpf status --shared-data-plane packet_rewrite --interface wlan0 --json
sudo sing-box tools ebpf status --local-data-plane cgroup --json
result: passed 才是通过;inconclusive 表示无法判定(多半是权限),fail 才是真的缺能力。
最终判据仍是真实启动成功(探测不挂载 hook,也不证明能挂上去)。
📌 2026-10-02 晚追注(本节保留原文):结论已复核为正确,四条数据面至今仍是
localcgroup✅ / localtc❌ / sharedpacket_rewrite✅ / sharedsocket_assign✅。
只补一处边界:上面那句”容器/虚拟机在 eBPF 下能被接管”要限定成桥接进来的——
VM 与桥接的容器可以,rootless podman(pasta)不行(见 §6)。
2b. register TC eBPF TCP listener: operation not supported 的语境
同一条错误出现在 local tc 上。文档说明 TCP listener 有两条路:内核能建BPF_MAP_TYPE_SOCKMAP 时优先用它,否则回退到不引用 SOCKMAP 的 legacy 段、直接调bpf_skc_lookup_tcp。且 shared socket_assign 需要的是 socket lookup/assignment helper
(bpf_skc_lookup_tcp / bpf_sk_lookup_udp / bpf_sk_assign / bpf_sk_release),
不需要 local TC 才要的那些(bpf_get_socket_uid、bpf_redirect、bpf_skb_store_bytes…)。
所以这两条路径的可用性必须分开看。
FATAL start service: start inbound/ebpf[ebpf-in]:
register TC eBPF TCP listener: operation not supported
本机 7.2.8-1-cachyos-bore-lto。而 shared 的 data_plane = packet_rewrite 走的是同一条 TC 路径
→ 一起不可用。
📌 2026-10-02 晚追注:这半句推断已被实测推翻(原文保留)。
shared.packet_rewrite在virbr0上
挂载成功并端到端跑通:api ebpf显示role=shared mechanism=tcx framing=ethernet,
直连与走代理两条路都通过(详见 §6)。local tc失败不影响 shared;因此本节末尾那条
“非passed就先拦下来”的守卫,其前提不成立。
最有价值的教训在这里:预检 --mode local 报 preflight_passed(PASS 37 / FAIL 0 / UNKNOWN 0),
看起来”全都支持”;但换成 --mode all 立刻变成 inconclusive(pass 13 / warn 2 / unknown 36,
其中 required_issues 28)。也就是说:
预检全绿 ≠ 能用。
local模式只覆盖 cgroup 路径;只有实跑才知道 TC 行不行。
✅ 2026-10-02 晚复核:非 root 跑
sing-box tools ebpf status --mode all --json仍得result: inconclusive、pass 13 / warn 2 / fail 0 / unknown 36(required_issues 28),与原文数字完全一致;
所以这一条是权限造成的,不是内核能力不足。(上文--mode local的preflight_passed/ PASS 37 那次是 root 跑的,非 root 复现不出来。)
所以我给脚本加了守卫:要动 tc/shared 之前先跑 --mode all,非 passed 就先拦下来,
别白重启一轮(--force 可以强行试)。
3. DNS 广告拦截别用 reject:会卡 5 秒
- sing-box 的 DNS
action: reject回的是 REFUSED; systemd-resolved收到上游 REFUSED 后不转告客户端(它认为”上游坏了”,去重试其他上游),
→ 客户端(应用)一直等到超时,约 5 秒;- 换成
action: predefined+rcode: NXDOMAIN后是正常应答,应用立刻失败并做负缓存。
实测:doubleclick.net从”约 5 秒”变成 11 ms。
诊断口径也要注意:resolvectl query 会明说 server or network returned error: REFUSED —— 这是
识破”到底是超时还是被拒”的关键工具。
4. 域名规则优先于 IP 规则(所以有些”误判”其实是设计)
日志里看到 74.125.250.129:443(Google 的 IP)走 direct 然后 i/o timeout,很容易以为是 geoip 误判。
查下来:三份 CN 规则集都不含这个 IP,google.cn / gstatic.com 也都正确走 Proxy。
最可能的解释是某个 geosite/cn 里的域名解析到了 Google 的 IP —— 域名命中直连规则,IP 再”国外”也走直连。
这是设计如此,代价是那条连接超时。
5. 官方 geoip/cn 会偏旧,需要第三份独立来源
实测本地 sing-geoip-rule-set 的 geoip/cn 只有 8045 条,上游 MetaCubeX 已经是 9648 条
(多出 8.130.0.0/15 等段)→ 阿里云 IP 8.130.222.175 被误判成国外、走了代理。
补一份新表(外加 mihomo_yamls 的 cncidr,17958 条)之后正确直连。
教训:IP 归属是”数据问题”,不是”配置问题”;官方包更新慢,值得自己滚动。
6. rootless 容器(podman + pasta)抓不到 —— 但原因不是原来写的那样
修正(2026-10-02 晚):原先这里写的是「pasta 不创建宿主 socket,所以 cgroup 钩子看不见」。
这句已被实测推翻:
$ ss -tnp | grep passt
ESTAB [fd00:6868:6868::71b]:34344 [<remote-ip>]:443 users:(("passt.avx2",pid=157862,fd=155))
pasta(进程名是 passt.avx2,不是 pasta)确实创建宿主 socket:DNS 转发与 TCP 转发都走宿主 socket。
这也解释了为什么容器内 DNS 能被劫持、国内直连能正常 —— 「钩子看不见」这个解释不成立。
仍然成立的是现象与处置(2026-10-02 晚复测):
| 容器网络 | 出口 | 结论 |
|---|---|---|
| rootless 默认(pasta) | http=000 失败;Clash API 里也没有这条连接 |
❌ 境外走代理这条路不通 |
| 容器内国内直连 | <home-ip> 电信 |
✅ 正常 |
| 容器内广告 DNS | NXDOMAIN |
✅ 劫持生效 |
--network=host |
<proxy-exit-ip> |
✅ 可行 |
✅ 2026-10-02 晚本机复测(轻量):
podman run起一个 alpine(rootless 默认 pasta)——容器内nc -w6 -z 104.26.13.205 443→ 失败(rc=1);nslookup doubleclick.net→NXDOMAIN(劫持生效);
对照域名example.com正常解析。同时宿主ss -tnp仍能看到passt.avx2的宿主 socket
(本次是 IPv4:192.168.31.29:58850 → <home-ip>:80)。结论与上表一致。
eBPF 的覆盖边界(修正后):local 管本机;shared 管桥接进来的包(VM / 桥接的 rootful 容器,
实测直连与代理都通过);pasta 属用户态转发 —— 既不是本机 socket 的常规路径,也不是桥上的包,
所以两条路都不覆盖它。具体卡在哪一环尚未确证。
一条待验线索:pasta 的宿主 socket 用的是宿主 ULA 私网源地址(fd00:6868:6868::71b),
所以值得临时关掉 bypass_private_address 再复测一次 —— 若这样就能通,根因就是
「私网地址绕过」的判定把 pasta 的流一起绕过了(而绕过的结果是走直连,境外自然被墙)。
📌 2026-10-02 晚补充证据:宿主 socket 的源地址也可能是宿主局域网 IPv4,不只是 ULA
(实测192.168.31.29:58850 → <home-ip>:80 users:(("passt.avx2",…))),所以”源地址是私网”这条线索
对 v4/v6 都成立。根因仍未确证(没有实测关掉bypass_private_address)。
处置不变:eBPF 模式下容器要么 --network=host(实测出口 = 代理出口 ✓),要么切回 TUN。
7. 切换/重启有 2–3 秒 DNS 空窗,而且缓存会骗过你的检查
旧模式拆掉、新模式还没挂上的那几秒,查询可能漏出去并被 systemd-resolved 缓存。
我第一次切 eBPF 就是被这个骗了:健康检查查 doubleclick.net 拿到缓存里的阳性答案,
判定”DNS 没生效”→ 自动回滚。实际 eBPF 是好的。
修法(都写进脚本了):
- 切换后
resolvectl flush-caches; - 检查用随机子域(
<hex>.doubleclick.net)—— 任何缓存都命不中; - 同时问一个对照域名(必须能解析),否则”空结果”可能只是查询失败(假通过)。
8. 非 root 跑 sing-box check 会污染 cache.db 权限
配置里若写死了 experimental.cache_file.path = /var/lib/sing-box/cache.db,以普通用户跑 check
会创建一个属于该用户的 cache.db → 之后 root 的服务启动时:
FATAL start service: initialize cache-file: open /var/lib/sing-box/cache.db: permission denied
修法:脚本里 check 时把 cache_file.path 指到临时目录;出过事就 chown sing-box:sing-box。
(我这台机器上这个坑留下了 19 条 FATAL,全在 08:55 —— 修完之后再没出现。)
📌 2026-10-02 晚追注:现行单元是
User=root(自建sing-box-ebpf包),/var/lib/sing-box/cache.db
现为root:root 644—— 所以”被非 rootcheck改成 emo 属主后服务写不了”这个具体故障在当前单元下
不会复现。另外本次没能复现”check会创建 cache.db”这一步:把cache_file.path指到可写临时目录后
跑sing-box check,exit=0 且临时目录里没有生成任何文件(带ebpf入站时则在更早的map create: operation not permitted就失败了)——所以那一环的确切机制存疑,但”check要隔离cache_file.path“这条保守做法建议保留。
9. 健康判据必须跟着模式走
TUN 看 tun0 是否出现;eBPF 没有 tun0。eBPF 最硬的判据是:
不设任何代理的请求,出口必须等于经本地代理口的出口。
这一条同时证明”内核接管生效”。辅助判据是 sing-box api ebpf(密钥与 clash_api.secret 相同):
{ "inbounds": [ { "tag": "ebpf-in", "state": "normal",
"localEnabled": true, "localDataPlane": "cgroup",
"sharedEnabled": true, "sharedDataPlane": "packet_rewrite",
"attachments": [
{ "role": "local", "mechanism": "cgroup", "interfaceName": "/sys/fs/cgroup" },
{ "role": "shared", "mechanism": "tcx", "interfaceName": "virbr0", "framing": "ethernet" } ] } ],
"kernelRuntime": { "programs": [ { "name": "sb_ebpf_conn4", "type": "CGroupSockAddr" },
{ "name": "sb_share_in", "type": "SchedCLS" },
{ "name": "sb_share_out", "type": "SchedCLS" } ] } }
(上为 2026-10-02 晚实测的精简结果:local 与 shared 两个附件都在,共用 11 个 kernel program。)
10. 规则集是”按需加载”的,内存数字要等热了再看
刚重启时量到 74 MB,跑一会儿变成 150 MB —— 不是泄漏,是规则集(尤其 anti-AD 那份 770 KB 二进制)
加载进内存。教训:比较内存要在同一”热度”下比。
📌 2026-10-02 晚复核:运行约 1 小时 20 分时
systemctl show -p MemoryCurrent≈ 101–105 MB、ps -o rss≈ 118 MB。量级与”热度”规律相符,但绝对值随规则集版本与运行时长变,别拿固定数字当判据。
二、工具链
11. fs.protected_regular=1:脚本里别写死 /tmp/<固定名>
内核这个开关的意思是:在 sticky 的世界可写目录里,即使 root 也不能写属于别人的已存在文件(防符号链接攻击)。
我干跑时留下了一个 emo 属主的 /tmp/xxx.json,之后 root 跑脚本就 EACCES:
PermissionError: [Errno 13] Permission denied: '/tmp/sing-box-audit-new.json'
修法:一律 mktemp -d / tempfile.mkstemp。这一条同时咬到了脚本、面板后端、试跑套件三个地方 ——
凡是”普通用户也可能先创建同名文件”的路径,都是隐患。
12. 丢掉 dig 的 stderr,会把”超时”看成”拦截”
我早期所有”DNS 拦截 ✅”的判定,用的是 dig +short <域名> 无输出,而我把 stderr 丢进了 /dev/null。
问题是:被 reject(REFUSED) 和 超时 在 stdout 上都是空。带完整输出复测才发现真实语义是 REFUSED。
教训:判定”被拦截”时,必须区分”拿到否定应答”和”什么都没拿到”。
13. re.sub 的替换串会自己处理反斜杠
re.sub(pat, "printf 'x\\n'", text) # ✗ 写进文件的是真实换行
re.sub(pat, lambda m: "printf 'x\\n'", text) # ✓ 用函数当替换器
正则引擎会把替换串里的 \n 转成真实换行。我在这上面栽了两次(还以为是 shell heredoc 的锅)。
教训:re.sub 的替换串里出现反斜杠,就用 lambda。
14. 自提权要换算绝对路径(sudo 的 secure_path)
脚本里写 exec sudo -- "$0" "$@",当用户在 shell 里敲的是裸命令名时 $0 就是裸名,
而 sudo 默认用 secure_path(/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin)——
里面没有 ~/.local/bin → sudo 那一层报”找不到命令”。
修法:
self="$0"
[[ $self == */* ]] || self="$(command -v -- "$self")"
exec sudo -- "$self" "$@"
15. 杂项(每个都咬过一次)
makepkg不接受子目录里的本地 source:UI 文件要摊平到包目录根。install -D多源语义奇怪:目标存在时用普通install -m更稳。- namcap 要求
LicenseRef-*有对应许可证文件:/usr/share/licenses/<pkg>/下得放一份。 namcap的”依赖可能不需要”是误报:它看不出运行期会 exec 那个命令。- 验证 TUI 要用
tmux capture-pane:伪终端尺寸是 0×0 时 TUI 根本不渲染(script里先stty rows/cols或直接用 tmux)。 - 内置动作不允许构建出空 argv:那是防”忘写
base_argv导致光跑pacman打印用法”的策略测试,
合法无参要显式allow_empty = true。
三、测量方法(我在这里犯过错,所以单独一节)
- 结论方向对 ≠ 机制对。我把 REFUSED 当成”拦截成功”,方向没错(应用确实拿不到解析),
但机制错(不是”安静地拒绝”,而是”卡 5 秒”)。如果不深挖,就永远不知道体验代价。 - 要留下”能证伪”的判据。比如”eBPF 生效”用「不设代理出口 == 代理出口」——任何一方不成立就会红;
而”服务 active”这种判据什么都证明不了。 - 警惕缓存:DNS 缓存、resolved 缓存、订阅缓存都会让”刚才明明改好了”变成假象。用随机化输入破缓存。
- 对照必不可少:验证广告拦截要同时问一个”必须能解析”的域名;
验证分流要同时问国内与国外各一个;验证”节点死”要换更长的超时再下结论。 - 分清”历史噪声”与”当前问题”:日志里 58 行告警,全在 12:23 之前;当前进程 0 行。
只看总数会误判成”一直有问题”。
四、流程
- 改配置的脚本一律走同一条管线:
check→ 时间戳备份 → 原子替换 → 重启 → 健康检查 → 失败自动回滚。
这台机器上它已经自动救过一次(tc 数据面试错)。 - 备份要带语义:
config.<时间戳>.pre-ebpf.json/pre-dataplane.json/pre-tun.json——
出问题时一眼知道回到哪个状态。 - 把”手工验证步骤”固化成脚本:我手工跑了十几轮同样的检查,后来写成了
sing-box-status
(一屏出结果),再后来集成进 TUI 当动作。验证脚本化之后,每次改动都敢动了。
五、外部资料评估(2026-10-02)
看过的东西与结论(都是”是否真的产生不同判定”,不是”看起来更全”):
| 资料 | 结论 |
|---|---|
| 217heidai/adblockfilters | ✅ 采用:215k 条域名后缀,与 anti-AD 只重叠 35%,独有 13.9 万条,对照组 0 误伤 → 作为第二层广告表 |
| lingqiqi5211/sing-box-rules | ❌ 只有 steam-cn(22 条 Steam 国服 CDN)值得考虑;geoip-cn 与已装文件同源,!cn/google 因 final = Proxy 而冗余 |
| lyc8503/sing-box-rules(v2ray-rules-dat 谱系) | ❌ 它的 geosite-cn 是 111,304 条,与 MetaCubeX 的 111,224 条重合 98% —— 同一批数据,换个名字而已 |
| 12 倍大的 CN 域名表(两家都一样) | ❌ 陷阱:抽样 24 个”只有它收录”的域名,14 个是国内 IP(现有 geoip/cn 早就直连了,加表零变化)、5 个是国外 IP(加表反而强制直连)、5 个已废域名 |
store_rdrc(社区文章常推荐) |
❌ 1.14 已废弃、1.16 移除;而且它缓存的是 legacy 地址过滤结果 |
download_detour + 远程规则集(博客推荐写法) |
➖ 不适用:这里全是本地规则集,没有运行期网络依赖(这是更优的设计) |
eBPF local.bypass_rule_set |
✅ 已启用([geoip/cn, geoip/cn-fresh, cncidr-mihomo])。逐句核对:• 「命中 CN IP 的流量绕过 eBPF、不进 sing-box」对 —— 规则集必须是 IP-CIDR(域名规则会被忽略),绕过发生在路由之前; • 「DNS 仍被劫持」对(策略顺序里 DNS 模式/来源筛选早于规则集绕过;实测 dig doubleclick.net 仍 NXDOMAIN);• 「省 CPU」方向对但量级易被高估:按域名数只有 0.8%(抽样 150 个走代理的域名 → 127 个解析成功 → 仅 1 个落在 CN IP),日常浏览≈0;真正的收益在大流量直连(Steam 下载、国内镜像、视频)——按体积而非按域名算才看得出来; • 「本该走代理却解析到 CN IP 的域名会变直连」对(按 IP 且先于路由,会盖掉按域名的 Proxy 判定),但实测代价仅 0.8%,且那例 gvt1-cn.com → 113.108.239.171 本身更像”本该直连”。另:它与 shared.bypass_rule_set 互相独立(只作用于 local) |
eBPF local.cgroup_path / exclude_uid / bypass_port |
🟡 可选:把接管范围收窄到用户会话、或按 UID/端口绕过(仍未评估) |
eBPF shared.data_plane |
✅ 已采用 packet_rewrite(virbr0)并实测:下游直连与走代理两条路都通过(api ebpf 可见 role=shared mechanism=tcx)。备用 socket_assign 探测也通过,可一条命令切换:switch-to-ebpf.sh --shared-plane socket_assign |
参考:博客 singbox.ygjc.cc(工程价值有限,但给出了 lyc8503 与 download_detour 两条线索)、
官方 eBPF 内核要求、
官方 eBPF 排查。
六、怎么排查 eBPF 接管问题(实战流程,含我踩的四个测量坑)
最终结论(2026-10-02 验证):本机 shared(packet_rewrite on virbr0)完全可用 ——
直连与走代理两条路径都实测通过。证据:
| 证据 | 内容 |
|---|---|
| Clash API | 客户端连接命中 规则=final 链路=Proxy → Auto → 🇸🇬 新加坡Z02 |
| 客户端实测 | curl -4 https://api.ipify.org → http=200;google → http=200 |
| 抓包(virbr0) | 11 SYN / 11 SYN-ACK,含 104.26.13.205:443(32 包)与 <ip-A>:443(74 包 / 100 kB)双向完整 |
| 出口 | 客户端出口 = 代理出口(<proxy-exit-ip>) |
🔒 脱敏说明(2026-10-02 晚):上表原来直接写了两个真实公网地址(抓包里的对端、以及本机代理出口),
公开仓库已分别替换为<ip-A>与<proxy-exit-ip>。(此处不再复述原值 —— 第一版说明把原值写进注解里,
等于没脱敏,已改。)104.26.13.205是 Cloudflare
的公开任意播地址,保留。
排查顺序(照这个顺序,能少走很多弯路)
1) api ebpf → 附件挂上没有?(role=local/shared、mechanism=cgroup/tcx…)
2) Clash API /connections → 下游的流进没进路由?命中的 rule 与 chains 是什么?
3) 抓包(服务端接口) → 报文有没有出去?双向都有吗?有没有 SYN-ACK/RST?
4) netns 内 curl -v → 客户端卡在哪一步(解析/Trying/refused/timeout)
5) debug 日志 → 只有前三步都指向 sing-box 时才有必要开
关键:这五步里任何一步单独看都可能得出错误结论 —— 我们这次就是靠”抓包里境外零报文“
才把范围从”shared 有问题”翻转到”客户端根本没发包”。
我踩的四个坑(全是”测试工具破绽伪装成被测对象问题”)
- netns 里
127.0.0.53是它自己的 loopback → 宿主 resolved 不可达,DNS 全挂。
修:给 netns 单独写/etc/netns/<ns>/resolv.conf。 - netns 只配 IPv4,双栈域名会先试 IPv6 →
ENETUNREACH,看起来像”境外连不通”。
修:探测时强制curl -4并打印curl -6对照。 - 双重接管(最难查、也最重要):netns 里的客户端是宿主上的一个进程,
它的 socket 仍在宿主 cgroup 里 → 同时被 local cgroup 钩子和 shared 钩子看到。
开着local.bypass_rule_set时,国内目标绕过 local(所以能从 shared 正常出去),
境外目标被 local 先截走 → 那条路径失败 → 表现出来就是”shared 直连能通、走代理不通“。
→ 要验证 shared,必须临时只启用 shared(local.enabled=false),或直接用真实 VM。 - 抓包没 flush 就读文件 → 分析出来是空的(分析前必须先停掉 tcpdump)。
顺带发现的一个 fork 校验细节
local.enabled=false 时不能再留着 local.data_plane,否则启动直接失败:
FATAL create service: initialize inbound[1]: local.data_plane requires local interception
必须精简成 "local": { "enabled": false }。并且:改完配置必须先 sing-box check 再重启 ——
我的测试脚本当时没 check 就重启,把服务弄停了约 20 秒(已修)。这条对所有运维脚本都成立。
一条值得上报上游的边角情况
宿主上的进程如果源地址是 veth/网桥地址(即进程在 netns 里,而流量经 virbr0),
开着 local 钩子时它的境外连接会被 local 路径截走并以 ECONNREFUSED 失败(1 ms、零报文、token 地址显示为 127.0.0.1)。
真实 VM 的流量不是宿主进程,不受影响;这是很窄的场景,但记录下来。
Singborg 10:sing-box 完整审计与优化报告(2026-10-08)
sing-box 完整审计与优化报告
本章目录(8 节)
- 主机:CachyOS(Arch rolling)· 内核 7.2.9-1-cachyos-bore-lto · systemd 262-1-arch
- 审计时间:2026-10-08 12:00 ~ 13:53(Asia/Shanghai)
- 审计者权限:普通用户 emo(uid=1000,属 wheel/network/docker/libvirt);sudo 需密码 → 所有破坏性/系统级改动由你执行
- 审计对象:/etc/sing-box/config.json(审计时 16693 B → 落地后 16641 B,0600 root)+ 单机 systemd 服务 sing-box.service
- 本报告不含任何节点地址、口令或密钥明文;所有凭据与可识别地址一律用占位符(
<home-ip>/<proxy-exit-ip>/<node-server>/<lan-gateway>/<clash-api-secret>)打码
一、第 0 步 · 环境事实清单
| 项目 | 实测值 | 来源/命令 |
|---|---|---|
| 发行版 | CachyOS(Arch rolling) | /etc/os-release |
| 内核 | 7.2.9-1-cachyos-bore-lto x86_64 | uname -r |
| init | systemd 262-1-arch(无 launchd / Windows 服务) | systemctl –version |
| 当前用户 | emo uid=1000;组 wheel,network,docker,libvirt,… | id |
| sudo | 需要密码(sudo -n 全部失败) | sudo -n true |
| 安装方式 | pacman 包 sing-box-ebpf 1.14.2.ref1nd-2 | pacman -Qi |
| 可执行文件 | /usr/bin/sing-box(root:root,83943392 B,2026-10-02 09:54),全盘唯一 | which/stat/find |
| 版本 | v1.14.2-reF1nd(go1.27.1-X:nodwarf5 linux/amd64,tags: with_gvisor,with_quic,with_ebpf,CGO disabled) | sing-box version |
| 上游最新 | v1.14.2 → 与上游持平,无需升级(本项目 fork 的 1.15 仍处 alpha) | GitHub Releases |
| 服务单元 | /usr/lib/systemd/system/sing-box.service:User=root、StateDirectory=sing-box、LimitMEMLOCK=infinity、LimitNOFILE=infinity、Restart=on-failure、RestartSec=10s、ExecStart=/usr/bin/sing-box -D /var/lib/sing-box -C /etc/sing-box run | systemctl cat |
| 实际进程 | root pid 144710 | ps |
| drop-in | /etc/systemd/system/sing-box.service.d/10-config-check.conf:ExecStartPre=/usr/bin/sing-box check -c /etc/sing-box/config.json(10-08 12:47 新增,是唯一 drop-in) | systemctl cat |
| 配置加载 | -C /etc/sing-box 目录 + 单文件 /etc/sing-box/config.json(16693 B,0600 root,mtime 2026-10-08 12:01:41);无 include 拆分;emo 读不了(EACCES),审计用世界可读的备份副本 | ls/stat |
| rule-set | 17 条全部 type=local format=binary,分布:/etc/sing-box/rule-set(2)+ /usr/share/sing-box-rule-set(7)+ /usr/share/sing-box-rule-sets(8),逐条路径实测存在 | jq + ls |
| 日志 | journald(emo 可读) | journalctl -u sing-box |
| 监听端口 | 127.0.0.1:7892 mixed-in / :9090 clash_api / :9091 services.api / :9096 sing-box-panel;eBPF 入站不占用户态端口 | ss -ltnp |
| 历史报错 | 10-07 12:53、17:08 network: missing default interface;10-07 19:26、19:39 connection upload handshake: i/o timeout;10-08 12:32 unsupported address;12:47 重启后至今无新 ERROR |
journalctl |
| ~/code/pkg/build | 未找到(用户提到的目录不存在)→ 第 2 步改为审计实际存在的两个同步脚本仓库 | ls |
拓扑事实(影响测试判据,务必知道)
本机 systemd-resolved(/etc/resolv.conf → 127.0.0.53)+ eBPF 入站,使宿主机自身的 DNS 与 TCP 都被 sing-box 透明接管:
- 不设代理的
curl https://www.google.com/generate_204= 204;Cloudflare trace 直连 colo=HKG(走代理出口) getent解析广告域全部 NXDOMAIN;example.com 正常 → 广告拦截在 DNS 层生效- 直接向 8.8.8.8:53 /
:53 / 223.5.5.5:53 发 UDP 查询,答案一致 → 均被劫持进 sing-box
二、第 1 步 · 官方文档与社区模板核对
2.1 官方「必须改」
结论:无。 按 v1.14.2 现行文档逐项核对,本配置没有使用任何已弃用/被拒的字段:
| 核对项 | 文档要求 | 本机实测 | 判定 |
|---|---|---|---|
| DNS server 格式 | 1.12 起新格式,旧 address 格式已弃用 | 全部新格式(type/ server/ domain_resolver/ detour) | 合规 |
| DNS 规则字段 | 1.14 起拒绝 ip_cidr / match_response / ip_version | dns.rules 未使用这三个字段 | 合规 |
| independent_cache | 1.14 已移除 | 未使用 | 合规 |
| Address Filter Fields | legacy 已移除 | 未使用 | 合规 |
| outbound 字段 | domain_resolver 需为 dns server tag | 11 个 shadowsocks 节点均为 dns-direct-ali(有效 tag) |
合规 |
| route 字段 | auto_detect_interface / find_process / default_domain_resolver 仍有效 | 均在用,find_process=false | 合规 |
| experimental | clash_api / cache_file 仍有效 | 均在用 | 合规 |
| services api | 1.14 起与 clash_api 并行的推荐控制面 | 已启用(127.0.0.1:9091) | 合规,无需改 |
来源:deprecated.md · service/api · outbound/urltest · Releases
2.2 社区「建议改」
| 社区模板 | 它主张的做法 | 本机情况 | 采纳 |
|---|---|---|---|
| LongLights/sing-box_template_merge_sub-store | DNS 按 geosite 分流、国内直连解析、防 DNS 泄漏 | 本配置已实现同类设计(dns.rules 6 条 + 双 dns server 组) | 已具备,不重复引入 |
| Toperlock/sing-box-subscribe | 远程 rule_set + urltest 自动测速模板 | 本机 17 个规则集全部本地 .srs,加载更快且不受远端可用性影响 | 不采纳远程化 |
| qixuancao/sing-box-config-templates | 本地渲染配置模板 + sing-box check 后才生效 |
与本方案第 4 步流程一致 | 已采纳为上线流程 |
三、第 2 步 · 云端同步 / 发布脚本审计
审计对象(~/code/pkg/build 不存在,改审实际存在的仓库):
- /home/emo/code/Singborg —— github.com/emoeem/Singborg
- /home/emo/pkgbuild-source —— github.com/emoeem/pkgbuild
3.1 严重问题
| # | 问题 | 证据 | 风险 | 处置 |
|---|---|---|---|---|
| S1 | 同名脚本双份漂移:Singborg/scripts/* 是 2026-10-02 22:59 的旧副本,pkgbuild-source/scripts/* 才是新版(apply-audit-fixes.sh 相差 112 行);Singborg/scripts/refresh-rule-sets.sh 旧版缺两个 lyc8503 源、且不拒绝未知参数 | diff / head | 照 Singborg README 执行会跑旧逻辑,静默漏更新 2 个规则集,广告/直连规则可能过期 | 统一入口:只从 pkgbuild-source 运行;删除或替换 Singborg 旧副本 |
| S2 | 配置明文外泄面:/etc/sing-box/backups/.json 与 /etc/sing-box/config.json.bak. 共 23 份完整配置副本、权限 644,含全部节点口令与 clash/api secret | ls -l | 本机任意用户可读全部节点凭据 | 已写进变更清单第 1 项(目录 700 / 文件 600) |
| S3 | secret 弱且 CORS 全开:clash_api.secret 与 services[api].secret 同值,11 字符(10 小写字母+1 符号)非随机;access_control_allow_origin=["*"] + allow_private_network: true,实测 9090 对任意 Origin(含 https://evil.example)都回 Access-Control-Allow-Origin: * |
curl -D- | 恶意网页在浏览器中跨域读本机 9090(可读取/切换代理节点) | 变更清单第 5、6 项:轮换为 secrets.token_urlsafe(32) + CORS 收紧到面板源 |
3.2 警告
| # | 问题 | 说明 | 处置 |
|---|---|---|---|
| W1 | apply-audit-fixes.sh 无备份保留策略 | find -delete / -mtime / prune 全无命中,备份无限增长 |
变更清单第 2 项(保留 10 份 + 7 天内不归档,超出 tar.gz 归档而非删除) |
| W2 | generate-sanitized-config.py 把 dns.rules[*].server(DNS 服务器 tag)误当主机名替换为 <node-server> |
公开快照的 DNS 分流引用失真——是错误不是泄漏 | 修正该脚本的替换白名单(仅替换 outbound.server / 订阅字段) |
| W3 | 我上一轮的工作区报告把明文 secret 写进了文件 | singbox-optimization-review.md:47、:48 与 singbox-panel-guide.md:114(均 0600、非 git 仓库、无远端) | 已打码,全工作区 grep = 0 命中 |
| W4 | pkgbuild-source 有 30+ 未提交改动 | 一次 push 会连带发布无关内容 | push 前先审查 git status / 分批提交 |
| W5 | 新发现(超出已授权清单):/var/lib/sing-box/cache.db 权限 644(BoltDB,存 DNS 缓存,store_dns=true) | 任意本地用户可读取该机 DNS 查询历史 | 建议后续单独加固(本脚本未包含,等你确认再加) |
3.3 良好实践(已确认无风险,保持即可)
- 两个公开仓库的工作区 + 完整 git 历史对 11 个节点域名、节点口令、clash secret 全部 0 命中 → 公开仓库未泄漏任何私密节点信息
- 三个会写 main 的 workflow 共用
concurrency: main-branch-updates(不会并发互踩)、actions 按 commit SHA 固定、用github.token而非 PAT、权限最小化 - 没有任何脚本会把本机配置 push 或上传到远端(这是本次审计最关心的越权路径,已逐文件确认)
- 用 github.token 而非 PAT
四、第 3 步 · 配置逐字段体检与变更清单
4.1 变更清单(14 项,全部已授权实现)
| # | 位置 | 现状 | 建议 | 依据 | 风险 | 重启 |
|---|---|---|---|---|---|---|
| 1 | /etc/sing-box/backups 与 config.json.bak.* | 目录 755、23 份文件 644,含明文凭据 | 目录 700、文件 600 | 最小权限原则 | 无(root 仍可读写) | 否 |
| 2 | 备份目录 | 无保留策略,无限增长 | 每目录留最新 10 份;7 天内的不归档;超出部分 tar.gz 归档(不删除) | 可回溯 + 控体积 | 无(归档保底) | 否 |
| 3 | 我自己的工作区报告 | 含明文 secret | 打码为 ****** | 防二次泄漏 | 无 | 否(已完成) |
| 4 | systemd drop-in | unit 有 After=network-online.target 但缺 Wants=,开机早期报 missing default interface |
新增 20-network-online.conf:[Unit] Wants=network-online.target |
官方 systemd 约定 | 极低(仅延后启动) | 需 daemon-reload |
| 5 | experimental.clash_api | secret 11 字符非随机;CORS ["*"] + allow_private_network:true |
secret 轮换为 token_urlsafe(32);CORS 收紧为 ["http://127.0.0.1:9096","http://localhost:9096"];移除 allow_private_network |
本机已复现跨域读 9090 | 中(外部工具若硬编码旧 secret 会 401——面板按 mtime 动态读取,不受影响) | 是 |
| 6 | services[api] | secret 与 clash_api 同值 | 轮换为独立随机值 | 凭据隔离 | 同上(仅 9091 使用者受影响) | 是 |
| 7 | 11 个 shadowsocks 节点 | 同一口令(distinct_passwords=1) | 服务商侧问题,建议逐节点独立口令 | 单点失陷面 | 需在服务商面板改 | 否(仅报告) |
| 8 | 已撤回:dns-proxy 的 detour 是 Auto,用它解析 Auto 自己的节点域名会形成解析环;现有「国内 DoH 直解节点域名」是正确设计 | 解析环风险 | — | — | ||
| 9 | outbounds | 存在未被任何 rule 引用的 {"type":"block","tag":"block"} |
移除(route 第 5 条已用 action: reject) |
死配置 | 无(实测 “block” 字面量仅 2 次即其自身声明) | 是 |
| 10 | selector Download / Auto / Auto-Japan | Download.default=Auto;「🇯🇵 日本Z05|下载专用」同时是 Auto 与 Auto-Japan 的测速候选 | Download.default 改为该节点,并把该节点从 Auto / Auto-Japan 的 urltest 候选中剔除 | tag 语义=下载专用,不应参与日常选路 | 极低(下载路径更明确) | 是 |
| 11 | route.find_process | false | 保持 false(无 process 匹配规则,开启只增开销) | 无收益 | 无 | 否 |
| 12 | dns.rules(6 条) | 已按 must-direct / 广告 NXDOMAIN / cn 直连 / !cn 走代理划分 | 无需改动 | 已符合最佳实践 | — | — |
| 13 | 版本 | v1.14.2-reF1nd = 上游最新 | 不升级 | 无新版可升 | — | — |
| 14 | cache_file / 双控制面(9090+9091) | 均已启用 | 保留 | 持久 DNS 缓存 + 新版控制面 | — | — |
4.2 高危项(已单独提示,均带回滚)
- 第 5、6 项(secret 轮换):唯一会影响外部脚本的改动。如果你的其它设备/脚本里硬编码了旧的 clash secret,轮换后它们会 401。已确认本机面板(127.0.0.1:9096)是按文件 mtime 动态读取密钥的,轮换对它透明;zashboard 入口用
#/setup?…&secret=深链自动带密钥,无需手输。 - 统一回滚一行(脚本每次运行都会打印,并把当次备份路径填进去):
sudo install -m 600 -o root -g root /etc/sing-box/config.json.bak.<时间戳> /etc/sing-box/config.json && sudo systemctl restart sing-box - 脚本对第 5~10 项任一步失败都会自动用当次备份回滚并重启服务(30 秒等待),且会打印同样的手动命令。
五、第 4 步 · 上线流程(先备份 → check → 才生效;失败 10 秒内回滚)
脚本:/home/emo/Documents/deepseek-harness/default-workspace/apply-hardening-2026-10-08.sh(320 行,bash -n 通过,755)
执行顺序:
- 前置备份:
install -m600生成/etc/sing-box/config.json.bak.<TS>,另存一份原配置到临时文件用于失败对照 - 备份加固 + 归档(清单 1、2)
- 用 python 改写候选配置(清单 5、6、9、10):轮换两个 secret、收紧 CORS/移除 allow_private_network、删除 block 出站、Download.default + Auto/Auto-Japan 剔除下载节点;仅改这些字段,其余字节不动 → 得到最小 diff
- 先停服再
sing-box check:避免与正在运行的 eBPF 争用 BPF 资源导致误报;check 失败则按原配置立即 start 并退出(不动线上配置) - 原子替换:
install+mv到 /etc/sing-box/config.json(0600 root) - 写 drop-in +
systemctl daemon-reload(清单 4) - 启动 + 全套健康检查:服务 active(30s)→ 端口 7892/9090/9091 → 启动后 journalctl ERROR 计数 → 新密钥 200 / 旧密钥非 200 / 无密钥 401 → CORS 面板源放行且 evil.example 不放行 → 9096 面板 200 → 面板 /api/state 能读到新密钥 → 经 7892 代理 gstatic/generate_204 = 204;任一失败即回滚
- 汇总:备份路径、打码 diff、一行回滚命令、新密钥前 6 字符与长度
用法(由你用 sudo 执行):
cd /home/emo/Documents/deepseek-harness/default-workspace
sudo bash apply-hardening-2026-10-08.sh --dry-run # 只生成候选+check+打印 diff,不落盘不重启(仍会短暂停服做 check)
sudo bash apply-hardening-2026-10-08.sh # 正式执行
可选:--keep-cors(跳过 CORS 收紧)、--no-routing(跳过选路调整)、--no-retention(跳过备份加固)
说明:脚本为了一次无资源冲突的 check 会短暂停服约 5~10 秒,随后立即以新配置启动;有 flock 单实例锁。
5.1 实际执行结果(2026-10-08)
第一次执行(13:35:51)——误报回滚,正式配置未落盘。
- 现象:
sing-box check通过 → 原子替换成功 → 健康检查报「端口未监听: 7892 9090 9091」→ 自动回滚,服务恢复。 - 排查证据:① systemd 日志显示新实例是
Stopping → Deactivated successfully(被脚本主动停),不是崩溃——没有Main process exited/Failed with result;② 新实例存活 586ms / 538ms、各耗 CPU 538ms,说明它正在正常启动过程中就被判失败;③ 同期测试脚本 T1.3 也报端口缺失,但几秒后 T2/T3 全部连通。 - 根因(脚本 bug,非配置问题):unit 为
Type=simple,systemctl is-active在进程 fork 后立即返回 active,而健康检查零等待就查端口;sing-box 加载 17 个 rule-set + eBPF 程序需要数秒。 - 修复:端口检查改为最长 60 秒轮询(进程若自行退出则立即判失败),真失败时先打印新实例最后 40 行日志再回滚。
第二次执行(13:45:03 生效)——成功。
- 流程:
sing-box check通过 → 原子替换(16641 B, mode 600)→ 启动 → 全套健康检查通过 → 未触发回滚。 - 独立复核(emo 权限,13:53 实测):systemd
active,MainPID 168960;Wants=network-online.target已进事务;7892/9090/9091/9096 全部 LISTEN;outbounds由 22 → 21(block出站已移除);CORS 对https://evil.example不再返回Access-Control-Allow-Origin,对http://127.0.0.1:9096/http://localhost:9096分别回显该源;无密钥访问 9090 → 401;经 7892 代理 gstatic → 204(0.33s);出口 IP;新配置启动后 journalctl 无新增 ERROR(最近一条 ERROR 为 13:34:32,属切换前的旧配置)。
六、第 5 步 · 测试
脚本:/home/emo/Documents/deepseek-harness/default-workspace/test-singbox-2026-10-08.sh(302 行,bash -n 通过,755,必须 root)
| 用例组 | 覆盖内容 |
|---|---|
| T1 服务面 | systemd active、config.json JSON 合法、7892/9090/9091 监听、启动后无 ERROR |
| T2 国内直连 | eBPF 路径下访问国内站点(注意:宿主机 TCP 也被透明接管,这是「direct 规则」路径验证) |
| T3 显式代理 | 经 127.0.0.1:7892 取出口 IP + 地区 + 当前节点(/proxies/Auto 的 now) |
| T4 TCP + UDP | SOCKS5 UDP ASSOCIATE 真实 DNS 中继、curl --http3 QUIC |
| T5 DNS 防泄漏 | T5.1 向系统解析器查 o-o.myaddr.l.google.com TXT,看 Google 侧回声是否落在国内;T5.2 记录 Akamai whoami 回声;T5.3 经代理 ipinfo.io 的 org |
| T6 稳定性 | N 秒(默认 60)混合并发负载的成功率 + CPU/RSS |
| T7 失效切换(可选) | nft 临时屏蔽当前节点 IP → 轮询 /proxies/Auto 的 now 直到切换 → 撤规则后再等回落,结束务必 nft delete table inet sbx_test |
用法:
sudo bash test-singbox-2026-10-08.sh # 60 秒负载
sudo bash test-singbox-2026-10-08.sh --load-seconds 120 --with-failover
6.1 审计期间已完成的真实验证(emo 权限可做的部分,全部通过)
| 验证项 | 结果 |
|---|---|
| 9090 无密钥 | 401 |
| 9090 带密钥 | 200 |
| 9091/dashboard/ | 200 |
| 9096 面板首页 | 200 |
| 经 7892 代理生成 204 | 204 |
| 出口 IP 归属 | AS134972 IKUUU NETWORK LTD( |
| 出口地区 | Cloudflare trace loc=HK colo=HKG |
| SOCKS5 UDP ASSOCIATE 中继真实 DNS | 收到应答 OK,ancount=1 |
| DNS 广告拦截 | doubleclick 等 5 域全部 NXDOMAIN |
| DNS 防泄漏 | o-o.myaddr 回声 172.64.214.13 = Cloudflare(境外递归)→ 无 ISP 泄漏 |
| 工具可用性 | curl(–http3)、jq、python3、flock、nft、ss 齐备 |
6.2 关于「Akamai 回声是国内 IP」的说明(重要,避免误判为泄漏)
whoami.akamai.net 的回声是
6.3 测试结果(脚本真实输出)
第一次 --load-seconds 60:通过 17 / 失败 0 / 未执行 2;第二次 --load-seconds 120 --with-failover 的 T1~T6 同样全通过。
| 用例 | 期望 | 实测 | 结论 |
|---|---|---|---|
| T1.1 服务状态 | active | active | PASS |
| T1.2 配置 JSON | 合法 | 合法 | PASS |
| T1.3 监听端口 | 全部监听 | 7892/9090/9091 全部监听 | PASS |
| T1.4 近 10 分钟 ERROR | 0 | 0 | PASS |
| T1.5 route.rules / outbounds | 11 / 21 | 11 / 21 | PASS(21 印证 block 出站已移除) |
| T1.6 版本 | v1.14.2 | v1.14.2-reF1nd | PASS |
| T2.1 / T2.2 国内站点 | 200 | baidu 200 / jd 200 | PASS |
| T2.3 被劫持的 DNS 解析 | 有 A 记录 | 183.2.172.177 | PASS |
| T3.1 出口 IP 地区 | 非 CN | JP( |
PASS |
| T3.2 / T3.3 国外站点 | 204 / 200 | 204 / 200 | PASS |
| T3.4 当前出站 | 有值 | Auto → 🇯🇵 日本Z01|IEPL | PASS |
| T4.1 SOCKS5 UDP 中继 | 收到 DNS 应答 | OK,54 字节 ancount=1 | PASS |
| T4.2 QUIC / HTTP3 | 有响应 | 200 | PASS |
| T5.1 宿主解析器归属 | 非国内递归 | 162.159.111.13(Cloudflare) | PASS |
| T5.2 出口 IP 归属 | 境外 IDC | AS134972 IKUUU NETWORK LTD | PASS |
| T5.2 Akamai 回声 | 参考项 | SKIP | |
| T6.1 混合负载成功率 | 失败 0 | 60 秒 137/0;120 秒 227/0(代理 114 / 直连 113) | PASS |
| T6.2 资源占用 | 无异常增长 | 120 秒内 CPU 0.62s / RSS 峰值 108 MB | PASS |
| T7 失效切换与回落 | 自动切换 | 见 6.4 | 待补 |
T6 的 137/0 与 227/0 值得单独强调:全程零失败,且两条路径都覆盖了——被 eBPF 透明接管的宿主机直连,与显式代理 127.0.0.1:7892。
6.4 T7(节点失效 → 自动切换 → 恢复回落)
已打印 T7.1:初态 Auto → 🇯🇵 日本Z01|IEPL,节点端口 475。该用例的后续步骤(nft 屏蔽节点 IP → 轮询 /proxies/Auto 的 now 直到切换 → 撤规则 → 等回落 → 删除 sbx_test 表)在贴回的输出里被截断,第二次运行的 结果汇总 表也未贴到,故本节暂记「待补」。
- 当前实测(13:53,emo 侧独立复核):经 7892 代理 gstatic → 204(0.33s),且存在到
<node-server>:475/<node-server>:475的已建立连接 → 链路当前是通的,无残留屏蔽。 - 补两条即可闭项:
sudo nft list tables | grep sbx_test || echo clean,以及 T7 从 T7.1 起到汇总表的剩余输出(若中途 Ctrl-C,脚本 trap 会清理 nft 表,上面那条命令可确认)。
6.5 测试方法自身的两次修正(都不是 sing-box 的问题,如实记录)
- 零等待误判端口:见 5.1,已改为 60 秒轮询,并在测试脚本开头同样加了等待,修掉 T1.3 的误报。
www.qq.com反爬导致的假失败:第一次运行时 T2.2 → 501、T6 出现 138 次「失败」,全部来自同一个 URL——腾讯边缘(Server: stgw)对 curl 默认 UA 直接返回 501,带浏览器 UA 即 200;同批次的 baidu 直连 200、代理路径全部 200。已改为jd.com并统一带浏览器 UA。另外把 T6 CPU 计数改成小数秒(原来 120 秒只显示「0s」),T7 的 nft 规则补上 IPv6 分支(本机默认走 IPv6 直连,原规则只写ip daddr拦不住),urltest 等待上限 360→480 秒(其检测周期为 5 分钟)。
七、第 6 步 · 回滚步骤与维护建议
7.1 回滚(三层,任选)
- 配置回滚(最常用):
sudo install -m 600 -o root -g root /etc/sing-box/config.json.bak.<时间戳> /etc/sing-box/config.json sudo systemctl restart sing-box systemctl is-active sing-box && ss -ltn | grep -E '7892|9090|9091' - drop-in 回滚(只撤开机等待网络):
sudo rm /etc/systemd/system/sing-box.service.d/20-network-online.conf && sudo systemctl daemon-reload - 备份权限回滚(一般不需要):
sudo chmod 755 /etc/sing-box/backups; sudo find /etc/sing-box -maxdepth 1 -name 'config.json.bak.*' -exec chmod 644 {} +
7.2 维护建议
- 每季度跑一次本审计的两个脚本(先 –dry-run 看 diff);sing-box 有新版时先在
--dry-run下核对 deprecated 列表。 - 统一脚本来源:只从 /home/emo/pkgbuild-source 运行同步脚本,删除或同步 Singborg 里的旧副本,消除 S1 漂移。
- 备份保留:保留「最新 10 份 + 7 天内不归档」,归档文件另存到加密盘更稳妥;备份里含明文凭据,务必保持 600。
- 密钥轮换:本次轮换后,建议每 6 个月轮换一次;轮换只需改配置并重启,面板自动跟上。
- 待你确认的新项:
chmod 600 /var/lib/sing-box/cache.db(当前 644,本地可读 DNS 查询历史)。这不在已授权的 14 项内,你点头我再加进脚本。 - 节点口令复用(第 7 项)属服务商侧,建议在服务商面板逐节点改独立口令。
- 监控:
journalctl -u sing-box -p warning --since today每周扫一遍;missing default interface应在 drop-in 生效后不再出现。 - 不要把
experimental.clash_api.secret写回任何会进 git 的文件;generate-sanitized-config.py 修好后再用它生成公开快照。
八、第 6 步交付物索引
| 交付物 | 文件 |
|---|---|
| 本报告(环境事实 / 调研 / 脚本审计 / 变更清单 / 回滚 / 维护建议) | docs/10-审计报告-2026-10-08.md |
| 变更实施脚本(含备份、check、原子替换、健康检查、自动回滚) | scripts/apply-hardening-2026-10-08.sh |
| 第 5 步验收测试脚本(T1~T7) | scripts/test-singbox-2026-10-08.sh |
Singborg:规则集与配置文档
配置记录
本章目录(10 节)
config.sanitized.json 是什么
本机 /etc/sing-box/config.json 的脱敏快照,作用是看懂结构。
✅ 2026-10-08 已按现役配置对齐线上:17 份规则集(含
adblockfilters与后加的lyc/cn、lyc/geolocation-!cn)、inbounds[ebpf].shared = {enabled: true, data_plane: packet_rewrite, interface: ["virbr0"]}、local.bypass_rule_set = ["geoip/cn", "geoip/cn-fresh", "cncidr-mihomo"]、clash_api的 CORS 收紧到本机面板源、
未引用的block出站已删除、Download组默认节点已改。
(配置一改就请重新生成,别让快照落后于线上。生成:sudo scripts/generate-sanitized-config.py --source /etc/sing-box/config.json——
源配置是 0600 root,必须用 root 跑;脚本自身只读本地文件,不联网、不执行 git/上传。)
- 保留:
inbounds/outbounds的结构与协议字段 /dns/route/experimental,
以及每个rule_set的path(路径本身是最有信息量的部分:一眼看出哪几份是自建的); - 换掉:代理节点的
server→<node-server>、server_port→0、password/uuid/sni/host
→ 占位符;两处secret(clash_api与services[api])→<clash-api-secret>
(两处在线上是各自独立的 43 字符随机值,快照里共用一个占位符); - 没有换掉:DNS 的公共 DoH 端点(
1.1.1.1/8.8.8.8/dns.alidns.com/doh.pub)——
那是公共设施,也是配置里最重要的思路之一。
生成脚本是「结构化脱敏」而不是「按键名替换」:
server这个键名同时出现在代理节点(要脱)
和 DNS 服务器(不该脱)里,按键名替换会把两者一起打掉。生成后有自检断言。⚠️ 2026-10-08 修掉一个真实缺陷(W2):
dns.rules[*].server里放的是 DNS 服务器 tag
(dns-direct/dns-proxy),而旧脚本只在「带type字段的dns.servers[*]」里认 DNS 上下文,dns.rules条目没有type→ 它的server被当成节点主机名,统统替换成<node-server>,
于是公开快照里 DNS 分流引用失真(是错误,不是泄漏)。修法:新增is_dns_rule()(同时含action、且server是字符串 → 视为 DNS 规则),并把递归上下文改为context in {"dns", "dns_server"} or is_dns_server(obj) or is_dns_rule(obj),遇到dns/default_domain_resolver键把子树上下文标成 DNS;另加两条拒写断言:逐条比对源与输出的dns.rules[*].server(不一致直接退出),以及dns段里出现<node-server>也退出。
本次快照已按真实 tag 还原:must-direct/apple@cn/cn+lyc/cn→dns-direct;category-ai-!cn、geolocation-!cn+lyc/geolocation-!cn→dns-proxy。
整体结构
| 段 | 内容 |
|---|---|
inbounds |
eBPF:local(cgroup 本机接管 + bypass_rule_set 让 CN IP 绕过)+ shared(packet_rewrite on virbr0,下游/VM 接管,实测直连与代理均通过);另有 mixed(127.0.0.1:7892,给面板/命令行当代理口)+ ebpf(本地 cgroup 数据面、dns_mode: hijack、bypass_private_address、IPv6 开) |
dns |
7 个 server:1 个 hosts 引导 + 4 个 DoH(2 个走代理、2 个走直连)+ 2 个 group 故障转移组 |
route |
17 份规则集 + 11 条规则(顺序即优先级,见下) |
experimental |
cache_file(store_dns: true)、clash_api(127.0.0.1:9090) |
services |
api(127.0.0.1:9091,官方 dashboard 与 sing-box api 命令都走它) |
DNS 设计(这是整套配置的关键)
hosts-bootstrap → 本地 hosts(用于引导解析 DoH 服务器域名本身)
dns-proxy-cf → https://1.1.1.1/dns-query 【走代理】
dns-proxy-google → https://8.8.8.8/dns-query 【走代理】
dns-direct-ali → https://dns.alidns.com/dns-query 【走直连】
dns-direct-tencent→ https://doh.pub/dns-query 【走直连】
dns-proxy = group[cf, google] ← dns.final 指这里(国外域名)
dns-direct = group[ali, tencent] ← 国内域名走这里
要点:DNS 也走 route 规则,所以”解析国内域名用国内 DoH、解析国外域名用代理侧 DoH”;
53 端口被 hijack-dns 全部接管(实测 @8.8.8.8 / @路由器 / @223.5.5.5 查广告域名全部拿不到结果,
出口回显是 Cloudflare 的地址 → 证明解析发生在代理侧)。
type: group 是这个 fork 支持、上游 1.14 不支持的特性,用来把闲置的备用 DNS 编成故障转移组。
dns.rules:6 条(顺序即优先级)
| # | 命中 | 动作 | server |
|---|---|---|---|
| 1 | must-direct |
route |
dns-direct |
| 2 | 广告五表(同 route 第 5 条) |
predefined + NXDOMAIN |
—(不对上游发查询) |
| 3 | geosite/apple@cn |
route |
dns-direct |
| 4 | geosite/cn + lyc/cn |
route |
dns-direct |
| 5 | geosite/google + geosite/category-ai-!cn |
route |
dns-proxy |
| 6 | geosite/geolocation-!cn + lyc/geolocation-!cn |
route |
dns-proxy |
未命中任何一条时走 dns.final = dns-proxy。第 1 条是 2026-10-08 才补上的:must-direct 里的域名
(STUN / 游戏主机 / LAN cache / Steam 国服 CDN)若拿代理侧 DNS 解析,会出现”连接判直连、解析却绕道境外”
的错配,补上后两条判定一致。
规则集清单(17 份)
| 来源 | tag | 路径 |
|---|---|---|
| 自建包(随 pacman 更新) | anti-AD |
/usr/share/sing-box-rule-sets/anti-ad.srs |
geoip/cn-fresh |
/usr/share/sing-box-rule-sets/geoip-cn-fresh.srs |
|
cncidr-mihomo |
/usr/share/sing-box-rule-sets/cncidr-mihomo.srs |
|
must-direct |
/usr/share/sing-box-rule-sets/must-direct.srs |
|
ads-extra |
/usr/share/sing-box-rule-sets/ads-extra.srs |
|
adblockfilters |
/usr/share/sing-box-rule-sets/adblockfilters.srs(215248 条域名后缀,--with-abf 启用) |
|
lyc/cn |
/usr/share/sing-box-rule-sets/lyc-geosite-cn.srs(2026-10-08 新增) |
|
lyc/geolocation-!cn |
/usr/share/sing-box-rule-sets/lyc-geosite-geolocation-!cn.srs(2026-10-08 新增) |
|
官方 sing-geosite/sing-geoip 包 |
geosite/category-ads-all geosite/cn geosite/geolocation-!cn geosite/google geosite/category-ai-!cn geoip/cn geosite/apple@cn |
/usr/share/sing-box/rule-set/… |
手工放在 /etc(早期方案遗留) |
Ads_AWAvenue |
/etc/sing-box/rule-set/AWAvenue-Ads-Rule.srs |
geoip/telegram |
/etc/sing-box/rule-set/geoip-telegram.srs |
| 自建包的八份 | 是什么 |
|---|---|
anti-ad.srs |
anti-AD 广告/追踪表(AdGuard 语法,构建时用 sing-box rule-set convert -t adguard 转) |
geoip/cn-fresh |
MetaCubeX 最新 geoip/cn(官方包那份实测偏旧:8045 条 vs 9648 条) |
cncidr-mihomo |
mihomo_yamls 的国内 IP 表(第三份独立来源,17958 条) |
must-direct |
自制两类:① STUN / 游戏主机 / LAN cache / NCSI(走代理会坏功能)② Steam 国服 CDN 18 条(st.dl.eccdnx.com、dl.steam.clngaa.com、csgo.com.cn…,直连明显更快);刻意不含 steamcontent.com/steamusercontent.com/cm.steampowered.com 等全球域名(强制直连有风险)。2026-10-02 用 sing-box rule-set decompile 实测:domain_suffix 24 条 + domain 9 条,其中 Steam 国服相关 正好 18 条 ✅ |
ads-extra |
自制:ad.duowan.com / sdkmob.com / ads.wps.cn(anti-AD 漏掉的国内广告端点,只用精确域名) |
adblockfilters |
217heidai/adblockfilters 聚合广告表(215,248 条 domain_suffix,与 anti-AD 只重叠 35%)—— 详见下节 |
lyc-geosite-cn.srs |
lyc8503 的国内域名表(2026-10-08 加入;refresh-rule-sets.sh 里有对应源) |
lyc-geosite-geolocation-!cn.srs |
同一来源的「非中国」域名表(与官方 geolocation-!cn 并联使用) |
许可:geoip/cn-fresh ← MetaCubeX/meta-rules-dat(GPL-3.0-or-later);cncidr-mihomo ← HenryChiao/mihomo_yamls(AGPL-3.0-or-later);anti-AD ← prprpi/anti-AD(上游未附许可证文件,个人使用);其余自制。
另一个规则仓库的评估:lingqiqi5211/sing-box-rules
定位:MIT、自用、以 MetaCubeX geo/full 为基底、多源合并去重、每天构建、sing-box 与 mihomo 双格式。
看起来正好能补”官方包偏旧”的短板,但逐个量下来只有一项值得考虑:
| 它提供 | 对比结果 | 结论 |
|---|---|---|
geoip-cn.srs |
81,593 B —— 与我们已经装的 mihomo cncidr 完全同样大小(同一上游) |
冗余 |
geosite-cn.srs |
111,224 条 vs 官方 8,702 条(+105,409) | 看着诱人,实则陷阱(见下) |
geolocation-!cn |
27,214 vs 官方 23,899(+14%) | 冗余:route.final = Proxy,外部域名本来就走代理 |
google-domain |
885 vs 官方 938 | 冗余:漏掉的 Google 域名也会经 !cn/final 走代理 |
ai |
222 条 vs 官方 181 条 | 在我列的 34 个 AI 服务域名上,两边漏的是同一批 10 个 → 没有实际覆盖优势 |
steam-cn |
22 条:st.dl.eccdnx.com、dl.steam.clngaa.com、steamchina.com… Steam 国服(完美世界)下载 CDN |
唯一有价值的一项 → ✅ 2026-10-02 已采纳 18 条(见下) |
为什么”12 倍大的 CN 域名表”是陷阱
抽了 24 个”只有它收录、官方没有”的域名,看它们解析到哪:
- 14 个是国内 IP → 我们已有的
geoip/cn(按 IP 判)早就让它们直连了,加域名表没有任何变化; - 5 个是国外 IP(阿里云国际、Shopify 等)→ 加域名表会把它们强制直连,反而可能更慢;
- 5 个解析失败(域名已废)→ MetaCubeX 不剔除失效域名,官方包剔除。
结论:不加。这也说明”表越大越好”是错觉 —— 得看它和已有规则是否真的产生不同的判定。
Steam 国服值得考虑(✅ 2026-10-02 已采纳)
现在配置里 Steam 全部走代理(实测 steamstatic.com / api.steampowered.com → Proxy,只有 lancache.steamcontent.com 在必须直连清单里)。
如果玩国服,把上述 22 个国内 CDN 域名加进 must-direct(或单独一份直连清单)会明显加快下载。
涉及不到 1 KB,随时可以加。
已执行:当天把上游 steam-cn(22 条)里剔掉 4 个全球域名后,18 条并入了must-direct(sing-box rule-set decompile /usr/share/sing-box-rule-sets/must-direct.srs 实测:
Steam 国服相关条目正好 18 条),已随 sing-box-rule-sets 包生效(见上表 must-direct 行)。
所以”Steam 全部走代理”这句已经不再成立。
广告表的第二层:adblockfilters(2026-10-02 评估)
217heidai/adblockfilters(7.6k stars,GPL-3.0,每 8 小时更新)
是个聚合器:EasyList / EasyPrivacy / AdGuard Base+Chinese+Mobile+DNS / AdRules / OISD /
DNS-Blocklists PRO / StevenBlack / AWAvenue… 合并去重后,还会先用 3+3 组 DNS 验证上游域名是否还有效,
去掉失效的;并且直接提供 sing-box 1.12+ 的 .srs(不用自己转)。
实测评估(拿它的 JSON 版做精确 domain_suffix 匹配,anti-AD 用线上 dig 实测):
| 域名规则数 | |
|---|---|
| anti-AD | 99,395 |
| adblockfilters | 215,248 |
| 两边都有 | 75,383(只重叠 35%) |
| 只有 adblockfilters 有 | 139,865 |
| 只有 anti-AD 有 | 24,012 |
| 并集 | 239,260(相对现在 +141%) |
- 不替换 anti-AD:它独有的 24,012 条里有一批国内广告 SDK 域名(
anti-AD在这块更强)。 - 误伤检查:27 个常用域名(google / github / baidu / bilibili / qq / taobao / zhihu / 微信 /
apple / microsoft / steam / cloudflare / wikipedia / youtube / x / telegram / jd / 美团 / 抖音 /
小米 / 华为 / 阿里云 / 头条 / 爱奇艺 / 优酷 / raw.githubusercontent / api.github)0 命中。 - Lite 版不要用:只有 5306 条(仅国内域名),实测在 26 个广告端点上只覆盖 3 个。
- 兼容性:
.srs用sing-box check实测可直接加载(1.14.2-reF1nd),decompile出 215,248 条domain_suffix。 - 代价:规则集文件 1.76 MB(anti-AD 是 0.77 MB),内存会多占一些 —— 值不值得看你怎么用。
启用(包已带上,配置里加一段):
sudo ./scripts/apply-audit-fixes.sh --with-abf # 会把它加进 dns 与 route 两条 reject 规则
路由规则顺序(11 条,顺序就是优先级)
| # | 规则 | 为什么在这个位置 |
|---|---|---|
| 1 | sniff |
必须先嗅探,后面才能按域名分流 |
| 2 | hijack-dns |
53 端口全部交给 sing-box |
| 3 | ip_is_private → direct |
局域网/内网地址不走代理 |
| 4 | must-direct → direct |
必须放在广告拦截之前:这些域名既不该被拦也不该走代理 |
| 5 | 广告五表 → reject |
连接层拦广告(浏览器自带 DoH 绕过 DNS 规则时靠这层兜底)。2026-10-02 实测是 5 份:geosite/category-ads-all + Ads_AWAvenue + anti-AD + ads-extra + adblockfilters |
| 6 | geosite/google + category-ai-!cn → Proxy |
明确要走代理的服务 |
| 7 | geosite/apple@cn → direct |
苹果国内服务直连(否则 App Store 下载会很慢) |
| 8 | geosite/cn + lyc/cn → direct |
国内域名(两份并联,2026-10-08 起) |
| 9 | geosite/geolocation-!cn + lyc/geolocation-!cn → Proxy |
其余国外域名(同上) |
| 10 | geoip/telegram → Proxy |
Telegram 的 IP 段 |
| 11 | geoip/cn + geoip/cn-fresh + cncidr-mihomo → direct |
三份一起匹配:任何一份认出来就直连(补官方表偏旧的漏) |
route.final = Proxy(兜底走代理)。
DNS 侧的广告规则
{ "rule_set": ["geosite/category-ads-all", "Ads_AWAvenue", "anti-AD", "ads-extra", "adblockfilters"],
"action": "predefined", "rcode": "NXDOMAIN" }
用 predefined + NXDOMAIN 而不是 reject,原因见
《踩坑与经验》第 3 条:reject 回 REFUSED,而 systemd-resolved
不把它转告客户端,应用每个广告域名要等约 5 秒;NXDOMAIN 是正常应答,11 ms 就失败。
2026-10-08 加固(已上线)
一轮完整审计(10-审计报告-2026-10-08)之后的落地改动,
全部由 scripts/apply-hardening-2026-10-08.sh 执行(备份 → sing-box check → 原子替换 →
健康检查 → 任一环节失败自动回滚):
| # | 改动 | 为什么 |
|---|---|---|
| 1 | experimental.clash_api.secret 与 services[api].secret 轮换为各自独立的 43 字符随机值 |
旧值只有 11 字符、是可猜的单词,且两个控制面共用同一个密钥 |
| 2 | access_control_allow_origin:["*"] → ["http://127.0.0.1:9096","http://localhost:9096"],并删掉 access_control_allow_private_network |
实测 9090 对任意 Origin(含 https://evil.example)都回 Access-Control-Allow-Origin: *,配合 private-network 放行,任意网页都能跨域读写本机代理控制面 |
| 3 | 删除未被引用的出站 {type: block, tag: block} |
连接层拦截已由 route 第 5 条的 action: reject 承担;这个出站全文只出现在它自己的声明里 |
| 4 | Download 组默认节点由 Auto 改为「🇯🇵 日本Z05|下载专用」,并把该节点从 Auto(11→10)与 Auto-Japan(5→4)的 urltest 候选里剔除 |
该节点标称”下载专用”,不该参与日常自动选优(它曾经死掉还占着默认位置) |
| 5 | 备份加固:/etc/sing-box/backups/ → 700、config.json.bak.* → 600;每个目录保留最新 10 份 + 7 天内的不归档,超出部分 tar.gz 归档(不直接删) |
那 23 份备份是完整配置副本(含全部节点口令与控制面密钥),原来 644 → 本机任意用户可读 |
| 6 | 新增 drop-in /etc/systemd/system/sing-box.service.d/20-network-online.conf(Wants=network-online.target) |
原 unit 只有 After= 没有 Wants=,开机早期会报 network: missing default interface |
有意不动的:版本(与上游 v1.14.2 已持平)、cache_file 与双控制面(9090 Clash API + 9091 services api)、route.find_process(保持 false,没有 process 匹配规则)、dns.rules 的结构。
待确认的一项:
chmod 600 /var/lib/sing-box/cache.db(当前 644,本地可读 DNS 查询历史)。
加固脚本带开关--harden-cache-db,默认关闭。
变更记录(都是脚本自动备份出来的)
备份在 /etc/sing-box/backups/,命名带语义:
config.<时间戳>.pre-audit.json 审计修复前
config.<时间戳>.pre-ebpf.json 切 eBPF 前(= TUN 配置,切回来时用它取 TUN 定义)
config.<时间戳>.pre-dataplane.json 改数据面前
config.<时间戳>.pre-tun.json 切回 TUN 前
config.json.bak.<时间戳> 2026-10-08 加固前的原样副本(install -m 600 生成)
2026-10-08 的回滚(一层就够,脚本健康检查失败时也是这么干的):
sudo install -m 600 -o root -g root /etc/sing-box/config.json.bak.<时间戳> /etc/sing-box/config.json
sudo systemctl restart sing-box && systemctl is-active sing-box && ss -ltn | grep -E '7892|9090|9091'
# 只撤开机等待网络的 drop-in:
sudo rm /etc/systemd/system/sing-box.service.d/20-network-online.conf && sudo systemctl daemon-reload
回滚永远是一条命令:
sudo cp -a /etc/sing-box/backups/<选定的那份> /etc/sing-box/config.json
sudo systemctl restart sing-box
Singborg:14 个 sing-box 运维脚本
运维脚本
本章目录(6 节)
这些脚本的共同点:改配置一律走同一条安全管线 ——
改配置(内存里)→ sing-box check(不过就什么都不做)→ 时间戳备份 → 原子替换
→ 重启 → 健康检查 → 任一环节失败就自动回滚 + 重启
所以它们可以放心重跑(幂等),也不怕”改一半失败”。这台机器上它已经自动救过一次
(tc 数据面试错那次)。
| 脚本 | 干什么 | 要 root |
|---|---|---|
sing-box-why |
查一个域名/IP:为什么被拦 / 走哪条规则 / 从哪个出口出去,被拦时直接给出放行片段 | ❌(只读) |
sing-box-status |
一屏体检:模式 / 入站 / 出口 / 分流 / DNS / 规则集 / 备份 | ❌(只读) |
apply-audit-fixes.sh |
审计修复:补 anti-AD 广告表、刷新 geoip/cn、清理冗余规则与 dns-local。7 个可选开关见下 |
✅ |
switch-to-ebpf.sh |
TUN → eBPF;已在 eBPF 时可原地调整数据面/共享/国内绕过。开关见下 | ✅ |
switch-to-tun.sh |
eBPF → TUN(TUN 用 auto_route 覆盖转发流量,容器/虚拟机也能被代理)。TUN 定义取自最近的 pre-backup |
✅ |
test-shared-interception.sh |
验证 eBPF shared 到底有没有接管下游:临时 netns+veth 接到 shared 接口上发请求,看出口与 Clash 链路、顺带抓包。跑完自动清理。开关见下 |
✅ |
enable-container-proxy.sh |
在网桥上开 eBPF shared 数据面;会逐个数据面用 root 探测(packet_rewrite / socket_assign)再决定用哪个,不再一刀切禁用;--disable 关闭 |
✅ |
refresh-rule-sets.sh |
刷新自建规则集包(~/pkgbuild-source)的源文件与 PKGBUILD 校验和/SKIP 计数;计数是数据驱动的 |
❌(只有构建包时需要) |
install-singbox.sh |
安装/迁移 sing-box 到本机(最早的引导脚本) | ✅ |
switch-to-local-rules.sh |
早期方案:把规则集切成本地文件(不依赖运行期远程下载) | ✅ |
apply-emoeem-mirrors.sh |
配置自建 Arch 仓库的镜像([emoeem] 段) |
✅ |
generate-sanitized-config.py |
生成 config/config.sanitized.json 脱敏快照,采用结构化脱敏和拒写断言;源配置权限为 0600 root,需以 root 执行 |
✅ |
apply-hardening-2026-10-08.sh |
2026-10-08 加固流程:备份、配置检查、原子替换、健康检查及失败回滚 | ✅ |
test-singbox-2026-10-08.sh |
验收测试 T1–T7,覆盖服务、分流、代理链路、TCP/UDP、DNS、并发和可选故障切换测试 | ✅ |
2026-10-08 加固脚本的用法
先运行 dry-run 检查候选配置和差异,再决定是否正式应用:
sudo ./apply-hardening-2026-10-08.sh --dry-run
sudo ./apply-hardening-2026-10-08.sh
sudo ./apply-hardening-2026-10-08.sh --keep-cors # 保留原 CORS 设置
sudo ./apply-hardening-2026-10-08.sh --no-routing # 不调整 Download 节点路由
sudo ./apply-hardening-2026-10-08.sh --no-retention # 不执行备份保留/归档
sudo ./apply-hardening-2026-10-08.sh --harden-cache-db # 可选:收紧 cache.db 权限
sudo ./test-singbox-2026-10-08.sh --load-seconds 120
sudo ./test-singbox-2026-10-08.sh --with-failover
加固脚本涉及控制面密钥、CORS、出站与节点路由、备份权限/保留策略以及 systemd 网络依赖。请先阅读上游脚本和 10 月 8 日审计报告,确认本机配置与预期一致后再执行。--with-failover 会临时写入 nftables 测试规则;若进程被强制中断,应检查并清理 inet sbx_test 测试表。
switch-to-ebpf.sh 的开关
sudo ./switch-to-ebpf.sh --data-plane cgroup # local 数据面(cgroup 可用;tc 在本机必失败,见文末)
--shared virbr0 # 开共享接管(VM/桥接容器);--no-shared 关闭
--shared-plane packet_rewrite # 共享数据面:packet_rewrite(默认)或 socket_assign
--bypass-cn # 让命中 CN IP 的流量绕过 eBPF(省 CPU,DNS 仍劫持)
--no-bypass-cn # 撤销上面这个
--force # 越过预检守卫(别用来白试 tc)
--bypass-cn 只影响 local;它与 shared.bypass_rule_set 互相独立。
test-shared-interception.sh 的开关
sudo ./test-shared-interception.sh # 默认:与当前配置一致的条件下测试
sudo ./test-shared-interception.sh --shared-only # ★ 验证 shared 请用这个
sudo ./test-shared-interception.sh --debug-log # 临时开 debug 日志,跑完自动还原
sudo ./test-shared-interception.sh --keep # 出问题保留 netns 现场
为什么验证 shared 一定要 --shared-only:脚本里的”下游客户端”是 netns 里的一个宿主进程,
它的 socket 仍在宿主 cgroup 里 → 开着 local 时会被 local 钩子先截走,于是表现得像
“shared 直连能通、走代理不通”(其实两条钩子都插了一脚)。临时关掉 local 后只剩 shared 钩子,
才等价于真实 VM/下游的路径 —— 这时实测直连与代理都通过。
apply-audit-fixes.sh 的开关
sudo ./apply-audit-fixes.sh \
--with-extra-ads # 补 3 个国内广告端点(精确域名,不动 apex)
--with-cncidr # 加 mihomo 国内 IP 表(第三份来源)
--with-direct-list # STUN / 游戏主机 / LAN cache / NCSI 强制直连
--with-dns-groups # 把闲置的备用 DNS 编成故障转移组(fork 支持 type=group)
--use-package-paths # 规则集改指 /usr/share/sing-box-rule-sets(随 pacman 更新)
--nxdomain-ads # DNS 广告拦截从 reject(REFUSED) 改成 predefined(NXDOMAIN)
--with-abf # 接入 217heidai/adblockfilters(215k 条后缀,与 anti-AD 只重叠 35%)
不带开关 = 只做基础修复(补广告表 + 刷新 geoip/cn + 清理冗余规则)。
--use-package-paths 只认它内置的 tag 表(anti-AD / geoip/cn-fresh / cncidr-mihomo /
must-direct / ads-extra / adblockfilters)。表里漏掉的 tag 会被留在 /etc/sing-box/rule-set/,
于是 pacman -Syu 更新规则集包时它不会跟着更新 —— adblockfilters 就这么漏过一次,已修。
健康判据(两种模式不一样)
| 模式 | 判据 |
|---|---|
| TUN | tun0 出现 + 不设代理的出口 == 经本地代理口的出口 + 国内直连 + DNS 广告被拦 |
| eBPF | 没有 tun0,所以只看:sing-box api ebpf 有活动入站 + 不设代理的出口 == 代理出口 + 国内直连 + DNS 广告被拦 |
DNS 那项一律用随机子域(<hex>.doubleclick.net)并问一个对照域名 ——
否则 systemd-resolved 的缓存会把”没生效”伪造成”生效”(我第一次切 eBPF 就是这么被骗着回滚的)。
注意
sing-box-why 是怎么工作的(值得记)
没被拦的域名:连一次真实连接,从 Clash API 读实际命中的
rule/rulePayload/chains—— 这是权威结果,不是猜的。被 DNS 拦下的域名:不会产生连接记录,所以退一步逐个规则集判定命中哪一个;AdGuard 来源的
.srs无法反编译,会**如实标注”无法判定”**而不是硬猜。抽样踩到的坑:直连时
metadata.host常为空、dig默认只查 A 而系统可能走 IPv6、
sniff 到的可能是 CNAME 目标(www.baidu.com→www.a.shifen.com)。
所以最后是先解析、再连指定 IP、但保留 SNI —— 目的 IP 确定、分流仍按原域名判。这套脚本在
emoeem/toolbox-hub里也有一份
(多了注解头与自提权前导,用来当 TUI 动作)。两边的业务逻辑应当保持一致,改的时候一起改。switch-to-ebpf.sh --data-plane tc在本机一定会失败:内核不支持 local 的 tc 数据面
(register TC eBPF TCP listener: operation not supported)。脚本会在--mode all预检不通过时
提前拦住,别加--force白试。别把这条推广到
shared:共享数据面走的是另一套(TCX),本机实测可用
(api ebpf里能看到role=shared mechanism=tcx),直连与代理两条路都通过。早期文档把两者
混为一谈过,已修。
Singborg:eBPF A/B 试跑套件
eBPF 试跑(A/B,可回滚)
历史记录与当前结论分开阅读。 上游
trial/README.md保留了 2026-10-02 早期试跑时的判断:“TC 数据面与 shared 都不可用”,当时因此倾向回到 TUN。这是当时的记录,不是当前结论。后续 root 级逐条探测已修正该判断:本机 local 的 TC 数据面不可用;shared 的packet_rewrite/ TCX 实测可用,桥接 VM 流量可以接管。本文保留历史经过和原有永久链接,当前结论以 01 审计报告、09 踩坑与经验 和 2026-10-08 完整审计报告 为准。
预检已通过(preflight_passed,51 PASS / 0 fail,见 02 eBPF 可行性验证 §3.1)。注意:预检通过不等于每一种数据面都能工作,TC 与 shared 必须分开实测。
这一包东西用来回答最后一个问题:换成 eBPF 后,你那套已经审计过的行为还在不在。
怎么用
cd <本仓库>/trial
sudo ./run-ebpf-trial.sh # 只接管本机流量
sudo SHARED_IFACE=virbr0 ./run-ebpf-trial.sh # 顺带接管 libvirt 虚拟机
脚本做的事(只停/起服务,不改任何配置文件和开机项):
- 从
/etc/sing-box/config.json现生成试跑配置到/tmp:删掉tun-in、加上ebpf-in
(local: cgroup+dns_mode: hijack+bypass_private_address+ipv6;给了SHARED_IFACE才开 shared)。 - 用 root 跑
sing-box check(eBPF 入站的 check 会真的建 map,非 root 必定失败,这是权限不是配置)。 - 在 TUN 模式下先打一遍分(基线)。
systemctl stop sing-box→ 前台起 eBPF 核心(日志/tmp/sing-box-ebpf-trial.log)→ 等sing-box started.- 同一套判据再打一遍分,并
diff两轮结果。 - 无论成功、失败、Ctrl-C,退出时都会把系统
sing-box(TUN)拉回来。
打分判据(沿用 singbox-network-audit.md)
| 判据 | 期望 |
|---|---|
direct_isp_ip |
你的电信 IP(国内直连) |
proxy_exit_ip |
代理节点出口 IP |
ipv6_http_code |
200(eBPF 下由 local.ipv6 接管) |
ipv6_blackhole |
0(不该再有 unreachable 黑洞规则) |
ads_dns_answer |
空(DNS 层广告拦截仍生效) |
resolve_github |
ok |
tun0_state |
eBPF 模式下应为 absent(不再有 TUN 设备) |
结果落在 /tmp/sing-box-ebpf-trial-result.<时间戳>.txt。
试跑过了之后再谈什么
- 要长期用 → 先打包(
~/pkgbuild-source加packages/sing-box-ebpf/),再改 systemd 单元
(补CAP_BPF CAP_PERFMON,或直接 root 跑;见报告 §5.2),并保留 pacman 级回滚(pacman -S sing-box)。 - 只是尝鲜 → 跑一次看完 diff 就结束,什么都不用改。
v2 修复(第一次试跑踩的坑,已解决)
第一次试跑暴露出脚本自身两个问题,与 sing-box/你的配置无关:
-D /var/lib/sing-box与服务共用cache.db:试跑的cache_file.path指向服务自己的缓存文件;
而该文件此前被非 root 的sing-box check(我为了验证配置跑的)重建过,属主变成了emo:emo。
服务用户是sing-box→ 重新启动时FATAL: initialize cache-file: permission denied→ 崩溃重启循环。
v2 已改为独立状态目录/tmp/sing-box-ebpf-trial-state与独立cache.db,并在开跑前自动纠正线上 cache.db 属主。如果你在别的场合也踩到这个(服务起不来说
permission denied),一条命令修:sudo chown sing-box:sing-box /var/lib/sing-box/cache.db && sudo systemctl restart sing-box(
cache.db只是缓存,直接sudo rm -f掉也会自动重建。)恢复逻辑太急:
systemctl start后只等 2s 就判定失败,撞上RestartSec=10s的重启退避窗口。
v2 改为:先等端口 7892 释放 →systemctl restart→ 轮询is-active最多 30s → 再验tun0是否回来。
另外打分函数也修了:广告 DNS 判据改为 dig @1.1.1.1(绕开 systemd-resolved 自己的缓存),
并在每轮打分前 resolvectl flush-caches——否则切换模式后可能读到上一轮的缓存答案,得出错误结论。
注意
- 试跑期间只有一个透明代理在跑(脚本先停旧的),不会两个同时开。
- 若 eBPF 核心起不来,脚本会打印日志尾部并立即恢复——网络最多中断几秒。
- 别把
local.cgroup_path写成你终端所在的 scope(预检里那条kitty-…scope只是探测进程自己的位置);
留空 = 挂 cgroup v2 根层级 = 全局接管。








