VPN 史前简介

VPN 这个名字里的 virtual 来自 1990 年代企业的实际诉求, 当时跨地办公要靠租用专线, 业界想用便宜的公网 Internet 模拟出专线效果, virtual 是说虚拟出一根原本要靠物理拉线才有的专网;

接下来 30 年里, VPN 的主流协议大致经过三代:

  • IPsec (1998, IETF), 工作在 IP 层, 由 ESP (加密 + 认证), AH (只认证), IKE (密钥协商) 三个子协议拼成; 内核态实现, 算法可协商, 规范文档加起来几千页, 配置极其复杂, 但靠厂商生态吃下了企业 site-to-site 市场; Linux 内核里对应的模块叫 XFRM;
  • OpenVPN (2001, James Yonan), 用户态实现, 复用 OpenSSL 和 TLS 做加密认证, 用 tun / tap 接管网络, 跑 UDP 1194 或 TCP 443; 部署只要一个二进制, 跨平台友好, 是 2000s 到 2010s 商业 VPN (NordVPN 等) 的默认协议; 但用户态架构和 TLS overhead 让它性能输给下一代, 吞吐只有 WireGuard 的四分之一左右;
  • WireGuard (2016, Jason Donenfeld), 一个人写出来的, 内核版 C 代码约 4000 行, 算法写死为 ChaCha20-Poly1305 + Curve25519 + BLAKE2s + HKDF, 握手用 Noise 协议的 IK 模式一个 1-RTT 完成, 2020 年进 Linux 5.6 内核主线;

把这三代的关系一句话拎出来: IPsec 代表什么都能配, OpenVPN 代表用 TLS 给 VPN 续命, WireGuard 代表拒绝协商, 算法写死, 代码极简; 后者的胜利不是技术天才, 而是工程克制;

为什么 VPN 走 UDP 而不是 TCP

读 abstract 时会注意到 WireGuard 明确写 “over UDP”, 这不是性能选择, 而是正确性选择;

核心问题是 TCP-over-TCP meltdown: 如果用 OpenVPN over TCP 跑一个 HTTP 下载, 内层应用 TCP-A 和外层隧道 TCP-B 各自有重传和拥塞控制, 一旦外层丢包, 两层 TCP 同时触发重传, 互相打架, 缓冲区堆积, 延迟雪崩; 经典分析见 2001 年 Olaf Titz 的 “Why TCP Over TCP Is A Bad Idea”;

用 UDP 做外层就完全没这个问题, UDP 不重传, 内层 TCP 感知到的网络行为就和直接跑公网一样, 自己的拥塞控制能正常工作; 顺带还有低延迟, NAT 友好, 无队头阻塞等好处;

OpenVPN 保留 TCP 模式只是为了实在没办法时的 fallback (突破只放 443/TCP 的企业防火墙), 性能换隐蔽性; WireGuard 拒绝这种妥协, 只支持 UDP, 要伪装就在外面单独套一层 (Tailscale 的 DERP, 或 Xray, Shadowsocks 这类专门做混淆的代理), 把传输和伪装解耦;

顺便一提, IPsec ESP 走的是 IP 协议号 50, 既不是 TCP 也不是 UDP, 这是它极难穿 NAT 的根本原因 (NAT 设备认 TCP/UDP 端口, 不认 ESP), 后来打补丁 NAT-T 才把 ESP 再包一层 UDP 4500; WireGuard 选 UDP 单端口正是吸取了这个教训;

为什么 Go 能写一个完整的 VPN

WireGuard 是个协议, 它有多个独立实现, Linux 走内核态, 其他平台几乎都靠 Donenfeld 自己写的 wireguard-go, 这是个约 4000 行 Go 的用户态实现, 走的是 /dev/net/tun 这条经典通路 (用户态进程通过虚拟网卡读 IP 包, 加密后用 UDP 发出去, 对端解密写回 tun); macOS, Windows, BSD, Android 客户端的统一底座都是它, Tailscale 这种跨平台 VPN 产品本质上就是 wireguard-go 加一层控制平面;

用户态加 Go runtime 调度让 wireguard-go 性能只有内核版的 1/5 到 1/10, 但跨平台收益换回来了, 也让它成为学习整个 WireGuard 协议的最佳读物 (比啃内核 C 容易得多);

接下来的笔记会:

  1. 把 WireGuard 论文 (NDSS 2017) 的核心机制 (Cryptokey Routing, Noise IK 握手, timer state machine) 过一遍;
  2. 读 wireguard-go 的源码结构, 重点看 device/, tun/, conn/ 三个目录;
  3. 用 Go 写一个不加密的 toy tun 隧道, 然后逐步加上 ChaCha20-Poly1305, 看看离真实的 wireguard-go 还差什么;