Vpn from WireGuard Impl
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 代表拒绝协商, 算法写死, 代码极简; 后者的胜利不是技术天才, 而是工程克制;
论文怎么批判 IPsec
WireGuard 论文 §1 开篇就把 IPsec 当反派来 frame, 批判分成三个角度;
协议复杂性
IPsec 不是一个协议, 而是 ESP, AH, IKEv1, IKEv2, ISAKMP, SAD/SPD, NAT-T, IPComp 七八个 RFC 拼出来的生态, 一个"协议"要 7 个 RFC 才能描述清楚, 设计本身就有问题;
最具体的批判点: 协议被硬拆成用户态 + 内核态两半, IKE daemon (用户态守护进程, 比如 strongSwan 的 charon) 跑密钥协商, 内核里的 XFRM (Linux Transform Layer, 源码 net/xfrm/, transform 的缩写) 跑数据加解密和 SA 管理, 两者通过 netlink 接口同步状态机, 任何一边状态不对整条隧道就废; 这种双状态机互相依赖的设计是 IPsec 运维噩梦的根源, “我的 phase 2 起不来” 几乎是每个网工的童年阴影;
配置复杂性
算法可协商带来组合爆炸, ESP 支持 AES-CBC, AES-GCM, 3DES, ChaCha20, 各种 HMAC, 各种 DH group; 双方必须协商一致, 协商过程本身又是攻击面 (TLS 历史上一半 CVE 来自 downgrade 攻击, IPsec 同理); 即使是有经验的管理员都经常配错, 普通用户基本不可能配对;
直观感受一下 strongSwan 一份典型配置和 WireGuard 的对比, 同样是 site-to-site, 前者要 20 多行 + 证书库 + CA, 后者就这么几行:
1 | [Interface] |
实现脆弱性
复杂必然带 bug, 代码量对比就很说明问题:
| 实现 | 大致代码行数 |
|---|---|
| strongSwan (IKE daemon + 工具链) | ~400,000 |
| Linux XFRM (内核 IPsec) | ~13,000 |
| WireGuard (内核版完整实现) | ~4,000 |
TCB (Trusted Computing Base) 大一两个数量级, CVE 多几个数量级是必然; 2018 年 IKEv1 还在出 Bleichenbacher 攻击 (CVE-2018-5389), 2019 年 strongSwan 有远程 DoS, 2020 年 Linux XFRM 多个堆溢出, 不是实现者不努力, 而是协议本身复杂度太高, 实现层无法保证正确性;
Donenfeld 没说 IPsec 该死, 他说的是 IPsec 不应该被推荐给新场景; IPsec 现在的市场基本盘是企业级 site-to-site VPN, 靠的是厂商生态 (Cisco / Juniper / Fortinet 路由器开箱即用), 网络认证体系 (CCNP / CCIE 写进考纲), 行业合规要求, 以及几百万台已部署设备的沉没成本; 但在 2016 年这个时间点设计新协议, 还沿用 IPsec 那套思路就是抱残守缺;
为什么 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 容易得多);
接下来的笔记会:
- 把 WireGuard 论文 (NDSS 2017) 的核心机制 (Cryptokey Routing, Noise IK 握手, timer state machine) 过一遍;
- 读 wireguard-go 的源码结构, 重点看
device/,tun/,conn/三个目录; - 用 Go 写一个不加密的 toy tun 隧道, 然后逐步加上 ChaCha20-Poly1305, 看看离真实的 wireguard-go 还差什么;
