网络知识
非 IETF 标准 · 官方白皮书 / protocol 页

WireGuard

基于 Noise IK 的 UDP VPN。没有 RFC。四种消息、1-RTT 握手、Cryptokey Routing、按时间重密钥。

性质
非 IETF 标准。对照源是官方协议页,不是 rfc-editor。
作者 / 站点
Jason A. Donenfeld · wireguard.com
原文
Protocol & Cryptography · Whitepaper PDF
握手
Noise_IKpsk2_25519_ChaChaPoly_BLAKE2s · 1-RTT
默认端口
UDP/51820(可改)
对照 IPsec
无 IKE、无 SPI 协商、无传输/隧道两种模式切换(固定为层 3 隧道)
读这一篇的目标:能在 UDP 载荷里认出四种 message_type(1 握手发起、2 握手响应、3 cookie、4 数据),说出 Noise IK 一轮往返之后谁先发数据,以及 AllowedIPs 既是路由表也是 ACL。
Primitives

固定套件,没有算法协商

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。

Four Message Types

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,用来证明地址所有权。

Noise_IK · 1-RTT

两包握手,发起方先发数据

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 是同一件事。

Cryptokey Routing · 官网首页

公钥绑定 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 匹配“是否来自这个隧道地址”,即可认定包已经认证。

Timers

按时间重握手,不按“上一个包的内容”

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_TIMEREKEY_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 场;第一字节就是消息类型。

udp.port == 51820 type 1 Handshake Initiation (148 字节(定长)) type 2 Handshake Response (92 字节(定长)) type 3 Cookie Reply (64 字节(定长)) type 4 Transport Data (头 16 字节 + AEAD) UDP 载荷开头: u8 message_type u8 reserved_zero[3] 然后按类型分 type 4: receiver_index[4] counter[8] ciphertext+tag tcpdump: udp port 51820 Wireshark: wg 或 udp.port == 51820 不要用 esp / isakmp 去找 WireGuard。

考点与易错点

  1. 不是 IETF RFC。套件写死:Noise IK + ChaCha20-Poly1305 + Curve25519 + BLAKE2s。
  2. 四种消息:1 发起、2 响应、3 cookie、4 数据。1-RTT 之后发起方先发数据;响应方要等第一包数据做密钥确认。
  3. Cryptokey Routing:AllowedIPs 发送时是路由表,接收时是 ACL。配置的是公钥,不是证书。
  4. 重握手按定时器,不按“上一个包里写了什么”。原响应方不因 REKEY_AFTER_TIME 主动发起。
  5. 抓包看 UDP/51820 和首字节类型。不要用 espisakmp
  6. 不用 PSK 时,PSK 输入是 32 字节全 0,不是“没有这一项”。