Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

介绍

对于Wireguard隧道,其域名的解析是一次性的,只在启动时将endpoint的域名解析为ip后就不再变化,当DNS记录更新时,WIreguard无法自动更新对端地址,之后则会断开连接。这种情况通常需要重新启动Wireguard隧道再次触发解析,但是借助powershell脚本自动更新会是更加便利智能的方法。在这里我基于 Kenvix的博客 做了二次修改,修改为主动从DNS服务器解析地址,避免路由器/运营商等的DNS缓存导致的隧道重连不及时的问题,并且可以指定查询的DNS服务器地址,指定为域名对应的权威解析服务器以获得更实时的结果;可以指定优先解析A记录或是AAAA记录。

脚本

以下是powershell脚本的代码,将其保存至Wireguard-Dynamic-Resolver.ps1文件,建议将其保存至系统盘的目录下,如C:\wireguard-tools\Wireguard-Dynamic-Resolver.ps1,以便系统服务调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# Copyright (C) 2021 Max Schulze. All Rights Reserved.
# Modified by Kenvix <i@kenvix.com> @ 2023/9/15
# Modified by CorneliaMo <SweetCorneliaMo@163.com> @ 2026/3/9
#
# near-literal Translation of the linux version by Jason A. Donenfeld
# to decrypt the dpapi Credentials, you have to be the same user as the wireguard tunnel service, i.e. "nt authority\system", check with "whoami"
# this script might be called by task scheduler as
# powershell -NoProfile -NoLogo -NonInteractive -ExecutionPolicy Bypass -Command ./WireGuard-Reresolve.ps1 -LoopRunAsCron -DelaySeconds 600
# if you want to try it in cmd, remember to elevate the user, i.e. with psexec from sysutils
# psexec -s -i powershell -NoPr...
#Requires -RunAsAdministrator
param(
[switch]$LoopRunAsCron = $false,
[int]$DelaySeconds = 600,
# Optional: specify DNS server address for Resolve-DnsName. If not set, use system default DNS.
[string]$DnsServer = $null,
# Optional: prefer AAAA record first (IPv6). Default is prefer A first (IPv4).
[switch]$PreferAAAA = $false
)
Set-StrictMode -Version 3
Add-Type -AssemblyName System.Security
function WireGuard-Reresolve {
param(
[Parameter(Mandatory = $true)]
[string]$File
)
Set-Variable CONFIG_FILE -Value $File.ToString().Trim('"')
Write-Host "Loading $CONFIG_FILE"
if (-not (Test-Path -Path $CONFIG_FILE)) {
throw New-Object System.IO.FileNotFoundException("Wireguard Config $CONFIG_FILE File not found.", $CONFIG_FILE)
}
$byteCrypted = ((Get-Content -LiteralPath $CONFIG_FILE -Encoding Byte -ReadCount 0))
$config = [System.Security.Cryptography.ProtectedData]::Unprotect($byteCrypted, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine)
$config = [System.Text.UTF8Encoding]::UTF8.GetString($config)
Set-Variable Interface -Option Constant -Value $(if ($CONFIG_FILE -match '.?([a-zA-Z0-9_=+.-]{1,64})\.conf.dpapi$') { $matches[1] } else { $null })
function resolve_endpoint_ip () {
param(
[Parameter(Mandatory = $true)][string]$Endpoint
)
# Endpoint format examples:
# example.com:51820
# 1.2.3.4:51820
# [2001:db8::1]:51820
Write-Host "Resolving $Endpoint"
$hostAddress = $null
$port = $null
if ($Endpoint -match '^\[([^\]]+)\]:(\d+)$') {
# IPv6 literal
return $Endpoint
}
elseif ($Endpoint -match '^([^:]+):(\d+)$') {
$hostAddress = $matches[1]
$port = $matches[2]
}
else {
return $Endpoint
}

# If hostAddress is already an IP literal, keep as-is.
if ($hostAddress -match '^\d{1,3}(\.\d{1,3}){3}$' -or $hostAddress -match '^[0-9a-fA-F:]+$') {
return $Endpoint
}

# Build Resolve-DnsName base parameters, add -Server only if user specified -DnsServer
$dnsArgsBase = @{
Name = $hostAddress
DnsOnly = $true
ErrorAction = 'Stop'
NoHostsFile = $true
}
if ($DnsServer -and $DnsServer.Trim().Length -gt 0) {
$dnsArgsBase.Server = $DnsServer.Trim()
}

# Resolve domain to IP. Default: prefer A (IPv4). If -PreferAAAA is set: prefer AAAA (IPv6).
$types = if ($PreferAAAA) { @('AAAA', 'A') } else { @('A', 'AAAA') }

foreach ($t in $types) {
try {
$ans = Resolve-DnsName @dnsArgsBase -Type $t
$ip = $ans |
Where-Object { $_.Section -eq 'Answer' -and $_.PSObject.Properties.Match('IPAddress').Count -gt 0 } |
Select-Object -First 1 -ExpandProperty IPAddress

if ($ip) {
if ($t -eq 'AAAA') {
Write-Host "Successfully resolve $Endpoint to [$ip]:$port"
return "[$ip]:$port"
} else {
Write-Host "Successfully resolve $Endpoint to $ip`:$port"
return "$ip`:$port"
}
}
} catch { }
}

return $Endpoint
}
function process_peer () {
if (-not $PEER_SECTION -or ($PUBLIC_KEY -eq $null) -or ($ENDPOINT -eq $null)) { return }
if (-not ((& wg show "$INTERFACE" latest-handshakes) -replace $PUBLIC_KEY -match ('[0-9]+'))) { return }
if (((Get-Date) - (New-Object -Type DateTime -ArgumentList 1970, 1, 1, 0, 0, 0, 0).AddSeconds($matches[0]).ToLocalTime()).TotalSeconds -le 135) { return }
$ENDPOINT_RESOLVED = resolve_endpoint_ip "$ENDPOINT"
(& wg set "$INTERFACE" peer "$PUBLIC_KEY" endpoint "$ENDPOINT_RESOLVED")
reset_peer_section
}
function reset_peer_section () {
Set-Variable PEER_SECTION -Value $null
Set-Variable PUBLIC_KEY -Value $null
Set-Variable ENDPOINT -Value $null
}
reset_peer_section
Set-Variable PEER_SECTION -Value $null
foreach ($line in $config.Split([Environment]::NewLine, [StringSplitOptions]::RemoveEmptyEntries)) {
if ($line.Trim().length -gt 0) {
$stripped = $line.Trim() -ireplace '\#.*'
$key = $stripped -ireplace '=.*'; $key = $key.Trim()
$val = $stripped -ireplace '^.*?='; $val = $val.Trim()
if ($key -match '\[.*') { process_peer; reset_peer_section; }
if ($key -eq '[Peer]') { $PEER_SECTION = $true }
if ($PEER_SECTION) {
switch ($key) {
"PublicKey" { $PUBLIC_KEY = $val; continue; }
"Endpoint" { $ENDPOINT = $val; continue; }
}
}
}
}
process_peer
}
function WireGuard-Reresolve-All-Active {
Get-Service -Name "WireGuardTunnel$*" `
| Where-Object { $_.Status -eq "Running" } `
| ForEach-Object { $_.Name.Substring(16) } `
| ForEach-Object { Get-ChildItem -File "$env:programfiles\wireguard\data\configurations\$_.conf.dpapi" } `
| ForEach-Object { WireGuard-Reresolve $_.FullName }
}
if ($LoopRunAsCron) {
Write-Output "Running as Cron DelaySeconds=$DelaySeconds"
while ($true) {
WireGuard-Reresolve-All-Active
Start-Sleep -Seconds $DelaySeconds
}
} else {
WireGuard-Reresolve-All-Active
}

