主题
twice-NAT:源和目的一起改
静态映射那一篇强调过一个优点:端口转发只改目的, 源地址原样保留,所以内网服务器能拿到真实客户端 IP。
这一篇讲的是那个优点变成致命缺陷的情况。
问题:内网服务器不把回包交给 NAT
端口转发能成立,隐含着一个前提:内网服务器的回包必须经过同一台 NAT 设备。
如果服务器的默认路由不指向这台 NAT,回包会按它自己的路由走掉:
这在真实网络里非常常见:
- 服务器所在的网段有自己的网关,NAT 设备只是旁挂的
- 多出口环境,服务器的默认路由指向另一条链路
- 服务器有多块网卡,回包从另一块出去了
统称非对称路由。
解法:把源地址也改成 NAT 自己的
既然服务器是按"回给谁"来选路的,那就让它以为客户端就在 NAT 这边。
一个包上做了两次地址翻译 —— 这就是 "twice" 的字面意思。
配置
应用配置
bash
./labctl reset base
./labctl apply base 07两步:
bash
# 1. 建一个 twice-NAT 专用地址池(注意 twice-nat 关键字)
vppctl nat44 add address 203.0.113.200 twice-nat
# 2. 静态映射加 twice-nat 修饰
vppctl nat44 add static mapping tcp \
local 10.10.0.12 9000 external 203.0.113.1 8080 twice-nattwice-NAT 用的是独立地址池
show nat44 addresses 会把两个池分开列:
text
NAT44 pool addresses:
203.0.113.1 ← 普通出方向用
tenant VRF independent
NAT44 twice-nat pool addresses:
203.0.113.200 ← twice-NAT 改写源地址时用
tenant VRF independent分开是有道理的:普通池的地址会被大量出方向会话占用端口, twice-NAT 需要稳定可控的地址,混在一起容易互相挤占。
验证
跑自检
bash
./labctl verify base 07自检把普通静态映射和 twice-NAT 并排跑一遍。
text
── 1) 对照组:普通静态映射 ────────────────────
h2 看到的源地址: 203.0.113.11:37827
✓ 是 s1 的真实地址
── 2) 换成 twice-NAT ─────────────────────────
h2 看到的源地址: 203.0.113.200:50291
✓ 变成了 twice-nat 池地址 203.0.113.200text
┌──────────────┬────────────────────┬──────────────────────┐
│ 配置 │ 内网服务器看到的源 │ 代价 │
├──────────────┼────────────────────┼──────────────────────┤
│ 普通静态映射 │ 203.0.113.11:37827 │ 服务器必须路由回 NAT │
│ twice-NAT │ 203.0.113.200:50291│ 丢失真实客户端 IP │
└──────────────┴────────────────────┴──────────────────────┘会话表:唯一会出现两个外网地址的地方
bash
./labctl vppctl base show nat44 sessionstext
i2o 10.10.0.12 proto TCP port 9000 fib 0
o2i 203.0.113.1 proto TCP port 8080 fib 0
external host o2i 203.0.113.11:44321 i2o 203.0.113.200:50291
───────────────────── ─────────────────────
真实客户端 替身地址
i2o flow: match: saddr 10.10.0.12 sport 9000 daddr 203.0.113.200 dport 50291
rewrite: saddr 203.0.113.1 sport 8080 daddr 203.0.113.11 dport 44321
o2i flow: match: saddr 203.0.113.11 sport 44321 daddr 203.0.113.1 dport 8080
rewrite: saddr 203.0.113.200 sport 50291 daddr 10.10.0.12 dport 9000
────────────────── 源也改 ───────────────── 目的也改
static translation关键在 o2i flow 的 rewrite:同时含 saddr 和 daddr。 普通静态映射的 o2i rewrite 里只有 daddr —— 这一行就是区分两者最快的方式。
external host 那一行罕见地列了两个地址:真实客户端和它的替身。 排障时想知道"某条会话背后的真实客户端是谁",看这里。
代价:真实客户端 IP 没了
这是必须接受的交换。内网服务器的访问日志里,所有客户端都变成了 203.0.113.200 这一个地址。受影响的包括:
- 访问日志与审计
- 按源 IP 的限流、风控、封禁
- 地域分析
- 基于 IP 的白名单
补救办法是应用层携带:HTTP 用 X-Forwarded-For, TCP 层可以用 PROXY protocol —— 但这两个都需要后端应用配合改造, 不是网络层能单方面解决的。
先想想能不能不用 twice-NAT
twice-NAT 是给非对称路由擦屁股的。如果你能改路由,优先改路由:
text
能不能给内网服务器加一条明细路由,让去往外网的回包走 NAT?
ip route add 203.0.113.0/24 via <NAT的内网地址>
能不能把服务器的默认路由改成指向 NAT?
能不能用策略路由,按源地址把回包引到 NAT?这三条任何一条能做到,都比 twice-NAT 好 —— 因为能保住真实客户端 IP。 只有在路由确实动不了(跨部门、跨厂商、云上托管网络)时,才上 twice-NAT。
它治不了发夹弯
发夹弯(hairpinning)指的是:内网客户端通过公网地址访问内网服务器。
text
h1 (10.10.0.11) ──→ 203.0.113.1:8080 ──→ 应该回到 h2 (10.10.0.12)
内网 公网地址 内网这个需求很实在 —— 内网员工和外网用户用的是同一个域名, DNS 解析出来就是公网地址。
twice-nat 和它的兄弟 self-twice-nat 看起来都该能解决这个问题。 但在 VPP 26.06 上实测,三种配置都不通:
text
── 4) twice-NAT 治不了发夹弯 ──────────────────
场景:内网的 h1 也想通过公网地址 203.0.113.1:8080 访问内网的 h2
✓ 如预期地不通
换 self-twice-nat 再试
✓ self-twice-nat 也不通为什么不通
VPP 其实建了发夹弯需要的会话,翻译逻辑也走通了。抓 trace 看得很清楚:
bash
./labctl vppctl base clear trace
./labctl vppctl base trace add af-packet-input 10
# 从 h1 访问 203.0.113.1:8080
./labctl vppctl base show tracetext
i2of match: saddr 10.10.0.11 sport 43821 daddr 203.0.113.1 dport 8080
rewrite: saddr 203.0.113.1 sport 43821 daddr 10.10.0.12 dport 9000
↑ 源改成了 NAT 地址 ↑ 目的换成了后端
去程完全正确
o2if match: saddr 10.10.0.12 sport 9000 daddr 203.0.113.1 dport 43821
rewrite: saddr 203.0.113.1 daddr 10.10.0.11 dport 43821
↑ 源地址改了 ↑ 少了 sport 8080!问题出在回程:源端口没有被还原。
h1 是发往 203.0.113.1:8080 的,但收到的回包来自 203.0.113.1:9000 (后端的真实端口)。TCP 栈看到一个从没见过的端口发来的包,判为非法,回 RST。 那个 RST 又打到 NAT 上,VPP 记一笔:
text
4 nat44-ed-in2out-slowpath non-SYN packet try to create session error看到这个计数器时先想到发夹弯
non-SYN packet try to create session 的意思是"有个非 SYN 的 TCP 包想建新会话"。 最常见的两个成因:
- 发夹弯没配通 —— 客户端对错误端口回了 RST(本节的情况)
- 会话老化后对端还在发 —— 见会话老化
实际怎么解决
不要在 NAT 上解决。标准做法是分离解析(split-horizon DNS):
text
内网 DNS: service.example.com → 10.10.0.12 ← 内网地址
外网 DNS: service.example.com → 203.0.113.1 ← 公网地址内网客户端解析到内网地址,流量根本不出去绕一圈,也就没有发夹弯这回事。 这个方案更简单、性能更好,而且不依赖任何 NAT 特性。
小结
| 解决什么 | 内网服务器的回包不经过 NAT(非对称路由) |
| 配置 | nat44 add address X twice-nat + 静态映射加 twice-nat |
| 效果 | 内网服务器看到的源地址变成 twice-nat 池地址 |
| 识别方式 | 会话表 o2i flow 的 rewrite 同时含 saddr 和 daddr |
| 代价 | 丢失真实客户端 IP,需要 XFF / PROXY protocol 补救 |
| 优先考虑 | 能改路由就改路由,twice-NAT 是退而求其次 |
| 治不了 | 发夹弯 —— 用分离解析(split-horizon DNS)解决 |
下一篇:VRF 多租户 —— 两个租户用完全相同的内网地址。