WireGuard 加密密钥路由与 AllowedIPs 原理
了解 WireGuard 如何将 AllowedIPs 同时用作路由表和访问控制列表,掌握 Noise 握手、密钥轮换、最长前缀匹配,以及 wg0.conf 的实际含义。
WireGuard 的工作原理:一个核心概念
WireGuard 会将每个数据包绑定到一个公钥。该机制称为加密密钥路由(cryptokey routing),也是整个设计的核心:对端旁边的 AllowedIPs 行,既是离开本机的数据包所使用的路由表,也是来自该对端的数据包所使用的访问控制列表。一项设置,承担两种作用。按这种方式理解 AllowedIPs,每个 WireGuard 配置文件都会变得易于阅读。
WireGuard 没有按 IP 地址建立的会话表,也没有用户数据库。对端由一个公钥和该公钥可以使用的地址集合组成。握手和计时器用于在底层网络发生变化时,维持这一绑定关系。如果您希望先建立一个可用的隧道,再了解理论,可以先按照在您自己的 VPS 上自托管 WireGuard VPN进行配置;之后再回到这里,分析某一行配置为何与预期不同。
AllowedIPs 是路由表,也是访问控制列表
先看出站方向。内核按通常方式通过主路由表,将数据包路由到 wg0 设备。然后,WireGuard 将该数据包的目标地址与一张包含所有对等端允许前缀的表进行匹配,并优先选择最长前缀。匹配结果指定一个对等端;该对等端对应一个公钥,公钥对应一个会话密钥和 UDP 端点。数据包使用该对等端的密钥加密后发送到对应端点。
如果没有任何对等端的 AllowedIPs 覆盖该目标地址,则不会发送任何内容,因为没有可用于发送的密钥。
ping: sendmsg: Required key not available该错误只表示一件事:您尝试访问的地址未列在任何对等端下。另一种错误 ping: sendmsg: Destination address required 表示已匹配到某个对等端,但 WireGuard 没有该对等端的端点,因为既未配置端点,也尚未学习到端点。
现在看入站方向。UDP 数据包到达监听端口。WireGuard 从报头中的接收方索引查找会话,根据滑动重放窗口检查计数器,然后解密并验证载荷。完成这些步骤后,WireGuard 才会读取内部数据包;该数据包的源地址必须位于发送方对等端的 AllowedIPs 范围内。否则,数据包将被丢弃。启用动态调试后,内核会打印原因,日志行类似如下:
wg0: Packet has unallowed src IP (10.8.0.9) from peer 2 (203.0.113.10:51820)这就是服务器端对等端获得 /32 的原因。配置为 AllowedIPs = 10.8.0.2/32 的对等端可以从 10.8.0.2 发送数据包,不能使用其他地址。将其改为 0.0.0.0/0 后,该客户端即可注入声称来自隧道内任意源地址的数据包,包括另一个客户端的地址。
重叠前缀会根据具体程度解决,因为查找使用最长前缀匹配。两个对等端配置相同前缀时,行为有所不同:该条目会移动到最后配置的对等端,之前的对等端将不再接收这部分流量,且任何位置都不会打印错误。wg show wg0 allowed-ips 打印内核中实际使用的表;当磁盘上的文件与运行状态不一致时,以该表为准。
理解加密密钥路由配置文件
服务器端:
[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = <server private key>
[Peer]
PublicKey = <laptop public key>
AllowedIPs = 10.8.0.2/32客户端:
[Interface]
Address = 10.8.0.2/32
PrivateKey = <laptop private key>
[Peer]
PublicKey = <server public key>
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25同一个关键字在两端的作用完全相反。在客户端,它表示“将每个目标发送到此对等端”。在服务器端,它表示“仅接受此对等端提供的这一个地址”。这种不对称性体现在取值上,而不在于角色本身。
其中一半配置项根本不属于协议。Address、DNS、MTU、PostUp 和 SaveConfig 属于 wg-quick,也就是负责启动接口的 shell 脚本。内核不会读取这些配置项。wg-quick strip wg0 会输出 wg 工具实际加载的精简配置,这是最快了解这种区别的方法。
握手实际执行的操作
WireGuard 的握手采用 Noise Protocol Framework 中的 Noise_IKpsk2。对系统管理员而言,IK 部分最重要:发起方已经知道响应方的静态公钥,因为该公钥就是 [Peer] 配置块中的 PublicKey;发起方则会在第一条消息中发送自己的静态公钥,并对其加密。因此不存在证书交换,也不需要往返传递身份信息。被动观察者无法判断哪个密钥正在发起连接,除非它持有响应方的私钥。
握手只需一次往返。发起消息为 148 字节,响应消息为 92 字节,随后即可传输数据。每一方在每次握手时都会生成一对新的临时 Curve25519 密钥,随后使用混合静态密钥和临时密钥的多次 Diffie-Hellman 结果生成会话密钥。临时私钥随后会被丢弃,因此具备前向保密性:即使有人今天记录了您的流量,并在明年窃取服务器私钥,也无法读取已记录的流量。
握手发起消息包含 TAI64N 时间戳。每个对等端都会记录从另一端收到的最大时间戳,因此会拒绝重放的发起消息。数据包使用 64 位计数器作为 nonce。接收方会维护一个最近计数器的滑动窗口,因此无需 TCP 风格的连接状态,也能处理重放和严重乱序。
会话密钥的有效期很短,相关计时器由程序编译时确定,无法配置。
The data behind this chart
[
{
"label": "REKEY_TIMEOUT",
"seconds": 5,
"notes": "resend a handshake initiation that got no answer"
},
{
"label": "KEEPALIVE_TIMEOUT",
"seconds": 10,
"notes": "send a keepalive after receiving data and sending none back"
},
{
"label": "REKEY_ATTEMPT_TIME",
"seconds": 90,
"notes": "give up on the handshake and report the peer as down"
},
{
"label": "REKEY_AFTER_TIME",
"seconds": 120,
"notes": "sender begins a fresh handshake for a new session key"
},
{
"label": "REJECT_AFTER_TIME",
"seconds": 180,
"notes": "the old session key is refused and traffic stops"
}
]这些是协议规范中的常量,不是测量值。其中 5 个计时器共同驱动整个会话生命周期。使用 120 秒后,发送方会开始新的握手;经过 180 秒后,旧密钥会被直接拒绝,因此必须等到新的握手完成后才能继续传输流量。发起消息没有收到响应时,会每隔 5 秒重传,并在 90 秒后放弃。这就是为什么 wg show 会将 latest handshake 显示为相对时间,也解释了为什么繁忙且正常的隧道会保持较小的该时间值。如果您正在主动发送流量,但该时间值持续增长,说明握手失败,而不是隧道处于空闲状态。
为什么对等端没有客户端或服务器角色
两端运行完全相同的代码,并使用相同的配置格式。系统不存在服务器模式。您感受到的不对称来自 Endpoint,而 Endpoint 是可选的。
配置了 endpoint 的对等端可以发起握手。未配置 endpoint 的对等端会等待,然后从第一个通过正确身份验证的数据包中获知另一端的地址和端口。系统会保存这个已获知的 endpoint;每当从新地址收到有效数据包时,系统都会更新它。这就是漫游的工作方式:笔记本电脑从 Wi-Fi 切换到移动网络后仍可保持同一隧道,因为会话通过密钥和索引标识,而不是通过 IP 地址标识。这里没有重新连接,因为从 TCP 的意义上说,两端从未建立连接。
同一机制还会产生一个值得了解的事实:拥有公网地址的对等端始终保存另一端最后已知的公网 IP,而 wg show 会将其打印出来。
固定原语,无需协商
WireGuard 没有密码套件列表。经过身份验证的加密使用 ChaCha20-Poly1305,密钥协商使用 Curve25519,哈希使用 BLAKE2s,密钥派生使用 HKDF。所有部署都使用这些算法,因此无需解析协商阶段,也不存在降级到较弱选项的路径。代价也很明确:如果其中某个原语被攻破,修复方式是发布整个协议的新版本,并更新两端,而不是修改配置。这一决定大幅减少了代码量和 TLS 隧道通常包含的大多数故障模式,这也是 WireGuard 与 OpenVPN 的对比 的核心。
扫描器无法获得端口响应
每条握手消息都包含一个名为 mac1 的字段。它是消息认证码(MAC),使用从响应方静态公钥派生的密钥对消息计算得出。不知道该公钥的发送方无法生成有效的 mac1,接收方会直接丢弃此类数据包,完全不作响应。不返回错误,不发送重置报文,也不发送 ICMP 消息。
从外部观察,UDP 扫描不会收到任何响应。
sudo nmap -sU -p 51820 vpn.example.comnmap 报告 open|filtered。这与防火墙静默丢弃数据包时对端口给出的结果相同。对于尚未持有你的公钥的客户端,无论 WireGuard 是否正在监听,该端口的行为都一样。
第二个字段 mac2 用于应对拒绝服务压力。接收方负载较高时,会向有效的初始化消息回复一个与发送方源地址关联的 64 字节 cookie,并在发送方回显该 cookie 之前拒绝执行开销较高的公钥运算。这样可以在消耗 CPU 处理请求之前验证源地址确实有效。此机制只会在负载较高时启用。
为什么 0.0.0.0/0 会将对等端变为默认路由
因为 AllowedIPs 是路由表,AllowedIPs = 0.0.0.0/0, ::/0 会为该对等端声明所有目标地址。这就是完整隧道设置。
使其生效的路由机制比这一行本身更复杂。直接通过 wg0 添加默认路由会形成路由循环,因为承载流量的加密 UDP 数据包也必须离开本机,并且会匹配这条默认路由。wg-quick 通过策略路由避免了这个问题。它为 WireGuard 自身发出的数据包设置 fwmark,将隧道默认路由放入单独的路由表,并添加规则,使只有未标记的流量才能进入该路由表。运行 ip rule show 后,可以看到结果:
32764: from all lookup main suppress_prefixlength 0
32765: not from all fwmark 0xca6c lookup 51820
32766: from all lookup main0xca6c 是 51820 的十六进制表示,而 51820 同时也是路由表编号。suppress_prefixlength 0 规则会让主路由表跳过自身的默认路由,因此本地子网等更具体的路由仍然优先,其他流量则进入隧道路由表。分流隧道不需要这些设置:例如 AllowedIPs = 10.8.0.0/24, 10.20.0.0/16 这样的更窄地址范围会作为普通路由添加到主路由表中。
完整隧道本身无法解决名称解析问题,因为客户端从本地网络获取的解析器通常仍会继续使用,其路由也通常更具体。这是另一项需要单独处理的工作,详见 泄漏到 WireGuard 隧道外部的 DNS。
PersistentKeepalive 的实际用途
没有流量时,WireGuard 不会发送任何数据包。不发送心跳,不刷新会话,线路上也没有数据。这种静默有助于节省电量,也有助于应对上文的扫描场景,但会导致一种特定配置无法工作。
位于 NAT(网络地址转换)或有状态防火墙之后的 peer,只有在该设备中存在映射时,外部才能访问它;而这个映射由出站数据包创建。常见的 UDP 映射生命周期约为 30 秒。映射过期后,中间盒会丢弃来自公网侧的数据包。此时隧道会表现为失效,直到 NAT 后的 peer 发送数据。PersistentKeepalive = 25 每 25 秒发送一个经过身份验证的空数据包,时间短于常见的最短生命周期,因此映射可以保持打开状态。
在 NAT 后的 peer 上设置该参数。具有公网地址且 UDP 端口已开放的服务器不需要设置它,在服务器上设置只会增加流量。不要将它与自动 keepalive 混淆。自动 keepalive 会在 peer 收到数据且自身没有数据要返回后的 10 秒触发。它始终启用,无法配置。
通过隧道转发 LAN 流量不是 WireGuard 的功能
假设对等端 B 位于家庭网络 192.168.50.0/24 中,并且对等端 A 需要访问该网络。两个独立系统必须完成相应配置,其中只有一个是 WireGuard。
WireGuard 负责的部分:在 A 上,将 192.168.50.0/24 添加到 B 的 AllowedIPs。这样,A 会将该前缀的流量路由到 B,并允许 B 发送源地址属于该前缀的数据包。如果缺少此配置,cryptokey routing 就没有用于该目标地址的密钥,也没有允许该源地址的权限。
内核负责的部分:在 B 上,net.ipv4.ip_forward 必须设置为 1,否则内核会丢弃所有目标地址不是 B 本身的已解密数据包。B 的防火墙 forward 链必须允许这些流量。LAN 中的主机需要添加返回 10.8.0.0/24 的路由;否则,B 必须执行源 NAT,确保回复流量通过 B 返回。
WireGuard 将解密后的数据包交给内核后,其工作就结束了。之后的处理属于普通的 Linux 路由和过滤流程。因此,这类故障会出现在 nft list ruleset 计数器或 ip -s link show wg0 中,而不是 wg show 中。如果您希望通过 Web 界面管理对等端,在 Docker 中运行 wg-easy 可以为您生成对等端条目,但转发规则仍由主机负责。即使由协调层为您分发前缀,这种职责划分仍然适用:使用 Tailscale 子网路由器从 VPS 发布私有网络 可以替代在每个对等端手动编辑 AllowedIPs,但路由器本身的转发 sysctl 设置和防火墙规则仍需由您配置。
WireGuard 为何运行在内核中
wg0 是网络设备驱动程序。数据包通过常规路由栈到达该设备,在 softirq 上下文中完成加密,然后通过 UDP 套接字发出,整个过程不会进入用户空间。这就是其高吞吐量的原因,也因此该模块始终只有约 4000 行代码,便于审查,并于 2020 年 3 月合并到主线 Linux 5.6。Ubuntu 24.04 和 Debian 13 已随系统提供该模块,因此只缺少 wireguard-tools 软件包。
作为普通网络接口,它具有实际意义。tcpdump -ni wg0 显示明文的内部数据包,tcpdump -ni eth0 udp port 51820 显示加密后的外部数据包;对比两者即可立即判断故障方向。netfilter 和流量整形会将 wg0 视为其他链路一样处理。在无法使用内核模块的环境中,例如共享宿主机内核的容器虚拟化环境,wireguard-go 会通过 TUN 设备在用户空间实现相同协议。由于每个数据包都要两次跨越内核边界,吞吐量会明显下降。
WireGuard 无法保护你的内容
其威胁模型有意保持狭窄,而这种安静的协议很容易让人产生不切实际的期待。应明确说明以下限制。
- 它不会隐藏你正在使用 WireGuard。握手消息的大小固定,第一个字节表示消息类型,传输协议是 UDP。深度数据包检测可以轻易识别它,禁止 VPN 的网络也可以阻断它。协议设计上没有加入混淆功能。
- 它不会隐藏流量大小或时间特征。有效载荷只填充到 16 字节边界,因此观察者仍能看到你何时发送数据,以及大致发送了多少数据。
- 它会保留上次已知的端点。拥有公网地址的对等端会保存另一端当前的公网 IP,
wg show会显示该地址。结合配置中固定的隧道地址,这会形成一个跨网络跟随用户的稳定标识符。在你自己的 VPS 上,这通常没有问题。这也是商业服务要在协议之上增加一层的原因。 - 它认证的是密钥,而不是人员。持有私钥文件的人就是该对等端。将
/etc/wireguard的权限设置为 mode 700,并将密钥文件的权限设置为 600。 - 它没有撤销列表,也没有过期机制。只有从保存该对等端配置的每台服务器中删除对应的 peer 条目,才能终止访问;静态密钥会一直有效,直到你将其删除。
这些限制并不意味着 WireGuard 不安全。它们说明 WireGuard 足够精简,而这正是它的目标:完成身份认证和加密,并将身份管理和地址分配交给你在其上构建的系统。WireGuard 与 Tailscale 的比较中介绍的这类协调层,正是用于填补这一空缺,并使用你刚刚了解的数据平面。部署此类协调层后,下一个问题就是谁可以访问你在隧道内运行的服务;对于单个端口,这由 Tailscale serve 与 funnel 的选择决定。
FAQ
WireGuard 中的加密密钥路由是什么?
加密密钥路由是一项将每个数据包绑定到公钥的规则。每个对等端条目都会在 AllowedIPs 中包含一个前缀列表。对于出站流量,WireGuard 会将数据包的目标地址与各对等端的列表进行匹配,并优先选择最长前缀,因此该列表同时充当路由表。对于入站流量,数据包解密并通过身份验证后,其内部源地址必须属于同一对等端的列表,否则将被丢弃。因此,该列表同时充当访问控制列表。WireGuard 没有单独的路由配置,也没有单独的内部防火墙,因为同一个列表可以完成这两项工作。
两个对等端都需要配置 PersistentKeepalive 吗?
不需要。应在位于 NAT(网络地址转换)之后或有状态防火墙之后的一端配置,通常是客户端。WireGuard 空闲时不会发送任何数据,因此允许远端连接到该对等端的映射会过期,通常不到 1 分钟,随后隧道会表现为单向失效。PersistentKeepalive = 25 每 25 秒发送一个经过身份验证的空数据包,并保持该映射有效。具有公网地址和开放 UDP 端口的对等端不需要配置它。
为什么通过隧道执行 ping 时会显示“Required key not available”?
因为目标地址不在任何对等端的 AllowedIPs 中,所以加密密钥路由找不到用于加密该数据包的密钥,内核拒绝发送该数据包。运行 wg show wg0 allowed-ips,并将输出与要 ping 的地址进行比较。类似的错误 Destination address required 表示另一个问题:某个对等端已匹配,但 WireGuard 没有该对等端的 endpoint,因为尚未配置 endpoint,且该对等端尚未发送经过身份验证的数据包。
防火墙能检测并阻止 WireGuard 吗?
可以。WireGuard 会对流量进行身份验证和加密,但不会尝试隐藏自身。握手消息的长度固定为 148 和 92 字节,每条消息的第一个字节标识其类型,传输协议为 UDP,因此深度数据包检测可以轻松识别该协议。阻止 UDP 或使用协议指纹识别的网络会阻止 WireGuard。隐藏隧道需要将其封装在其他协议或工具中,这属于独立工具的功能,而不是 WireGuard 的设置。