fix(tun): TUN 地址被占用致 Windows 起核无限重试,改为避让 + 报出真因 - #332
Merged
Sway-Chan merged 2 commits intoJul 28, 2026
Conversation
根因:TUN 地址硬编码 172.19.0.1,撞上本机已有同址接口时 sing-box 在 `configure tun interface: set ipv4 address` 直接 FATAL 自杀(Windows CreateUnicastIpAddressEntry 返回 ERROR_OBJECT_ALREADY_EXISTS),外层只会 无限「正在自动重试」,用户无从得知原因。报告者机器上的占用方是一张 Disconnected 的 TAP-Windows 适配器,设备管理器默认不显示。 主要改动: - 起核前地址冲突预检 + 候选池避让(172.19/172.20/172.31/10.255.255.1)。 确证冲突才换,探测不可用一律沿用默认(fail-open);旧核仍在跑时跳过探测 并沿用上次已选地址,避免地址在重启之间乒乓。Windows 走 Get-NetIPAddress 并按 InterfaceAlias 排除自家网卡——Node 的接口枚举看不到 Disconnected 适配器,正是本 issue 的冲突源形态。 - 解析核 stderr 的 FATAL 并上屏。启动失败的 FATAL 只写 stderr、永不进 log.output,此前只有诊断报告导出时才读它,起核失败路径靠适配器存在性 反推,用户看到的永远是「TUN 初始化未完成」。三条起核路径(helper 就绪门 / wrapper PID 等待 / 运行期健康检查)现在都读,run 边界按写侧分叉:wrapper 截断语义整读、helper O_APPEND 按锚点切。 - 地址冲突转终态,停止无谓重试。该类失败是确定性的,占用方不挪走重试多少次 都是同一条 FATAL,而 wrapper 路径每腿还会重弹一次 UAC。终态携 TUN_INIT_PERSISTENT,主进程据此跳过核心自动回滚(环境冲突与核版本无关)。 - FATAL 原文不进 CoreStartRetryError.message:其中 permission/enoent 等词会 命中不可重试黑名单,把本可重试的失败静默判成终态。 验证:新增单测 60+ 例,全量 3498 passed;config-snapshot 37 份金样零变化 (默认路径下发字节不变);变异验证 38 条全部 KILLED;4 轮独立 review 收敛。 Windows 真机验证待补。 Closes dododook#324
三处 PowerShell 探测都靠 execFile 回调的 err == null 承载「查询成功且结果为空」。 这个假设不成立:-ErrorAction SilentlyContinue 只抑制错误的显示,不改 $?,查不到对象时 powershell.exe 退出码仍是 1,Node 据此把 err 置非 null。于是 free / absent 这两个结论 在真实 Windows 上永远得不到(真机实测 Windows 11 26200,空闲地址与不存在的网卡均 exit=1 且 stdout 为空): - 地址冲突预检:候选1 判 in-use 后,候选 2/3/4 明明空闲却逐个落 unverified, 循环耗尽经 exhausted 退回候选1 —— 也就是那个被占用的地址。自动避让 100% 失效。 - 正向就绪门:sawAbsent 恒 false,outcome 恒 unknown,TUN 就绪验证形同虚设。 改为脚本自带 exit 0,用 PROBE_OK 哨兵首行承载「探测链路可用」,结论完全由 stdout 承载, 退出码只兜真正的执行失败(PowerShell 缺失/被拦/超时)。脚本经 -EncodedCommand 传入。 哨兵不能无条件发:-EA SilentlyContinue 吞掉的非终止性错误不进 catch、管道照样空, 照发哨兵会把 CIM/WMI 被 EDR 拦住的健康机器判成「TUN 网卡从未创建」,进而触发硬闸 stopCore 与永久拒连 —— 比原缺陷更糟。真机实测表明两类可分(目标不存在 → ObjectNotFound;CIM 不可用 → ResourceUnavailable),故哨兵受「被吞错误全是 ObjectNotFound」判据门控。 测试侧:脚本原文是与 powershell.exe 的契约且已在真机逐条验过五态,故以全文精确断言钉死, 另补 argv/spawn 参数断言与 err 非 null + stdout 非空的输入组合。19 条变异全部 KILLED。 Refs dododook#324
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #324
根因
TUN 地址硬编码
172.19.0.1,撞上本机已有同址接口时 sing-box 在configure tun interface: set ipv4 address直接 FATAL 自杀(WindowsCreateUnicastIpAddressEntry返回ERROR_OBJECT_ALREADY_EXISTS),外层只会无限「正在自动重试」。报告者机器上的占用方是一张 Disconnected 的 TAP-Windows 适配器(人工配了
172.19.0.1/29),设备管理器默认不显示——所以「没有残留网卡」这类自查会得到反向结论。mihomo 的 TUN 默认用198.18.0.1,不撞这张卡,这解释了「以前跑 mihomo 正常、第一次跑 sing-box TUN 就不行」。决定性证据在
singbox_startup.log,而不在singbox.log——见下。改动
1. 起核前地址冲突预检 + 候选池避让
候选池
172.19.0.1 → 172.20.0.1 → 172.31.0.1 → 10.255.255.1,掩码仍按平台取(Win/16、mac/30)。纪律:Get-NetIPAddress并按InterfaceAlias排除自家网卡——Node 的接口枚举(libuv)跳过非 Up 适配器,看不到本 issue 这种 Disconnected 的冲突源tunConfig.inet4Address优先,不做避让2. 解析核 stderr 的 FATAL 并上屏
sing-box 启动失败的 FATAL 只写 stderr、永不进 config 里
log.output指定的文件(log/export.go的包级 logger 在 init 时固定绑os.Stderr)。此前只有诊断报告导出时才读singbox_startup.log,起核失败路径靠「TUN 适配器有没有出现」反推,用户看到的永远是「TUN 初始化未完成」。三条起核路径现在都读:helper 就绪门的 dead 分支 / wrapper 的 PID 等待失败 / 运行期健康检查(wrapper 的核是 detached 由看护脚本托管,死亡不产生 exit 事件,这条才是它的唯一检测通道)。run 边界按写侧分叉:wrapper 是截断语义整读,helper 是
O_APPEND按起核前锚点切——FATAL[0000]只有启动相对秒、没有墙钟时间戳,不切就会把上次甚至更早的 FATAL 当成本次原因。3. 地址冲突转终态,停止无谓重试
该类失败是确定性的:占用方不挪走,重试多少次都是同一条 FATAL,而 wrapper 路径每条重试腿还会重弹一次 UAC。终态携
TUN_INIT_PERSISTENT,主进程据此跳过核心自动回滚——地址冲突是环境问题,与核版本无关,回滚健康的新核属误伤。4. FATAL 原文不进
CoreStartRetryError.message其中的
permission/enoent等词会命中NON_RETRYABLE_START_ERROR_PATTERNS,把本可重试的起核失败静默判成终态。真因只走日志与终态错误。5. 修 PowerShell 探测的退出码契约(真机实测抓出,曾使 1 与就绪门全部失效)
三处探测原本写成
execFile(powershell, ['-Command', '<cmdlet> -ErrorAction SilentlyContinue | Select-Object ...']),靠err == null承载「查询成功且结果为空」。真机实测(Windows 11 26200):-ErrorAction SilentlyContinue只抑制错误的显示,不改$?;NodeexecFile对非零退出码置err非 null。于是「查询成功且为空」这个结论在真实 Windows 上永远得不到——地址避让 100% 失效(所有空闲候选都落unknown被跳过,循环耗尽后退回被占用的172.19.0.1),#327 的正向就绪门也恒 fail-open。改法:抽
runPsProbe单一原语经-EncodedCommand传入,用哨兵行把「探测确实跑过」从隐式假设(退出码)变成显式可观测事实,结论只由 stdout 承载。哨兵受判据门控——零错误、或被吞的错误全部是ObjectNotFound才发:这一层必须区分「目标确实不存在」(
ObjectNotFound)与「查询根本没跑通」(如 CIM 被 EDR 拦,CimJob_BrokenCimSession)——后者若被当成「网卡从未创建」,健康机器会走absent-timeout → stopCore → 三腿耗尽 → 永久拒连,比原 bug 更糟。两类的可分性已在真机上按$Error.CategoryInfo取证。验证
config-snapshot37 份金样零变化 —— 默认路径(未预检 / 探测不可用)下发给核的配置逐字节不变,正常机器不受影响shouldRetry对终态返回 true、v4/v6 文案串台powershell.exe的契约,用全文比对钉死(合法重构也会让用例失败,那是设计意图——脚本文本变了就要回真机重验五态),另加 19 条变异全 KILLED 与 argv/spawn 参数守卫Windows 真机验证(Windows 11 Build 26200,随包核 1.14.0-beta.2)
flowz-tun0Up 带172.19.0.1/16,app.log 零absent-timeout、零终态错误172.19.0.1/16)172.20.0.1,核正常起、代理可用,零重试风暴、零重复 UACsingbox_startup.log拿到 FATAL → 真因上屏并透到 UI →TUN 地址冲突,停止重试(终态)第三项复现出与 issue 里一字不差的
set ipv4 address: The object already exists.与[watchdog] sing-box exited by itself,且不再出现「wintun 被拦/驱动异常」与「UAC 授权失败,退出码: null」这两条误报。已知边界
singbox_startup.log的 ACL 未验(真机三轮走的都是 UAC 看护路径,文件属提升的用户而非 SYSTEM)OperStatus=Up的接口,mac/linux 上「接口已断开但地址仍占位」会漏检(此时由 FATAL 终态兜底给出准确原因,不会无限重试)