NNetpath
实验预计 40 分钟

WireGuard:内核里的极简 VPN

不到 4000 行代码、一个 UDP 端口、两个密钥。现代 VPN 的地基几乎都建在它上面。

学完这节你能做到

  • 从零写出一对可用的 wg 配置并解释每一行
  • 说清 AllowedIPs 的双重含义,并据此排查单向不通
  • 算出该给 wg0 设多大的 MTU,并验证

建议先学跳过这几节会看不懂本节的部分推导

设计上做减法

WireGuard 的所有特点都来自一个决定:不给选择

传统 VPN(IPsec / OpenVPN)WireGuard
一堆加密套件,需要协商只有一套:Curve25519 + ChaCha20-Poly1305 + BLAKE2s
复杂的握手与重协商1-RTT 握手(Noise_IK)
几十万行代码约 4000 行,在内核里
TCP 或 UDP,可配只有 UDP
有连接状态、有登录无状态,公钥即身份

代价是不灵活:套件被攻破就得整体升版本,没有算法降级余地。收益是攻击面小到可以审计完, 而且性能能跑到接近线速——它就在内核里,不像 OpenVPN 要在用户态来回拷贝。

还有一个安静的特性:对未通过认证的包一律不回应。所以对外扫端口扫不出 WireGuard, nmap 看到的就是一个没人应答的 UDP 端口。

Cryptokey Routing:公钥就是路由表

这是理解 WireGuard 唯一需要真正想清楚的概念。它没有"用户",只有一张对应表:

公钥 A  ←→  AllowedIPs 10.0.0.2/32, 192.168.50.0/24
公钥 B  ←→  AllowedIPs 10.0.0.3/32

AllowedIPs 有两个作用,方向相反:

方向含义
出方向目的地址落在这个范围里 → 用这个 peer 的公钥加密并发给它
入方向从这个 peer 解密出来的包,源地址必须落在这个范围里,否则丢弃

也就是说它既是路由表又是访问控制表。这一条解释了 90% 的 WireGuard 故障:

×AllowedIPs 不对称 = 单向通

一端写 AllowedIPs = 10.0.0.0/24,另一端只写 AllowedIPs = 10.0.0.2/32, 结果是去得了回不来(或反之),而且没有任何报错——包在入方向校验时被静默丢掉。

排查方法:两端各跑一次 wg show,把 allowed ips 抄下来对照。 再看 transfer 那一行:如果一端只有 sent 在涨、received 是 0,方向就定位到了。

最小可用配置

先生成密钥。私钥永不外传,只交换公钥:

umask 077
wg genkey | tee privatekey | wg pubkey > publickey

服务端 /etc/wireguard/wg0.conf

