Skip to content

NAT64:让纯 IPv6 客户端访问 IPv4 服务

前四章都是 IPv4 到 IPv4。这一章处理一个更棘手的问题: 通信双方连协议族都不一样。

问题的形状

IPv6 推了二十多年,新建的网络越来越多是 IPv6-only —— 运营商的移动网、 大型云平台的内部网络、新兴市场的宽带。但互联网上仍有海量服务是纯 IPv4 的, 而且短期内不会变。

问题的核心不是"路由不通",而是客户端根本没法把这个目的地址写进包里。 IPv6 报文头的目的地址字段是 128 位,203.0.113.11 是 32 位 —— 塞不进去, 也没有合法的方式表达。

解法:把 IPv4 地址塞进 IPv6 地址里

RFC 6052 定义了一套地址嵌入规则。最常用的形式是: 预留一段 IPv6 前缀,把 32 位 IPv4 地址直接拼在后面。

text
                    64:ff9b::/96 前缀(96 位)        IPv4(32 位)
  ┌──────────────────────────────────────────────┬────────────────┐
  │ 0064 : ff9b : 0000 : 0000 : 0000 : 0000      │  cb00 : 710b   │
  └──────────────────────────────────────────────┴────────────────┘

                                    203 . 0 . 113 . 11
                                    cb   00   71    0b

  合成结果:64:ff9b::cb00:710b

64:ff9b::/96 是 RFC 6052 指定的 Well-Known Prefix(知名前缀), 专门用于这个用途。也可以用自己的网络前缀(Network-Specific Prefix)。

手算合成地址

bash
python3 -c "
import ipaddress
v4 = ipaddress.IPv4Address('203.0.113.11')
print(ipaddress.IPv6Address(int(ipaddress.IPv6Address('64:ff9b::')) + int(v4)))
"
text
64:ff9b::cb00:710b

看懂 cb00:710b 的话其实不用算:cb=203,00=0,71=113,0b=11。 每两个十六进制字符对应 IPv4 的一段。

实验拓扑

启动 IPv6 底座

bash
./labctl up v6

h6 只有 IPv6 地址,s4 只有 IPv4 地址。s6 留给下一篇的 NAT66

底座关掉了 h6 的 RA 自动配置

netlab 生成的 VPP 配置里带 ip6 nd ... ra-interval 5,VPP 会在内网口上发路由通告。 h6 收到后会额外生成一个 EUI-64 地址(2001:db8:1:0:a8c1:abff:feaf:196c), 而且 Linux 会优先拿它做源地址

结果是 BIB 表里出现又长又随机、每次重建都不一样的地址,实验现象没法对照。 所以 up.sh 关掉了 h6 的 accept_ra 并清掉自动生成的地址, 让它老老实实用拓扑里声明的 2001:db8:1::11

真实的 IPv6 网络里 SLAAC 是常态,这里是为了实验可复现才关掉的。

配置

应用配置

bash
./labctl apply v6 01
bash
vppctl nat64 plugin enable

# 1. 声明 NAT64 前缀
vppctl nat64 add prefix 64:ff9b::/96

# 2. IPv4 侧的地址池(IPv6 客户端出去时借用)
vppctl nat64 add pool address 203.0.113.100 - 203.0.113.102

# 3. 划分内外侧 —— 注意是分两条,每条只给一个方向
vppctl set interface nat64 in host-eth1
vppctl set interface nat64 out host-eth2

接口命令的写法和 nat44 不同

text
  nat44:  set interface nat44 in <内侧> out <外侧>     一条命令给两个
  nat64:  set interface nat64 in  <内侧>              分成两条
          set interface nat64 out <外侧>

验证

跑自检

bash
./labctl verify v6 01
bash
ssh chenzhong@192.168.49.182 \
  'docker exec clab-nat64-h6 ping -6 -c 3 -W 2 64:ff9b::cb00:710b'
text
3 packets transmitted, 3 packets received, 0% packet loss

一台只有 IPv6 地址的主机,ping 通了一台只有 IPv4 地址的服务器。

再问问 s4 看到了什么:

text
  s4 看到的源地址:203.0.113.102:11570
✓ s4 看到的是 IPv4 地址池里的地址 —— 它完全不知道对方是 IPv6 主机

BIB:NAT64 特有的一张表

bash
./labctl vppctl v6 show nat64 bib tcp
text
NAT64 tcp BIB entries:
 2001:db8:1::11 38183 203.0.113.102 11570 protocol tcp vrf 0 dynamic 1 sessions
 ─────────────────────  ────────────────────
 IPv6 侧的地址和端口       IPv4 侧借到的地址和端口

BIB(Binding Information Base,绑定信息库)只记录 "这个 IPv6 端点对应哪个 IPv4 端点",和对端是谁无关

这个特性像 nat44-ei 的会话键 —— 端点无关。 一个 IPv6 端点无论访问多少个不同的 IPv4 服务器,都共用同一条 BIB 绑定。

真正带对端信息的是会话表。

会话表:一行横跨两个协议族

bash
./labctl vppctl v6 show nat64 session table tcp
text
NAT64 tcp sessions:
 2001:db8:1::11 43103 64:ff9b::cb00:710b 9000  203.0.113.102 14257 203.0.113.11 9000
 └──────┬──────┘ └─┬─┘ └───────┬────────┘ └┬─┘  └──────┬─────┘ └─┬─┘ └─────┬────┘ └┬─┘
     v6 源      v6源端口    v6 目的      v6目端口    v4 源     v4源端口   v4 目的  v4目端口
 └──────────────── IPv6 视角 ────────────────┘  └──────────── IPv4 视角 ──────────┘

