土法炼钢兴趣小组的算法知识备份

【网络工程】WireGuard 内部实现:Cryptokey Routing、Noise IK 握手与内核数据路径

文章导航

分类入口
network
标签入口
#wireguard#vpn#noise-protocol#kernel#chacha20-poly1305#curve25519#cryptokey-routing#secure-channel#netlink

目录

IPSec/IKEv2 的协议栈有多重?RFC 7296(IKEv2 核心)142 页,加上 EAP、MOBIKE、IKEv2 Redirect 等几十个扩展 RFC,StrongSwan 源码约 30 万行。内核侧的 xfrm 框架把安全策略数据库(SPD)和安全关联数据库(SAD)拆成两层——配置时看起来很灵活,出问题时很难判断是哪一层的行为和预期不一致。

WireGuard 的答案是:用约 4000 行内核代码做同样的事(Linux 5.6 初合并时的规模;当前主线 .c 源码约 5000 行,仍远小于 IPSec 栈)。这不是少写功能——WireGuard 提供了认证加密隧道需要的完整体验——而是系统地拒绝了很多”工程正确”的东西:拒绝证书体系、拒绝算法协商(cipher suite negotiation)、拒绝 IKE 的多轮状态机、拒绝 SPD/SAD 的分层抽象。选型层面的对比见 VPN 技术工程对比 第三节。

替代这些的是三个核心设计:

  1. Cryptokey Routing:公钥就是路由表,身份 = 加密 = 转发
  2. Noise IKpsk2 握手:固定密码学原语,一次 1.5 RTT 的握手解决认证+密钥协商+前向保密
  3. 多核分段队列:平行加解密 + per-peer 串行保序
flowchart LR
    subgraph IPSec[IPSec/IKEv2 协议栈]
        IKE[IKEv2 守护进程<br/>RFC 7296, 142页]
        XFRM[xfrm 框架<br/>SPD + SAD 双层]
        CERT[证书体系<br/>X.509 + CRL/OCSP]
        NEGO[算法协商<br/>cipher suite negotiation]
    end

    subgraph WG[WireGuard]
        CK[公钥 ↔ AllowedIPs<br/>一张表]
        NOISE[固定 Noise IKpsk2<br/>无协商]
        SEND[发送路径<br/>~300 行 C]
        RECV[接收路径<br/>~400 行 C]
    end

    IPSec --> IPSEC_COMPLEX[数十万行代码<br/>配置易出错]
    WG --> WG_SIMPLE[~5000 行 C<br/>形式化可验证]

    classDef bad fill:#f8514922,stroke:#f85149,color:#adbac7;
    classDef good fill:#3fb95022,stroke:#3fb950,color:#adbac7;
    class IPSEC_COMPLEX bad
    class WG_SIMPLE good

上图是本文要拆解的三个问题的全景:左边是 WireGuard 拒绝的东西,右边是替代方案。下面从 cryptokey routing 开始,逐一钻进内核源码和协议规范。

一、Cryptokey Routing:公钥即路由表

传统 VPN 的数据模型是三层分离的:身份(证书/PSK)→ 密钥协商(IKE 的多轮握手)→ 安全关联(xfrm SA,绑定 SPI + 加密算法 + 密钥 + 源/目的 IP 范围)。配置时在三个不同的命名空间操作,出错时排查三个维度。

WireGuard 的数据模型只有一层:每个 peer 是一个 (公钥, AllowedIPs, Endpoint?) 三元组。

1.1 数据结构

Interface:
  Private Key: yAnz...fBmk
  Listen Port: 51820

Peer #1:
  Public Key:  xTIB...p8Dg
  Allowed IPs: 10.192.122.3/32, 10.192.124.0/24
  Endpoint:    198.51.100.1:51820

Peer #2:
  Public Key:  TrMv...WXX0
  Allowed IPs: 10.192.122.4/32, 192.168.0.0/16
  Endpoint:    (自动学习)

这就是一个 WireGuard 接口的完整配置。没有证书链、没有 CA、没有加密算法选择、没有 IKE proposal 列表。公钥本身就是 peer 的唯一标识符——不需要把公钥包装进 X.509 证书再签名。

1.2 出方向:密钥选路

