Skip to content

五分钟跑通第一个 NAT

这一篇不解释原理,只做一件事:让你亲眼看到 NAT 生效前后的差别。原理从第 1 章开始讲。

先看没有 NAT 时是什么样

底座刚起来时,VPP 只是一台普通的三层转发设备,没有任何 NAT 配置。

让 h1 试着访问外网

bash
./labctl sh base h1
# 在 h1 里:
ping -c 3 -W 2 203.0.113.11

或者不进 shell 直接执行:

bash
ssh chenzhong@192.168.49.182 \
  'docker exec clab-natbase-h1 ping -c 3 -W 2 203.0.113.11'
text
PING 203.0.113.11 (203.0.113.11): 56 data bytes

--- 203.0.113.11 ping statistics ---
3 packets transmitted, 0 packets received, 100% packet loss

全丢。但奇怪的是,网关是通的:

bash
docker exec clab-natbase-h1 ping -c 2 -W 1 10.10.0.1
text
64 bytes from 10.10.0.1: seq=0 ttl=64 time=0.379 ms
64 bytes from 10.10.0.1: seq=1 ttl=64 time=0.084 ms

为什么不通:去程没问题,回程无路可走

问题不在去的方向。h1 的默认路由指向 10.10.0.1,VPP 收到包、查路由、 知道 203.0.113.11 在 eth2 那边,老老实实转发出去了。s1 也确实收到了。

卡住的是回程。看一眼 s1 的路由表:

bash
docker exec clab-natbase-s1 ip route
text
172.30.101.0/24 dev eth0 scope link  src 172.30.101.104
203.0.113.0/24 dev eth1 scope link  src 203.0.113.11

s1 只知道两个直连网段,完全不知道 10.10.0.0/24 在哪里。 它想回包给 10.10.0.11,查不到路由,包就地丢弃。

这不是实验环境的缺陷,而是故意做成这样的 —— 它就是真实互联网的样子。 10.0.0.0/8 是 RFC 1918 私有地址,公网上的任何路由器都不会为它转发流量。 你家里的电脑之所以能上网,唯一的原因就是路由器帮你做了 NAT。

NAT 存在的根本理由

不是"IPv4 地址不够用"这么笼统。具体到每一个包上,NAT 解决的是: 让回程流量有一个公网上可路由的目的地址。

把源地址换成一个公网地址,服务器就知道往哪回;NAT 设备再根据自己记下的对应关系, 把回包还原成内网地址。地址复用是这个机制带来的副产品,虽然后来变成了它最主要的用途。

开启 NAT:三条命令

应用第一步配置

bash
./labctl apply base 01

脚本干的事就是这三条 vppctl 命令:

bash
# 1. 启用 nat44 插件(端点相关模式,VPP 的主力 NAT 实现)
vppctl nat44 plugin enable

# 2. 把出接口自己的地址当作转换后要用的地址
vppctl nat44 add interface address host-eth2

# 3. 告诉 VPP 哪个接口在"内"、哪个在"外"
vppctl set interface nat44 in host-eth1 out host-eth2

第三条是 VPP NAT 模型里最需要转过弯的地方。VPP 不像 iptables 那样靠规则匹配来判断 "这个包该不该做 NAT",而是先给接口贴标签。包从贴了 in 的接口进来,走 in2out 分支(改源地址); 从贴了 out 的接口进来,走 out2in 分支(查会话表还原目的地址)。

再试一次

跑自检

bash
./labctl verify base 01
text
── 2) h1 现在能到外网了吗 ────────────────────────────
PING 203.0.113.11 (203.0.113.11): 56 data bytes
64 bytes from 203.0.113.11: seq=0 ttl=63 time=0.217 ms
64 bytes from 203.0.113.11: seq=1 ttl=63 time=0.168 ms
64 bytes from 203.0.113.11: seq=2 ttl=63 time=0.098 ms

--- 203.0.113.11 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
✓ h1 → s1 全通

通了。s1 的路由表一个字都没改 —— 变的是它收到的包长什么样。

让服务器自己告诉你它看到了什么

