sing-box 在 Arch / CachyOS 笔记本上的透明代理、规则分流和本地面板实践。这份长文把审计、验证、面板、测试、优化、历史方案、踩坑、脚本、试跑套件和脱敏配置合并成一份文档,按实际使用顺序排列。

上游来自 emoeem/Singborg,每小时自动同步;原来的单篇链接会自动跳转到对应章节。项目状态概览见 Singborg 项目页。

本文由 13 篇文档自动合并,内容最后同步于 2026-10-29。每章末尾可跳转上游原文。

Singborg 01:sing-box 现状审计

sing-box 现状审计(2026-10-02,真流量实测)

本章目录(13 节)

对象:正在运行的 sing-box v1.14.2-reF1nd(审计当时是 TUN 模式,/etc/sing-box/config.json,22 出站 / 9 规则集)
方法:全部为实测——DNS 回显、DoH 绕行基准、Clash API chains+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 条缺)。两条路:

  1. 刷新系统包:sudo pacman -Syu sing-geoip-rule-set(当前 20260912,落后 3 周);
  2. 本地双保险:把上游最新文件放 /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 长期怎么吃这个仓库(两条路)

  1. 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 正好能复用为下载出口);
  2. (更契合你)在自己的 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(MemoryPeak 108,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 判据,而是:

  1. systemctl is-active
  2. sing-box api ebpf --url http://127.0.0.1:9091 --secret <clash密钥> → inbounds 非空(附件真的挂上了)
  3. 不设任何代理的 curl https://api.ipify.org 必须等于经本地代理口的出口(这是”内核接管成功”的铁证)
  4. curl https://myip.ipip.net 仍返回国内电信 IP(分流没坏)
  5. dig doubleclick.net / tr.outbrain.com 仍为空(DNS 劫持没坏)
  6. 任一失败 → 自动回滚到备份的 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 写成”只有 local cgroup 一个数据面”已不符合现状(当前是 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”那一步要么没走这个脚本、要么没留下这个备份,”备份链完整”这句只对前两类成立。

上游文档:emoeem/Singborg/docs/01-审计报告-2026-10-02.md

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_rewrite on virbr0 ✅(共享附件以 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 配置(check exit=0),并对本机跑了内核能力预检。

而且不必用 alpha:分支在 stable 基线 v1.14.2-reF1nd(与你现装的 1.14.2 同基线)上同样带
eBPF,我也编好并验证过了(check -C /etc/sing-box exit=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 也已在预检中确认就位。

两处尚需一次干净重跑才能定论:

  1. ads_dns_answer 那次超时:切换瞬间 systemd-resolved 的上游(TUN 的 172.19.0.2)刚消失,
    属于瞬时;且该判据当时还受 systemd-resolved 自身缓存干扰(脚本 v2 已改为 dig @1.1.1.1 +
    每轮 flush-caches)。
  2. 第二轮 SHARED_IFACE=virbr0 的”基线”其实是无代理直连(因为第一轮恢复失败),
    所以那份 diff 无参考价值;虚拟机共享路径仍未端到端验证(当时 virbr0 是 DOWN,没有虚拟机在跑)。

✅ 2026-10-02 晚追注:这一条已经补上了。 shared(packet_rewrite on virbr0)已做端到端实测
(netns + veth 接到 virbr0):直连与走代理两条路都通过——Clash API 命中
链路=Proxy → Auto → 新加坡Z02,virbr0 抓包 11 SYN / 11 SYN-ACK,curl 200。
另外,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 alpha 那条基线(1.15)能让你评审里惦记的 DNS type: "group" 故障转移组用上 ❌ 2026-10-02 晚更正: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 -6 200、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 属主导致服务写不了”这个具体故障
在当前单元下不会复现;但下面两条铁律(别拿线上配置跑非 root check、active ≠ 就绪)仍然成立。
⚠️ 另:本次没能复现”check 初始化 cache-file”这一步——把 cache_file.path 指到可写临时目录后
sing-box check exit=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

两条铁律:

  1. 不要用非 root 对线上配置跑 sing-box check / run。要验证就先复制一份,并把
    experimental.cache_file.path 改到 /tmp(试跑脚本 v2 已内置这个隔离)。
  2. systemctl 报 active ≠ TUN 已就绪:实测有 1 秒级竞态,is-active 为真时 tun0
    可能还没建出来。判断健康要 is-active + ip link show tun0 一起看(脚本 v2 已改成轮询 10s)。
    (eBPF 模式下没有 tun0,判据要换成 api ebpf 的附件状态 + “不设代理的出口 == 经代理的出口”,见 09 文档 §9。)

上游文档:emoeem/Singborg/docs/02-eBPF可行性验证.md

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)会被跳过并在提示里列出,不会污染配置
  • 写入配置:把启用订阅的节点写入 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_rewrite on virbr0,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 → 没有那个文件或目录)。

上游文档:emoeem/Singborg/docs/03-面板指南.md

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_rewrite on virbr0,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,
源地址是宿主 ULA fd00: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

上游文档:emoeem/Singborg/docs/04-功能测试报告.md

Singborg 05:网络层审计记录

sing-box 网络全面审计报告

本章目录(5 节)

审计时间: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_rewrite on virbr0,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 残留 ⚠️ daens netns 仍在 ✅ 已清理: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 → 200
DNS 劫持完整性 ✅ 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% loss
NTP / 时钟 ✅ ✅ systemd-timesyncd active、System clock synchronized: yes
MSS / 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 不可达但绝不泄露” 状态。两种都安全。

次要观察

  1. 订阅里有失效节点:日志反复出现
    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 当结论。

  2. 一个国内直连目标超时:182.140.248.155:443(无 PTR、直连也 000)→ 目标本身不可达,
    很可能是某个广告/追踪域名解析到的死 IP,不是配置问题。
  3. 运行中的配置仍是旧版(20:31 部署):所以现在还不包含
    DNS 广告拦截顺序修复(第 12 项)、MetaCubeXD 面板(第 11 项)、IPv6 修复(第 13 项)。

    追注(2026-10-02 晚):这条已过期 —— 配置早已更新,上述三项都已落地(其中 MetaCubeXD 反过来是被摘除),
    运行模式也从 TUN 换成了 eBPF。

  4. resolvectl 显示 tun0 已成为系统当前 DNS(172.19.0.2,DNS Domain: ~.)→ 系统解析直连 sing-box ✅。

    追注(2026-10-02 晚,eBPF 模式):现在 resolvectl 指向 wlan0(DNS fd00: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

上游文档:emoeem/Singborg/docs/05-网络审计.md

Singborg 06:透明代理优化评审

sing-box 配置优化评审 + 参考项目分析 + GUI 选型

本章目录(9 节)

本文所有改动都用 sing-box 1.14.2 真二进制 验证过(sing-box check exit=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 服务器组(多上游故障转移) ❌ 你的 1.14.2 不支持:实测 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/ 代码);你用 TUN,跨内核/跨版本更省心 ⚠️ 2026-10-02 追注:本机当天已改用 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.<时间戳>。

上游文档:emoeem/Singborg/docs/06-优化评审.md

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-emo 1.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 错误”

  1. pacman → DNS 查询被 dae 拦下;
  2. dae 要把查询转发到 tcp+udp://8.8.8.8:53,但经代理节点转发(outbound=proxy);
  3. 节点域名的 bootstrap 解析失败 / 节点拨号超时;
  4. dae 回 SERVFAIL → pacman 报 Resolving timed out / Could not resolve host;
  5. 已建连的会话在等待上游时被打断 → 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)按顺序检查:

  1. DNS 上游不要走代理:把 DNS upstream 从 8.8.8.8(当前经代理转发)改成国内可直连的
    223.5.5.5 / 119.29.29.29,并设为 direct / 直连;顺带配置 bootstrap resolver 为直连 IP,
    避免”解析节点域名又依赖节点”的循环。

  2. 刷新/更换订阅:183 次节点域名解析失败 + 95 次拨号超时说明节点大面积失效,更新订阅或换机场。

  3. 换掉 daed-emo:它是 emoeem 仓库里的第三方打包变体(Unknown Packager),
    与上游 daed 行为可能有差异。可换官方发布版对比,能立刻排除”打包/版本 bug”。

  4. 让系统别用被劫持的 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 整片报错。

上游文档:emoeem/Singborg/docs/07-daed-DNS故障诊断.md

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 网卡 / daens netns ✅ 已完成 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.service active、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. 三条铁律

  1. 永远不要同时开两个透明代理(daed + FlClash/sing-box 同时 TUN = 之前那种 DNS/TLS 全乱的局面)。
  2. 换客户端前先确认 dae 的 tc 钩子已清干净(第 3 节)。(✅ 2026-10-02 实测:wlan0 上已只剩
    noqueue,dae0 与 daens 都不存在)⚠️ 但注意:/etc/daed 目录还残留着(见文首核对表)
  3. DNS 一律偏 IPv4:这条线路 IPv6 只有 ULA、没有全局出口,任何 prefer_ipv6 都会拖垮体验。

上游文档:emoeem/Singborg/docs/08-daed-迁移计划.md

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、local tc、shared packet_rewrite、shared socket_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 晚追注(本节保留原文):结论已复核为正确,四条数据面至今仍是
local cgroup ✅ / local tc ❌ / shared packet_rewrite ✅ / shared socket_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 —— 所以”被非 root check 改成 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。

三、测量方法(我在这里犯过错,所以单独一节)

  1. 结论方向对 ≠ 机制对。我把 REFUSED 当成”拦截成功”,方向没错(应用确实拿不到解析),
    但机制错(不是”安静地拒绝”,而是”卡 5 秒”)。如果不深挖,就永远不知道体验代价。
  2. 要留下”能证伪”的判据。比如”eBPF 生效”用「不设代理出口 == 代理出口」——任何一方不成立就会红;
    而”服务 active”这种判据什么都证明不了。
  3. 警惕缓存:DNS 缓存、resolved 缓存、订阅缓存都会让”刚才明明改好了”变成假象。用随机化输入破缓存。
  4. 对照必不可少:验证广告拦截要同时问一个”必须能解析”的域名;
    验证分流要同时问国内与国外各一个;验证”节点死”要换更长的超时再下结论。
  5. 分清”历史噪声”与”当前问题”:日志里 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 有问题”翻转到”客户端根本没发包”。

我踩的四个坑(全是”测试工具破绽伪装成被测对象问题”)

  1. netns 里 127.0.0.53 是它自己的 loopback → 宿主 resolved 不可达,DNS 全挂。
    修:给 netns 单独写 /etc/netns/<ns>/resolv.conf。
  2. netns 只配 IPv4,双栈域名会先试 IPv6 → ENETUNREACH,看起来像”境外连不通”。
    修:探测时强制 curl -4 并打印 curl -6 对照。
  3. 双重接管(最难查、也最重要):netns 里的客户端是宿主上的一个进程,
    它的 socket 仍在宿主 cgroup 里 → 同时被 local cgroup 钩子和 shared 钩子看到。
    开着 local.bypass_rule_set 时,国内目标绕过 local(所以能从 shared 正常出去),
    境外目标被 local 先截走 → 那条路径失败 → 表现出来就是”shared 直连能通、走代理不通“。
    → 要验证 shared,必须临时只启用 shared(local.enabled=false),或直接用真实 VM。
  4. 抓包没 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 的流量不是宿主进程,不受影响;这是很窄的场景,但记录下来。

上游文档:emoeem/Singborg/docs/09-踩坑与经验.md

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 不存在,改审实际存在的仓库):

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 节点 server 的 domain_resolver dns-direct-ali 已撤回: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)