当内核协议栈有一个 plaintext IP 包要经由 wg0 发出时,ndo_start_xmit(即 wg_xmit)的处理逻辑:

  1. 取出目的 IP 地址
  2. 在 AllowedIPs trie 中查找——目的 IP 落在哪个 peer 的 AllowedIPs 范围里?
  3. 找到 peer → 用该 peer 的 current_keypair.sending_key 加密
  4. 没找到 → 丢弃,返回 -ENOKEY,生成 ICMP “no route to host”

关键点:路由决策和加密密钥选择是同一个操作。不需要先查路由表再查 SPD 再查 SAD——一步完成。这对应于内核源码 allowedips.c 中的 wg_allowedips_lookup_dst() 调用点(device.cwg_xmit()):

// device.c, wg_xmit() — 简化的发送入口
peer = wg_allowedips_lookup_dst(&wg->peer_allowedips, skb);
if (!peer)
    goto err_nokey;  // -ENOKEY

keypair = get_keypair(peer);  // 来自 peer->keypairs.current_keypair
nonce = atomic64_inc_return(&keypair->sending_counter) - 1;
// 用 keypair->sending_key 和 nonce 做 AEAD 加密

1.3 入方向:双向绑定

收到加密 UDP 包后的验证路径(receive.c;解密在 worker,源 IP 校验在 NAPI):

  1. wg_packet_consume_data() 从包头提取 receiver index → 查 hashtable 定位 keypair
  2. 包进入 decrypt_queue,由 wg_packet_decrypt_worker() 解密 + 认证(ChaCha20-Poly1305)
  3. 解密失败 → 静默丢弃
  4. wg_packet_rx_poll()counter_validate() 检查 nonce 防重放
  5. wg_packet_consume_data_done() 取出内层源 IP,调用 wg_allowedips_lookup_src() 并与 keypair 所属 peer 比对
  6. 不匹配 → 静默丢弃

这意味着:如果 peer A(公钥 xTIB...p8Dg,AllowedIPs 10.192.122.3/32)试图发送一个源 IP 为 10.10.10.230 的包,即使它的加密和认证都是正确的,包也会被丢弃——因为 10.10.10.230 不在 10.192.122.3/32 里。

这个特性让基于 wg0 接口的 iptables/nftables 规则天然可靠:wg0 上看到的源 IP 是有密码学保证的,不可能被其他 peer 伪造。

1.4 为什么静默丢弃

WireGuard 对任何认证失败的包都静默丢弃,不返回 ICMP 错误、不发送 RST。这不是遗漏,而是安全设计:

1.5 layering violation 的自觉

在 NDSS 2017 论文中 Donenfeld 明确承认 cryptokey routing 是一个故意的 layering violation——它把 L3 路由(网络层)和 L5+ 身份/加密(应用层/会话层)合并了。传统网络架构教条会说这是错的。但 IPSec 把这两个层分开的结果是 SPD/SAD 的配置地狱和调试噩梦。WireGuard 的选择是务实的:安全策略的最简表达就是”这个公钥可以使用这些 IP 地址”——多一层抽象不会让它更安全,只会让它更复杂。

二、Noise IKpsk2 握手:1.5 RTT 的完整路径

2.1 为什么是 Noise IK

WireGuard 选择了 Noise Protocol Framework 中定义的 IK 握手模式。Noise 框架用简洁的记号系统描述握手:

IK(s, rs):
  <- s
  ...
  -> e, es, s, ss, {t}
  <- e, ee, se, psk, {}

记号解释:

记号 含义
<- s 发起方预先知道响应方的静态公钥(配置在 peer 的公钥字段中)
-> e 发起方发送自己的临时公钥
es \(DH(e_i, s_r)\)——发起方临时 × 响应方静态
s 发起方发送自己的静态公钥(用 es 派生的密钥加密)
ss \(DH(s_i, s_r)\)——发起方静态 × 响应方静态
{t} ss 派生的密钥加密的 timestamp(防重放)
<- e 响应方发送自己的临时公钥
ee \(DH(e_i, e_r)\)——临时 × 临时(前向保密的来源)
se \(DH(s_i, e_r)\)——发起方静态 × 响应方临时
psk 可选预共享密钥
{} 响应方可以在此携带加密 payload