这是本教程最常用的观察手段。s1 上跑着一个「你是谁」服务,你连上去,它把它看到的源地址回给你:

问一下 s1

bash
docker exec clab-natbase-h1 nc -w 2 203.0.113.11 9000
text
[s1 tcp/9000] 我看到你的地址是 203.0.113.1:39637

h1 的真实地址是 10.10.0.11,但 s1 看到的是 203.0.113.1 —— VPP 出接口的地址。 这就是 NAT。

再让 h2 也连一次:

text
[s1 tcp/9000] 我看到你的地址是 203.0.113.1:45381

两台内网主机在外网看来是同一个地址,只有端口不同:

text
  内网真实来源            外网看到的来源
  ─────────────────────   ─────────────────────
  10.10.0.11 (h1)         203.0.113.1:39637
  10.10.0.12 (h2)         203.0.113.1:45381

这就是 NAPT(Network Address Port Translation,网络地址端口转换)。 严格来说我们平时说的 "NAT" 绝大多数时候指的都是 NAPT —— 靠端口这个维度, 一个公网地址可以撑起成千上万个内网主机。

看一眼 VPP 记了什么

会话表

bash
./labctl vppctl base show nat44 sessions
text
NAT44 ED sessions:
-------- thread 0 vpp_main: 1 sessions --------
    i2o 10.10.0.11 proto TCP port 39637 fib 0
    o2i 203.0.113.1 proto TCP port 39637 fib 0
       external host 203.0.113.11:9000
       i2o flow: match: saddr 10.10.0.11 sport 39637 daddr 203.0.113.11 dport 9000 proto TCP
                 rewrite: saddr 203.0.113.1 sport 39637 daddr 203.0.113.11 dport 9000
       o2i flow: match: saddr 203.0.113.11 sport 9000 daddr 203.0.113.1 dport 39637 proto TCP
                 rewrite: saddr 203.0.113.11 daddr 10.10.0.11 dport 39637
       index 0
       last heard 484.35
       timeout in 235.88
       total pkts 9, total bytes 541
       dynamic translation

现在不用全看懂,只要注意三点:

  1. 一条会话记了两个方向i2o(inside → outside)和 o2i(outside → inside), 每个方向都写明了"匹配什么"和"改成什么"。NAT 不是一个改写动作,是一对。
  2. 有一行 external host 203.0.113.11:9000:这条会话不只记了内网端,还记了外网端是谁。 这个细节就是 "ED"(端点相关)的全部含义,第 1 章会讲它为什么重要到值得单开一篇。
  3. 有超时timeout in 235.88。会话不是永久的,闲置久了会被回收。

顺手看一个 ICMP 的例子

刚才 ping 的时候也产生了会话。ICMP 没有端口,NAT 改什么?

text
    i2o 10.10.0.11 proto ICMP port 32 fib 0
    o2i 203.0.113.1 proto ICMP port 63327 fib 0
       i2o flow: rewrite: saddr 203.0.113.1 daddr 203.0.113.11 icmp-id 63327
       o2i flow: rewrite: saddr 203.0.113.11 daddr 10.10.0.11 icmp-id 32

icmp-id 32 被改成了 icmp-id 63327。ICMP Echo 报文头里有个 Identifier 字段, 本来是给主机自己配对请求和应答用的,NAT 就借用它来充当"端口"的角色。

注意这里和 TCP 的一个区别:TCP 那条会话里源端口 39637原样保留了, 而 ICMP 的 id 被换掉了。为什么?下一章讲端口分配策略时会回答。

清场

回到干净底座

bash
./labctl reset base

两秒。容器不销毁,只是把 NAT 配置清空。之后 h1 又会 ping 不通 203.0.113.11 —— 你可以随时用这个来确认自己在哪个状态。

这五分钟里发生了什么

你看到了 NAT 最基本的形态,并且拿到了三个观察手段:

  • 「你是谁」服务:外部视角 —— 别人看到我是谁
  • show nat44 sessions设备视角 —— NAT 记了什么状态
  • show trace包的视角 —— 一个包经历了什么(还没用,第 1 章开始用)

也留下了几个问题,正好是第 1 章的目录:

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