主题
为什么值得学 VPP 的 NAT
先说 VPP 是个什么东西
VPP(Vector Packet Processing)是一个跑在用户态的软件转发器。它不走 Linux 内核的网络栈, 而是自己把网卡接过来、自己收包、自己查表、自己发包。内核那一整套 netfilter / conntrack 在它眼里根本不存在。
它最核心的一个设计,藏在名字里的 "Vector":一次处理一批包,而不是一个一个处理。
传统内核转发是"标量"的 —— 一个包进来,走完协议栈全程(解以太网头、查路由、做 NAT、算校验和、发出去), 然后才轮到下一个包:
text
标量处理(内核)
包1 ─→ [解链路层] [查路由] [做NAT] [重写] [发送]
包2 ─→ [解链路层] [查路由] [做NAT] [重写] [发送]
包3 ─→ [解链路层] [查路由] [做NAT] [重写] [发送]
↑ 每个包都要把这一整串代码重新在 CPU 上"跑热"一遍VPP 反过来:先攒一批包(最多 256 个,这批包就叫一个 vector), 然后让这批包整体通过第一个处理阶段,再整体通过第二个阶段:
text
向量处理(VPP)
[包1 包2 包3 … 包N] ─→ [解链路层] ─→ [查路由] ─→ [做NAT] ─→ [重写] ─→ [发送]
↑ 这段代码只需要被"跑热"一次,
然后连续作用在 N 个包上这个差别为什么重要?因为现代 CPU 上,取指令的开销经常比执行指令本身更贵。 处理一个包要走过的代码路径很长,指令缓存(i-cache)装不下全程。标量处理时每个包都在 反复触发 i-cache miss;向量处理时,一段代码加载进 i-cache 后连续服务几十上百个包, 这个加载成本被摊薄了。同时 VPP 还能在处理当前包时预取下一个包的数据到 d-cache 里。
那些"阶段"是什么:graph node
上面画的 [解链路层] [查路由] [做NAT] 每一个方块,在 VPP 里都是一个 graph node(图节点)。 整个转发逻辑就是一张有向图,包在节点之间流动,每个节点决定把包交给下一个哪个节点。
NAT 就是插在这张图里的几个节点。等到第 1 章你会亲眼看到一个包在图上走过的完整路径 —— VPP 有个 show trace 命令,会把每个包经过的每个节点、以及节点内部发生了什么, 逐行打印出来。这是学 NAT 时一个近乎"作弊"的观察工具:
text
→ af-packet-input 收包
→ ethernet-input 解以太网头
→ ip4-input 解 IP 头
→ nat-pre-in2out 判断这个包要不要做 NAT
→ nat44-ed-in2out 查会话表
→ nat44-ed-in2out-slowpath 没查到,新建一条会话并完成转换
→ ip4-lookup 用改过的地址重新查路由
→ ip4-rewrite 改写链路层头
→ host-eth2-output 发出去内核的 NAT 你很难得到这样一份逐跳清单。这是选 VPP 作为学习载体的第一个理由。
为什么用 NAT 作为切入点
VPP 能做的事很多(路由、交换、隧道、ACL、QoS……),单挑 NAT 来讲,是因为 NAT 有三个特别适合学习的性质:
它是有状态的。 路由是无状态的查表,讲完就讲完了。NAT 要维护会话表、要处理会话建立和老化、 要在多核之间分配状态 —— 这些是真实网络设备里最难也最有价值的部分。搞懂 NAT 的状态管理, 再看防火墙、负载均衡、隧道封装,会顺很多。
它的效果肉眼可见。 你不需要专门的测试仪。让内网主机连一下外网服务器, 问服务器"你看到我的地址是什么",答案立刻告诉你 NAT 有没有生效、改成了什么。 本教程的实验环境里就跑着这么一个服务。
它的每个变体都对应一个真实痛点。 端点无关还是端点相关,直接决定了用户的 P2P 应用能不能打洞; 确定性 NAT 存在的唯一理由是运营商要能事后追溯"某个时刻某个公网端口是谁在用"; NAT64 存在是因为 IPv6 已经推了二十年但 IPv4-only 的服务还活着。学 NAT 的过程里, 你会顺带理解一大片网络工程的现实约束。
和 iptables 的对照
如果你熟悉 iptables -t nat -A POSTROUTING -j MASQUERADE,下面这张表能帮你快速换轨:
| Linux netfilter | VPP NAT | |
|---|---|---|
| 状态表 | nf_conntrack,全局共享 | NAT 插件自己的会话表,按 worker 线程分片 |
| 配置方式 | 规则链,按顺序匹配 | 接口打标签(in / out)+ 地址池 + 映射表 |
| "内网/外网"的表达 | 靠规则里的 -i / -o 和地址匹配 | 显式声明 set interface nat44 in X out Y |
| 出方向伪装 | -j MASQUERADE | nat44 add interface address <出接口> |
| 端口转发 | -j DNAT --to-destination | nat44 add static mapping ... external ... |
| 可观察性 | conntrack -L,看不到逐跳 | show nat44 sessions + show trace 逐节点 |
| 端点相关性 | 由内核实现决定,不可选 | 两个插件可选:nat44-ed / nat44-ei |
最需要转过来的观念是最后一行和第三行。netfilter 里"哪些流量要做 NAT"是靠规则匹配表达的; VPP 里则是先给接口贴上"内侧/外侧"的标签,NAT 节点根据包从哪个接口进来,决定走 in2out 还是 out2in 分支。 这个模型更死板,但也更好推理 —— 不会出现"两条规则顺序写反了导致行为诡异"这类问题。
VPP 26.06 里都有哪些"NAT"
一个容易困惑的地方:VPP 里叫 NAT 的插件不止一个,而且名字很像。先建立个总览,不必现在就记住:
| 插件 | 全称 | 干什么用 | 本书章节 |
|---|---|---|---|
nat44-ed | Endpoint-Dependent NAT44 | 主力。会话按"内网端 + 外网端"双向键索引,行为等价于对称型 NAT | 第 2 章 |
nat44-ei | Endpoint-Independent NAT44 | 会话只按内网端索引,行为接近锥型 NAT,对 P2P 友好 | 第 3 章 |
det44 | Deterministic NAT (CGN) | 按算法把内网地址映射到固定的公网端口块,无需查表即可追溯 | 第 4 章 |
nat64 | NAT64 | 让纯 IPv6 客户端访问 IPv4 服务 | 第 5 章 |
nat66 | NAT66 | IPv6 到 IPv6 的地址转换 | 第 5 章 |
pnat | Policy 1:1 NAT | 按策略做无状态的 1:1 字段改写 | 第 6 章 |
cnat | CNat Translate | 带负载均衡的翻译,偏向云原生场景 | 第 6 章 |
名字里的 ed / ei 很容易看漏
nat44-ed 和 nat44-ei 是两个互斥的插件,配置命令也不一样 (nat44 add address vs nat44 ei add address)。同时启用会打架。 教程里每个实验步骤开头都会先把所有 NAT 插件关掉再启用需要的那个,就是为了避免这类混乱。
本书会带你走到哪
读完并且做完实验之后,你应该能:
- 看着一条
show nat44 sessions的输出,说清这条会话是谁建的、来回两个方向各自怎么改写、什么时候会老化 - 拿到"某个用户反馈他的应用连不上"这类模糊报障,知道该看哪几个计数器、该抓哪一段 trace
- 说清端点无关和端点相关的区别,以及为什么这个区别决定了 P2P 能不能打洞
- 根据场景选对插件:家用网关式的出方向伪装、企业的端口转发、运营商的可追溯 CGN、IPv6 过渡
- 知道本实验环境的性能数字为什么不能当生产参考,以及生产环境该怎么测
下一步
先把环境搭起来。整个过程是一条命令,但有几个不那么显然的坑(VPP 容器的启动时序、 AF_PACKET 的两个版本、polling 的 CPU 代价),下一篇会讲清楚为什么要那样做。
如果你更想先看到效果,可以直接跳到五分钟跑通第一个 NAT。