1. 简介

在今天这个云计算、远程办公和物联网设备无处不在的分布式时代里,异地组网需求变的越来越多,实现异地组网的工具也凸显其重要性。WireGuard 就是当前最合适的工具之一。本篇文章将会深入讲解 WireGuard 原理、快速配置和进阶配置方案,后面如果有机会也会再介绍基于 WireGuard 实现的一些其他工具(Tailscale 之流),不过对于我个人来说我使用 wg 其实是用来为3节点的 k8s 集群做组网的,所以我个人还是更加偏向于原生的 wg 使用而非其他工具。

2. WireGuard 的哲学和机制

2.1 设计哲学

WireGuard 不提供加密算法的选择,而是直接内建了一套被认为是当前最安全且高效的密码学原语组合。

  • 密钥交换 (ECDH): Curve25519,现代椭圆曲线算法的最佳标准之一
  • 对称加密: ChaCha20,在缺乏 AES 硬件加速的 CPU(如 ARM)上性能卓越,同时具备极高的安全性
  • 消息认证 (MAC): Poly1305,与 ChaCha20构成 AEAD(认证加密),确保数据完整性和真实性
  • 哈希: BLAKE2s,性能和安全性均超越 SHA-2/SHA-3
  • 哈希表键: SipHash24,有效抵御哈希碰撞攻击 (Hash-DoS)
  • 握手协议: 基于Noise Protocol Framework (Noise_IK),提供前向保密、身份验证和抗重放攻击能力

这种设计的直接好处是消除了因配置错误导致的安全漏洞,极大地降低了部署和审计的复杂性。

2.2 核心机制

传统的 VPN 依赖复杂的策略或 ACL 来决定哪些流量进入隧道,而 WireGuard 则采用一种极为优雅的机制,即: 通过将对等(Peer)的公钥与其允许的IP地址列表进行强绑定,构建出一张“密码学路由表”。

当一个数据包到达 wg 接口时:

  1. 内核查找该数据包的目标 IP 地址属于哪个 Peer 的 AllowedIPs 范围
  2. 一旦匹配,内核就使用与该 Peer 公钥关联的对称会话密钥对数据包进行加密3. 反之,当收到一个加密数据包时,内核通过其身份信息(一个静态的 key index)找到对应的解密密钥,解密后验证其源 IP 是否在该 Peer 的 AllowedIPs 列表中。如果不在,数据包将被直接丢弃。

AllowedIPs 因此扮演了路由表和防火墙ACL的双重角色,机制极其简单高效!

3. WireGuard 流量路径


3.1 WireGuard 拓扑

WireGuard 的核心设计: 所有节点皆为 Peer。这赋予了其无与伦比的灵活性,使其能够通过简单的配置文件,构建出多种网络拓扑以适应不同的业务场景。本节具体讲解 WireGuard 组网拓扑的四个场景。

3.1.1 点对点

这是最基础的拓扑,也是所有复杂拓扑的基础。它仅连接两个节点,创建一个简单、安全的加密通道。

  • 结构: [节点 A] <–> [节点 B]
  • 配置特点:
    • 节点 A 的配置文件中只有一个 [Peer] 区块,指向节点 B
    • 节点 B 的配置文件中也只有一个 [Peer] 区块,指向节点 A
  • 适用场景:
    • 安全地连接两台重要的服务器(例如,应用服务器与数据库服务器)
    • 为单个开发人员提供对其远程开发环境的专线访问

3.1.2 中心辐射型/星型

这是最常见的一种拓扑,一个中心节点作为网络的枢纽,所有其他分支节点都只与中心节点连接

  • 结构: 中心节点连接到其他所有节点,其他节点之间没有连接
  • 配置特点:
    • 中心节点: 配置文件中有多个 [Peer] 区块,分别对应每一个分支节点。通常拥有固定的公网 IP 并监听端口
    • 分支节点: 配置文件中只有一个 [Peer] 区块,指向中心节点。通常 Endpoint 指向中心节点的公网地址
  • 流量路径: 任何两个分支节点之间的通信,都必须经过中心节点进行中转。例如,A 要访问 B,流量路径是 A -> 中心节点 -> B
  • 优势:
    • 配置简单: 新增节点只需在中心节点上添加一个 Peer,并在新节点上配置指向中心节点即可
    • 集中管理与审计: 所有流量都经过中心节点,便于统一管理安全策略、进行流量监控和日志审计
  • 劣势:
    • 单点故障: 中心节点一旦宕机,整个网络瘫痪
    • 性能瓶颈: 所有分支间的流量都汇集于中心,可能造成中心节点的带宽和 CPU 瓶颈
    • 延迟增加: 分支间的通信需要绕行中心节点,延迟较高