执行顺序:

  1. 前置备份:install -m600 生成 /etc/sing-box/config.json.bak.<TS>,另存一份原配置到临时文件用于失败对照
  2. 备份加固 + 归档(清单 1、2)
  3. 用 python 改写候选配置(清单 5、6、9、10):轮换两个 secret、收紧 CORS/移除 allow_private_network、删除 block 出站、Download.default + Auto/Auto-Japan 剔除下载节点;仅改这些字段,其余字节不动 → 得到最小 diff
  4. 先停服再 sing-box check:避免与正在运行的 eBPF 争用 BPF 资源导致误报;check 失败则按原配置立即 start 并退出(不动线上配置)
  5. 原子替换:install + mv 到 /etc/sing-box/config.json(0600 root)
  6. 写 drop-in + systemctl daemon-reload(清单 4)
  7. 启动 + 全套健康检查:服务 active(30s)→ 端口 7892/9090/9091 → 启动后 journalctl ERROR 计数 → 新密钥 200 / 旧密钥非 200 / 无密钥 401 → CORS 面板源放行且 evil.example 不放行 → 9096 面板 200 → 面板 /api/state 能读到新密钥 → 经 7892 代理 gstatic/generate_204 = 204;任一失败即回滚
  8. 汇总:备份路径、打码 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 的回声是 (AS4134 中国电信四川),但这是直连解析分流造成的:该名字命中 must-direct / dns-direct 规则,由国内递归解析器解析,Akamai 权威侧看到的是国内递归地址。CDN/国内域名走直连解析是常见且正确的设计,不是 DNS 泄漏。真正的泄漏判据是 T5.1 的 Google ECS 回声是否落在国内网段——实测落在 Cloudflare,判定通过。

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( / NRT) 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 回声 参考项 (直连解析所致,非泄漏,见 6.2) 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 的问题,如实记录)

  1. 零等待误判端口:见 5.1,已改为 60 秒轮询,并在测试脚本开头同样加了等待,修掉 T1.3 的误报。
  2. 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 回滚(三层,任选)

  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'
  2. drop-in 回滚(只撤开机等待网络):sudo rm /etc/systemd/system/sing-box.service.d/20-network-online.conf && sudo systemctl daemon-reload
  3. 备份权限回滚(一般不需要):sudo chmod 755 /etc/sing-box/backups; sudo find /etc/sing-box -maxdepth 1 -name 'config.json.bak.*' -exec chmod 644 {} +

