前言
摘要
- WireGuard 是一种安全的网络隧道协议,工作于网络层(第三层),实现形式为
Linux
内核中的一个虚拟网络网卡。 - WireGuard 旨在替代
IPsec
和基于TLS
的解决方案(如OpenVPN
),因为其更安全、性能更高且易于使用。 - WireGuard 虚拟隧道接口基于对等方的公钥与隧道源
IP
地址的关联 - WireGuard 使用基于
NoiseIK
的单次往返密钥交换,并通过一种新颖的定时器状态机机制透明地处理所有会话的创建。 - WireGuard 使用的短预共享静态密钥(
Curve25519
)用于类似OpenSSH
的相互认证。WireGuard 除了提供强完美前向保密性外,还具备高度的身份隐藏特性,使用ChaCha20Poly1305
签密算法保护隧道中的数据。WireGuard 的加密和认证功能,通过改进的IP
绑定Cookie
机制缓解拒绝服务攻击,显著优化了IKEv2
和DTLS
的Cookie
机制。 - WireGuard 的
Linux
实现代码量不足 4000 行,易于审计和验证。
注:
工作在第三层:代表可以解析网络层的数据包(IP包),可以分析出数据包的源目的ip。路由器就是工作在第三层的设备
Linux 内核中的虚拟网络网卡:指 Linux 中的 TUN 设备。目前 WireGuard 协议不仅是 Linux 内核实现,还有 Golang 实现,Haskell 实现等;另外还有 Android、Windows 和 Mac 等客户端的实现,仓库列表,在这些平台上也有类似于 Linux TUN 设备的实现方式
- 在 Linux 中,TUN 设备是一种虚拟网络设备,与 TAP 设备类似,但 TUN 设备工作在网络层(Layer 3),处理的是 IP 数据包,而不是以太网帧。TUN 设备通常用于实现用户空间的网络协议栈、VPN、隧道等场景。TUN 设备在内核中创建,并通过字符设备文件(如
/dev/tun0
)与用户空间程序交互- TUN/TAP 是内核虚拟网络设备,其中 TUN 是三层设备,TAP 是二层设备。TUN 设备的一端连接着内核的网络协议栈,另一段连接着一个应用层程序(在 wireguard 的 Golang 实现中,这个应用层程序是 wireguard-go )
单次往返密钥交换:只需要一轮交互就可以完成密钥交换
前向保密:长期使用的主密钥泄漏不会导致过去的会话密钥泄漏,WireGuard 中每隔 120s 会更换一个临时密钥(一次性的)来加密数据包
引言与动机
IPsec
执行了严格的分层(密钥交换层、传输层、加密层等等),这从网络的角度看是很不错的,但同时也加大了开发和部署的难度。
- 部署
WireGuard
仅仅需要配置一个虚拟网卡(例如wg0
),该虚拟网卡可以通过ip
或ipconfig
等工具管理。配置接口的私钥(及可选的预共享对称密钥)和对等方的公钥后,隧道即可自动运行,密钥交换、建立、断开、数据传输、重连等过程对用户透明,管理员无需关注细节。
OpenVPN
基于用户态 TUN/TAP
接口并使用 TLS
。其至少有两个问题:ASN.1
、x509
以及TLS
相关的诸多 bug 使其安全性不容乐观。由于工作在用户空间,需在内核态与用户态之间多次复制数据,且依赖长驻守护进程,故 OpenVPN 的性能较低
WireGuard
工作在内核空间,密钥分发机制受OpenSSH
启发,两个peer
之间交换公钥的机制并不是固定的,支持PGP
签名邮件、LDAP
证书等多种方式交换公钥,公钥仅需 32 字节(Base64 编码为 44 字符),便于传输。
WireGuard 在加密算法选择上保持高度“固执”,避免协议灵活性。若底层原语存在漏洞,所有终端需强制更新。其使用 Noise 协议变体(1-RTT 密钥交换)、Curve25519(ECDH)、HKDF(密钥扩展)、ChaCha20Poly1305(认证加密)和 BLAKE2s(哈希),并通过加密 Cookie 机制防御拒绝服务攻击。WireGuard 仅支持第3层(IPv4/IPv6),简化协议设计并确保包的认证与归属。
加密密钥路由表
本章主要介绍了 WireGuard 中一些核心概念:Peer、Interface、路由表(CryptoKey Routing)、端点(Endpoint)
Peer
首先要指出 WireGuard 协议是一种 P2P 的协议,没有所谓的客户端和服务端的区分。因此,WireGuard隧道两端的机器被称为 Peer,WireGuard 使用长度为 32 B 的公钥(椭圆曲线公钥算法)唯一标识一个 Peer。这种设计形成了一种简单的映射关系:每个 Peer 的公钥对应一组允许的隧道源 IP 地址(AllowedIPs)。
Interface
为了把自己与其他 Peer 区分开,WireGuard 引入了 Interface 的概念,Interface 可以理解为自己这台机器上的那张 wg 虚拟网卡,由 私钥 和 监听的端口组成。
路由表
路由表记录了所有与自己建立隧道的 Peer 的信息,包括公钥和允许的 ip 列表(AllowedIPs)两部分,用于正确地加密、解密以及验证所有流经 wg 网卡的网络包。举例:
- 发送数据包:当一个向外发送的包要通过 wg 网卡时候,系统会根据目标 IP 地址查询路由表,发现其 dst ip 在 PeerA 的 AllowedIPs 中,WireGuard 会对这个包用与 PeerA 协商出的密钥(对称)加密,这样 PeerA 在收到这个包后,才能正确地解密。
- 接收数据包:当一个外面发送过来的包要通过 wg 网卡时候,首先用与 PeerA 协商出的密钥(对称)解密,如果解密失败,直接 drop;然后去查 src ip 在不在 Peer A 的 AllowedIPs 中,如果不在,直接 drop。
端点
Endpoint 实质上是其他 Peer 的 Interface,由公网 IP + 端口组成。其作用是让 WireGuard 能够把一个包发给正确的 Peer。
在 WireGuard 中,可以预先定义好 Peer 的 Endpoint,也可以不定义。如果不定义,WireGuard 将取下次来的加密数据包中的外层公网 IP 和端口 作为这个 Peer 的 Endpoint
这样做的好处是,WireGuard 允许 Peer 的公网 IP 发生变化(换句话说:漫游),这是很有用的,尤其在一些 nat 场景下(如大公司的公网出口等)。
安全性
- 抗中间人攻击:攻击者可能篡改未认证的外层源 IP,但无法解密或修改有效载荷,仅能引发拒绝服务攻击(与直接丢弃数据包效果相同)。
- 静默丢弃机制:若对等方无法解密或响应数据包,其端点信息将被快速遗忘(详见 被动保活 章节)。
技术优势
- 简化防火墙规则:来自 WireGuard 接口的数据包具有可靠认证的源 IP,管理员可基于此配置标准防火墙规则。
- 例如,允许
wg0
接口源 IP 为10.10.10.230
的流量,等同于信任对等方gN65...z6EA
。
- 例如,允许
- 严格网络分层:WireGuard 仅支持第 3 层(IPv4/IPv6),避免传统 VPN(如 L2TP/IPsec)在混合网络设计中的复杂性。
- 反向路径过滤:通过全局路由表验证接收包的源 IP 是否与发送方的公钥匹配,确保双向一致性。
发送/接收流程
本章主要介绍 发送与接收 数据包的核心步骤:
Interface 设置:
网卡名 | wg0 |
ListenPort | 41414 |
PublicKey | HIg0…8ykw |
Peer 设置:
PublicKey | TrMv…WXX0 |
AllowedIPs | 10.192.122.4/32, 192.168.0.0/16 |
Endpoint | 123.123.123.123:12345 |
发送流程
当本地生成或转发的数据包准备通过 WireGuard 接口(如 wg0
)发送时,执行以下步骤:
- 明文数据包到达
wg0
网卡 - 目标 IP 匹配对等方:
- 检查数据包的目标 IP(例如
192.168.87.21
),通过加密密钥路由表确定对应的对等方公钥(如TrMv...WXX0
)。匹配时,对于单IP,相等;对于 CIDR,包含 - 若无匹配对等方,丢弃数据包,并向源 IP 返回一个标准的 ICMP “no route to host” 包,同时向 User Space 发送一个
-ENOKEY
错误
- 检查数据包的目标 IP(例如
- 加密数据包:使用
ChaCha20Poly1305
签密算法加密签名该数据包 - 封装协议头:在加密后的数据包前添加 WireGuard 协议头
- 发送 UDP 数据包:
- 将协议头与加密数据包组合为 UDP 负载,发送至对等方的 Endpoint 123.123.123.123:12345(IP:Port)
- Endpoint 可为预配置值,或通过最近接收的有效数据包外层源地址动态更新
- 若 Endpoint 没有提前设置好,也没能够根据最近一次包学习到,则丢弃数据包并向源 IP 返回一个标准的 ICMP “no route to host” 包,同时向 User Space 发送一个
-EHOSTUNREACH
错误
wireguard
协议是对原始的 IP
包加密签名之后,加上 wireguard header
,然后加上 udp header
,最后在最外层再加上 IP header
。
可以看出来有内层 IP header
(隧道 IP - AllowedIPs)和 外层 IP header
(公网 IP - Endpoint) 两个 IP header
,路由器等网络设备是通过外层 IP header
转发的。等网络包到达 wireguard
隧道的另外一端之后,内核中的网络协议栈会剥掉外层 IP header
,然后交给 wireguard
协议栈。
wireguard
协议栈会对加密数据进行解密、验签等后续工作,就能看到内层的 IP
包(包括 IP header
)了
接受流程
当 UDP 数据包到达 WireGuard 接口的监听端口(如 41414
)时,处理流程如下:
- 接收 UDP 数据包:验证数据包目标端口是否为 WireGuard 接口的监听端口。
- 解密与认证:
- 根据该数据包的
wireguard header
,确定关联的对等方会话(如TrMv...WXX0
) - 验证消息计数器
message counter
有效性后,使用对等方的接收对称密钥解密数据包。 - 校验签名,如果校验失败,丢弃;解密数据包,如果解密失败,丢弃
- 根据该数据包的
- 更新对等方端点:认证成功后,使用 UDP 数据包的外层源 IP(公网 ip) 和端口更新
TrMv...WXX0
的Endpoint
- 验证源 IP(隧道 ip) 合法性:
- 如果数据包(现在已经是明文的了)不是 IP 包,则丢弃
- 解密后的明文数据包需检查其源 IP 是否属于对等方的
AllowedIPs
允许列表(如192.168.87.21
) - 若源 IP 未授权(例如
10.192.122.3
),丢弃数据包
- 注入接收队列:通过验证的明文数据包被插入
wg0
接口的接收队列,供上层协议处理
设计优势
- 反向路径过滤:发送与接收共用同一路由表,确保源 IP 与目标 IP 的全局一致性。例如,接收数据包的源 IP 必须与发送时选择的对等方公钥匹配
- 所有
dest ip
是192.168.87.21
的包都会被发送给 peer A - 如果
src ip
是192.168.87.21
,那可以确认这个包来自 peer A
- 所有
- 高效性
- 协议头仅 16 字节(含 3 字节保留字段),支持内存对齐操作,便于硬件加速。
- 认证加密(AEAD)与协议处理在软中断(softirq)中完成,减少上下文切换开销。
- 抗重放攻击:使用滑动窗口机制跟踪接收消息的随机数计数器,支持乱序数据包处理(基于 RFC6479)
实例分析发送与接收流程
客户端通过浏览器访问AllowedIPs
中的某个ip

客户端流量走向
- 应用层程序
chrome
创建网络包,并发送给对应的socket
; - 通过对应的
socket
,该网络包到达了内核协议栈。在网络协议栈会判断路由,通过路由把流量发到wg0
网卡; - 到达
wg0
网卡的包,会通过字符驱动直接发送给应用层程序wireguard
; wireguard
会对网络包进行加密、签名等操作(参考上面的发送流程),之后发送给对应的socket
;此时网络包的目的ip
已经是 vpn 服务端的公网ip
了(Endpoint)socket
发出的包会通过内核网络协议栈,在内核网络协议栈会判断路由,通过路由把流量发到eth0
网卡;- 网络包通过
eth0
发送出去
wireguard 工作在第三层,即网络层,这意味着当应用层产生了一个数据包,经过路由表的决策到达 wg0 网卡的时候,wireguard 看到的是 IP Header + IP Data,那么 wireguard 就可以对 IP Header + IP Data 签名加密。这是很重要的一点,因为这保证了 IP Header 中的 src addr 和 dest addr 是可信的、不能被篡改的。
只有这样,wireguard 才能保证 allowed ip 列表是可信的,进而保证如下几点:
- 发给 peer a 的包不会发送给 peer b
- 可以解密别的 peer 发给自己的包,而别人无法解密
- 收到一个包时,根据 src ip 就可以知道这个包来自于哪个 peer
服务端(中继服务器)网络包的流向如下

服务端(中继服务器)流量走向
- 网络包通过公网,到达
eth0
网卡 - 由于目标端口是被
wireguard
监听的,所以网络包发送到对应的socket
socket
将网络包发送给应用层程序wireguard
wireguard
会对网络包进行解密、验证签名等操作(参考上面章节的接受流程)之后,通过字符驱动发送给对应的虚拟网卡wg0
;(此时网络包的目的ip
已经是AllowedIPs
中的某个ip
了)(中继服务器只做转发)wg0
发出的包会通过内核网络协议栈,在内核网络协议栈会判断路由,通过路由把流量发到eth0
网卡;- 网络包通过
eth0
发送出去
基本用法
本章介绍了 wireguard 的基础用法,为下一章中介绍密码学细节和实现细节打好基础。
环境
- linux 环境
- 物理网卡
eth0
,该网卡可以访问互联网,公网 IP 为192.95.5.69
wireguard
虚拟网卡wg0
,IP 为10.192.122.3/24
- 通过
wg0
接口路由10.0.0.0/8
网段流量
配置流程
首先建立 wg0 网卡,然后为该网卡设置 IP 地址,并添加一条路由:
|
|
设置加密路由表(wireguard 中的路由表的概念参考前文),并拉起 wg0 网卡:
|
|
现在,这台机器上目标地址为 10.10.10.230/32 的包会被发送到wg0网卡(笔者注:还记得前面加了一条 10.0.0.0/8 的路由吗),wg0 网卡会签名加密这个包(使用 gN65…z6EA 对应的 session),并将这个经过签名加密的包(使用udp)发送给 192.95.5.70:54421
如果这台机器的 wg0 网卡收到了源 IP 为 10.10.10.230 的包,可以确认这个包来自 peer gN65…z6EA
协议与加密
本章介绍 wireguard 协议的细节,以及其涉及到的密码学知识
WireGuard 的安全通信建立于 1-RTT 握手协议,基于 NoiseIK 模式实现密钥交换,并结合多项创新机制抵御攻击。本章详细阐述其协议设计、加密算法及安全特性。
沉默是金
介绍了 wireguard 的一个重要的设计原则:尽量不回复未经过认证的包
wireguard 的一个设计目标是在完成对数据包的认证之前不存储任何的状态,也不回复这个数据包。这样做的好处是:
- 对于非法的 peers(预期内不会与其建立连接的 peer) 和 网络扫描器是不可见的
- 避免了一些攻击(如 DoS 攻击)
- 只有这样,才能做到在处理数据包的时候不需要额外的内存(注:这是很重要的,是 wireguard 被纳入 linux 内核的前提)
为了实现上面的这个设计原则,responder
必须要对 Handshake Initiation
进行认证,这带来了新的问题:重放攻击
- 攻击者截获到
Handshake Initiation
数据包,将这个包重放给responder
; responder
收到这个包之后,会认为initiator
希望重新建立隧道,因此重新生成一个加密session
(注:包含临时公钥、临时密钥、KDF 状态等,后面会详细介绍);- 由于
initiator
不知道responder
已经修改了加密session
,所以其之后发送的所有的包都会被认为是非法的,现在initiator
和responder
之前建立隧道事实上已经断开了。
为了解决这个问题,initiator
在 Handshake Initiation
包中加入了时间戳,responder
会记录收到的最大时间戳。并丢弃所有 initiator
发来的、时间戳小于等于最大时间戳的所有的 Handshake Initiation
数据包。
如果 responder
重启,会发生什么事情呢?
- 由于之前和
initiator
建立的隧道已经断开了,此时攻击者重放的Handshake Initiation
包是会被responder
接受的,但攻击者也只能做到这一步了,它无法发送数据给responder
。 - 等到
initiator
感知到隧道已经断开之后,会尝试重新建立隧道。所以在这种情况下,攻击者也是无法进行重放攻击的。
这种机制也隐含着一个 wireguard
使用原则:永远不要为两个 peer
设置同一个私钥。否则,这两个 peer
会互相破坏对方建立的加密 session
可选预共享对称密钥模式
介绍了 wireguard 是如何抵抗量子计算的
为应对量子计算威胁,WireGuard 支持在 ECDH 基础上叠加 256 位预共享对称密钥(PSK):
- 混合加密
- PSK 与 Curve25519 密钥联合保护数据,即使未来 ECDH 被量子计算攻破,PSK 仍可确保历史数据安全。
- PSK 泄露不影响当前通信安全(依赖 ECDH 保护),但需定期轮换以实现后量子安全。
- 密钥管理
- PSK 需通过带外(OOB)安全渠道预共享,例如物理交换或量子安全信道。
缓解DoS攻击与 Cookie
介绍了 wireguard 的 cookie 机制是如何缓解 DoS 攻击的
responder
收到 initiator
发来的 Handshake Initiation
之后,为了校验 Handshake Initiation
,需要计算一次 Curve25519
椭圆曲线乘法。这种机制可能会导致一种 DoS
攻击,具体来说:攻击者不断地向目标发送 Handshake Initiation
,直至耗尽攻击目标的 CPU 资源,使其无法正常提供服务。
wireguard 协议规定,为了缓解 DoS
攻击,当 responder
过载时,可以选择不处理 Handshake Initiation
数据包,而是返回一个 Cookie Reply Message
,initiator
需要保存下这个 Cookie
,并根据这个 Cookie
中的信息决定什么时机重发 Handshake Initiation
数据包
过载状态
在 wireguard-go
中,判断当前是否为过载状态的逻辑在 device.go
中:
|
|
上面的代码逻辑为:如果当前待处理的 Handshake 包(笔者注:包含 Handshake Initiation、Handshake Response 和 Cookie Reply Message 三种)的数量超过 128 个,则进入过载状态,持续 1 s
消息格式
介绍了 wireguard 协议中的各种包
下面先介绍 wireguard 用到的密码学原语
notation
首先介绍一些符号,这些符号会在后面中使用到:
initiator
是发起者,responder
是响应者
注:前面说过
wireguard
是一种p2p
协议,没有服务端和客户端的区别,所以白皮书中使用initiator
和responder
代表协议的两个参与方,initiator
是发起协商流程的一方,而response
是响应协商流程的一方;
- $S_i^{priv}$ 是
initiator
的永久私钥,$S_i^{pub}$ 是initiator
的永久公钥,$E_i^{priv}$ 是initiator
的临时私钥,$E_i^{pub}$ 是initiator
的临时公钥
注:
S - Static
,E - ephemeral
,priv - private key
,pub - public key
- 上面的底标
i
表示initiator
,同样的,底标为r
的时候表示responder
的永久私钥、永久公钥、临时私钥、临时公钥 ||
表示将两个字符串连接在一起:=
表示将右边的值赋值给左边
Elliptic-curve
wireguard
中使用椭圆曲线算法生成密钥对,具体为:
- $S_i^{pub}=S_i^{priv}*G$
- $S_r^{pub}=S_r^{priv}*G$
- $E_i^{pub}=E_i^{priv}*G$
- $E_r^{pub}=E_r^{priv}*G$
其中 $S_i^{priv}$ 、 $S_r^{priv}$ 、 $E_i^{priv}$ 、 $E_r^{priv}$ 这四个私钥是随机生成的,且仅生成方知道,椭圆曲线上的离散对数难题保证了其他人无法通过公钥推导出私钥。
Elliptic-curve Diffie-Hellman
ECDH
是一种密钥交换算法,允许协议双方在不安全的网络中协商出一个共享密钥。wireguard
中密钥协商的步骤如下:
initiator
生成一对临时的椭圆曲线密钥 $E_i^{pirv}:=random, E_i^{pub}=E_i^{priv}*G $initiator
计算出共享密钥 $ss_1=E_i^{priv}*S_r^{pub}$initiator
将 $E_i^{pub}$ 发送给responder
responder
计算出共享密钥 $ss_2=S_r^{priv}*E_i^{pub}$- $ss_1=E_i^{priv}S_r^{pub}=E_i^{priv}(S_r^{priv}G)=S_r^{priv}(E_i^{priv}*G)=S_r^{priv}*E_i^{pub}=ss_2$
注:
*
为椭圆曲线乘法,G
为椭圆曲线单位点
wireguard 中使用到相关原语有两个:
DH(Point 1, Point 2)
代表椭圆曲线乘法,返回一个长度为 32 的字节数组,在wireguard-go
中,代码实现如下:
|
|
DH-GENERATE()
代表生成一对密钥对,实现的原理为 $pk = sk*G$ ,在wireguard-go
中,代码实现为:
|
|
AEAD & XAEAD
带有关联数据的认证加密算法 Authenticated Encryption with Associated Data (AEAD)
用于加密数据,并对额外的一段数据进行签名,用于证明发送者的身份。wireguard
中使用到了 ChaCha20Poly1305
AEAD 算法,代码实现为:
注:
ChaCha20Poly1305
具体的原理参考:rfc7539
|
|
XAEAD
是指带 nonce
的 AEAD
算法,wireguard
中使用了 XChaCha20-Poly1305
算法,代码实现为:
|
|
Hash
哈希算法用于计算一段消息的哈希值,wireguard
使用到了 BLAKE2s
哈希算法,代码实现为:
|
|
MAC
消息认证码算法 Message Authentication Code (MAC)
用于生成一段消息的标签,wireguard
使用到了基于 BLAKE2s
哈希算法的MAC
,代码实现为:
|
|
KDF
密钥生成函数 Key Derivation Function (KDF)
用于密钥生成,wireguard
中使用到了 HMAC-based Key Derivation Function (HKDF)
算法(基于HMAC的密钥推导函数),基本原理为:
- $\tau_0:=HMAC(KEY,INPUT)$
- $\tau_1:=HMAC(\tau_0,1)$
- $\tau_2:=HMAC(\tau_0,\tau_1||2)$
- $\tau_3:=HMAC(\tau_0,\tau_2||3)$
- …
- $\tau_i:=HMAC(\tau_0,\tau_{i-1}||i)$
注:具体的原理参考 rfc5869
代码实现为:
|
|
协议概述
第一种
在大多数情况下,只需要 1-RTT 即可完成握手流程,然后协议双方就可以相互传输数据了

第二种
当协议双方中的任意一方负载过大的时候,会引入 cookie reply 数据包,比如下图中展示了 responder 负载过大时,会回复一个 cookie reply 数据包,用于防止 DoS 攻击

- 发起方收到 Cookie 后,会发送一个携带 Cookie 的握手请求
TCP 协议中的类似设计
为什么需要考虑负载过大的情况:在收到 handshake initiation
包之后,responder
为了确定握手包是否合法,至少需要做一次 Curve25519
乘法操作。而计算 Curve25519
乘法是 CPU
密集型的操作,这意味着攻击者可以不断地向某个 peer
发送 handshake initiation
包,直到耗尽该 peer
的CPU
,导致其无法和其他 peer
正常建立隧道
TCP 也有类似的攻击方式 SYN Flood
,也存在一种类似的解决方案 SYN_cookies
Handshake Initiation
当 initiator
想要和 responder
建立隧道的时候,initiator
需要发送一个 Handshake Initiation
,其数据包结构如下:

可以借助 wireshark
直观地看到 Handshake Initiation
包的结构:

可以注意到 wireshark
抓包的长度为 148 B
,这个值和上一张图中 1 + 3 + 4 + 32 + 32 + 12 + 16 + 16 = 116 B
对不上。这是因为 static
和 timestamp
上面有一个小尖号,这个小尖号代表长度为n + 16
,多出来的 16
是 Poly1305 authentication tag
的长度
wireguard-go
中,Handshake Initiation
的定义在 device/noise-protocol.go
中:
|
|
在 wireguard-go
中,构造包的逻辑在 device/noise-protocol.go
的函数 CreateMessageInitiation
中:
type 字段
type 字段用于指示该包的类型,其定义如下:
|
|
reserved 字段
保留字段,固定为零
sender 字段
随机生成一个长度为 4
的字节数组,用于唯一标识该包的发送者。在 wireguard-go
的实现如下,引入了索引表 IndexTable
来维护所有的 Peer
的 sender
,方便快速地通过 sender
找到对应的 peer
:
|
|
ephemeral 字段
临时公钥,长度为 32 的字节数组,Curve25519
|
|
static 字段
永久公钥,长度为 32 + 16 bytes
,其中的 32 bytes
是公钥的长度,16 bytes
是 Poly1305
的 authentication tag
的长度。
为了保密,不可以明文传输公钥;另外,为了证明发送者的身份,必须要加上消息认证算法。wireguard 使用的是 ChaCha20Poly1305
AEAD 算法,其中 ChaCha20
为一种流加密算法,其加密得到的密文的长度等于明文的长度;Poly1305
是一种 MAC 算法,其计算得到的 authentication tag
长度为 16 bytes
ChaCha20Poly1305
算法的基本原理为首先使用ChaCha20
对明文加密(对称加密),然后使用Poly1305
计算出authentication tag
,并将其加到密文的后面,所以最后输出的长度为32 + 16
|
|
timestamp 字段
时间戳,经过 AEAD
算法的加密输出,原理和 static
字段相同
|
|
mac1 和 mac2 字段
双重消息认证码

Handshake Response
responder
接受到 initiator
发送的 Handshake Initiation
之后,会生成并返回一个 Handshake Response
数据包,其数据包结构如下:

可以借助 wireshark
直观地看到 Handshake Response
包的结构:

wireshark
抓包的长度为 92 B
,这个值和上一张图中 1 + 3 + 4 + 4 + 32 + 0 + 16 + 16 = 76 B
对不上。这是因为 empty
上面有一个小尖号,这个小尖号代表长度为n + 16
,多出来的 16
是 Poly1305 authentication tag
的长度
wireguard-go
中,Handshake Response
的定义在 device/noise-protocol.go
中:
|
|
在 wireguard-go
中,构造包的逻辑在 device/noise-protocol.go
的 CreateMessageResponse
函数中
type 字段
type
字段用于指示该包的类型,在 Handshake Response
包中,该字段固定为 2
sender 字段
随机生成的长度为 32 的字节数组,用于唯一标识该包的发送者
|
|
receiver 字段
该字段表示该包的接收者:
|
|
其中 handshake.remoteIndex
是在处理 Handshake Initiation
包时候赋值的,代码在函数 ConsumeMessageInitiation
中:
|
|
ephemeral 字段
临时公钥,长度为 32 的字节数组。
|
|
empty 字段
长度为 16 字节,该字段是对 12 字节的全零数据计算 Poly1035
的 authentication tag
|
|
mac1 和 mac2 字段
双重消息认证码
放大攻击
放大攻击是指攻击者通过发送小型请求来导致大规模响应,达到四两拨千斤的效果。常见放大攻击方式为DNS 放大攻击
针对 wireguard 协议,一种可能的攻击方式是攻击者从网络链路中获取到攻击目标发送给 wireguard server 的 Handshake Initiation 数据包。不断地给多个 wireguard server 重放这个数据包,wireguard server 会根据 wireguard 协议生成 Handshake Response 并发送给攻击目标,从而消耗掉攻击目标的网络流量。
在 wireguard 协议中, Handshake Response 包比 Handshake Initiation 数据包小,这意味着攻击者要消耗掉目标 1 G 的流量,必须消耗掉大于 1 G 的流量,这从根本上杜绝了放大攻击。
Cookie MAC
WireGuard 通过加密 Cookie 抵御 CPU 资源耗尽攻击,其机制优于 DTLS 和 IKEv2:
- Cookie 生成
- 接收方每 2 分钟更换一次 秘密随机值,并基于此计算 Cookie:$Cookie=MAC(秘密随机值,源 IP/端口)$
- 握手消息处理
- 所有握手消息携带 双重 MAC(
msg.mac1
和msg.mac2
):- msg.mac1:使用接收方公钥生成,验证对等方身份。
- msg.mac2:使用 Cookie 生成,验证 IP 所有权(仅在负载高时检查)。
- 所有握手消息携带 双重 MAC(
- 抗中间人攻击
- Cookie 通过 XChaCha20Poly1305 AEAD 加密传输,绑定到特定握手消息,阻止攻击者伪造 Cookie 耗尽对等方资源。
Transport Data Key Derivation
本节介绍握手流程完成之后,initiator
和 responder
如何协商出用于加密传输数据的对称密钥
wg握手过程中有四个状态点:
- 状态 1:
initiator
创建了Handshake Initiation
数据包之后;在wireguard-go
中,该状态名称为handshakeInitiationCreated
- 状态 2:
responder
接收到并消费了Handshake Initiation
数据包之后;在wireguard-go
中,该状态名称为handshakeInitiationConsumed
- 状态 3:
responder
创建了Handshake Response
数据包之后;在wireguard-go
中,该状态名称为handshakeResponseCreated
- 状态 4:
initiator
接收到并消费了Handshake Response
数据包之后;在wireguard-go
中,该状态名称为handshakeResponseConsumed
其中,当 responder
处于状态 3 时,会计算传输密钥(计算密钥的过程中使用到的 KDF 在前文消息格式开头有介绍):$T_r^{send},T_r^{recv}=KDF_2(C_r,nil)$
当 initiator
处于状态 4 的时候,会计算传输密钥: $T_i^{recv},T_i^{send}=KDF_2(C_i,nil)$
在 wireguard-go
中,代码实现在 noise-protocol.go
的 BeginSymmetricSession
函数中:
|
|
可以看出来,要保证 $T_r^{send}=T_i^{recv},T_r^{recv}=T_i^{send}$ ,需要保证 $C_r=C_i$ 。下面看下 $C_r$ 和 $C_i$ 的计算过程:
如下的几点共同保证了 $C_r=C_i$ :
CONSTRUCTION
是一个固定的字符串Noise_IKpsk2_25519_ChaChaPoly_BLAKE2s
|
|
如下的几点共同保证了 $C_r=C_i$ :
- 根据 ECC 原理:$E_i^{priv}*S_r^{pub}=E_i^{priv}*S_r^{priv}*G=S_r^{priv}*E_i^{priv}*G=S_r^{priv}*E_i^{pub}$
- 根据 ECC 原理:$S_i^{priv}*S_r^{pub}=S_i^{priv}*S_r^{priv}*G=S_r^{priv}*S_i^{priv}*G=S_r^{priv}*S_i^{pub}$
- 根据 ECC 原理:$E_r^{priv}*E_i^{pub}=E_r^{priv}*E_i^{priv}*G=E_i^{priv}*E_r^{priv}*G=E_i^{priv}*E_r^{pub}$
- 根据 ECC 原理:$E_r^{priv}*S_i^{pub}=E_r^{priv}*S_i^{priv}*G=S_i^{priv}*E_r^{priv}*G=S_i^{priv}*E_r^{pub}$
Transport Data Messages

可以借助 wireshark 直观地看到 Transport Data Messages 包的结构:

type 字段
type 字段用于指示该包的类型,在 Transport Data Messages 包中,该字段固定为 4。
reserved 字段
保留字段,该字段至少有如下两个作用:
- 程序可以方便地以小端整数读出 type 字段
- 保证了 header 的长度为 16 bytes,这意味着对于大多数的CPU架构,内存地址是对齐的,这会使赋值和加解密操作更加容易、性能更高。
receiver 字段
这里引入了新的 notation $m$ 和 $m'$ ,含义为:如果该包是 initiator
创建的,则 $m=i,m'=r$ ;如果该包是 responder
创建的,则 $m=r,m'=i$ 。在wireguard-go
中,其代码实现在 send.go
的函数 RoutineEncryption
中:
|
|
counter 字段
该字段有两个作用:
- 对于发送方,用于
ChaCha20Poly1305
签密算法的nonce
(Number Once) - 对于接收方,用于记录发送方的最大
nonce
,防止重放攻击
|
|
packet 字段
使用 $P$ 代表要发送的明文数据,使用 $\vert \vert P \vert \vert$ 代表 $P$ 的长度,使用 $\widehat{\vert \vert P \vert \vert}$ 代表 $\vert \vert P \vert \vert + 16$ ,其中 16 是 Poly1305 的 authentication tag 的长度
需要注意的是,明文数据的长度是无法保证的,所以 wireguard 中会对其进行补齐,代码实现如下:
|
|
签密的实现如下:
|
|
Cookie Reply Message
responder
接收到 initiator
发送的 Handshake Initiation
之后,如果此时处于过载状态,会生成并返回一个 Cookie Reply Message
数据包,其数据包结构如下:

wireguard-go 中,代码实现为:
|
|
type 字段
type
字段用于指示该包的类型,在 Cookie Reply Message
包中,该字段固定为 3
reserved 字段
保留字段,该字段至少有如下两个作用:
- 程序可以方便地以小端整数读出 type 字段
- 保证了 header 的长度为 8 bytes 的倍数,这意味着对于大多数的CPU架构,内存地址是对齐的,这会使赋值和加解密操作更加容易、性能更高。
receiver 字段
该包的接收者,也就是 Handshake Initiation
数据包中 Sender
字段。
nonce 字段
随机 nonce
,代码实现为(代码位置在 cookie.go
的 CreateReply
函数中,下同):
|
|
cookie 字段
cookie,计算逻辑为:
$\tau := MAC(R_m,A_{m'})$
$msg.cookie := XAEAD(HASH(LABEL-COOKIE || S_m^{pub}),msg.nonce,\tau,M)$
其中 MAC
代表带密钥的 BLAKE2s
哈希算法; $R_m$ 代表密钥,该密钥每 2 分钟更换一次,代码实现为:
|
|
$A_{m'}$ 表示接收者的endpoint
;计算 $\tau$ 的代码实现为:
|
|
XAEAD
为 ChaCha20Poly1305
签密算法,LABEL-COOKIE
是固定的字符串, $S_m^{pub}$ 代表 responder
的公钥,代码实现为:
|
|
M
代表 Handshake Initiation
数据包中的 mac1
字段,代码实现为:
|
|
攻击方式
- 重放攻击:攻击者可以截获
responder
发送给initiator
的Cookie Reply Message
数据包,然后截获信道中的其它Handshake Initiation
数据包,重放Cookie Reply Message
给initiator
。- 由于
Cookie Reply Message
数据包中的cookie
字段中包含了Handshake Initiation
数据包的mac1
字段,所以Cookie Reply Message
和Handshake Initiation
数据包是一一对应的,防止了这种攻击方式
- 由于
- 放大攻击:攻击者从网络链路中获取到攻击目标发送给
wireguard server
的Handshake Initiation
数据包。不断地给多个wireguard server
重放这个数据包,wireguard server
会根据wireguard
协议生成Handshake Response
或者Cookie Reply Message
并发送给攻击目标,从而消耗掉攻击目标的网络流量。- 由于
Cookie Reply Message
数据包比Handshake Initiation
数据包小很多,从根本上防止了这种攻击方式
- 由于
抗攻击特性总结
攻击类型 | 防御机制 |
---|---|
重放攻击 | TAI64N 时间戳 + 滑动窗口计数器 |
中间人攻击 | 静态密钥绑定 IP + Cookie 加密传输 |
DoS 攻击 | 无状态处理 + 双重 MAC 验证 + 速率限制 |
密钥泄露 | 完美前向保密(PFS) + 短期会话密钥 |
流量分析 | 固定长度填充 + 零拷贝加密 |
定时器与无状态用户体验
WireGuard 的用户体验设计追求无状态感知,管理员仅需配置接口私钥与对等方公钥,无需手动管理会话状态。其背后的核心是简洁可靠的定时器状态机,通过精心设计的超时机制与自动重传逻辑,确保通信的健壮性。本章详述各定时器的功能与交互。
预备知识
以下常量定义定时器系统的核心参数:
符号 | 值 | 描述 |
---|---|---|
Rekey-After-Messages |
260 条消息 | 发送指定数量消息后触发密钥轮换 |
Reject-After-Messages |
264−213−1 条 | 达到此阈值后拒绝继续使用当前会话 |
Rekey-After-Time |
120 秒 | 会话最大存活时间,超时后主动发起新握手 |
Reject-After-Time |
180 秒 | 会话绝对有效期,超时后强制废弃 |
Rekey-Attempt-Time |
90 秒 | 握手重试总时长(含退避) |
Rekey-Timeout |
5 秒 | 握手消息重传间隔 |
Keepalive-Timeout |
10 秒 | 无数据发送时触发保活消息的等待时间 |
传输消息限制
- 密钥轮换触发条件:
- 消息数限制:发送
Rekey-After-Messages
条消息后,主动发起新握手。 - 时间限制:当前会话存活超过
Rekey-After-Time
秒时,发起方主动轮换密钥。
- 消息数限制:发送
- 会话终止条件:
- 当会话存活超过
Reject-After-Time
秒 或 接收消息数超过Reject-After-Messages
时,废弃当前会话。
- 当会话存活超过
- 被动触发机制:
- 若接收数据后会话年龄接近
Reject-After-Time
,且无等待的发送数据,发起方主动轮换密钥,避免通信中断。
- 若接收数据后会话年龄接近
密钥轮换
- 多会话共存:
- 系统维护当前会话、前一会话和待确认会话,支持平滑过渡。
- 新会话建立后,旧会话移至“前一”槽位,确保正在传输的包仍可解密。
- 会话清理:
- 若超过 $3×Reject-After-Time3$ 秒未建立新会话,强制清除所有会话状态与临时密钥
握手初始化重传
- 首次发送触发:
- 用户首次通过接口发送数据时,若无活跃会话,自动发送握手请求消息。
- 超时重传逻辑:
- 发送握手请求后,若
Rekey-Timeout
秒内未收到响应,生成新临时密钥并重传。 - 持续重试最长
Rekey-Attempt-Time
秒,若仍无响应,放弃并返回错误。
- 发送握手请求后,若
- 退避改进计划:
- 当前采用固定重传间隔(5 秒),未来将支持指数退避以优化网络拥塞场景
被动保活
- 保活消息生成:
- 若接收有效数据包后,
Keepalive-Timeout
秒内无数据需发送,自动生成零长度保活包。 - 保活包通过当前会话加密,含 16 字节认证标签(Poly1305),无实际负载。
- 若接收有效数据包后,
- 连接健康检测:
- 若超过 $Keepalive-Timeout+Rekey-Timeout$ 秒未收到数据,判定会话失效,触发握手重试。
与 Cookie 回复系统的交互
- Cookie 的存储与使用:
- 收到 Cookie 回复消息后,存储解密后的 Cookie 及其接收时间,不立即重传握手请求。
- 等待
Rekey-Timeout
定时器触发时,携带有效 Cookie 重传握手请求。
- 抗洪泛攻击:
- 延迟重传机制避免因大量 Cookie 回复导致的带宽滥用,缓解对等方负载压力
设计优势总结
- 无状态管理:用户仅配置静态密钥,会话建立、维护与终结全自动化。
- 抗网络波动:通过定时重试与保活机制,适应高延迟、不稳定网络环境。
- 资源高效:无动态内存分配,握手处理与加密操作在软中断中完成,减少内核开销。
- 对称角色:任意对等方可主动发起握手,避免传统 VPN 的主从模式复杂性。
示例场景
- 移动网络切换:
对等方 A 从 Wi-Fi 切换到蜂窝网络,IP 从
192.168.1.100
变为10.0.0.5
。- WireGuard 通过接收数据包更新对等方 B 的端点信息,无需人工干预。
- 若切换期间会话超时(
Reject-After-Time
),自动发起新握手恢复通信。
- NAT 穿透: 对等方位于对称 NAT 后,通过周期性保活包维持 NAT 映射表项,防止超时断开
Linux 内核实现
WireGuard 的 Linux 内核实现遵循以下核心目标:
- 代码精简:总代码量不足 4000 行(不含加密库),便于审计与维护。
- 极致性能:与 IPsec 竞争,支持高吞吐场景。
- 零动态内存分配:接收路径无内存分配,避免资源耗尽攻击。
- 原生集成:兼容标准网络工具(如
ip
和netlink
)。 - 模块化支持:可作为外部内核模块编译,无需修改内核主线代码。
队列系统
- 超级数据包处理:
- 启用 GSO(Generic Segmentation Offload,通用分段卸载) 和 分散/聚集 I/O,允许内核将大包聚合为“超级数据包”提交给 WireGuard。
- 加密后分片为 MTU 大小,批量发送,减少路由查询和缓存未命中,提升吞吐量 35%。
- 零拷贝优化:
- 数据包加解密和协议头封装均 原地操作,避免内存复制。
- 支持
sendfile(2)
系统调用,直接处理用户态文件数据。
- 延迟队列:
- 握手未完成时,待发送数据包暂存队列,握手成功后批量处理。
软中断与并行化
- 并行加密框架:
- 使用
padata
系统分配并行工作线程,充分利用多核 CPU。 - 并行阶段:多线程并行加密数据包。
- 串行阶段:按序发送加密后的数据包,避免乱序。
- 使用
- 低优先级任务:
- 握手消息处理与 Cookie 生成在后台低优先级线程执行,防止 CPU 被占满。
- 小包优化:
- 若数据包小于 256 字节或单核环境,直接在软中断(softirq)中处理,减少调度开销。
基于 RTNL 的虚拟接口与容器化
- 接口管理:
- 通过 RTNL(Routing Netlink) 注册虚拟接口,支持
ip-link(8)
和ip-set(8)
工具配置。 - 使用 Generic Netlink 协议与用户态工具(如
wg(8)
)通信,未来计划整合至ip(8)
。
- 通过 RTNL(Routing Netlink) 注册虚拟接口,支持
- 容器化支持:
- 利用 网络命名空间 隔离 WireGuard 接口与物理接口。
- 示例:容器内仅保留
wg0
接口,加密流量通过宿主机物理网卡发送,实现端到端安全。
数据结构与基本操作
- 路由表实现:
- 选用 基数树(Radix Trie) 实现加密密钥路由表,支持 RCU (Read-Copy Update)无锁查找,优化查询性能。
- 哈希表:
- 使用 SipHash2-4 抗碰撞哈希表存储对等方公钥,防止 DoS 攻击。
- 加密原语:
- 直接调用优化后的 ChaCha20-Poly1305 实现(支持 Intel AVX2/AVX512、ARM NEON),绕过内核 Crypto API 的开销。
- 临时密钥与中间值使用
memzero_explicit
清零,防止内存泄漏。
- 速率限制:
- 集成 Netfilter 的
hashlimit
模块,基于 Cookie 对握手消息进行令牌桶限速。
- 集成 Netfilter 的
FIB 注意事项
- 路由环路预防:
- 提案修改 FIB 路由查询逻辑,支持 排除指定接口(如
wg0
)的路由决策。示例:
- 提案修改 FIB 路由查询逻辑,支持 排除指定接口(如
|
|
- 命名空间隔离方案:
- 创建独立网络命名空间,将 WireGuard 接口与物理接口分离,彻底避免环路。
潜在的用户态实现
为了使 wireguard 协议获得更加广泛的使用,作者使用了一些安全的编程语言来实现 wireguard 协议,这些 wireguard 实现是基于 tun 网卡的、跨平台的
- 跨平台适配:
- 计划使用 Rust、Go 或 Haskell 开发用户态 TUN 实现,支持非 Linux 系统。
- 特性取舍:
- 用户态实现性能低于内核版本,但便于移植和快速迭代。
目前在 wireguard 官方仓库 中可以找到如下实现:
- (内核态实现)Linux 内核实现
- (内核态实现)OpenBSD 内核实现
- (内核态实现)FreeBSD 内核实现
- (内核态实现)Windows NT 内核实现
- (跨平台的用户态实现)rust 实现
- (跨平台的用户态实现)golang 实现
- (跨平台的用户态实现)Haskell 实现(不再维护)
- (客户端实现)Android 客户端实现
- (客户端实现)macOS 和 iOS 客户端实现
- (客户端实现)Windows 客户端实现
性能
WireGuard 的性能经过严格测试,结果显示其在吞吐量与延迟上均显著优于 IPsec 和 OpenVPN。以下是基于 Intel Core i7 处理器的千兆以太网环境下的测试结果(数据取 30 分钟平均值):
测试环境
- 设备 A:Intel Core i7-3820QM,网卡 Intel 82579LM(千兆)
- 设备 B:Intel Core i7-5200U,网卡 Intel I218LM(千兆)
- 测试工具:
iperf3
性能对比
指标 | WireGuard | IPsec (AES-GCM + AES-NI) | IPsec (ChaCha20Poly1305 + AVX2) | OpenVPN |
---|---|---|---|---|
吞吐量 | 1.98 Gbps | 1.34 Gbps | 1.12 Gbps | 0.38 Gbps |
平均延迟 | 0.01 ms | 0.23 ms | 0.45 ms | 1.87 ms |
关键发现
- 吞吐量优势:
- WireGuard 跑满千兆链路(1.98 Gbps),而 IPsec 的两种模式分别为 1.34 Gbps(AES-GCM)和 1.12 Gbps(ChaCha20Poly1305)。
- OpenVPN 因用户态-内核态数据拷贝开销,吞吐量仅为 0.38 Gbps。
- 延迟优势:
- WireGuard 延迟低至 0.01 毫秒,IPsec 的 AES-GCM 模式为 0.23 毫秒,ChaCha20Poly1305 模式为 0.45 毫秒,而 OpenVPN 高达 1.87 毫秒。
- CPU 利用率:
- OpenVPN 与 IPsec 测试中 CPU 满载(100%),而 WireGuard 未完全占用 CPU,表明其效率更高,瓶颈在网卡而非计算。
技术分析
- 加密算法效率:
- AES-GCM:依赖硬件加速(AES-NI),在支持指令集的设备上表现优异,但 WireGuard 仍以更简协议栈胜出。
- ChaCha20Poly1305:纯软件实现,无侧信道风险,在无 AES-NI 的嵌入式平台(如 ARM、RISC-V)中更具优势。
- 未来趋势:随着 AVX512 等宽向量指令普及,ChaCha20Poly1305 的吞吐量有望进一步提升。
- 架构优势:
- 内核态实现:WireGuard 避免用户态-内核态切换,减少数据拷贝与上下文切换开销。
- 零拷贝与批处理:支持 GSO 和分散/聚集 I/O,加密操作批量处理,优化缓存利用率。
适用场景
- 数据中心:
- WireGuard 的高吞吐与低延迟适合跨机房加密通信,替代传统 IPsec 网关。
- 移动设备:
- ChaCha20Poly1305 在手机等无 AES-NI 的设备中性能更优,延长电池续航。
- 物联网:
- 低资源消耗特性适合嵌入式设备,保障边缘计算安全。
总结
WireGuard 通过精简协议栈、内核态优化与现代加密算法,实现了接近线速的加密性能。其设计不仅满足当前网络需求,更为未来硬件演进预留扩展空间,成为高性能安全通信的理想选择。
结论
WireGuard 以不足 4000 行代码的体量,证明了一种安全网络隧道协议的可行性:它实现简洁、性能卓越、采用现代加密技术,且易于管理。其简洁性使得它能够轻松通过独立验证,并广泛移植到多样化平台。所使用的加密构造与基础原语确保了跨设备的高效性(从数据中心服务器到智能手机),同时提供了面向未来的可靠安全特性。其易部署性也将消除当前 IPsec 部署中常见的诸多隐患。正如 Ferguson 和 Schneier 在其提出时所言:“IPsec 令我们大失所望。考虑到参与者的专业水准与投入的时间,我们本期待更好的结果。……我们对 IPsec 的主要批评在于其复杂性。”
WireGuard 则反其道而行,聚焦于简洁性与可用性,同时提供可扩展的高安全性系统。通过静默处理未认证数据包、避免动态内存分配及最小化资源占用,它可部署于网络边缘,作为可靠的安全接入点,既不会向攻击者暴露自身存在,也不易成为有效攻击目标。加密密钥路由表范式易于掌握,有助于推动安全的网络设计。该协议基于密码学严谨的保守原则,使用经过充分验证的现代加密原语。WireGuard 从实用角度出发,致力于解决现实世界中的安全网络通信问题。
番外篇
allowed ip 的实现
第三章中介绍了 allowed ip
的用法,但是说的很模糊,至少没有给出如下几个问题的答案:
- 不同的
peer
可以有相同的allowed ip
吗?即peer a
和peer b
的allowed ip
都为1.1.1.1/32
,或者都为1.1.1.0/24
- 不可以,后面添加的
peer
会刷掉前面添加的peer
- 不可以,后面添加的
- 如果一个
peer
的allowed ip
包含了另外一个peer
的allowed ip
,那么会落到哪个peer
上呢?即peer a
的allowed ip
为1.1.1.0/24
,peer b
的allowed ip
为1.1.1.1/32
,那目的IP
为1.1.1.1/32
的包会发送给peer a
还是peer b
呢?- 会落到
peer b
- 会落到
为了解释上面的问题,看一下 wireguard-go
中关于 allowed ip
的具体实现:wireguard-go
中使用前缀树维护 allowed ip
,根结点为 0.0.0.0/0
,第1
层的掩码长度为1
,第2
层的掩码长度为2
,… … ,第32
层的掩码长度为 32

节点的数据结构如下,其中最关键的字段为 child
和 cidr
,child
为左右子树的指针,cidr
为掩码长度:
|
|
根据 ip
查找 peer
的逻辑如下:
|
|
wg-quick、wg、wireguard-go 三者关系
wg-quick
是平台相关的,用于完成前置工作(如配置路由、MTU
、防火墙等)、读取配置文件、调用 wg
来启动 wireguard
;
wg
是跨平台的,本质上是一个 shell
,用于和 wireguard-go
交互;wireguard-go
是真正的协议实现。
其中 wg-quick 和 wg 的实现都在仓库 wireguard-tools - Required tools for WireGuard, such as wg(8) and wg-quick(8) 中;协议实现是在不同的仓库中,汇总在 Repositories - WireGuard

实验
使用 golang 实现 在 Mac 上做一个实验,首先进行编译:
|
|
然后启动:
|
|
如果使用 wg-quick up wg0
启动,则需要删除 wireguard
内核模块,否则 wg-quick
会优先加载内核 wireguard
。wg-quick
的代码实现:
|
|
使用如下步骤删除 wireguard 内核模块:
|
|
Cross-platform Userspace Implementation 需要遵守的规则
随着各种用户态实现的涌现,为了避免产生分叉,wireguard 官网制定了所有的用户态实现必须要遵守如下的几个要求,包括:
- 支持且只能支持一个命令行指令作为入口,执行这个指令之后,这个用户态实现需要创建好对应的 TUN 设备:
|
|
- 需要创建一个
UNIX domain socket
并监听这个socket
,需要支持两种指令:
|
|
第二个:
|
|
其中 key 和 value 也是被严格规定的,参考 Cross-platform Interface - WireGuard
- 如果收到了
SIGINT/SIGTERM
信号,需要优雅地退出,包括删除TUN
设备,删除UNIX domain socket
文件等
参考