3.1.3 全连接网状

网络中的每一个节点都与其他所有节点直接建立连接

  • 结构: 每个节点都像星型拓扑中的中心节点,与其他所有节点互为 Peer
  • 配置特点:
    • 一个包含 N 个节点的网络,每个节点的配置文件中都必须有 N-1Peer
  • 流量路径: 任何两个节点之间都存在直接路径
  • 优势:
    • 高可用性: 任意一个节点的故障不会影响其他节点之间的通信
    • 最低延迟: 节点间通信无需中转,性能最优
  • 劣势:
    • 配置复杂: 配置量随着节点数量的增加呈指数级增长。对于多节点的组网,手动配置令人绝望,必须依赖自动化脚本或上层管理平台(如 Netmaker 等)。

3.1.4 部分连接网状

这是一种介于星型和全连接网状之间的混合拓扑。网络中的关键节点之间相互直连,而非关键节点则可能只连接到部分核心节点。

  • 结构: 按需连接,形成一个不规则的网状
  • 配置特点: 每个节点的 Peer 数量根据其在网络中的角色和需求而定
  • 适用场景:
    • 在多集群场景下,让各个集群的核心网关互相直连,而每个集群内部的服务器则与本地网关连接
    • 性能敏感的应用服务器之间建立直接连接,普通服务器则通过一个或多个集中网关通信

3.1.5 简单总结

拓扑类型配置复杂度可靠性性能/延迟适用场景
点对点极低-最高两个节点间的安全通道
星型低 (单点故障)差 (需中转)远程办公、分支机构互联
全连接网状极高最高最优少节点、高性能、高可用集群
部分连接网状中等中到高中到优复杂、分层的企业网络

3.1 场景设定

这里以一个具体的案例来说明在 wg 组网环境下,数据包的流量路径。在描述案例之前必须要说明的是:从 WireGuard 的设计、架构、拓扑等层面来看,它是完全没有客户端和服务端之分的,每一个运行WireGuard的节点,无论其配置如何,在技术上都是一个完全对等的 “对等体(Peer)”,它们遵循相同的协议,拥有相同的功能。后文中提到的服务端和客户端都是不准确(严格说就是错误)的描述,用这个说法的原因是仅仅是方便 “角色定位”,请注意这个细节。

前提: Host A 与 Host B 之间已经完成了首次握手,并已协商出会话所需的对称加密密钥

  • Host A:
    • 物理网卡 eth0 IP: 192.168.1.10
    • 虚拟网卡 wg0 IP: 10.0.0.1
    • 动作: curl Host B
  • Host B:
    • 物理网卡 eth0 IP: 192.168.1.20
    • 虚拟网卡 wg0 IP: 10.0.0.2
    • 动作: Nginx 已监听 10.0.0.2:80

3.2 出站流量(Host A)

3.2.1 阶段一

用户空间 -> 内核协议栈 (Host A):

  1. 应用层 (Userspace): 用户在 Host A 上执行命令 curl http://10.0.0.2curl 程序通过 socket()connect()write() 等标准 POSIX 系统调用(syscall),请求操作系统建立一个到 10.0.0.2:80 的 TCP 连接并发送 HTTP GET请求

  2. 内核协议栈 (Kernel Space - TCP/IP Stack):

    • 内核的 TCP 模块接收到请求,开始构建 TCP 段(Segment)。首先是三次握手的 SYN 包,随后是承载 HTTP 数据的 PSH, ACK
    • IP 模块将 TCP 段封装成 IP 数据包(Packet)。此刻,这个 “内部数据包” 的样子是:
      • IP Header: Source IP: 10.0.0.1, Destination IP: 10.0.0.2
      • TCP Header: Source Port: <随机端口>, Destination Port: 80, Payload: "GET / HTTP/1.1..."

3.2.2 阶段二

内核路由 -> WireGuard 加密 (Host A):

  1. 路由决策 (Routing): 内核需要决定从哪个网络接口发送这个 IP 包。它会查询路由表 (ip route show),发现一条类似 10.0.0.0/24 dev wg0 的规则。因此内核决定将此包交给 wg0 虚拟接口处理

  2. WireGuard 核心处理 (wg_xmit):

  • 数据包进入 wg0 接口的发送队列,触发 WireGuard 内核模块的 wg_xmit() 函数
  • 密码学密钥路由 (Cryptokey Routing): 这是 WireGuard 的灵魂。此模块识别到数据包的目标 IP: 10.0.0.2, 接着会遍历其对等(Peer)列表,查找哪个 Peer 的 AllowedIPs 配置包含了 10.0.0.2。最终会找到 Host B 的条目
  • 内核态加密 (Encryption): WireGuard 模块从与 Host B 的会话中取出预先协商好的对称密钥,使用 ChaCha20-Poly1305 算法对 整个内部 IP 数据包 (从 IP 头到 TCP 数据结束)进行加密