WireGuard 的实现是 Noise_IKpsk2_25519_ChaChaPoly_BLAKE2s,在标准 IK 之上加了一个 preshared key 字段——psk 可以是空的,但如果配了,就额外引入一个对称密钥进入密钥派生,提供对量子计算的临时抗性(quantum resistance)。WireGuard 不支持 cipher suite negotiation——所有原语是固定的:

2.2 握手第一步:Initiation

sequenceDiagram
    participant I as Initiator
    participant R as Responder

    Note over I: 已知 R 的静态公钥 s_r
    Note over I: 生成临时密钥对 e_i

    I->>R: Message #1: Initiation
    Note over I,R: 明文: e_i (临时公钥)
    Note over I,R: es = DH(e_i, s_r) → 派生临时密钥 k0
    Note over I,R: 加密: s_i (静态公钥)
    Note over I,R: ss = DH(s_i, s_r) → 派生临时密钥 k1
    Note over I,R: 加密: timestamp + MAC1

    Note over R: 用 s_r 私钥 + e_i 算出 es
    Note over R: 解密得到 s_i
    Note over R: 用 s_r 私钥 + s_i 算出 ss
    Note over R: 验证 timestamp 防重放
    Note over R: 生成临时密钥对 e_r

第一步(noise.c 中的 wg_noise_handshake_create_initiation())发送:

  1. 明文 e_i(发起方临时公钥)——响应方用它与自己的静态私钥做 DH,得到 es,派生临时加密密钥 \(k_0\)
  2. 加密 s_i(发起方静态公钥)——用 \(k_0\) 加密。只有真正的响应方能解密(因为除了响应方自己,没人有 \(s_r\) 的私钥),这提供了发起方的身份隐藏(identity hiding):被动观察者看不到谁的密钥在跟响应方握手
  3. 加密 timestamp + mac1——用 ss\(DH(s_i, s_r)\))派生的密钥 \(k_1\) 加密。mac1 是 cookie 机制的第一层防御(详见第五章)

这一步同时完成了两个 DH:essses 提供加密,ss 提供发起方认证——因为要算出正确的 ss,发起方必须持有 \(s_i\) 的私钥。

2.3 握手第二步:Response

sequenceDiagram
    participant I as Initiator
    participant R as Responder

    Note over R: 已完成 initiation 验证
    Note over R: 提取 s_i,生成 e_r

    R->>I: Message #2: Response
    Note over I,R: 明文: e_r (临时公钥)
    Note over I,R: ee = DH(e_i, e_r)
    Note over I,R: se = DH(s_i, e_r)
    Note over I,R: 全部 DH 结果 + psk → HKDF 派生 session keys
    Note over I,R: 加密: empty payload + MAC1/MAC2

    Note over I: 用 e_i 私钥 + e_r 算出 ee
    Note over I: 用 s_i 私钥 + e_r 算出 se
    Note over I: 验证 session keys 正确性
    Note over I: 保存 sending_key + receiving_key

第二步(noise.c 中的 wg_noise_handshake_create_response())发送:

  1. 明文 e_r(响应方临时公钥)
  2. 加密 empty payload + mac1/mac2

这一步完成剩下的两个 DH:ee(临时-临时,前向保密的唯一来源——因为临时密钥对在握手后销毁)和 se(认证响应方——只有持有 \(s_r\) 私钥的人才能算出正确的 se)。

至此四个 DH 全部完成。WireGuard 把 essseese 四个 DH 结果和可选的 psk 一起送入 HKDF(BLAKE2s),派生 session。双方各自得到两把对称密钥——sending_key 和 receiving_key,两边的 sending_key 互为对方的 receiving_key。同时各自获得一个初始化为 0 的 64-bit nonce counter。

2.4 为什么是 1.5 RTT

Noise IK 名义上是 1-RTT 握手(两条消息交换后双方都有 session keys),但响应方在收到发起方的第一条 transport 消息之前不能安全地发送自己的数据。原因在于前向保密:

因此实际可安全发送数据的延迟:

0 RTT:  发起方发送 Initiation
0.5 RTT: 响应方收到,发送 Response
1 RTT:  发起方收到 Response → 可以安全发送数据了
1.5 RTT: 响应方收到发起方的第一条 Transport → 可以安全发送数据了

这就是”1.5 RTT”的真实含义——不是协议规范定义的,而是安全边界推导的结果。Dowling & Paterson(ACNS 2018)在形式化分析中确认了这一属性。