7.2 维护建议

  1. 每季度跑一次本审计的两个脚本(先 –dry-run 看 diff);sing-box 有新版时先在 --dry-run 下核对 deprecated 列表。
  2. 统一脚本来源:只从 /home/emo/pkgbuild-source 运行同步脚本,删除或同步 Singborg 里的旧副本,消除 S1 漂移。
  3. 备份保留:保留「最新 10 份 + 7 天内不归档」,归档文件另存到加密盘更稳妥;备份里含明文凭据,务必保持 600。
  4. 密钥轮换:本次轮换后,建议每 6 个月轮换一次;轮换只需改配置并重启,面板自动跟上。
  5. 待你确认的新项:chmod 600 /var/lib/sing-box/cache.db(当前 644,本地可读 DNS 查询历史)。这不在已授权的 14 项内,你点头我再加进脚本。
  6. 节点口令复用(第 7 项)属服务商侧,建议在服务商面板逐节点改独立口令。
  7. 监控:journalctl -u sing-box -p warning --since today 每周扫一遍;missing default interface 应在 drop-in 生效后不再出现。
  8. 不要把 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

上游文档:emoeem/Singborg/docs/10-审计报告-2026-10-08.md

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;
google+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

上游文档:emoeem/Singborg/config/README.md

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,可回滚)

本章目录(5 节)

历史记录与当前结论分开阅读。 上游 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 虚拟机

脚本做的事(只停/起服务,不改任何配置文件和开机项):

  1. 从 /etc/sing-box/config.json 现生成试跑配置到 /tmp:删掉 tun-in、加上 ebpf-in
    (local: cgroup + dns_mode: hijack + bypass_private_address + ipv6;给了 SHARED_IFACE 才开 shared)。
  2. 用 root 跑 sing-box check(eBPF 入站的 check 会真的建 map,非 root 必定失败,这是权限不是配置)。
  3. 在 TUN 模式下先打一遍分(基线)。
  4. systemctl stop sing-box → 前台起 eBPF 核心(日志 /tmp/sing-box-ebpf-trial.log)→ 等 sing-box started.
  5. 同一套判据再打一遍分,并 diff 两轮结果。
  6. 无论成功、失败、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/你的配置无关:

  1. -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 掉也会自动重建。)

  2. 恢复逻辑太急: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 根层级 = 全局接管。