为了脚本高效率地循环执行,建议采用Shawl包装为系统服务:GitHub - mtkennerly/shawl: Windows service wrapper for arbitrary commands · GitHub

详细操作

  1. 先 Win+X 打开 终端管理员,启动 Powershell ,执行如下命令以允许脚本执行

    1
    Set-ExecutionPolicy unrestricted
  2. 使用 Winget 或者 Scoop 安装

    1
    2
    scoop bucket add extras && scoop install shawl
    winget install --scope=machine -e --id mtkennerly.shawl
  3. 将脚本放到易于访问的目录,之后采用 Shawl 包装服务,脚本的命令行参数如下

    1. -LoopRunAsCron 可选,循环执行,推荐添加
    2. -DelaySeconds 可选,循环执行的时间间隔,单位为秒,默认为600秒
    3. -DnsServer 可选,指定查询的DNS服务器,支持v4和v6的地址,不填写则默认使用系统的DNS服务器
    4. -PreferAAAA 可选,指定优先查询域名的AAAA记录,错误回落到A记录;不选择则默认优先解析A记录,错误回落到AAAA记录

    示例命令行:以250秒的间隔循环执行,使用阿里的公共DNS,且优先AAAA记录

    1
    shawl add --name WireGuard-Reresolve -- powershell -NoProfile -NoLogo -NonInteractive -ExecutionPolicy Bypass -Command "C:\wireguard-tools\Wireguard-Dynamic-Resolver.ps1" -LoopRunAsCron -DelaySeconds 250 -DnsServer 223.5.5.5 -PreferAAAA
  4. Win+R 输入services.msc打开服务管理台,找到服务WireGuard-Reresolve,双击属性点击启动服务

    image-20260309212417553

    在"启动类型"下拉菜单中选择自动(延迟启动),即可设置脚本开机自启

    image-20260309212548614

附录