2456 字
12 分钟
连上 Wi-Fi 才启动 VPN:Windows 计划任务的两个默认陷阱

我的笔记本在学校图书馆连 Wi-Fi 会断网。手机在同一个 Wi-Fi 下好好的。

排查了大半天,最后定位到:是我的 VPN(Clash 系,TUN 模式)启动太早了。

这台笔记本开机后我如果不开 VPN,一切正常。开了 VPN 去图书馆,必断。


一、为什么 TUN 模式的 VPN 会让图书馆连不上网#

TUN 模式在干什么#

普通代理模式(系统代理 / HTTP 代理)只接管明确配置了走代理的应用。TUN 模式不一样 —— 它在系统里创建一张虚拟网卡,然后把默认路由抢过来:

正常状态:
0.0.0.0/0 → 192.168.1.1 via WLAN (metric 25) ✓ 生效
0.0.0.0/0 → 10.0.0.1 via Clash-TUN (metric 5) ✗ 被压制
TUN 启动后:
0.0.0.0/0 → 10.0.0.1 via Clash-TUN (metric 5) ✓ 现在生效
0.0.0.0/0 → 192.168.1.1 via WLAN (metric 25) ✗ 还在表里,但不承载流量

Windows 同时只会用一条默认路由(多条时按 metric 取最小)。TUN 的目标就是把这条唯一的名额抢走,这样才能做到”全局代理”。

但图书馆的 WLAN 有一个特殊之处#

家里的 Wi-Fi 连上就是通的。学校图书馆的 WLAN 不是 —— 它通常要过 802.1X(企业级认证) 或 Portal(网页认证)。这个过程有几个硬性要求:

