Skip to content

fix(tun): TUN 地址被占用致 Windows 起核无限重试,改为避让 + 报出真因 - #332

Merged
Sway-Chan merged 2 commits into
dododook:mainfrom
Sway-Chan:fix/324-tun-addr-conflict-fatal-surface
Jul 28, 2026
Merged

fix(tun): TUN 地址被占用致 Windows 起核无限重试,改为避让 + 报出真因#332
Sway-Chan merged 2 commits into
dododook:mainfrom
Sway-Chan:fix/324-tun-addr-conflict-fatal-surface

Conversation

@Sway-Chan

@Sway-Chan Sway-Chan commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Closes #324

根因

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.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)。纪律:

  • 确证冲突才换,探测不可用(杀软拦 PowerShell 等)一律沿用默认(fail-open)——换地址本身有代价,用户钉着旧地址的路由/防火墙规则会失效
  • 旧核仍在跑时跳过探测并沿用上次已选地址:预检早于 [Bug]偶尔还是会出现singbox进程意外退出问题。 #159 的适配器释放门,不这样做会把自家残留判成冲突,地址在重启之间乒乓
  • Windows 走 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):

Get-NetIPAddress -IPAddress 172.19.0.1   (空闲)    → exit=1  stdout=[]
Get-NetIPAddress -IPAddress 192.168.10.207 (占用)  → exit=0  stdout=[192.168.10.207]
Get-NetAdapter   -Name flowz-tun0 (不存在)         → exit=1  stdout=[]

-ErrorAction SilentlyContinue 只抑制错误的显示,不改 $?;Node execFile 对非零退出码置 err 非 null。于是「查询成功且为空」这个结论在真实 Windows 上永远得不到——地址避让 100% 失效(所有空闲候选都落 unknown 被跳过,循环耗尽后退回被占用的 172.19.0.1),#327 的正向就绪门也恒 fail-open。

改法:抽 runPsProbe 单一原语经 -EncodedCommand 传入,用哨兵行把「探测确实跑过」从隐式假设(退出码)变成显式可观测事实,结论只由 stdout 承载。哨兵受判据门控——零错误、或被吞的错误全部ObjectNotFound 才发:

$ErrorActionPreference = 'SilentlyContinue'
$Error.Clear()
try {
  $r = @(<pipeline>)
  if (@($Error | Where-Object { $_.CategoryInfo.Category -ne 'ObjectNotFound' }).Count -eq 0) {
    Write-Output 'PROBE_OK'
    foreach ($x in $r) { Write-Output $x }
  }
} catch { }
exit 0

这一层必须区分「目标确实不存在」(ObjectNotFound)与「查询根本没跑通」(如 CIM 被 EDR 拦,CimJob_BrokenCimSession)——后者若被当成「网卡从未创建」,健康机器会走 absent-timeout → stopCore → 三腿耗尽 → 永久拒连,比原 bug 更糟。两类的可分性已在真机上按 $Error.CategoryInfo 取证。

验证

  • 新增单测 60+ 例,全量 3513 passed / 0 failed,tsc / eslint 零 warning
  • config-snapshot 37 份金样零变化 —— 默认路径(未预检 / 探测不可用)下发给核的配置逐字节不变,正常机器不受影响
  • 变异验证 45 条全部 KILLED,含几条关键逃逸:探测不可用改 fail-closed(会把所有杀软拦 PowerShell 的机器拖下水)、预检异常穿透起核路径、shouldRetry 对终态返回 true、v4/v6 文案串台
  • 改动 5 的脚本原文是与 powershell.exe 的契约,用全文比对钉死(合法重构也会让用例失败,那是设计意图——脚本文本变了就要回真机重验五态),另加 19 条变异全 KILLED 与 argv/spawn 参数守卫
  • 4 轮独立 review 收敛至零 High/Med/Low

Windows 真机验证(Windows 11 Build 26200,随包核 1.14.0-beta.2)

场景 结果
正常起核(无冲突) TUN 起得来,flowz-tun0 Up 带 172.19.0.1/16,app.log 零 absent-timeout、零终态错误
构造同址冲突(给以太网加 secondary 172.19.0.1/16 预检判 in-use → 避让到 172.20.0.1,核正常起、代理可用,零重试风暴、零重复 UAC
候选池 4 个全占满 回落默认 → singbox_startup.log 拿到 FATAL → 真因上屏并透到 UITUN 地址冲突,停止重试(终态)

第三项复现出与 issue 里一字不差的 set ipv4 address: The object already exists.[watchdog] sing-box exited by itself,且不再出现「wintun 被拦/驱动异常」与「UAC 授权失败,退出码: null」这两条误报。

已知边界

  • helper 服务路径下 singbox_startup.log 的 ACL 未验(真机三轮走的都是 UAC 看护路径,文件属提升的用户而非 SYSTEM)
  • run 边界在 3 腿重试下的分段未验(地址冲突一腿即终态,没制造出多腿场景)
  • 非 Windows 的占用探测只覆盖 OperStatus=Up 的接口,mac/linux 上「接口已断开但地址仍占位」会漏检(此时由 FATAL 终态兜底给出准确原因,不会无限重试)
  • Linux 直起路径(setcap 回退、helper 缺位)无 startup log 写侧,冲突不会短路,会烧完重启预算才停
  • TUN 地址仍未开放到 UI,用户无法自行改地址绕开——单独跟进

根因: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
@Sway-Chan
Sway-Chan merged commit 47508d7 into dododook:main Jul 28, 2026
2 checks passed
@Sway-Chan
Sway-Chan deleted the fix/324-tun-addr-conflict-fatal-surface branch July 28, 2026 01:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Windows10下tun模式无法开启

1 participant