Skip to content

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]

matchrewrite 后面可以跟这些字段的任意组合:

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 10
bash
# 出方向:把 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 interfaces
text
[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: DA

mask 那一行说明这条规则匹配哪些字段

mask含义
SASource Address,只匹配源地址
DADestination Address
SP / DPSource Port / Destination Port
DA DP目的地址 + 目的端口一起匹配

匹配得越窄,规则越精确。{10.10.0.11:*,*,*:*} 里的 * 表示这个字段不参与匹配。

验证

跑自检

bash
./labctl verify base 10
text
── 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 in
text
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 in
text
  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 del

VPP 当场崩溃:

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 而生的翻译器。

实验环境基于 netlab + containerlab + VPP 26.06