flowchart TD A["网卡关联 AP
(物理层已连接)"] --> B["4 次握手
拿到 IP / 网关 / DNS"] B --> C{"需要认证吗?"} C -->|家用WiFi| H["可以直接上网 ✓"] C -->|图书馆 802.1X / Portal| D["必须能发出
未认证的 EAPOL / Portal 流量"] D --> E{"TUN 已接管默认路由?"} E -->|否 未启动| F["认证包正常走 WLAN ✓"] E -->|是 已启动| G["认证包被路由进 TUN
或被 kill-switch 丢弃 ✗"] G --> I["认证一直不完成
= 连上了 Wi-Fi 但没网"]

问题的本质:“物理上关联成功”和”能上网”之间,还隔着一个必须依赖默认路由的认证阶段。 而 TUN 恰好在这个阶段把默认路由拿走了。

具体的死法有几种,通常几个一起#

sequenceDiagram autonumber participant W as 开机/唤醒 participant OS as Windows 网络栈 participant AP as 图书馆 AP + 认证服务器 participant V as VPN 客户端 (TUN) participant NIC as 物理网卡 WLAN W->>V: 用户开机,VPN 自启 V->>OS: 创建虚拟网卡,注入 0.0.0.0/0 (低 metric) Note over OS: 默认路由此刻已被 TUN 占据 W->>OS: 开始连接 Wi-Fi OS->>AP: 关联 + 4 次握手 AP-->>OS: DHCP Offer(网关 192.168.x.1) Note over OS: 路由表已有默认路由(TUN 的),
无法改走物理网卡 OS->>AP: 发起 802.1X 认证 / Portal 探测 alt strict-route / kill-switch 开启 AP--xOS: 认证响应被丢弃 Note over OS,AP: ✗ 永远认证不通过 else 认证包被路由进 TUN OS->>V: 认证包进了隧道 V--xAP: 出不去,或回包进不来 Note over OS,AP: ✗ 同样卡死 end V->>OS: DNS 已劫持到隧道内 (1.1.1.1 等) Note over OS,V: 要解析出口域名才能建隧道,
但 DNS 在隧道里 → 先有鸡还是先有蛋 Note over OS: 重试 → 认证失败 → 认证不通过 → 系统拿不到 IP

把上面压缩成一句话:

TUN 模式假设”你已经能上网了”。而图书馆的 WLAN 假设”你还没认证完”。两个假设互相打架。

还有一个更隐蔽的:DNS 死锁。TUN 模式通常会把 DNS 也劫持到隧道内的公共 DNS(1.1.1.1 / 8.8.8.8)。而隧道要建连,得先能解析出口服务器的域名 —— 解析请求被送进一个还没建好的隧道。配置不讲究的客户端就会一直卡在这里反复重试。

WARNING

成熟客户端一般会维护一份 bypass 列表,把出口服务器 IP 和内网网段排除在隧道之外,避免自噬。这份列表配得好不好,直接决定你会不会遇到”自己把自己套进去”。


二、解法:把 VPN 启动推迟到”网络就绪之后”#

不要在开机时启动 VPN。等系统告诉你”网络连接已经建立”,再启动。

Windows 里这件事的官方触发器就是 Microsoft-Windows-NetworkProfile/Operational 事件日志的 EventID 10000(网络连接建立)。

任务计划程序 → 创建基本任务 → 触发器 → 自定义 → 事件触发器:

项值
日志Microsoft-Windows-NetworkProfile/Operational
事件 ID10000
任务启动 VPN 客户端

我本机跑的任务实际长这样(\用户程序库\Clash Network Start):

<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<Principals>
<Principal id="Author">
<UserId>S-1-5-21-xxxxxxxx-1001</UserId>
<LogonType>InteractiveToken</LogonType>
</Principal>
</Principals>
<Settings>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine>
</Settings>
<Triggers>
<EventTrigger>
<Subscription>&lt;QueryList&gt;&lt;Query Id="0" Path="Microsoft-Windows-NetworkProfile/Operational"&gt;&lt;Select Path="Microsoft-Windows-NetworkProfile/Operational"&gt;*[System[Provider[@Name='Microsoft-Windows-NetworkProfile'] and EventID=10000]]&lt;/Select&gt;&lt;/Query&gt;&lt;/QueryList&gt;</Subscription>
</EventTrigger>
</Triggers>
<Actions Context="Author">
<Exec>
<Command>"C:\Program Files\YourVPN\YourVPN.exe"</Command>
</Exec>
</Actions>
</Task>
一键注册脚本(PowerShell,管理员)
Terminal window
$xml = @'
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo><Author>me</Author></RegistrationInfo>
<Principals>
<Principal id="Author">
<UserId>DOMAIN\user</UserId>
<LogonType>InteractiveToken</LogonType>
</Principal>
</Principals>
<Settings>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
</Settings>
<Triggers>
<EventTrigger>
<Subscription>&lt;QueryList&gt;&lt;Query Id="0" Path="Microsoft-Windows-NetworkProfile/Operational"&gt;&lt;Select Path="Microsoft-Windows-NetworkProfile/Operational"&gt;*[System[Provider[@Name='Microsoft-Windows-NetworkProfile'] and EventID=10000]]&lt;/Select&gt;&lt;/Query&gt;&lt;/QueryList&gt;</Subscription>
</EventTrigger>
</Triggers>
<Actions Context="Author">
<Exec><Command>"C:\path\to\vpn-client.exe"</Command></Exec>
</Actions>
</Task>
'@
Register-ScheduledTask -TaskName "VPN On Network Up" -Xml $xml -Force

验一下:

Terminal window
Get-ScheduledTask -TaskName "VPN On Network Up" | Select-Object TaskName, State

三、你问的那两个坑:你的理解是对的,而且很常见#

坑 1:默认 72 小时后任务被杀掉#

是真的。

Task Scheduler 的 ExecutionTimeLimit(“如果任务运行时间超过此时间,则停止任务”)默认值是 PT72H,也就是 72 小时。文档里的原话是”如果未指定该值,默认使用 72 小时”。

一个开机常驻的任务 —— 比如 VPN 客户端 —— 跑满三天就会被系统主动干掉,而且:

  • 日志里只记一句”任务因超出时间限制而终止(任务已完成)”
  • 不会弹任何提示
  • 你不会意识到是计划任务干的

改法:PT0S = 不限时。

<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>

坑 2:拔电源会停任务#

也是真的,而且是两个独立的开关。

任务计划程序创建任务时,GUI 里默认勾着「如果计算机使用电池电源,则停止任务」。这个勾选会写出两个 XML 节点:

<DisallowStartIfOnBatteries>true</DisallowStartIfOnBatteries> <!-- 电池上干脆不启动 -->
<StopIfGoingOnBatteries>true</StopIfGoingOnBatteries> <!-- 运行中拔电就杀掉 -->

两个默认都是 true。表现是:笔记本拔掉电源线的那一瞬间,VPN 客户端进程被计划任务管理器直接结束。

改法:两个都置 false。

<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>

等价的 cmdlet 写法:

Terminal window
New-ScheduledTaskSettingsSet `
-ExecutionTimeLimit ([TimeSpan]::Zero) `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries `
-MultipleInstances IgnoreNew
TIP

我本机这个任务的 XML 里这两个坑都已经改过来了(PT0S + 两个 false)。所以如果你现在遇到的是我之前那个症状 —— 图书馆连不上网但别的地方正常 —— 那就不是这两个默认值的问题,回到第一节的路由抢占去看。

顺带第三个坑:IdleSettings#

有些任务里还会看到这一段:

<IdleSettings>
<Duration>PT10M</Duration>
<WaitTimeout>PT1H</WaitTimeout>
<StopOnIdleEnd>true</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>

它的含义是「仅当计算机空闲至少 10 分钟时才启动任务」,加上「一旦不空闲就停止」。只有 <RunOnlyIfIdle>true</RunOnlyIfIdle> 时才生效 —— 默认是 false,所以一般无害。

但如果你哪天在 GUI 里手滑勾上了「仅当计算机空闲时才启动任务」,拔电源断网这种玄学问题就又能重现了。


四、EventID 10000 的两个副作用#

它不只对 Wi-Fi 触发#

EventID 10000 是任何网络配置建立都会触发,包括:

  • 有线网
  • 手机 USB 网络共享
  • VPN 自己那张虚拟网卡重连
  • Docker / WSL2 虚拟交换机

所以完全可能出现”VPN 自己断了 → 触发 10000 → 计划任务又去启动 VPN → 反复重启”。

想严格只认 Wi-Fi,把动作改成调一个脚本做判断:

start-vpn-if-wifi.ps1
$wifi = Get-NetAdapter | Where-Object { $_.Status -eq 'Up' -and $_.NdisPhysicalMedium -eq 9 }
if ($wifi) { Start-Process 'C:\Program Files\YourVPN\YourVPN.exe' }

NdisPhysicalMedium -eq 9 就是 Native 802.11。

事件日志通道必须处于启用状态#

Microsoft-Windows-NetworkProfile/Operational 默认是开的。但如果被组策略关掉或者日志被清空过,触发器会静默失效 —— 任务状态显示 Ready,永远不触发,也没有任何错误提示。

验证:

Terminal window
wevtutil gl Microsoft-Windows-NetworkProfile/Operational | Select-String enabled

五、排查这类问题的通用顺序#

以后再遇到”某个网络下连不上”,按这个顺序查,能少走很多弯路:

1. 关掉 VPN 试试
└─ 好了 → 是路由/DNS/防火墙问题,不是 Wi-Fi 的问题
2. route print 看 0.0.0.0/0 有几条、哪条 metric 最小
└─ TUN 的那条生效 → 就是本文说的抢占
3. nslookup 域名 + ping 网关
└─ 网关通、域名不通 → DNS 劫持/死锁
4. ipconfig /all 看 IPv4 是不是 169.254.x.x
└─ 是 → DHCP 续租失败,认证阶段就被卡住了
5. netsh wlan show interfaces 看 认证状态
WARNING

第 2 步是分水岭。如果你看到一个软件的虚拟网卡出现在默认路由里且 metric 最小,那基本可以确定问题出在它身上,不用再往 Wi-Fi 配置上找。


小结#

  • 图书馆连不上网的根因:TUN 抢占了默认路由,而 802.1X / Portal 认证必须在认证完成前使用默认路由。TUN 假设”你已能上网”,图书馆 WLAN 假设”你还没认证”,直接对撞。
  • 解法:用计划任务监听 Microsoft-Windows-NetworkProfile/Operational 的 EventID 10000,等网络建立后再启动 VPN。
  • 你记得的两个默认坑都是真的:ExecutionTimeLimit 默认 PT72H(改成 PT0S);DisallowStartIfOnBatteries / StopIfGoingOnBatteries 默认 true(改成 false)。
  • 还有第三个:IdleSettings 的 StopOnIdleEnd,勾上「仅空闲时运行」就会重现玄学掉线。
  • EventID 10000 不区分网络类型,严谨点要加 Wi-Fi 判断,否则 VPN 重连会自触发。

Windows 计划任务这玩意儿的默认值都是为”跑一次就完事的短任务”设计的。你一旦用它跑常驻服务,就得把这几个默认一个个翻过来 —— 症状还都是”过几天突然不好使了”,不查根本想不到是它。

连上 Wi-Fi 才启动 VPN:Windows 计划任务的两个默认陷阱
https://ycxdev.com/posts/windows-scheduled-task-vpn-on-network-up/
作者
YCX
发布于
2026-10-12
许可协议
CC BY-NC-SA 4.0