左半边是 IPv6 客户端眼里的连接,右半边是 IPv4 服务器眼里的连接。 NAT64 的工作就是维持这两个视角的对应关系。

注意 v6 目的是合成地址 64:ff9b::cb00:710b,v4 目的是它还原出来的 203.0.113.11 —— 同一个东西的两种表达。

VPP 输出里有个拼写错误

text
... protcol tcp vrf 0
    ^^^^^^^ 少了个 o

无害,但写脚本 grep 时别按正确拼写去匹配。

反方向:让 IPv4 客户端访问 IPv6 服务

前面是"IPv6 找 IPv4"。反过来也可以,用静态 BIB

bash
vppctl nat64 add static bib 2001:db8:1::11 9000 203.0.113.100 8080 tcp

意思是:外网访问 203.0.113.100:8080 时,送到 IPv6 主机 2001:db8:1::11 的 9000 端口。

在 h6 上起个服务,让 s4 连过来:

text
  h6 上的服务看到的源地址:64:ff9b::cb00:710b:39159

h6 看到的是 64:ff9b::cb00:710b —— s4 的 IPv4 地址 203.0.113.11 被同一个前缀合成后的样子。

text
  漂亮的对称性

    h6 看 s4   →  64:ff9b::cb00:710b     IPv4 地址被合成成 IPv6
    s4 看 h6   →  203.0.113.100          IPv6 主机借用的 IPv4 地址

  双方都以为对方和自己是同一个协议族。

静态 BIB 的 IPv4 地址必须取自地址池

用池外地址会静默失败

bash
vppctl nat64 add static bib 2001:db8:1::11 9000 203.0.113.200 9090 tcp

命令执行成功,show nat64 bib 里也看得到这条,但外部根本连不进来。

原因和 nat44 地址池那一篇讲的一样: VPP 只把池里的地址装进 FIB 当作本地地址并代答 ARP。 池外的 203.0.113.200 不属于 VPP,外网发 ARP 没人应答,包到不了。

自检脚本里专门做了这个对照实验。

DNS64:让客户端不用手算地址

上面所有例子里,客户端都是直接输入合成地址 64:ff9b::cb00:710b。 真实用户不会这么干 —— 他们输入的是域名。

补上这一环的是 DNS64

DNS64 干的事就是:当一个域名只有 A 记录没有 AAAA 记录时, 按 NAT64 前缀合成一条假的 AAAA 记录返回给客户端。

客户端全程以为自己在用纯 IPv6 上网,完全无感。

VPP 不做 DNS64

DNS64 是 DNS 服务器的活,不是转发面的活。常见实现有 BIND(dns64 配置段)、 Unbound、以及各种专用的 DNS64 服务。

本实验环境没有部署 DNS64,所以我们直接用合成地址访问 —— 这样反而更清楚地暴露了地址合成这一层,而不是被 DNS 藏起来。

生产部署时 DNS64 和 NAT64 必须用同一个前缀,否则合成出来的地址 NAT64 网关不认识。这是部署时最常见的配置错误之一。

NAT64 的固有局限

地址字面量绕不过去

DNS64 只能拦截 DNS 查询。如果应用里写死了 IPv4 地址字面量(不查 DNS), DNS64 就无能为力:

text
  访问 http://example.com/        ✓ DNS64 能合成
  访问 http://203.0.113.11/       ✗ 没有 DNS 查询,客户端无法构造目的地址

这在实际中很常见:配置文件里写死的 IP、硬编码的回源地址、 某些 App 内置的 IP 列表、以及 FTP 这类在载荷里传 IPv4 地址的协议。

解决这类问题需要 464XLAT(见后续章节)在客户端侧 额外做一层翻译。

其他需要注意的:

text
  ✗ 不支持 IPv4 分片的完整语义      分片重组和转换会有边界情况
  ✗ 依赖 DNSSEC 时会冲突            DNS64 伪造记录,会破坏 DNSSEC 验证
  ✗ 地址池仍然是 IPv4               NAT64 没有消灭对 IPv4 地址的需求,
                                    只是把它集中到了网关上

最后一条值得强调:NAT64 不能让你摆脱 IPv4 地址。 IPv6 客户端出去时仍然要借一个 IPv4 源地址,池子还是得有。 它省的是"每个终端一个 IPv4 地址",不是"完全不需要 IPv4"。

小结

解决什么纯 IPv6 客户端访问纯 IPv4 服务
核心机制把 32 位 IPv4 地址嵌进 IPv6 前缀(RFC 6052)
知名前缀64:ff9b::/96
配置nat64 add prefix + nat64 add pool address + set interface nat64 in/out(分两条)
BIBIPv6 端点 ↔ IPv4 端点的绑定,端点无关
会话表一行八个值,横跨两个协议族
反向访问nat64 add static bib,IPv4 地址必须取自池
配套组件DNS64(VPP 不做),必须和 NAT64 用同一前缀
绕不过的坑IPv4 地址字面量、DNSSEC

下一篇:NAT66 —— IPv6 到 IPv6 也需要 NAT?

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