WireGuard
基于 Noise IK 的 UDP VPN。没有 RFC。四种消息、1-RTT 握手、Cryptokey Routing、按时间重密钥。
固定套件,没有算法协商
The following protocols and primitives are used:
ChaCha20 for symmetric encryption, authenticated with Poly1305, using RFC7539's AEAD construction Curve25519 for ECDH BLAKE2s for hashing and keyed hashing, described in RFC7693 SipHash24 for hashtable keys HKDF for key derivation, as described in RFC5869
[……下接 Noise 构造名]
CONSTRUCTION: the UTF-8 value Noise_IKpsk2_25519_ChaChaPoly_BLAKE2s, 37 bytes IDENTIFIER: the UTF-8 value WireGuard v1 zx2c4 Jason@zx2c4.com, 34 bytes
注解
ChaCha20-Poly1305(RFC 7539 的 AEAD)、Curve25519、BLAKE2s、SipHash24、HKDF。没有 IKE 那种套件列表。
CONSTRUCTION 的 UTF-8 值是 Noise_IKpsk2_25519_ChaChaPoly_BLAKE2s(37 字节),这就是 Noise IK + 可选 PSK 的标识。不用 PSK 时,PSK 视为 32 字节全 0。
1 发起 · 2 响应 · 3 cookie · 4 数据
msg = handshake_initiation {
u8 message_type
u8 reserved_zero[3]
u32 sender_index
u8 unencrypted_ephemeral[32]
u8 encrypted_static[AEAD_LEN(32)]
u8 encrypted_timestamp[AEAD_LEN(12)]
u8 mac1[16]
u8 mac2[16]
}[……下接 type=1 的赋值]
msg.message_type = 1
msg.reserved_zero = { 0, 0, 0 }
msg.sender_index = little_endian(initiator.sender_index)[……下接握手响应格式]
msg = handshake_response {
u8 message_type
u8 reserved_zero[3]
u32 sender_index
u32 receiver_index
u8 unencrypted_ephemeral[32]
u8 encrypted_nothing[AEAD_LEN(0)]
u8 mac1[16]
u8 mac2[16]
}[……下接 type=2 的赋值]
msg.message_type = 2
msg.reserved_zero = { 0, 0, 0 }
msg.sender_index = little_endian(responder.sender_index)
msg.receiver_index = little_endian(initiator.sender_index)[……下接传输数据格式]
msg = packet_data {
u8 message_type
u8 reserved_zero[3]
u32 receiver_index
u64 counter
u8 encrypted_encapsulated_packet[]
}[……下接 type=4 的赋值]
msg.message_type = 4
msg.reserved_zero = { 0, 0, 0 }
msg.receiver_index = little_endian(responder.sender_index)
encapsulated_packet = encapsulated_packet || zero padding in order to make the length a multiple of 16
counter = initiator.sending_key_counter++
msg.counter = little_endian(counter)
msg.encrypted_encapsulated_packet = AEAD(initiator.sending_key, counter, encapsulated_packet, [empty])[……下接 cookie 回复]
msg = packet_cookie_reply {
u8 message_type
u8 reserved_zero[3]
u32 receiver_index
u8 nonce[24]
u8 encrypted_cookie[AEAD_LEN(16)]
}
msg.message_type = 3
msg.reserved_zero = { 0, 0, 0 }
msg.receiver_index = little_endian(initiator.sender_index)
msg.nonce = RAND(24)
cookie = MAC(responder.changing_secret_every_two_minutes, initiator.ip_address)
msg.encrypted_cookie = XAEAD(HASH(LABEL_COOKIE || responder.static_public), msg.nonce, cookie, last_received_msg.mac1)注解
四种消息的第一字节就是类型:1 handshake initiation,2 handshake response,3 cookie reply,4 transport data。接着 3 字节 0。没有第五种。
发起消息带临时公钥、加密的静态公钥、加密的 TAI64N 时间戳,再加 mac1/mac2。响应消息多一个 receiver_index,AEAD 保护的是空明文(key confirmation 材料)。数据消息是 receiver_index + 64 位计数器 + AEAD 载荷。
cookie 回复在负载下发出:type=3,24 字节 nonce,XChaCha20-Poly1305 包住 cookie。cookie 是“每两分钟换一次的服务器秘密 + 发起方 IP”的 MAC,用来证明地址所有权。
两包握手,发起方先发数据
WireGuard uses the Noise_IK handshake from Noise, building on the work of CurveCP, NaCL, KEA+, SIGMA, FHMQV, and HOMQV. All packets are sent over UDP.
[……下接握手完成后谁先用新会话]
After a handshake is completed, with a message from initiator to responder and then responder back to initiator, the initiator may then send encrypted session packets, but the responder cannot. The responder must wait to use the new session until it has recieved one encrypted session packet from the initiator, in order to provide key confirmation. Thus, until the responder receives that first packet using the newly established session, it must either queue up packets to be sent later, or use the previous session, if one exists and is valid. Therefore, after the initiator receives the response from the responder, if it has no data packets immediately queued up to send, it should send an empty packet, so as to provide this confirmation.
[……下接派生发送/接收密钥]
temp1 = HMAC(initiator.chaining_key, [empty]) temp2 = HMAC(temp1, 0x1) temp3 = HMAC(temp1, temp2 || 0x2) initiator.sending_key = temp2 initiator.receiving_key = temp3 initiator.sending_key_counter = 0 initiator.receiving_key_counter = 0 temp1 = HMAC(responder.chaining_key, [empty]) temp2 = HMAC(temp1, 0x1) temp3 = HMAC(temp1, temp2 || 0x2) responder.receiving_key = temp2 responder.sending_key = temp3 responder.receiving_key_counter = 0 responder.sending_key_counter = 0
注解
Noise 模式是 IK:发起方预先知道响应方静态公钥(配置里的 Peer PublicKey),第一包就把自己的静态公钥加密送过去。所以是 1-RTT:发起 → 响应,之后发起方即可发 type=4。
响应方必须等到收到发起方用新会话密钥发来的第一包数据,才能用新会话往回发——这是密钥确认。若当时没有数据要发,发起方应补一包空载荷。在这之前,响应方可以排队,或继续用还有效的旧会话。
两包之后用 HKDF 从 chaining_key 抽出一对发送/接收密钥,计数器归零,临时密钥和哈希清零。协议页把这写成 HMAC-Blake2s 的 0x1 / 0x2 展开,和白皮书里的 KDF 是同一件事。
公钥绑定 AllowedIPs:既是路由表也是 ACL
注解
这一节不在 protocol 页。以下按 www.wireguard.com/#cryptokey-routing 叙述,不放进原文 pre。
每张网卡一把私钥、一份对等体列表;每个对等体一把公钥,再挂一份 AllowedIPs。发送时按目的地址匹配 AllowedIPs,决定用哪把公钥加密、寄到该对等体最近的外网端点——这是路由表。接收时解密认证之后,源地址必须落在该对等体的 AllowedIPs 里,否则丢掉——这是访问控制表。
客户端常见 AllowedIPs = 0.0.0.0/0,表示“所有地址都走这一条隧道”。服务端则给每个客户端写精确前缀(例如 /32)。外网 Endpoint 可以只配在客户端;服务端靠“认证通过的包从哪来”学习并漫游。
和 IPsec 的差别:没有 SPD/SAD 两张大表,也没有 IKE 身份与流量选择器的二次映射。管理员在接口上用 iptables 匹配“是否来自这个隧道地址”,即可认定包已经认证。
按时间重握手,不按“上一个包的内容”
The following timers are at play:
A handshake initiation is retried after REKEY_TIMEOUT + jitter ms, if a response has not been received, where jitter is some random value between 0 and 333 ms. If a packet has been received from a given peer, but we have not sent one back to the given peer in KEEPALIVE ms, we send an empty packet. If we have sent a packet to a given peer but have not received a packet after from that peer for KEEPALIVE + REKEY_TIMEOUT ms, we initiate a new handshake. All ephemeral private keys and symmetric session keys are zeroed out after REJECT_AFTER_TIME * 3 ms if no new keys have been exchanged. After sending a packet, if the number of packets sent using that key exceeds REKEY_AFTER_MESSAGES, we initiate a new handshake. After sending a packet, if the sender was the original initiator of the handshake and if the current session key is REKEY_AFTER_TIME ms old, we initiate a new handshake. If the sender was the original responder of the handshake, we do not reinitiate a new handshake after REKEY_AFTER_TIME ms like the original initiator does. After receiving a packet, if the receiver was the original initiator of the handshake and if the current session key is REKEY_AFTER_TIME - KEEPALIVE_TIMEOUT - REKEY_TIMEOUT ms old, we initiate a new handshake. Handshakes are only initiated once every REKEY_TIMEOUT ms, with this strict rate limiting enforced. Packets are dropped if the session counter is greater than REJECT_AFTER_MESSAGES or if its key is older than REJECT_AFTER_TIME ms. After REKEY_ATTEMPT_TIME ms of trying to initiate a new handshake, the retries give up and cease, and clear all existing packets queued up to be sent. If a packet is explicitly queued up to be sent, then this timer is reset.
[……下接指数退避尚未做]
Future work involves adjusting REKEY_TIMEOUT to use exponential back-off.
[……下接重放窗口]
Nonces are never reused. A 64bit counter is used, and cannot be wound backward. UDP, however, sometimes delivers messages out of order. For that reason we use a sliding window, in which we keep track of the greatest counter received and a window of roughly 2000 prior values, checked after verifying the authentication tag. This avoids replay attacks while ensuring nonces are never reused and that UDP can maintain out-of-order delivery performance.
注解
握手重试:REKEY_TIMEOUT + jitter(jitter 0–333 ms)。保活:收到对端包但自己超过 KEEPALIVE 没回,发空包。发出去却收不到,等 KEEPALIVE + REKEY_TIMEOUT 就重新握手。
密钥老化:原发起方在 REKEY_AFTER_TIME 或 REKEY_AFTER_MESSAGES 之后重握手;原响应方不因时间主动重握手。拒绝门槛是 REJECT_AFTER_TIME / REJECT_AFTER_MESSAGES。临时私钥和会话密钥在 REJECT_AFTER_TIME * 3 后清零。
握手严格限速:每 REKEY_TIMEOUT 最多发起一次。REKEY_ATTEMPT_TIME 内仍握不上就停、清空发送队列。协议页写明未来才把 REKEY_TIMEOUT 改成指数退避。
数据面防重放:64 位计数器只增不减,认证通过后再查大约 2000 的滑动窗口,兼顾 UDP 乱序。
抓包对照
WireGuard 默认 UDP/51820。没有 IKE,没有 SPI 场;第一字节就是消息类型。
- 握手两包之后就是大量 type=4。内层 IP/TCP 在 AEAD 里,没密钥看不到 L4。
- 没有 NAT-T 切换:本身就是 UDP,过 NAT 不改端口逻辑。对端地址变化靠 Cryptokey Routing 的漫游更新 Endpoint。
- 负载下才看得到 type=3。静默丢弃未认证的握手,是协议的默认姿态。
- Wireshark 解码器名字是
wg,不是wireguard。
考点与易错点
- 不是 IETF RFC。套件写死:Noise IK + ChaCha20-Poly1305 + Curve25519 + BLAKE2s。
- 四种消息:1 发起、2 响应、3 cookie、4 数据。1-RTT 之后发起方先发数据;响应方要等第一包数据做密钥确认。
- Cryptokey Routing:AllowedIPs 发送时是路由表,接收时是 ACL。配置的是公钥,不是证书。
- 重握手按定时器,不按“上一个包里写了什么”。原响应方不因
REKEY_AFTER_TIME主动发起。 - 抓包看 UDP/51820 和首字节类型。不要用
esp或isakmp。 - 不用 PSK 时,PSK 输入是 32 字节全 0,不是“没有这一项”。