WireGuard原理:AllowedIPs加密密钥路由
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_IKpsk2,来自 Noise Protocol Framework。对系统管理员而言,IK 部分最有用:发起方已经知道响应方的静态公钥,因为该公钥就是 [Peer] 配置块中的 PublicKey;发起方则会在第一条消息中加密发送自己的静态公钥。因此不存在证书交换,也不需要往返确认身份。被动监听者无法判断是哪个密钥在发起连接,除非它持有响应方的私钥。
代价是需要 1 次往返。握手发起消息为 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-byte 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 混淆。后者会在 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,由它生成对等端配置项;但转发规则仍由主机负责。
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。深度数据包检测可以轻易识别 WireGuard,禁止 VPN 的网络也可以将其阻断。协议设计上没有加入混淆功能。
- 它不会隐藏流量大小或传输时间。有效负载只会填充到 16 字节边界,因此观察者仍能看到您何时发送数据,以及大致发送了多少数据。
- 它会保留最近的已知端点。拥有公网地址的对等端会存储另一端当前的公网 IP,
wg show会显示该地址。再结合配置中固定的隧道地址,这就构成了一个会随用户跨网络移动的稳定标识符。在您自己的 VPS 上,这通常没有问题。这也是商业服务会在协议之上增加一层的原因。 - 它认证的是密钥,而不是人员。持有私钥文件的人就是该对等端。请将
/etc/wireguard的权限设为 700,并将密钥文件的权限设为 600。 - 它没有撤销列表,也没有过期机制。只有从持有该条目的每台服务器中删除对等端配置后,访问权限才会终止;静态密钥会一直有效,直到您将其删除。
这些限制并不意味着 WireGuard 不安全。它们说明 WireGuard 足够精简,而精简正是其设计目标:它负责身份认证和加密,并将身份管理和地址分配交给您在其上构建的组件。WireGuard 与 Tailscale 的比较中介绍的这类协调层,正好用于填补这一空缺,同时使用您刚刚了解的同一数据平面。
FAQ
WireGuard 中的 cryptokey 路由是什么?
cryptokey 路由是一条将每个数据包绑定到公钥的规则。每个对等端条目都在 AllowedIPs 中包含一个前缀列表。出站时,WireGuard 将数据包的目标地址与每个对等端的列表进行匹配,并优先选择最长前缀,因此该列表充当路由表。入站时,数据包解密并完成身份验证后,其内部源地址必须位于同一对等端的列表中,否则将被丢弃。因此,该列表同时充当访问控制列表。WireGuard 没有单独的路由配置,也没有单独的内部防火墙,因为同一个列表同时完成这两项工作。
是否需要在两端都设置 PersistentKeepalive?
不需要。在位于 NAT(网络地址转换)或有状态防火墙之后的一端设置,通常是客户端。WireGuard 在空闲时不发送数据,因此允许远端访问该对等端的映射会过期,通常在一分钟内失效,随后隧道看起来会变成单向中断。PersistentKeepalive = 25 每 25 秒发送一个经过身份验证的空数据包,并保持映射有效。具有公网地址和开放 UDP 端口的对等端不需要设置该选项。
为什么通过隧道执行 ping 时会显示 “Required key not available”?
因为目标地址不在任何对等端的 AllowedIPs 中,cryptokey 路由找不到用于加密数据包的密钥,因此内核拒绝发送该数据包。运行 wg show wg0 allowed-ips,并将其输出与要 ping 的地址进行比较。类似的错误 Destination address required 表示另一个问题:匹配到了对等端,但 WireGuard 没有该对等端的 endpoint,因为未配置 endpoint,且此前没有收到来自该对等端的经过身份验证的数据包。
防火墙能否检测并阻止 WireGuard?
可以。WireGuard 会对流量进行身份验证和加密,但不会尝试隐藏自身特征。握手消息的长度固定为 148 和 92 字节,每条消息的第一个字节用于标识其类型,传输协议为 UDP,因此深度数据包检测可以轻松识别该协议。阻止 UDP 或对协议进行指纹识别的网络会阻止 WireGuard。隐藏隧道需要将其封装在其他协议或工具中,这属于独立工具的功能,而不是 WireGuard 的设置。