3.2.3 阶段三

UDP 封装 -> 物理网络 (Host A):

  1. UDP 封装 (Encapsulation):
  • 加密后的数据块成为一个新的 “外部数据包” 的 Payload
  • WireGuard 模块为其添加一个简短的 WireGuard 头部(包含密钥索引等信息)
  • 内核网络栈为其添加 UDP 头部。源端口由操作系统分配,目标端口是 Host B 配置中指定的 Endpoint 端口 51820 (默认)
  • 最后添加外部 IP 头部。源 IP 是物理网卡 IP 192.168.1.10,目标 IP 是 Host B 的 Endpoint IP 192.168.1.20
  • 此刻即将在物理网络上传输的数据包结构是:
    Outer IP Header: Source IP: 192.168.1.10, Destination IP: 192.168.1.20
    Outer UDP Header: Source Port: <随机>, Destination Port: 51820
    WireGuard Header
    Encrypted Payload: (加密后的 [内部IP包] )
  1. 物理发送: 这个完整的外部 UDP 包根据主路由表,通过物理网卡 eth0 发送出去,并在其外面包上一层以太网帧头

3.3 入站流量(Host B)

3.3.1 阶段一

物理网络 -> WireGuard 解密 (Host B):

  1. 物理接收: Host B 的 eth0 网卡接收到以太网帧,剥离帧头后,将外部 IP 包递交给内核

  2. 内核协议栈 (Kernel Space - IP/UDP Stack): 内核 IP 模块检查 IP 头,发现协议是 UDP。UDP 模块检查目标端口是 51820 (默认),发现 WireGuard 模块已经注册监听此端口。于是,该 UDP 包的 Payload 被 直接递交给 WireGuard 模块处理

  3. WireGuard 核心处理 (wg_packet_receive):

  • WireGuard 模块接收到数据。它读取 WireGuard 头部,识别出这是来自 Peer A 的数据包
  • 内核态解密与验证: 模块使用与 Peer A 关联的对称会话密钥进行解密,并用 Poly1305 验证数据的完整性和真实性
  • 解密成功,内部 IP 数据包被还原

3.3.2 阶段二

ACL 校验 -> 内核协议栈 -> 用户空间 (Host B)

  1. ACL 校验:
  • WireGuard 模块会检查还原后的内部 IP 包的源地址 10.0.0.1
  • 核对 Peer A 的配置,确认 10.0.0.1 是否在其 AllowedIPs (10.0.0.1/32) 范围内
  • 匹配成功,数据包被接受。如果不匹配,数据包将被静默丢弃
  1. 重新注入协议栈:
  • 通过验证的内部 IP 包通过 netif_rx()(或 gro_receive())函数会被注入到 wg0 虚拟接口的接收队列
  • 对于内核的其他部分来说,这个过程是透明的。它看起来就像 wg0 网卡“凭空”收到了一个源地址为 10.0.0.1 的普通 IP包。
  1. 内核协议栈处理:
  • 内核的 IP/TCP 栈开始处理这个“新”收到的 IP 包。它看到目标是 10.0.0.2:80
  • TCP 模块处理 TCP 协议状态机。
  • 内核发现 Nginx 进程正在监听此端口,于是将 HTTP GET 请求数据从内核缓冲区拷贝到 Nginx 进程的用户空间缓冲区,并唤醒 Nginx 工作进程
  1. 应用层: Nginx 收到 HTTP 请求,开始处理并准备 HTTP 响应

3.4 返回流量(Host B)

