Skip to content

静态映射与端口转发

前两篇解决的都是"内网出去"。这一篇解决反方向:让外网能主动连进内网

为什么需要它

动态会话是内网发起时才建立的。外网主机想主动连进来,NAT 表里查不到任何对应关系, 包只能被丢弃 —— 这正是上一篇no translation 的正常场景。

静态映射就是提前把这条路铺好:告诉 NAT "以后凡是发到这个公网地址端口的包, 就送到那台内网主机去"。

两种映射

text
  端口转发(NAPT 静态映射)
  ┌──────────────────────┐        ┌──────────────────────┐
  │ 203.0.113.1 : 8080   │  ───→  │ 10.10.0.12 : 9000    │
  └──────────────────────┘        └──────────────────────┘
    只占用公网地址的一个端口,同一地址可以转发给很多台内网主机

  1:1 映射(address-only)
  ┌──────────────────────┐        ┌──────────────────────┐
  │ 203.0.113.200        │  ───→  │ 10.10.0.11           │
  │ 全部协议、全部端口     │        │                      │
  └──────────────────────┘        └──────────────────────┘
    整个公网地址独占给一台内网主机,配置最简单,地址消耗大

配置

应用配置

bash
./labctl reset base
./labctl apply base 03
bash
vppctl nat44 plugin enable
vppctl set interface nat44 in host-eth1 out host-eth2

# 出方向仍然需要一个地址来源,否则内网主机出不去
vppctl nat44 add interface address host-eth2

# 端口转发:把公网 8080 转到 h2 的 9000
vppctl nat44 add static mapping tcp local 10.10.0.12 9000 external 203.0.113.1 8080

# 1:1 映射:203.0.113.200 就是 h1
vppctl nat44 add static mapping local 10.10.0.11 external 203.0.113.200

别忘了出方向的地址来源

只配静态映射,内网主机是出不去的。静态映射管"进来",出方向仍然需要 nat44 add interface address 或者 nat44 add address 提供地址。

例外是 1:1 映射 —— 它是双向的,被映射的那台主机出去时也会用这个地址。 但其他没被映射的内网主机还是需要地址池。

命令的完整形态

bash
nat44 add static mapping tcp|udp|icmp \
    local <内网地> [<端口>] \
    external <公网地址|接口> [<端口>] \
    [vrf <表号>] \
    [twice-nat|self-twice-nat] \
    [out2in-only] \
    [exact <池地址>] \
    [del]

几个关键选项:

选项作用
省略协议和端口address-only 1:1 映射,覆盖所有协议(含 ICMP)
external <接口名>用接口当前地址,地址变化时自动跟随(动态 IP 场景必备)
out2in-only只对进方向生效,内网主机出去时不用这个映射
twice-nat源和目的都改写,第 2 章后续会讲
vrf <表号>绑定到指定 VRF,多租户场景

验证

跑自检

bash
./labctl verify base 03
bash
./labctl vppctl base show nat44 static mappings
text
NAT44 static mappings:
 TCP local 10.10.0.12:9000 external 203.0.113.1:8080 vrf 0
 local 10.10.0.11 external 203.0.113.200 vrf 0

注意第二条没有协议字段 —— 这就是 address-only 映射的样子。

端口转发:外网连进来了

bash
ssh chenzhong@192.168.49.182 \
  'docker exec clab-natbase-s1 nc -w 3 203.0.113.1 8080'
text
[h2 tcp/9000] 我看到你的地址是 203.0.113.11:42779

这一篇最重要的一点:端口转发不改源地址

看清上面那行输出。h2 看到的是 203.0.113.11 —— s1 的真实地址

text
  s1 发出        203.0.113.11:x  →  203.0.113.1:8080
  VPP 改写后     203.0.113.11:x  →  10.10.0.12:9000
                 ─────────────      ─────────────
                 源:原封不动         目的:改写了
  h2 因此看到    203.0.113.11:x

对比出方向 NAT:那里改的是,所以外网服务器看不到内网真实地址。 端口转发改的是目的,所以内网服务器能拿到真实客户端 IP

这个差别在实际工作中很重要

把服务发布到公网时,内网服务器的访问日志里记的是真实客户端 IP, 不是一堆相同的网关地址。风控、限流、地域分析、审计都依赖这一点。

如果哪天发现内网服务器日志里所有客户端 IP 都变成了同一个, 那说明中间有人配了 twice-NAT(源和目的都改)—— 通常是为了解决回程路由问题。 代价就是丢失真实客户端 IP,需要靠 X-Forwarded-For 之类的应用层手段补回来。

从会话表也能看出来:

text
    i2o 10.10.0.12  proto TCP port 9000
    o2i 203.0.113.1 proto TCP port 8080
       external host 203.0.113.11:41789
       i2o flow: rewrite: saddr 203.0.113.1 sport 8080
       o2i flow: rewrite: daddr 10.10.0.12 dport 9000
       static translation                      ← 注意这里

o2i 的 rewrite 只有 daddrdport没有 saddr —— 源地址确实没被改。

