主题
PNAT:无状态的策略改写
前面五章的所有插件都是有状态的:首包建会话,后续包查表,会话有超时。 这一章的两个插件走的是另一条路。
PNAT(Policy NAT)最简单:它就是一组改写规则,没有会话,没有状态。
它是什么
text
有状态 NAT(nat44 / nat64 / det44)
┌────────────────────────────────────────────┐
│ 首包 → 挑端口 → 建会话 → 改写 │
│ 后续包 → 查会话 → 按记录的规则改写 │
│ 超时 → 回收会话 │
└────────────────────────────────────────────┘
PNAT
┌────────────────────────────────────────────┐
│ 每个包 → 匹配规则 → 改写 → 走 │
│ 设备上不留任何痕迹 │
└────────────────────────────────────────────┘show pnat 底下只有两条命令,而且没有 sessions:
bash
./labctl vppctl base show pnat ?text
show pnat interfaces show pnat interfaces
show pnat translations show pnat translations没有会话表,就没有"会话表满"这类故障,状态开销为零。 代价是来回两个方向都要你自己配。
命令藏在 set 底下
第一个坑是找不到命令。pnat ? 会直接报错:
bash
./labctl vppctl base pnat ?text
unknown input `pnat ?'因为配置命令在 set 底下:
bash
set pnat translation interface <名字> match <五元组> rewrite <元组> {in|out} [del]match 和 rewrite 后面可以跟这些字段的任意组合:
text
match src <addr> dst <addr> proto tcp|udp sport <n> dport <n>
rewrite src <addr> dst <addr> sport <n> dport <n>配置
应用配置
bash
./labctl reset base
./labctl apply base 10bash
# 出方向:把 h1 的源地址换成 203.0.113.150
vppctl set pnat translation interface host-eth1 \
match src 10.10.0.11 \
rewrite src 203.0.113.150 in
# 回方向:把目的地址还原成 h1
vppctl set pnat translation interface host-eth2 \
match dst 203.0.113.150 \
rewrite dst 10.10.0.11 in两条规则,两个方向,缺一不可。 有状态 NAT 建会话时会自动把两个方向都算好 (回顾会话表那一篇的 i2o / o2i),PNAT 不会。
外部地址仍然需要外部路由
和 nat66 一样:VPP 不认领 PNAT 里写的外部地址, 不会代答 ARP。外网侧的邻居解析会停在 INCOMPLETE。
bash
# 实验里手工加一条
ip route add 203.0.113.150/32 via 203.0.113.1这一点和 nat44 地址池截然相反 —— nat44 会把池地址装进 FIB 当作本地地址,零配置可达。
看规则
bash
./labctl vppctl base show pnat translations
./labctl vppctl base show pnat interfacestext
[0] match: {10.10.0.11:*,*,*:*} rewrite: {203.0.113.150:*,*:*}
[1] match: {*:*,*,203.0.113.150:*} rewrite: {*:*,10.10.0.11:*}
sw_if_index: 1 input mask: SA
sw_if_index: 2 input mask: DAmask 那一行说明这条规则匹配哪些字段:
| mask | 含义 |
|---|---|
SA | Source Address,只匹配源地址 |
DA | Destination Address |
SP / DP | Source Port / Destination Port |
DA DP | 目的地址 + 目的端口一起匹配 |
匹配得越窄,规则越精确。{10.10.0.11:*,*,*:*} 里的 * 表示这个字段不参与匹配。
验证
跑自检
bash
./labctl verify base 10text
── 2) 出方向:s1 看到的源地址 ────────────────
s1 看到:203.0.113.150:40249
✓ 源地址已被改写成 203.0.113.150
注意端口没变 —— PNAT 只改你让它改的字段,不做端口分配端口没变这一点值得注意。nat44 会按需重新分配端口(端口分配那一篇), PNAT 不会 —— 你没让它改端口,它就不碰。这也意味着 PNAT 做不了多对一: 两台内网主机用同一个源端口时,改写后会撞车,而 PNAT 没有会话表来区分它们。
删掉回程规则会怎样
自检里做了这个对照:
text
── 4) 把回程规则删掉,看会发生什么 ─────────────
已删除回程规则,只剩出方向
✓ 立刻单向不通 —— 有状态 NAT 会自动处理回程,PNAT 不会这是无状态设计最直接的后果。任何一个方向漏配,就是单向不通。
硬限制:每个接口每个方向只能挂一条规则
这是 PNAT 最大的能力边界
在同一个接口的同一个方向上加第二条规则会被拒绝:
bash
vppctl set pnat translation interface host-eth1 \
match dst 203.0.113.11 proto tcp dport 8888 \
rewrite dst 203.0.113.12 dport 9000 intext
set pnat translation: Attaching binding to interface failed -2换成 out 方向就可以 —— 说明每个接口有两个独立槽位:
text
sw_if_index: 1 input mask: SA output mask: DA DP
^^^^^ in 槽位 ^^^^^^ out 槽位一个接口总共只能有两条 PNAT 规则。
所以 PNAT 不是通用的策略引擎,没法在一个接口上堆几十条匹配规则。 想做复杂策略,只能拆到多个接口,或者用 ACL / 策略路由先分流再交给 PNAT。
五元组重定向
在槽位允许的前提下,PNAT 能做一些 nat44 静态映射做不到的事 —— 比如按目的地址+端口把流量导到另一台服务器的另一个端口:
bash
# 去程:访问 s1:8888 的流量导到 s2:9000
vppctl set pnat translation interface host-eth1 \
match dst 203.0.113.11 proto tcp dport 8888 \
rewrite dst 203.0.113.12 dport 9000 in
# 回程:把 s2:9000 的应答伪装成 s1:8888
vppctl set pnat translation interface host-eth2 \
match src 203.0.113.12 proto tcp sport 9000 \
rewrite src 203.0.113.11 sport 8888 intext
h1 访问 203.0.113.11:8888 的回话:[s2 tcp/9000] 我看到你的地址是 10.10.0.11:45807
✓ 流量被导到了 s2 —— 目的地址和端口都被改写了注意 s2 看到的源地址是 10.10.0.11(内网真实地址)—— PNAT 只改了目的, 源地址原封不动。所以 s2 必须有回内网的路由。
回程规则为什么必须有
h1 是发往 203.0.113.11:8888 的,如果回包直接从 203.0.113.12:9000 过来, h1 的 TCP 栈会认为这是一个陌生连接的包,直接丢弃或回 RST。
回程规则把源地址端口伪装回 h1 期待的样子,连接才能成立。 这和第 2 章 twice-NAT 那篇里发夹弯失败的原因是同一个 —— 只不过那里是 VPP 自己没还原源端口,这里是要你手工还原。
一个会打崩 VPP 的操作
删除不存在的 PNAT 规则 → VPP 段错误崩溃
这个坑非常容易踩,因为它恰好命中"写幂等清理脚本"这个最自然的习惯。
在一台全新的 VPP 上执行:
bash
vppctl set pnat translation interface host-eth1 \
match src 10.10.0.11 rewrite src 203.0.113.150 in delVPP 当场崩溃:
text
received signal SIGSEGV, PC 0x..., faulting address 0x...
#0 clib_bihash_search_16_8 + 0x46
from /lib/x86_64-linux-gnu/libvnet.so.26.06
#1 pnat_output_node_fn_x86_64_v4 + 0xe2a
from /usr/lib/x86_64-linux-gnu/vpp_plugins/pnat_plugin.so
#3 vlib_cli_input + 0xd2b稳定复现,不需要有任何流量。
更麻烦的是 PNAT 没有"清空全部"的命令。 想写一个"先清干净再配置"的脚本, 唯一安全的做法是:
text
1. 先 show pnat translations 看有没有规则
2. 只删确认存在的规则
3. 有残留又不确定是什么,就重建 VPP本教程的 apply.sh 就是这么做的 —— 它调用一个 require_clean_pnat 检查规则数是否为 0,不为 0 就中止并提示重建,绝不盲删。
崩了怎么恢复
bash
./labctl down base && ./labctl up base什么时候用 PNAT
text
✓ 适合 ✗ 不适合
────────────────────────────────── ────────────────────────────────
固定的 1:1 地址改写 多对一(需要端口复用)
规则数量极少(每接口最多 2 条) 复杂策略(规则一多就放不下)
不希望有任何状态开销 需要动态会话跟踪
已知且稳定的流量重定向 需要自动处理回程和其他插件横向比一比:
text
┌────────────────┬──────────┬──────────┬────────────┬──────────────┐
│ │ 有状态? │ 端口复用 │ 回程处理 │ 规则数量 │
├────────────────┼──────────┼──────────┼────────────┼──────────────┤
│ nat44 静态映射 │ 是 │ 支持 │ 自动 │ 无实际限制 │
│ det44 │ 半 │ 算法分配 │ 自动 │ 前缀级 │
│ nat66 │ 否 │ 不支持 │ 需手工配 │ 无实际限制 │
│ pnat │ 否 │ 不支持 │ 需手工配 │ **每接口 2 条** │
└────────────────┴──────────┴──────────┴────────────┴──────────────┘看完这张表就明白:大多数场景 nat44 的静态映射更合适。 PNAT 的价值在于零状态和精确的字段级控制, 适合那种"就是要把这一条特定的流改写成那样"的场景。
小结
| 定位 | 无状态的策略改写,没有会话表 |
| 命令位置 | set pnat translation ...(不是 pnat ...) |
| 匹配能力 | src/dst 地址 + 协议 + sport/dport 的任意组合 |
| 硬限制 | 每个接口每个方向只能一条规则,超了报 failed -2 |
| 回程 | 必须手工配,漏了就单向不通 |
| 外部地址 | VPP 不认领,需要外部路由 |
| 致命坑 | 删除不存在的规则会让 VPP 崩溃,且没有 clear all |
下一篇:CNAT —— 为 Kubernetes 而生的翻译器。