流量返回的流程路径与上述完全对称,只是源和目的颠倒,这里快速描述一下:

  1. Host B (Nginx): 生成 HTTP 200 OK 响应。
  2. Host B (Kernel): 创建 内部 IP 包: [IP_H(src=10.0.0.2, dst=10.0.0.1)][TCP_H(data=HTTP Response)]
  3. Host B (Routing): 路由决策指向 wg0 接口。
  4. Host B (WireGuard): wg_xmit 触发,根据目标 10.0.0.1 找到 Peer A,加密整个内部 IP 包。
  5. Host B (Encapsulation): 封装成 外部 UDP 包: [IP_H(src=192.168.1.20, dst=192.168.1.10)][UDP_H][...]
  6. Host B (Egress): 通过 eth0 发送。
  7. Host A (Ingress & Decryption): eth0 收到 UDP 包,递交 WireGuard 模块解密,还原出内部 IP 包。
  8. Host A (ACL & Re-injection): 校验源 IP 10.0.0.2 是否在 Peer B 的 AllowedIPs 内,校验通过后,将包注入 wg0
  9. Host A (Kernel & Userspace): 内核 TCP/IP 栈处理响应包,将数据交给 curl 进程。
  10. Host A (curl): curl 收到完整的 HTTP 响应,将其打印到标准输出,流程结束。

4. 适用场景

4.1 核心适用场景

  • 将分散的设备(服务器、电脑、IoT设备)连接成一个逻辑上统一的私有网络
  • 在不可信的物理网络(公共互联网、公有云)之上创建一个可信私有网络平面
  • 对网络访问进行强身份认证(基于密钥),而不是弱认证(基于IP地址)

4.2 不建议使用场景

以下场景虽然都有解决(妥协)方案,但是仍然强烈不建议去使用 WireGuard,没有必要为了使用而使用。

  1. 需要动态路由协议: WireGuard 本身是静态的点对点隧道,不广播路由信息
    • 解决方案: 结合 BGP 守护进程 (如 BIRD, FRR)。让 BGP 在 WireGuard 隧道之上运行,动态交换路由,构建复杂的大规模网络。
  2. 网络环境只允许 TCP: WireGuard 只使用 UDP,在某些严格限制或 UDP 丢包率极高的网络中可能无法工作
    • 解决方案: 使用 OpenVPN over TCP 作为备选,或使用 udp2raw 等工具将 UDP 流量伪装成 TCP
  3. 需要二层(L2)隧道: WireGuard 工作在三层(L3),无法传输 ARP、DHCP 等二层广播流量
    • 解决方案: 使用 VXLAN over WireGuard 或 GRE over WireGuard 的组合,先用 L2协议封装,再用 WireGuard 加密传输

5. 快速配置

WireGuard 各节点配置条目基本都一样,这里仅以两个节点互联(全连接)为例说明。

5.1 安装命令行工具(所有组网节点)

1
2
3
4
5
6
# Ubuntu
apt update && apt install wireguard

# RHEL
dnf install epel-release -y
dnf install wireguard-tools

5.2 生成密钥(所有组网节点)

1
wg genkey | tee privatekey | wg pubkey > publickey

5.3 节点 A

/etc/wireguard/wg0.conf 详解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[Interface]
# 本端私钥
PrivateKey = xxxxxxxxxxxx
# 本端在组网中的IP地址
Address = 10.66.66.1/24
# 监听UDP端口,每个节点都建议配置上
ListenPort = 51820
# 统一设置 MTU
MTU = 1420

[Peer]
# 对端的公钥
PublicKey = xxxxxxxxxxxx
# 对端的公网IP和端口
Endpoint = hostB.ip:51820
# 对端的组网中的IP地址,也可以用掩码写一个网段
AllowedIPs = 10.66.66.0/24
# 保持连接,25秒发一次心跳,最佳建议值
PersistentKeepalive = 25

5.4 节点 B

/etc/wireguard/wg0.conf 和节点 A 配置文件基本一致:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[Interface]
# 本端私钥
PrivateKey = xxxxxxxxxxxxx
# 本端在组网中的IP地址
Address = 10.66.66.2/24
# 监听UDP端口,每个节点都建议配置上
ListenPort = 51820
# 统一设置 MTU
MTU = 1420

[Peer]
# 对端的公钥
PublicKey = xxxxxxxxxxxx
# 对端的公网IP和端口
Endpoint = hostA.ip:51820
# 对端的组网中的IP地址,也可以用掩码写一个网段
AllowedIPs = 10.66.66.0/24
# 保持连接,25秒发一次心跳,最佳建议值
PersistentKeepalive = 25

5.5 启动并验证

1
2
3
4
5
6
7
8
9
# 启动并设置开机自启
sudo wg-quick up wg0
sudo systemctl enable wg-quick@wg0

# 查看状态
sudo wg show
# 验证网络
ping 10.66.66.1
ping 10.66.66.2

6. 注意事项

6.1 MTU/MSS 配置