2.5 与 TLS 1.3 的对比

维度 TLS 1.3 (EC)DHE WireGuard (Noise IKpsk2)
往返 1-RTT(0-RTT 可选) 1-RTT(1.5 RTT 到安全数据)
DH 操作 1 次(ECDHE) 4 次(es, ss, ee, se)
身份隐藏 仅 server cert(client cert 在加密通道内) 发起方身份对被动观察者隐藏
KCI 抗性 取决于模式 transport 消息具有 KCI 抗性
算法协商 支持 cipher suite negotiation 固定原语,不协商
证书 X.509(强制) 无——公钥即身份

四次 DH 操作看起来比 TLS 1.3 的”贵”,但实际上:Curve25519 的 DH 运算极快(微秒级),四次 DH 的总开销远小于 TLS 1.3 的证书链验证(RSA/ECDSA 签名验证 + 证书链遍历)。WireGuard 用计算开销(四次快速的 X25519)换掉了 I/O 开销(没有证书链要传输和验证)——对网络延迟敏感的 VPN 场景,这个 trade-off 是正确的。

三、内核数据路径:平行加密 + 串行保序

WireGuard 内核模块(drivers/net/wireguard/)的代码组织如下(行数取自 torvalds/linux 主线,2026-06):

文件 职责 行数
device.c net_device 注册、ndo_open/ndo_stop/ndo_start_xmit 475
noise.c Noise IKpsk2 握手状态机、keypair 管理 861
send.c 发送路径:排队 → 加密 → UDP 发送 414
receive.c 接收路径:UDP → cookie 验证 → 解密 → NAPI 上栈 586
queueing.c / queueing.h 多核队列基础设施(ptr_ringprev_queue 313
timers.c 握手重传 / keepalive / rekey / zero-key-material 定时器 246
cookie.c MAC1/MAC2 cookie DoS 防御 236
allowedips.c Trie IP 查找(路由) 424
peerlookup.c 公钥 / handshake index hashtable 226
netlink.c Generic Netlink 配置接口 607
socket.c UDP socket 收发与 endpoint 管理 436
peer.c / main.c / ratelimiter.c peer 生命周期、模块入口、限速 540

.c 文件合计约 5160 行。下面以发送和接收两条数据路径为轴,拆解这些文件如何协同工作。

3.1 发送路径:从 IP 包到加密 UDP

flowchart TD
    STACK[内核协议栈<br/>plaintext IP 包] --> XMIT[ndo_start_xmit<br/>wg_xmit]
    XMIT --> ALLOWED{wg_allowedips_lookup_dst<br/>目的 IP 查找}
    ALLOWED -->|命中| STAGE[入队<br/>peer.staged_packet_queue]
    ALLOWED -->|未命中| DROP[ICMP HOST_UNREACH<br/>-ENOKEY]
    STAGE --> STAGED[wg_packet_send_staged_packets<br/>分配 nonce]

    STAGED --> PQ[peer.tx_queue<br/>prev_queue 保序]
    STAGED --> EQ[device.encrypt_queue<br/>ptr_ring]

    EQ --> ENC1[encrypt_worker #1]
    EQ --> ENC2[encrypt_worker #2]
    EQ --> ENCN[encrypt_worker #N]

    ENC1 --> PQ
    ENC2 --> PQ
    ENCN --> PQ

    PQ --> TX[transmit_packet_work<br/>per-peer 串行发送]
    TX --> UDP[wg_socket_send_skb_to_peer<br/>send4/send6]

    classDef kernel fill:#388bfd22,stroke:#388bfd,color:#adbac7;
    class XMIT,ALLOWED,STAGE,STAGED,PQ kernel
    classDef parallel fill:#f0883e22,stroke:#f0883e,color:#adbac7;
    class ENC1,ENC2,ENCN parallel

发送路径的核心设计是设备级并行加密 + peer 级串行发送

  1. 入队与 nonce 分配wg_xmit() 把包放进 peer->staged_packet_queuewg_packet_send_staged_packets()atomic64_inc_return(&keypair->sending_counter) 分配 nonce,再通过 wg_queue_enqueue_per_device_and_peer() 同时写入 peer->tx_queueprev_queue,保序)和 wg->encrypt_queueptr_ring,并行)。

  2. 并行加密阶段wg_packet_encrypt_worker()packet_crypt_wq 上运行,每个 CPU 核一个 worker,从 encrypt_queue 取包,调用 chacha20poly1305_encrypt_sg_inplace() 原地加密,完成后把状态标为 PACKET_STATE_CRYPTED 并唤醒对应 peer 的 transmit_packet_work

  3. 串行发送阶段transmit_packet_worktx_queue 的 FIFO 顺序等待加密完成再发 UDP。加密可以乱序完成,但发送顺序与入队顺序一致。

这种设计解决了”要并行加密以获得吞吐量”和”要保序发送以满足 IP 语义”之间的矛盾。prev_queue 负责 peer 内保序,ptr_ring 负责跨 CPU 分发加密任务(定义见 queueing.h)。

发送路径有一个重要的安全实现细节:nonce 上限messages.h 定义 REKEY_AFTER_MESSAGES = 2^{60}REJECT_AFTER_MESSAGES 接近 \(2^{64}\)。计数器越过 REKEY_AFTER_MESSAGES 或 keypair 超过 REJECT_AFTER_TIME(180s)后,keep_key_fresh() 会触发新握手,而不是继续用旧 keypair 发包。

3.2 接收路径:从 UDP 到网络栈

flowchart TD
    UDP[UDP 包到达<br/>wg_packet_receive] --> SKB[prepare_skb_header<br/>检查包类型]

    SKB -->|handshake/cookie| HQ[handshake_queue<br/>ptr_ring]
    HQ --> HW[handshake_receive_worker<br/>wg_packet_handshake_receive_worker]
    HW --> H_NOISE[noise 握手消费<br/>initiation / response / cookie]

    SKB -->|data| DEC[wg_packet_consume_data<br/>定位 keypair]
    DEC -->|未找到 keypair| DROP[静默丢弃]
    DEC -->|找到 keypair| DECQ[peer.rx_queue +<br/>device.decrypt_queue]

    DECQ --> D1[decrypt_worker #1]
    DECQ --> D2[decrypt_worker #2]
    DECQ --> DN[decrypt_worker #N]

    D1 --> NAPI[peer NAPI poll<br/>wg_packet_rx_poll]
    D2 --> NAPI
    DN --> NAPI

    NAPI --> REPLAY{counter_validate<br/>RFC 6479}
    REPLAY -->|重放| DROP
    REPLAY -->|合法| ALLOWED{wg_allowedips_lookup_src<br/>源 IP 验证}
    ALLOWED -->|不匹配| DROP
    ALLOWED -->|匹配| GRO[GRO / 上栈<br/>napi_gro_receive]

    classDef kernel fill:#388bfd22,stroke:#388bfd,color:#adbac7;
    class UDP,SKB,DEC,REPLAY,ALLOWED kernel
    classDef parallel fill:#f0883e22,stroke:#f0883e,color:#adbac7;
    class D1,D2,DN parallel

接收路径的关键设计决策:

1. 不认证不分配内存。在 prepare_skb_header() 中检查包的基本结构(长度、类型、receiver index),如果结构不合法直接丢弃,不调用 kmalloc()。这是对内存耗尽 DoS 的防御编码——任何未认证的包都不会导致内核内存分配。

2. Receiver index 快速定位。每个包携带 32-bit receiver index,接收方用 index 在 hashtable 中 O(1) 查找对应的 keypair。不需要像传统 IPSec 那样用 SPI + 目的 IP 复合查找。

3. per-peer NAPI 轮询。每个 wg_peer 自带 napi_structwg_packet_rx_poll()peer->rx_queue 取解密完成的包,在 softirq 上下文中批量上栈,并与 GRO 配合聚合小包。

4. 防重放窗口。解密后的 nonce 需要过滑动窗口验证(counter_validate(),实现遵循 RFC 6479)。messages.h 用 8192-bit 位图记录近期 nonce(64 位平台上有效窗口约 8128),超出窗口的旧序号会被拒绝——大量丢包后可能需要重新握手。

3.3 per-peer 隔离

encrypt_queue / decrypt_queue 挂在 wg_device 上,由多核 worker 共享;但每个 wg_peer 有自己的 tx_queuerx_queueprev_queue)、staged_packet_queuenapi 和 keypairs。这意味着:

这与 IPSec 的差异:xfrm SA 是全局的——如果有 1000 个 SA 同时需要 rekey,IKE 守护进程可能需要串行处理。WireGuard 没有这个问题,因为每个 peer 的 rekey 是独立 timer 驱动的。

四、Timer 状态机

WireGuard 没有 TCP 那样的”连接”概念——它只有 timer 驱动的状态转换。

stateDiagram-v2
    [*] --> Idle: peer 创建

    Idle --> HandshakeInitiated: 有数据要发<br/>或 keep_key_fresh 触发
    HandshakeInitiated --> HandshakeInitiated: timer_retransmit_handshake<br/>5s + jitter,最多 18 次
    HandshakeInitiated --> SessionEstablished: response 收到 + session 派生

    SessionEstablished --> SessionEstablished: 正常收发数据

    SessionEstablished --> HandshakeInitiated: REKEY_AFTER_TIME<br/>120s(发起方)
    SessionEstablished --> HandshakeInitiated: REKEY_AFTER_MESSAGES<br/>2^60 包

    SessionEstablished --> HandshakeInitiated: timer_new_handshake<br/>15s 无入站包
    SessionEstablished --> SessionDead: timer_zero_key_material<br/>540s 无新密钥
    HandshakeInitiated --> SessionDead: 握手重传超限

    SessionEstablished --> SessionEstablished: timer_persistent_keepalive<br/>(保持 NAT 映射)
    SessionDead --> [*]: peer 销毁或配置移除

各 timer 的语义(常量定义在 messages.h):

Timer 默认值 作用
timer_retransmit_handshake REKEY_TIMEOUT = 5s + jitter;MAX_TIMER_HANDSHAKES = 18 发起方没收到 response 时重传 initiation
timer_send_keepalive KEEPALIVE_TIMEOUT = 10s 收到包后一段时间未发送,补发空 transport 包
timer_new_handshake KEEPALIVE_TIMEOUT + REKEY_TIMEOUT = 15s + jitter 发送后长时间未收到任何认证包,发起新握手
timer_zero_key_material REJECT_AFTER_TIME * 3 = 540s session 派生后长期收不到新密钥,清零密钥材料
timer_persistent_keepalive 未配置时关闭 persistent_keepalive_interval 周期发包维持 NAT 绑定

Rekey 与上述 liveness timer 是两套机制:keep_key_fresh()send.c)在发送路径检查 REKEY_AFTER_TIME(120s)和 REKEY_AFTER_MESSAGES\(2^{60}\)),满足条件就排队新握手;timer_new_handshake 则处理”发了但对面没回”的活性检测。

Rekey 过程是无缝的——新握手产生 next_keypair,握手成功后 next_keypair 变成 current_keypair,旧的 current_keypair 退为 previous_keypairprevious_keypair 保留到 REJECT_AFTER_TIME(180s)到期,用于接收用旧密钥发出的在途包。这个设计确保 rekey 不会丢包。

所有 timer 的定义和回调都在 timers.c 中——它使用内核标准的 timer_listmod_timer(),没有自定义的 timer wheel。

五、Cookie:无状态 DoS 防御

WireGuard 的 DoS 防御分为两档,核心思想是”攻击者不能让服务器做昂贵的 Curve25519 DH 运算”。

5.1 正常模式:仅 MAC1

在正常负载下,服务器只验证 mac1——这是一个基于发起方公钥 + 服务器 cookie secret 的快速 BLAKE2s MAC。计算 mac1 是纯对称操作,比 Curve25519 DH 便宜几个数量级。

但攻击者如果知道一个合法 peer 的公钥(可以从公开的 wg 配置中获取),就能生成有效的 mac1——这不是防御,只是基本过滤。

5.2 高负载模式:MAC1 + MAC2(Cookie)

当全局握手队列深度达到 MAX_QUEUED_INCOMING_HANDSHAKES / 8(512)时进入 under_load 状态,并在负载回落后保留约 1 秒迟滞,避免抖动。此时仅带 MAC1 的 initiation 会被拒绝并返回 Cookie Reply:

sequenceDiagram
    participant I as Initiator
    participant R as Responder (under load)

    I->>R: Initiation + MAC1 (仅 MAC1)
    Note over R: handshake_queue_len ≥ 512<br/>MAC1 正确但不够

    R-->>I: Cookie Reply + MAC2<br/>(cookie = MAC(secret, src_ip, src_port))

    I->>R: Initiation + MAC1 + MAC2 (携带 cookie)
    Note over R: MAC1 ✓, cookie ✓<br/>消耗 CPU 做 DH
    R-->>I: Response

握手队列硬上限是 MAX_QUEUED_INCOMING_HANDSHAKES(4096);超过一半(2048)后入队路径会走 spin_trylock 快速失败,进一步保护内存。

Cookie 由服务器用只有自己知道的 cookie secret(发起方源 IP, 源端口) 做 MAC 计算生成。发起方必须在重试的 initiation 包中回显这个 cookie。攻击者无法伪造 cookie——即使知道 cookie 的生成算法,没有 cookie secret 也算不出有效的 MAC2。

正常用户只多一个 round trip(第一次 initiation 被拒,收到 cookie reply,第二次 initiation 带上 cookie 成功),攻击者因为没有 cookie secret 无法让服务器做 DH 运算——攻防不对称完全倒向防御方。

cookie.c 的实现只有约 150 行。cookie secret 定期轮换(cookie_checker timer),防止 secret 泄露导致历史 cookie 被回溯利用。

六、三密钥对轮换

每个 wg_peer 维护三个 keypair 槽位:

struct wg_peer {
    struct noise_keypairs {
        struct noise_keypair *current_keypair;   // 当前用于发送的 keypair
        struct noise_keypair *previous_keypair;  // 上一个 keypair(仍可用于接收)
        struct noise_keypair *next_keypair;      // 正在握手中的新 keypair(尚未激活)
    } keypairs;
};

生命周期:

needs rekey → 创建 next_keypair → 握手 → 成功:
  previous_keypair = current_keypair
  current_keypair  = next_keypair
  next_keypair     = NULL

所有 previous_keypair 在 REJECT_AFTER_TIME 到期后密钥材料清零

rekey 的触发条件(send.ckeep_key_fresh()messages.h 常量):

wg set 可配置 persistent-keepalivefwmark;内核没有 rekey-after-bytes 这类按流量阈值 rekey 的选项。

握手成功后,keypair 通过 RCU 指针切换:rcu_assign_pointer() 更新 current_keypair,数据路径用 rcu_dereference() 读取,握手与收发无需额外互斥锁。

WireGuard 不使用 ioctl、sysfs、procfs,也没有自定义 socket 族——它使用 Generic Netlink 作为唯一的配置接口。

Generic Netlink 是 Linux netlink 协议的”多路复用”层——允许多个子系统共享同一个 netlink socket 族。WireGuard 注册的 family name 是 WG_GENL_NAME"wireguard")。

配置通过两个核心命令:

用户态工具 wg(8)(约 2000 行 C)通过这两个命令与内核通信。wg show 的输出(peer 公钥、endpoint、allowed ips、latest handshake、transfer)都是从 WG_CMD_GET_DEVICE 的响应中解析出来的。

这个设计比 IPsec 的 PF_KEY/PF_INET 双 socket 方案干净:只有一个控制通道、一个族名、两个命令——没有历史遗留。

八、局限与不该用的场景

WireGuard 的简洁来自于明确的功能边界。以下场景不适合用 WireGuard:

L2 桥接 / 二层隧道。 WireGuard 是纯 L3(IP)隧道。如果需要二层扩展(跨站点 VLAN、ARP 广播穿透),用 VXLAN/GRE/IPSec。这不是 WireGuard 的缺点——拒绝 L2 是为了保证 cryptokey routing 的正确性(L2 没有 IP 地址来做 AllowedIPs 绑定)。

路由与 AllowedIPs 必须自洽。 WireGuard 会随合法入站包自动学习 peer endpoint,UDP 往返路径不必完全对称。真正的运维陷阱是宿主机路由表与 AllowedIPs 脱节——内层 IP 由 AllowedIPs 决定加密 peer,与系统路由独立配置;三者不一致时会出现”能解密但到不了目的地”的现象。这不是协议禁止非对称路由,而是要求 endpoint、AllowedIPs 与宿主机路由对齐。

动态路由协议集成。 WireGuard 不内置 OSPF/BGP。可以手动在 wg0 接口上跑 FRR/Quagga 做动态路由(WireGuard 接口本身是一个合法的 net_device,可以被路由栈使用),但这不是 WireGuard 提供的功能。OpenBSD 上的 WireGuard 实现和 rdomain(4) 配合能做一些动态路由场景,但同样属于外部编排。

UDP 被封锁的环境。 WireGuard 只用 UDP。在某些企业/国家网络环境下 UDP 被深度包检测阻断。这种情况下可以用 udp2raw(UDP over TCP 伪装)或 phantun(UDP → TCP obfuscator),但引入了额外的用户态组件和延迟。

需要证书体系的合规环境。 某些企业合规要求使用 X.509 证书做身份管理——IT 审计要求看到证书链、CRL 分发点、OCSP 响应。WireGuard 的公钥不具备这些——如果需要,考虑 IPSec/IKEv2 + StrongSwan。

后量子安全。 WireGuard 基于 Curve25519(椭圆曲线 DH),不抗 Shor 算法。psk 模式提供一定的过渡抗性,但不是长期方案。NIST 后量子算法标准敲定后,WireGuard 可能需要一个”PQ-WireGuard”变体——目前已有实验性实现(如 wireguard-pq),但尚未进入主线。

九、交叉影响与生态

WireGuard 的设计思想已经开始渗透到其他领域:

十、总结

WireGuard 用极简代码栈做到了完整 VPN 体验,靠的是系统性地拒绝传统复杂度(初合并约 4000 行,当前主线约 5000 行,仍远小于 IPSec 栈):

拒绝的东西 替代方案 换来了什么
IKE 多轮协商 Noise IKpsk2 固定握手 ~5000 行 vs 300,000 行
证书体系(X.509/CRL/OCSP) 公钥 = 身份 不需要 PKI 基础设施
cipher suite negotiation 固定 ChaCha20-Poly1305 + Curve25519 无降级攻击面
SPD/SAD 分层 cryptokey routing 单表 配置和运行时行为一致
动态参数协商 所有参数编译期固定 协议可形式化验证
算法敏捷性 单一密码学原语栈 没有 transition 期的安全漏洞

每一条”拒绝”都缩小了攻击面和代码量,但也都缩小了适用范围。如果你需要 L2 桥接、证书体系或与路由表脱节的复杂拓扑——WireGuard 不适合你,IPSec/IKEv2 是正确答案。但如果你只需要”在两个 IP 端点之间建立一条认证加密隧道”,WireGuard 证明了这件事不需要几十万行代码。

Linus Torvalds 在 2018 年 8 月 netdev 邮件列表中称其为”艺术品”(work of art),相较 OpenVPN 与 IPSec 的复杂度;代码在 2020 年 3 月随 Linux 5.6 合入主线。这句评价的分量在于:在内核领域,说一个东西”简单”通常是贬义(暗示功能不全),但 WireGuard 的简单是高质量的简单——每一行代码都在做正确的事,没有一行是在做”工程上正确但安全上无关”的事。


参考文献

论文与规范

源码

邮件与演讲


上一篇RDMA 与 CXL:两条不同的绕开 CPU 路线 下一篇HTTP/3 实战:从 QUIC 到 H3 的完整请求链路

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2025-07-28 · network

【网络工程】VPN 技术工程对比:IPSec vs WireGuard vs OpenVPN

VPN 是企业网络互联和远程访问的核心技术。本文从 IPSec 的 IKE/ESP/AH 协议栈、WireGuard 的 Cryptokey Routing 设计、OpenVPN 的 TLS 隧道模型,到三者在性能、安全、运维复杂度上的工程对比,系统讲解 VPN 技术的选型与部署实践。

2026-04-22 · network

网络工程索引

汇总本站网络工程系列文章,覆盖分层模型、以太网、IP、TCP、DNS、TLS、HTTP/2/3、CDN、BGP 与故障诊断。

2025-07-30 · network

【网络工程】内核网络参数调优:sysctl 全景与实战

Linux 内核网络参数是系统网络性能的基础旋钮。本文从 /proc/sys/net/ 出发讲收发缓冲区自动调优、TCP backlog、conntrack、SYN flood 防护、TIME_WAIT 管理,并系统化展开多队列网卡 RSS、软件分发 RPS / RFS / XPS 的取舍与调优方法。


By .