主题
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:710b64: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 v6h6 只有 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 01bash
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 01bash
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 tcptext
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 tcptext
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:39159h6 看到的是 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(分两条) |
| BIB | IPv6 端点 ↔ IPv4 端点的绑定,端点无关 |
| 会话表 | 一行八个值,横跨两个协议族 |
| 反向访问 | nat64 add static bib,IPv4 地址必须取自池 |
| 配套组件 | DNS64(VPP 不做),必须和 NAT64 用同一前缀 |
| 绕不过的坑 | IPv4 地址字面量、DNSSEC |
下一篇:NAT66 —— IPv6 到 IPv6 也需要 NAT?