问题: WireGuard 会增加约60-80字节的头部开销。不调整 MTU,可能会存在大数据包通过隧道时被分片影响性能或者直接被丢弃
解决方案:
1、调整 MTU: 在 wg 接口上设置 MTU = 物理接口MTU - 80。例如,物理接口 eth0 MTU 为1500,则 wg0 接口 MTU 应设为1420
2、MSS 钳制: MTU= MSS + TCP头部 (20字节) + IP头部 (20字节),MSS钳制的核心思想是在TCP三次握手的过程中,修改双方通告的MSS值,强制它们使用一个更小的MSS,从而确保后续生成的TCP数据包加上IP和TCP头部后,不会超过我们设定的隧道MTU。在防火墙 PostUp 规则中添加 MSS 钳制规则

1
2
3
4
5
# wg0.conf
[Interface]
MTU = 1420
PostUp = iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -o %i -j TCPMSS --set-mss 1380
PostDown = iptables -t mangle -D FORWARD -p tcp --tcp-flags SYN,RST SYN -o %i -j TCPMSS --set-mss 1380

6.2 NAT与防火墙穿透

  • ListenPort: 确保服务端防火墙放行该UDP端口
  • PersistentKeepalive: 在客户端或位于NAT后的Peer上设置(如20秒),定期发送“心跳包”,以保持NAT会话和状态防火墙的连接跟踪条目活跃

6.3 密钥安全管理

  • 在任何环境/程序中私钥都是唯一身份凭证,绝不可泄露
  • 建立密钥轮换制度,例如每6-12个月更换一次密钥对

6.4 路由与DNS泄漏

  • 路由黑洞: AllowedIPs 配置必须精确,错误的配置会导致流量无法路由或被丢弃
  • DNS泄漏: 当客户端 AllowedIPs 设为 0.0.0.0/0, ::/0 以接管所有流量时,必须确保客户端的DNS解析器也指向隧道内的DNS服务器(例如,通过 wg-quickDNS 配置项),否则DNS查询将绕过隧道,存在暴露隐私的安全风险

7. 进阶优化

以下优化为可选项,对大部分的普通 VPS/家里云 玩家来说性能提升不大,但是在高负载环境、高性能服务器之间会有明显的性能改善。

7.1 启用多队列 (Multi-Queue)

WireGuard 会为每个队列创建一个独立的加密/解密工作线程,并将其绑定到不同的 CPU 核心上,实现并行处理。 WireGuard 原生支持多队列,可将数据包处理压力分散到多个CPU核心,配置方法如下:

1
2
3
4
# 查看当前接口支持的最大队列数
ethtool -l wg0
# 将wg0的收发队列设置为4个 (假设CPU有4核以上)
sudo ethtool -L wg0 combined 4

7.2 调整内核网络缓冲区

7.2-7.4 这部分是 linux 内核的调优项目不仅仅是针对 WireGuard 的使用。
在高吞吐量场景下, linux 内核默认的 socket 缓冲区可能成为瓶颈,增大 TCP/UDP 的读写缓冲区上限,允许 WireGuard 在处理突发流量时有更多的缓冲空间,减少丢包风险

1
2
3
4
5
# /etc/sysctl.conf
net.core.rmem_max = 20971520 # 20MB
net.core.wmem_max = 20971520
# 生效
sysctl -p

7.3 设置中断亲和性 (IRQ Affinity)

将数据包接收(eth0)和加解密(wg0)分别固定在不同的 CPU 核心上,最大化地利用 CPU 缓存,减少跨核调度开销,避免资源争抢。同时建议将处理网卡中断和处理 WireGuard 加解密任务的CPU核心绑定在同一个NUMA节点上,以避免跨节点内存访问带来的延迟

1
2
3
4
5
6
7
# 1. 查找wg和eth0的中断号(IRQ)
cat /proc/interrupts | grep -E 'wg|eth0'
# 2. 假设eth0的队列中断号为30-33, wg0的为34-37
# 绑定eth0的队列到CPU 0-3
for i in {0..3}; do echo $i > /proc/irq/$((30+i))/smp_affinity_list; done
# 绑定wg0的队列到CPU 4-7
for i in {0..3}; do echo $((4+i)) > /proc/irq/$((34+i))/smp_affinity_list; done

7.4 开启 GRO/GSO (通用接收/发送卸载)

GSO 允许 WireGuard 在加密前将多个小包聚合成一个大包;GRO 则是在物理网卡层面将收到的多个相关小包聚合成大包再交给上层处理。这能极大减少内核处理数据包的次数,降低 CPU 负载

1
2
3
# 一般默认是开启的,使用以下命令可以确认
ethtool -k eth0 | grep 'generic-receive-offload'
ethtool -k wg0 | grep 'generic-send-offload'