[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <服务端私钥>

[Peer]
PublicKey = <客户端公钥>
AllowedIPs = 10.0.0.2/32

客户端 /etc/wireguard/wg0.conf

[Interface]
Address = 10.0.0.2/24
PrivateKey = <客户端私钥>

[Peer]
PublicKey = <服务端公钥>
Endpoint = vpn.example.com:51820
AllowedIPs = 10.0.0.0/24
PersistentKeepalive = 25
wg-quick up wg0        # 起
wg show                # 看状态
wg-quick down wg0      # 停
systemctl enable --now wg-quick@wg0    # 开机自启

注意两处不对称:

  • 只有服务端写 ListenPort,客户端不需要(它主动出去,用随机源端口)
  • 只有客户端写 Endpoint,服务端不需要——它靠收到的认证包自动学习对方地址。 这就是 WireGuard 的漫游能力:客户端从 WiFi 切到 4G,IP 变了,隧道不断。

PersistentKeepalive:NAT 后面的那一侧要配

WireGuard 默认完全静默:没有流量时一个包都不发。这在 NAT 后面会出问题—— NAT 表项超时后(通常 30 秒到几分钟),外面就再也进不来了。

所以规则是:在 NAT 后面、且需要被对方主动连接的那一端,配 PersistentKeepalive = 25。 25 秒是个经验值,比多数 NAT 的 UDP 超时短。

服务端在公网上有固定地址,不需要配。

MTU:必须算的一笔账

WireGuard 把原包塞进 UDP,外层开销是固定的:

IPv4 外层:20(IP)+ 8(UDP)+ 16(WG 头)+ 16(Poly1305 认证标签)= 60 字节
IPv6 外层:40(IP)+ 8(UDP)+ 16(WG 头)+ 16(认证标签)      = 80 字节

所以物理 MTU 1500 时:

  • 只走 IPv4:1500 − 60 = 1440
  • 可能走 IPv6:1500 − 80 = 1420wg-quick 的默认值就是按这个算的

1420 是个保守但安全的选择。如果外层路径本身 MTU 更小(PPPoE 拨号常见 1492),还要再减。

!又是那个「小包通、大包卡」

MTU 配大了的症状和 L1 里 overlay 的 MTU 黑洞一模一样:ping 通、SSH 能登、 curl 小接口正常,一传大文件或返回大 JSON 就卡住

验证方法同样是发不允许分片的大包:

# 隧道内测:1420 − 28 = 1392
ping -M do -s 1392 10.0.0.1

不通就往下调,直到通为止,然后把这个值写进配置的 MTU =

wg-quick 替你做的事

直接用 wg 命令只配隧道本身;wg-quick 是个 shell 脚本,额外做了这些:

  • 创建接口、配地址、设 MTU
  • AllowedIPs 自动装路由 ← 最关键的一步
  • DNS = 时改 resolv.conf(通过 resolvconfsystemd-resolved
  • 执行 PreUp / PostUp / PreDown / PostDown 钩子

所以 AllowedIPs = 0.0.0.0/0 的效果是"所有流量都进隧道"——因为 wg-quick 会装一条 默认路由(用 fwmark + 策略路由实现,避免把发给对端 Endpoint 自己的包也送进隧道, 否则会形成死循环)。

两个常用形态

子网路由:访问对端整个内网

服务端不只做隧道端点,还要转发:

[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <私钥>
PostUp   = sysctl -w net.ipv4.ip_forward=1
PostUp   = iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE
PostDown = iptables -t nat -D POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE

客户端把要访问的内网段加进 AllowedIPs

AllowedIPs = 10.0.0.0/24, 192.168.50.0/24
×隧道通了但访问不了对端内网的其它机器

这时候检查三件事,顺序别乱:

  1. 客户端的 AllowedIPs有没有那个内网段(没有 = 包压根不进隧道)
  2. 服务端 net.ipv4.ip_forward 是不是 1(没开 = 收到了不转发)
  3. 有没有 MASQUERADE(没有 = 内网机器收到源地址是 10.0.0.x 的包,不知道往哪回)

三件事各自都会造成"隧道显示正常但业务不通",而 wg show 全都看不出来。

全流量出口

客户端 AllowedIPs = 0.0.0.0/0, ::/0,服务端配置同上。 此时所有出网流量都从服务端出去,别忘了给客户端配 DNS =,否则 DNS 还在用本地的。

走一遍完整路径

路径推演一个包经过的每一跳

应用完全不知道自己在用 VPN:它只是往一张普通网卡上发包。

6延迟量级 接近物理 RTT
这条路径要记住的一句话

WireGuard 的路由决策靠 AllowedIPs(Cryptokey Routing):它既是「发给谁」也是「允许谁发来」,配错就静默丢包。

排障:三个地方看完就够

wg show
interface: wg0
  public key: xxxxx
  private key: (hidden)
  listening port: 51820

peer: yyyyy
  endpoint: 203.0.113.5:51820
  allowed ips: 10.0.0.2/32
  latest handshake: 43 seconds ago
  transfer: 1.24 GiB received, 3.81 GiB sent
  persistent keepalive: every 25 seconds
看哪一行说明什么
latest handshake没有这一行 = 从没握手成功(网络不通、端口被封、公钥配错)
transfer只有一个方向在涨 = AllowedIPs 不对称或对端没回
allowed ips两端对照,这是最高频的配错项

握手都建立不起来时,按顺序排除:

# 1. UDP 端口到底通不通(TCP 工具在这里没用)
nc -zvu vpn.example.com 51820

# 2. 服务端确认包收到了
tcpdump -i eth0 -nn udp port 51820 -c 10

# 3. 公钥有没有配错(最常见:把私钥当公钥贴上去了)
wg show wg0 public-key
iUDP 被整体阻断时 WireGuard 无解

WireGuard 只跑 UDP,也不会自动降级到 TCP。有些网络(酒店、部分企业出口) 把 UDP 全封了,这时候唯一的办法是在外面再套一层 TCP 隧道 (比如用 gost 的 wss 传输把 UDP 包背过去),或者改用本身就走 TCP 的方案。

Tailscale 解决这个问题的方式是 DERP 中继 —— 下一节讲。

检查点

检查点单选

WireGuard 里 AllowedIPs 的作用是什么?

检查点单选

客户端在 NAT 后面,隧道建好后能主动访问服务端,但服务端过一会儿就无法主动连客户端。缺了什么?

检查点单选

wg-quick 默认把 wg0 的 MTU 设成 1420 而不是 1440,为什么?

这节课的落点

  • 只有一套加密算法、只走 UDP、约 4000 行代码在内核里;未认证的包一律不回应
  • Cryptokey Routing:公钥是身份,AllowedIPs 同时是路由表和入方向 ACL
  • 两端 AllowedIPs 不对称 = 单向通且无报错,这是最高频的故障
  • 只有服务端写 ListenPort,只有客户端写 Endpoint(服务端自动学习,因此支持漫游)
  • NAT 后面需要被主动连接的一侧配 PersistentKeepalive = 25
  • MTU:IPv4 减 60、IPv6 减 80,wg-quick 默认 1420;症状同样是"小包通、大包卡"
  • 访问对端内网要三件套:客户端 AllowedIPs 含该网段 + 服务端 ip_forward + MASQUERADE
  • 排障只看三行:latest handshaketransferallowed ips
  • UDP 被整体封锁时 WireGuard 无解,需要外套 TCP 隧道

延伸资料