另外注意最后一行是 static translation 而不是 dynamic translation。 静态映射产生的会话有这个标记,排障时可以据此区分"这条会话是配置来的还是用户流量建的"。

1:1 映射:连 ping 都通

bash
ssh chenzhong@192.168.49.182 \
  'docker exec clab-natbase-s1 ping -c 3 -W 2 203.0.113.200'
text
64 bytes from 203.0.113.200: seq=0 ttl=63 time=0.108 ms
3 packets transmitted, 3 packets received, 0% packet loss

address-only 映射不带协议,所以 ICMP 也被覆盖了。这是它和端口转发的一个明显区别 —— 端口转发只转你指定的那个协议那个端口,ping 是不通的。

1:1 映射是双向的

text
  s1 看到 h1 出方向的地址是  203.0.113.200:46639

h1 主动出去时,用的也是 203.0.113.200,而不是地址池或接口地址。 1:1 映射把这台主机的外网身份完全固定了 —— 进出都是这个地址。

这个性质在部署对外服务时很有用:服务器的入向和出向地址一致, 对端做反向 DNS 校验、白名单、或者双向 TLS 时不会出问题。

如果你不想要这个双向性(只想让外网能进来,出去还是走地址池),加 out2in-only

bash
vppctl nat44 add static mapping tcp local 10.10.0.11 8080 \
    external 203.0.113.200 8080 out2in-only

动态公网地址怎么办

家用宽带、云主机的公网地址可能会变。地址写死在静态映射里,地址一变映射就失效了。

解法是 external 后面填接口名而不是地址:

bash
vppctl nat44 add static mapping tcp local 10.10.0.12 9000 \
    external host-eth2 8080

试一下接口形式

bash
./labctl vppctl base nat44 add static mapping tcp local 10.10.0.12 9000 external 203.0.113.1 8080 del
./labctl vppctl base nat44 add static mapping tcp local 10.10.0.12 9000 external host-eth2 8080
./labctl vppctl base show nat44 static mappings

然后再从 s1 连一次 203.0.113.1:8080,行为完全一样 —— 但如果接口地址变了, 映射会自动跟着变。

接口形式的映射会显示成两行,别以为 del 没生效

text
NAT44 static mappings:
 TCP local 10.10.0.12:9000 external 203.0.113.1:8080 vrf 0     ← 解析后的地址条目
 local 10.10.0.11 external 203.0.113.200 vrf 0
 TCP local 10.10.0.12:9000 external host-eth2:8080 vrf -1      ← 你配的接口条目

第一行和第三行是同一条映射:VPP 把接口解析成当前地址后,额外挂了一条已解析的条目 (vrf 0),你配的那条则以接口形式保留(vrf -1)。接口地址变化时, VPP 会重新解析并更新第一行。

第一次看到会以为"我刚才 del 掉的那条又回来了"。它不是残留,是接口形式映射的正常显示。

常见问题

外网连不上,怎么查?

bash
# 1. 映射配了吗,端口和协议对吗
./labctl vppctl base show nat44 static mappings

# 2. 有没有落到 no translation
./labctl vppctl base show errors | grep -i nat

# 3. 包到底走到哪一步了
./labctl vppctl base clear trace
./labctl vppctl base trace add af-packet-input 10
# ...从外网发起连接...
./labctl vppctl base show trace

内网服务确实在监听吗?

一半的"端口转发不通"其实是内网服务没起来。先在内网主机本机上验证:

bash
ssh chenzhong@192.168.49.182 \
  'docker exec clab-natbase-h2 nc -w 2 127.0.0.1 9000'

协议写错了?

nat44 add static mapping tcp ... 只转 TCP。如果服务是 UDP 的(DNS、游戏、VoIP), 需要单独配一条 udp 的。这是一个很常见的疏漏。

什么时候用哪种

text
  端口转发                          1:1 映射
  ────────────────────────────      ────────────────────────────────
  公网地址稀缺,要复用               公网地址够用
  只发布少数几个服务端口             服务器要开很多端口,或端口不固定
  同一地址服务多台内网主机           需要 ICMP 也能通(ping、traceroute)
  想精确控制暴露面                   需要进出地址一致(反向 DNS、白名单)

  ✗ 不适合的场景
  ────────────────────────────────────────────────────────────
  端口范围很大 —— VPP 的静态映射是逐条配的,没有"端口段转发"语法,
  一千个端口就要一千条命令。这种需求应该用 1:1 映射,或者考虑 pnat 插件。

小结

端口转发1:1 映射
命令... tcp local IP PORT external IP PORT... local IP external IP
覆盖协议只有指定的那个全部,含 ICMP
方向默认双向,可用 out2in-only 限制双向
源地址不改,内网服务器能看到真实客户端 IP同样不改
地址消耗一个端口一整个地址
会话标记static translationstatic translation

第 2 章前三篇讲完了 NAT44-ED 的基础三件套:出方向伪装、地址池、静态映射。 后续篇目(identity mapping、负载均衡映射、twice-NAT、VRF 多租户、超时调优) 会在第 2 期补齐。

如果你想先把原理再过一遍,第 1 章值得重读 —— 配置命令会忘,那四篇里的模型不会。

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