平台工程

2026-08-19 · vpn-guard 出口归属门槛与四处假绿修复 · 迭代复盘

入档:2026-08-19 来源:一次从「我从没被封、同事却反复被封,为什么」起步的会话,一路做成 vpn-guard 的一轮加固 状态:已落地,分支 feat/exit-provenance-gate 6 个 commit,CI 硬闸门 48 项全绿;app-vpn.* 待作者自行提交 环境:Windows 10 Pro + Clash Verge Rev(verge-mihomo,TUN 模式),机场「国外默认」组 41 节点


事实记录(不可修改区)

实测数据(2026-08-19,auto-select-node.ps1 -DryRun

Top10 低延迟节点里 9 个是机房出口,其中 6 个直接租自公有云:

节点出口 ISPASNip-api hosting
🇹🇼 台湾07Chunghwa TelecomAS3462(16 位)false —— 唯一真住宅
🇯🇵 日本01IPS INCAS131939(32 位)false
🇹🇼 台湾01Pittqiao Network InformationAS131642(32 位)false
🇭🇰 香港03Mejiro Network LimitedAS209642(32 位)false
🇸🇬 新加坡01DigitalOceanAS14061true
🇯🇵 日本03 / 🇰🇷 韩国01 / 🇮🇳 印度01AmazonAS16509true
🇰🇷 韩国03 / 🇦🇺 澳洲02OracleAS31898true

commit

6036d0d 轮换检测 + ASN 归属门槛 + 两个评分 bug · 355cd54 互补性文档 · b63d9d8 审计器三态判定 + 两处假 FAIL · c04b571 回归测试挂 CI · b907d8e README 定位重写 + 14 处文档订正 · bdb2a58 Unix 系统代理假绿


弧线:问题不在工具里

起点是一个归因问题,不是功能需求。答案落在工具碰不到的那一层——账号出身,已抽成 ⭐ 信任资产保得住造不出_账号基线vs人群先验_v1

但归因过程反过来暴露了工具的一串真问题:代码量集中在最低权重的那一层(技术指纹几百行),而最高权重的 IP 信誉只有两个布尔值。


工具侧的四个发现

1. hosting=false 不等于住宅(判据要找结构性事实)

ip-api 的 hosting 标记漏报了三家小主机商。有效的判据不是名字长相,而是ASN 分配年代:16 位 ASN 空间(1–65535)在 2014 年前后被各注册局分配殆尽,而做大众家宽需要巨量地址和多年运营史,所以在位运营商全持低号段;32 位 ASN 绝大多数是之后注册的小主机商。IPv4 与 16 位 ASN 双双耗尽,这条分界线不会再移动

两个机制沿号段年代互补,覆盖不重叠(实测验证,不是巧合):

机制覆盖本次命中
ip-api hosting=true老牌大云(16 位老号段)AWS / DO / Oracle,6/6
ASN 年代规则新小主机商(32 位)IPS / Pittqiao / Mejiro,3/3,ip-api 全部漏报

关键不变量:6 个大云无一被 ASN 规则误判为住宅——那是唯一不可接受的方向。

2. 四处「假绿 / 假红」,全是既有代码

位置症状
评分公式的延迟项delay 存 hashtable 键,Measure-Object 只认 PSObject 属性 → 取不到极值 → 归一化退化成常数,实际生效的是「带宽 80% + TLS 20%」而非文档写的 60/25/15。错误被脚本头部 SilentlyContinue 吞掉
单节点池Sort-Object 返回标量 Hashtable,$ranked[0] 退化成按键取值返回 $null(部署目标为空),.Count 返回键数 → 结果表打十几行空行。新门槛的本职就是造出单节点池@() 包裹是必需的
第 2 项(Unix)两侧都是 curl,而 curl 不读 scutil/gsettings → 系统代理下两侧都直连、拿到同一 IP → 打绿「不认代理的程序也被隧道接管」。影响面覆盖第 3/4/5 项(都拿第 1 项当基准)
第 2 项(两版)轮换池下两个探测都走隧道却拿到不同 IP → 报了个不存在的「真实 IP 正在泄露」

「延迟项失效」的证明方式值得记:用坏公式重算,精确复现了实跑打印的全部四个分数(100 / 98.2 / 49.4 / 35.7)。能复现输出的模型才是被证明的模型,比读代码下结论硬。

3. 有一条结构性论断可以不测就确信

TAKEOVER="sysproxy" 只在 case "$route_if"*) 分支设置,TUN_ROUTED=1 只在 TUN 分支设置——二者互斥。于是「不认代理的程序也被隧道接管」这句话在非 TUN 下恒为假,与两侧 IP 是否相同无关。这条修复不需要取任何代理地址,纯靠代码结构推理。

判据:先找能靠结构推出来的那部分,它不需要环境就能验证。 剩下需要环境的部分(取 scutil 地址)再单独处理,而且要能在取不到时诚实降级,绝不猜。

4. 内联三份拷贝反而是对的

ASN 名单要进 3 个脚本。共享库文件会让分类器的唯一正面判定依赖一个用户没有的文件——而这个仓库的分发方式就是「拷一个脚本走」。剥掉机构名后名单只是 93 个整数、26 行,且任何分歧都只影响召回:漏一条把「住宅」降级为「未识别」,永远不可能产出虚假的安全声明(两条会告警的判据根本不读这张表)。

判据:允许重复的前提,是重复的那部分「错了只会更保守」。 再由 CI 做机械比对兜住漂移。


方法论教训

测试不会失败,就等于没有测试

写完 30 条断言后先投毒证明它会挂:改判定阈值 → FAIL=2;往机房词表加裸词 data → 应 FAIL;只改一版措辞 → FAIL=1 带逐行 diff。

第二种投毒通过了 30/30——我自己的哨兵是坏的。 原因:哨兵样本的 ISP 写成 "Some Regional Telecom Co.",其中 Telecom 命中运营商词表 −3,正好抵消裸 data 的 +3,那条断言在任何情况下都触发不了。这是 全站文字截断体检_检测工具本身要先被证伪_v1 的新变体:

那篇是工具的判据不可信,本篇是哨兵被自己的样本中和——断言写对了、样本选错了,症状是永远绿。 对口手法:投毒。不投毒就永远发现不了,因为它和「真的没问题」表现完全一致。

顺带纠正了主脚本里一条说过头的注释(称裸 data 会「误杀唯一一个真住宅节点」——实际中华电信被名单在第 1 步短路,根本走不到词表)。

文档会从代码上漂走,而且双语同漂时 diff 查不出来

三路核查文档 vs 代码,33 条原始发现确认 18 条。其中若干是我自己刚写的:

判据:自己写的两份平行文档不能互为校验,必须拿代码当第三方。

会用工作流,也要会怀疑工作流

两次多路设计工作流里,第二次 3 个设计 agent 挂了 2 个(StructuredOutput 重试超限),产出实质是「单一方案 + 自评」,对抗性并没拿到。此时不能按「三路综合」的信任度采纳:13 个 block 只取骨架,每条 premise 独立复核,状态机自己写。

判据:读工作流结果前先读它的失败计数。

用户漏掉的操作风险,可能就在工具自己身上

两版 README 都没提:扫描过程本身会把全局出口逐个切过每个候选节点(默认最多十余次,含随后才被淘汰的)。开着 Claude 跑,会话几分钟内跨多国跳变——正是文档开头权重表里第 3 层的「国家跳变」。-DryRun 同样会切。


关联文档

类型/平台工程主题/账号安全主题/代理链路主题/回归测试