Windows 没有为它的 VPN 客户端提供任何重拨策略,所以午夜断开的连接到了早餐时仍然断着。一个监视断开事件并运行 rasdial 的计划任务,无需第三方客户端就能补上这个缺口。任务究竟像一个运维人员还是一个死循环,取决于三个细节:它匹配哪些事件、它能看到哪个凭据、它如何处理结果。这三点都不难,而漏掉其中任何一点,就会得到一个永远拨号的任务。

为什么定时器是错误的触发方式

定时器按自己的时间表拨号,不管隧道是否断开。这会产生没人需要的尝试,而工作会话中的一次尝试可能中断会话。事件触发器只在连接改变状态时触发,更便宜也更容易推理。事件还携带原因,而原因正是把网络故障与人为故意断开区分开的东西。

这个区别就是有用的任务与每晚和用户较劲的任务之间的差别。它也是那些只描述动作的配方里通常缺失的部分。

这对事件,以及需要抑制的那一个

远程访问服务客户端会在 RasClient 提供程序下把生命周期记录写入 Application 日志。20226 标记一次结束的连接,20227 标记一次失败的尝试,所以两者都能为新的拨号提供依据。任务中的其他一切都取决于这两个标识符能否精确匹配。它们多年来一直稳定,所以基于它们的配方仍在流传,但一次改名提供程序或 ID 的构建会让查询什么都返回不了。每次功能更新后都要重跑证明步骤,而不是等任务停止触发。

有一种结束需要排除。错误 631 表示端口被用户断开,此时重拨就是在和刚刚故意切换网络的人较劲。把排除放在触发器过滤器里而不是脚本里,这样任务根本不会启动。它必须只适用于一个事件 ID,因为 EventData 谓词只有在指定元素存在时才匹配,而共享的子句会悄悄丢掉另一个 ID 的每个事件。写过滤器之前,先从一个真实记录里读出元素名:

Get-WinEvent -LogName Application -MaxEvents 5 -FilterXPath "*[System[Provider[@Name='RasClient'] and EventID=20226]]" | Select-Object -First 1 -ExpandProperty ToXml

XML 会显示该记录中 Data 的确切名称。ErrorCode 是常见写法,如果记录用的是别的名称,就要在下方的过滤器里替换。

<QueryList><Query Id="0" Path="Application"><Select Path="Application">*[System[Provider[@Name='RasClient'] and (EventID=20226 or EventID=20227)]] and (EventData[Data[@Name='ErrorCode'] != '631'] or System[EventID=20227])</Select></Query></QueryList>

把它粘贴到触发器的自定义过滤器里,并在它成为触发器之前先作为查询验证一次。同一个 XPath 可以从 PowerShell 对实时日志运行,这是在任务历史还没牵涉进来时看清它匹配什么的最便宜办法。

Get-WinEvent -LogName Application -MaxEvents 40 -FilterXPath "*[System[Provider[@Name='RasClient'] and (EventID=20226 or EventID=20227)]] and (EventData[Data[@Name='ErrorCode'] != '631'] or System[EventID=20227])" | Select-Object TimeCreated,Id,Message

一个悄悄什么都不匹配的触发器,看起来和一个运行后失败的任务完全一样。两者都表现为“自动化不工作”,也都不说缺的是哪一半。先检查查询输出,因为它能一步把两种情况分开。

拨号失败,因为没有人来输入密码

操作会带着连接名运行 rasdial。当配置文件没有缓存的凭据时,拨号会立刻失败,因为没有人去回答提示。这是正确触发的任务却不做任何可见事情的最常见原因,所以在碰触发器之前先检查这一点。从命令提示符手动拨号,可以把凭据问题和计划问题分开。

在任务将要运行的账户下保存一次凭据,并把这个账户作为设计的一部分。以 SYSTEM 运行的任务读不到你的交互账户缓存的凭据,以别人身份运行的任务也读不到。名称还必须与配置文件完全一致,包括用户字段里任何域前缀。

rasdial
rasdial "Office VPN"
(Start-Process rasdial -ArgumentList '"Office VPN"' -Wait -PassThru -NoNewWindow).ExitCode

第一条命令列出 Windows 知道的名称。第二条用缓存的凭据按名称拨号,第三条才是 PowerShell 操作应该使用的形式,因为它会等待并返回任务可以记录的值。

读退出代码,而不是看窗口

启动进程却不等待的任务,只要进程存在就报告成功。对于一秒后可能失败的拨号来说,这个结论毫无价值。等待退出代码才能把操作变成真正结果。仅凭任务历史只能说明进程启动了,永远说明不了隧道起来了。

每次运行都用时间戳记录该值。中断之后单次失败是正常的,而同一代码连续失败三次,指向的是凭据或配置文件,而不是网络。正是这个模式让历史值得一读。

加一个很短的重复间隔作为保险,并明确说明它与事件触发器如何相互作用。连续五分钟每分钟重复一次,能在网络随隧道一起恢复时提供第二次尝试,而在“设置”下,“如果任务已在运行,则应用以下规则”里,任务应选择“不启动新实例”,这样重复就不会与仍在进行的拨号重叠。没有这项设置,两个 rasdial 进程会争抢同一个端口,第二个会在没有任何有用理由的情况下失败。

证明恢复路径

有意测试两个方向。先手动断开,确认没有发生重拨;然后禁用网络适配器,确认链路恢复时任务会触发。这两个测试覆盖了抑制规则和真实的故障情况,别的做法都做不到。

之后要读任务历史而不是连接图标。历史记录了任务何时运行、用了哪个账户、操作返回了什么,而这些正是下个月同一台机器再次掉线时需要的证据。

相关文章