1. 从一个被忽略的角落说起:为什么还要关心地址翻译器
翻到《TCP/IP详解》第2版第2章“互联网地址架构”的时候,很多人会直接跳过2.5节,觉得“有特殊用途的地址”无非就是回环、私有地址、链路本地这些老生常谈。但2.5.1这个小节——IPv4/IPv6寻址翻译器——恰恰是整个地址架构里最容易被低估、又最容易在实际组网中踩坑的部分。
我最早接触这个概念是在一个双栈过渡期的项目里:一边是只认IPv4的老旧业务系统,一边是新上的IPv6管理网段,两边要互通,但又不想在每个节点上都跑双栈。当时第一反应是上NAT,结果发现NAT只能解决“地址不够”的问题,解决不了“协议族不同”的问题。真正把这两件事串起来的,是地址翻译器这一类机制——它不只是把地址换个写法,而是在两种寻址体系之间做语义映射。
这篇文章面向的是正在做网络规划、协议栈开发、或者准备啃《TCP/IP详解》第2版第2章笔记的读者。不管你是刚接触IPv6的运维,还是已经写过双栈socket的程序员,只要你的系统里同时存在IPv4和IPv6两种地址,就一定会遇到“怎么让它们互相看见”的问题。我会把2.5.1这一小节背后的东西拆开,补上原书里点到为止的实操细节,再结合我自己踩过的坑,把地址翻译器的选型、配置、排查讲透。
核心关键词先摆出来:IPv4/IPv6寻址翻译器、地址族转换、双栈过渡、SIIT、NAT64、地址嵌入、协议翻译。这些词在后面的章节里会反复出现,每一个都对应着一类具体的工程决策。
2. 地址翻译器到底在翻译什么:先搞清问题边界
2.1 不是NAT,也不是隧道,而是第三类过渡机制
很多人一听到“翻译”两个字,第一反应就是NAT(网络地址转换)。但NAT和地址翻译器解决的是完全不同层面的问题。NAT做的是同一协议族内的地址重写,比如把内网10.0.0.0/8的地址换成公网地址,IP包的头部结构不变,只是源/目的地址字段被替换。而地址翻译器做的是跨协议族的转换,IPv4的包头和IPv6的包头结构都不一样,字段长度、校验和计算方式、分片处理逻辑全都不同。
打个比方:NAT像是把一封信的收件人地址改一下,信封还是那个信封;地址翻译器则是把中文信翻译成英文信,连信封格式都要换。这就是为什么地址翻译器不能简单地在NAT基础上改改就完事。
《TCP/IP详解》第2版在第2.5.1节里把这类机制归为“有特殊用途的地址”的一部分,其实是在强调:翻译器使用的地址不是普通的单播地址,而是带有特定嵌入规则的地址。比如IPv4映射的IPv6地址(::ffff:0:0/96),它本身就是一个特殊用途的地址块,专门用来表示“这个IPv6地址其实对应一个IPv4节点”。
2.2 三种翻译模式的本质区别
在实际工程中,地址翻译器主要分三种模式,理解它们的区别比记住配置命令更重要。
第一种是无状态IP/ICMP翻译(SIIT)。它的核心思想是:既然IPv4地址是32位,IPv6地址是128位,那我就定义一个算法,把IPv4地址嵌入到IPv6地址的特定位置,反过来也能提取出来。这样每个包独立翻译,不需要维护会话状态。优点是简单、快、不怕设备重启;缺点是只能处理地址和协议头的翻译,遇到需要端口映射的场景就无能为力。
第二种是有状态翻译(典型代表是NAT64)。它维护一张会话表,把IPv6侧的连接映射到IPv4侧的某个地址和端口上。这样多个IPv6节点可以共享一个IPv4地址,解决了IPv4地址枯竭场景下的互通问题。代价是状态同步复杂,设备故障时会话丢失,而且对某些嵌入IP地址的协议(比如FTP的PORT命令)需要做应用层网关处理。
第三种是双重翻译(如464XLAT)。它在终端侧做一次无状态翻译,在网络侧再做一次有状态翻译,目的是让只支持IPv6的应用能够访问IPv4服务。这种模式在移动网络里比较常见,但配置复杂度最高。
注意:选哪种模式不取决于技术先进性,而取决于你的地址资源、会话规模和对故障恢复的要求。如果只是实验室里做协议验证,SIIT足够;如果是生产环境给大量终端提供IPv4访问能力,NAT64更现实。
2.3 地址嵌入规则:翻译器的“字典”
地址翻译器能工作的前提是双方对“地址怎么嵌入”有共识。IPv4地址只有32位,要放进128位的IPv6地址里,必须定义明确的嵌入位置。常见的规则有几种:
- IPv4兼容地址(::/96):高96位全0,低32位放IPv4地址。这种格式早期用来表示IPv6节点本身也支持IPv4,但现在基本废弃了。
- IPv4映射地址(::ffff:0:0/96):高80位为0,接着16位全1,低32位放IPv4地址。这种格式用在双栈节点的socket API里,表示“这个连接其实走的是IPv4”。
- NAT64前缀(通常为64:ff9b::/96):高96位是一个特定的前缀,低32位放IPv4地址。NAT64网关看到这个前缀的包,就知道要把低32位提取出来作为IPv4目的地址。
这些嵌入规则不是随便定的,它们直接决定了翻译器能不能正确提取地址。我在一次测试中曾经把NAT64前缀配错了一位,结果所有翻译后的包都被送到了错误的目的地,排查了半天才发现是前缀长度写成了/64而不是/96。这种错误在配置时很容易犯,因为很多设备的命令行界面不会主动校验前缀长度和嵌入格式是否匹配。
3. 核心细节拆解:从包头到会话表的完整链路
3.1 IPv4和IPv6包头的关键差异
要理解翻译器的工作量,先得看两个协议头的差异。IPv4头部最小20字节,包含版本、IHL、TOS、总长度、标识、标志、分片偏移、TTL、协议、头部校验和、源地址、目的地址、选项。IPv6头部固定40字节,包含版本、流量类别、流标签、载荷长度、下一个头部、跳数限制、源地址、目的地址,没有校验和,分片信息放在扩展头部里。
翻译器要做的字段映射包括:
| IPv4字段 | IPv6对应字段 | 处理方式 |
|---|---|---|
| 版本(4) | 版本(6) | 直接改写 |
| IHL | 无 | 丢弃,IPv6头部长度固定 |
| TOS | 流量类别 | 低6位语义相近,高位需映射 |
| 总长度 | 载荷长度 | 减去头部长度后填入 |
| 标识 | 无(分片扩展头有) | 分片时重新生成 |
| 标志/分片偏移 | 分片扩展头 | 需要重组或重新分片 |
| TTL | 跳数限制 | 语义相同,直接复制 |
| 协议 | 下一个头部 | 数值相同,直接复制 |
| 头部校验和 | 无 | 丢弃,IPv6不校验头部 |
| 源/目的地址 | 源/目的地址 | 按嵌入规则转换 |
这张表看起来简单,但实际操作中有几个坑。比如TOS到流量类别的映射,IPv4的TOS字段有8位,其中前6位是DSCP,后2位是ECN;IPv6的流量类别也是8位,但DSCP和ECN的位置略有不同。如果翻译器不做正确处理,QoS策略就会失效。我在一个视频会议系统里就遇到过这个问题:IPv4侧的语音包标记了EF(加速转发),翻译到IPv6后DSCP变成了0,结果语音质量直线下降。
另一个坑是分片。IPv4允许中间路由器分片,IPv6要求端到端分片,中间路由器不分片。翻译器遇到一个大的IPv4包时,如果目的链路MTU小于包长,它必须自己完成分片,并生成IPv6的分片扩展头。反过来,IPv6的分片包翻译到IPv4时,需要把分片扩展头的信息映射回IPv4的标识、标志和分片偏移字段。这个过程如果实现不完整,就会出现大包不通、小包正常的情况。
3.2 无状态翻译的算法细节
无状态翻译的核心是一个可逆的地址映射函数。以SIIT为例,假设我们使用64:ff9b::/96作为翻译前缀,那么:
- IPv4地址a.b.c.d翻译成IPv6地址:64:ff9b::a.b.c.d(即前缀后接32位IPv4地址)
- IPv6地址64:ff9b::x.y.z.w翻译回IPv4地址:x.y.z.w
这个算法看起来简单,但有几个边界条件需要处理:
第一,校验和的中和。IPv4头部有校验和,IPv6没有。翻译器在把IPv4转IPv6时,需要重新计算传输层校验和(因为伪头部变了)。TCP/UDP校验和包含伪头部,伪头部里有IP地址和协议号。地址一变,校验和就必须重算。有些实现为了省事,直接把校验和字段置为0(UDP允许校验和为0,TCP不允许),这会导致TCP包被丢弃。
第二,ICMP类型映射。ICMPv4和ICMPv6的类型码并不完全对应。比如ICMPv4的“目的不可达”类型3有多个代码,ICMPv6的“目的不可达”类型1也有多个代码,但两者并不是一一对应。翻译器需要维护一张映射表,把常见的错误类型正确转换。如果映射错了,ping命令就会显示奇怪的错误信息。
第三,地址范围的排除。不是所有IPv4地址都应该被翻译。私有地址、回环地址、组播地址在翻译时需要特殊处理。比如IPv4的127.0.0.1翻译成IPv6后不应该变成64:ff9b::7f00:1,而应该映射到::1。这些特殊地址的处理规则在RFC里有详细定义,但很多简易实现会忽略。
3.3 有状态翻译的会话表设计
有状态翻译(NAT64)的核心是一张会话表,每条会话记录包含:
- IPv6侧源地址和源端口
- IPv4侧转换后的源地址和源端口
- IPv4侧目的地址和目的端口
- 协议类型
- 会话状态和超时时间
当IPv6节点发起连接时,NAT64网关从自己的IPv4地址池里选一个地址和端口,建立映射关系,然后把包翻译成IPv4发出去。当IPv4侧回包时,网关根据目的地址和端口查表,找到对应的IPv6侧地址和端口,再翻译回去。
这张表的设计直接决定了网关的性能和可靠性。我见过一些实现把会话表放在内存里,设备一重启所有连接都断;也见过用外部数据库存会话表的,性能瓶颈明显。比较合理的做法是内存表加定期持久化,同时设置合理的超时时间——TCP会话通常几分钟到几小时,UDP会话通常几十秒到几分钟。
实操心得:NAT64的端口分配策略很关键。如果采用“端口保留”策略(即IPv6侧源端口和IPv4侧源端口相同),可以简化某些应用层协议的处理,但端口冲突的概率会增大。如果采用“端口重写”策略,灵活性更高,但需要处理应用层协议里嵌入的端口信息。我一般建议在通用场景下用端口重写,在需要支持FTP、SIP等协议时再开应用层网关。
4. 实操过程:从零搭一套翻译环境
4.1 环境规划与地址分配
假设我们要在一个实验环境里验证IPv4/IPv6翻译。拓扑很简单:左边一个IPv6-only节点,右边一个IPv4-only节点,中间一台Linux主机做翻译器。
地址规划如下:
- IPv6节点:2001:db8:1::2/64,网关2001:db8:1::1
- 翻译器IPv6侧:2001:db8:1::1/64
- 翻译器IPv4侧:192.0.2.1/24
- IPv4节点:192.0.2.2/24,网关192.0.2.1
- NAT64前缀:64:ff9b::/96
在翻译器上,我们需要开启IPv6转发和IPv4转发,然后配置NAT64规则。Linux内核从3.7版本开始支持NAT64,但需要加载相应的模块。
# 开启转发 sysctl -w net.ipv6.conf.all.forwarding=1 sysctl -w net.ipv4.ip_forward=1 # 加载NAT64模块 modprobe nf_nat_siit modprobe nf_nat_nat64 # 配置NAT64前缀 ip -6 route add 64:ff9b::/96 dev eth1这些命令只是最基础的框架,实际配置还需要iptables或nftables规则来定义哪些流量需要翻译。我建议用nftables,因为它的语法更清晰,调试也方便。
4.2 无状态翻译的配置与验证
先做无状态翻译(SIIT)的验证。在翻译器上,我们需要把IPv6侧发往64:ff9b::/96的包翻译成IPv4包,源地址用翻译器的IPv4地址。
nft add table ip6 nat64 nft add chain ip6 nat64 prerouting { type nat hook prerouting priority -100; } nft add rule ip6 nat64 prerouting ip6 daddr 64:ff9b::/96 \ snat to 192.0.2.1然后在IPv6节点上ping一个IPv4地址的翻译形式:
ping6 64:ff9b::c000:0202如果配置正确,应该能看到回包。在翻译器上用tcpdump抓包,可以看到IPv6的ICMPv6 Echo Request被翻译成了IPv4的ICMP Echo Request,源地址是192.0.2.1,目的地址是192.0.2.2。
注意:无状态翻译不需要维护会话表,所以设备重启后连接不会断。但它要求翻译器有一个固定的IPv4地址,而且不能做端口复用。如果多个IPv6节点同时访问同一个IPv4地址,翻译器无法区分它们的连接,除非每个节点使用不同的IPv4源地址。
4.3 有状态翻译的配置与验证
有状态翻译(NAT64)的配置稍微复杂一些。我们需要定义一个IPv4地址池,然后配置SNAT规则。
nft add table ip6 nat64 nft add chain ip6 nat64 prerouting { type nat hook prerouting priority -100; } nft add chain ip6 nat64 postrouting { type nat hook postrouting priority 100; } nft add rule ip6 nat64 prerouting ip6 daddr 64:ff9b::/96 \ dnat to 192.0.2.2 nft add rule ip6 nat64 postrouting ip6 saddr 2001:db8:1::/64 \ ip6 daddr 64:ff9b::/96 masquerade这里的masquerade会自动从翻译器的IPv4地址里选一个作为源地址,并维护会话表。验证方法和无状态类似,但这次在翻译器上可以看到会话表:
conntrack -L -f ipv6应该能看到类似这样的条目:
ipv6 2 2001:db8:1::2 64:ff9b::c000:0202 12345 80 \ [UNREPLIED] ipv4 2 192.0.2.1 192.0.2.2 54321 80这条记录表示:IPv6节点2001:db8:1::2的12345端口访问64:ff9b::c000:0202的80端口,被翻译成了192.0.2.1的54321端口访问192.0.2.2的80端口。
4.4 应用层协议的坑:FTP和SIP
地址翻译器最头疼的不是IP层,而是应用层。很多协议在载荷里嵌入了IP地址和端口,翻译器如果只改包头不改载荷,这些协议就会失败。
以FTP为例。FTP的PORT命令里包含客户端的IP地址和端口,格式是h1,h2,h3,h4,p1,p2。如果客户端在IPv6侧,它发送的PORT命令里是IPv6地址,翻译器需要把这个地址转换成IPv4地址,并修改端口。这需要翻译器解析FTP控制连接的内容,属于应用层网关(ALG)的范畴。
我在一次测试中遇到过FTP主动模式无法工作的问题。客户端在IPv6侧,服务器在IPv4侧,登录和列目录都正常,但一到数据传输就卡住。抓包发现客户端发送的PORT命令里是IPv6地址,服务器无法解析。后来在翻译器上开启了FTP ALG模块,问题解决。
SIP协议也有类似的问题。SIP消息的SDP部分包含媒体流的IP地址和端口,翻译器需要修改这些字段。如果翻译器不支持SIP ALG,语音通话就会单通或无声。
实操心得:如果你的环境里只有HTTP、SSH这类不嵌入地址的协议,无状态翻译就够用。但如果要支持FTP、SIP、RTSP等协议,必须选择支持ALG的有状态翻译方案。在选型时,一定要问清楚设备支持哪些ALG,最好用实际业务协议做验证。
5. 常见问题与排查技巧实录
5.1 翻译后ping不通的排查思路
这是最常见的问题。按以下顺序排查:
- 检查路由:翻译器是否有到IPv4侧和IPv6侧的路由?用
ip -6 route和ip route确认。 - 检查转发开关:
net.ipv6.conf.all.forwarding和net.ipv4.ip_forward是否都为1? - 检查翻译规则:nftables或iptables的规则是否正确匹配?用
nft list ruleset或iptables -t nat -L -n -v查看计数器。 - 检查地址嵌入:ping的目标地址是否符合翻译前缀?比如NAT64前缀是64:ff9b::/96,目标地址必须是64:ff9b::开头的。
- 检查防火墙:翻译器本身的防火墙是否放行了ICMPv6和ICMPv4?
我遇到过一次诡异的情况:所有配置都正确,但ping就是不通。后来用tcpdump在翻译器两侧同时抓包,发现IPv6侧的包到了,但IPv4侧没有发出。进一步检查发现是nftables的prerouting链优先级设成了-100,但内核的conntrack模块在-200优先级就处理了,导致包被conntrack丢弃。把优先级改成-200之后问题解决。
5.2 大包不通、小包正常的典型原因
这个问题几乎100%是MTU和分片处理导致的。IPv6要求链路MTU至少1280字节,IPv4常见MTU是1500字节。如果翻译器没有正确处理分片,大包就会被丢弃。
排查方法:
- 用
ping -s 1400和ping -s 100对比测试,确认是否是大包问题。 - 在翻译器上抓包,看是否有ICMPv6的“Packet Too Big”消息。
- 检查翻译器的MTU设置,确保IPv6侧和IPv4侧的MTU匹配。
解决方案通常是开启翻译器的分片重组功能,或者调整MTU让包不需要分片。在Linux上,可以设置net.ipv6.conf.all.mtu和net.ipv4.ipfrag_high_thresh来调整。
5.3 会话表满导致新连接失败
有状态翻译的会话表有容量限制。当并发连接数超过表容量时,新连接会被拒绝。表现是:已经建立的连接正常,但新连接超时。
排查方法:
- 用
conntrack -C查看当前会话数。 - 用
sysctl net.netfilter.nf_conntrack_max查看最大会话数。 - 如果接近上限,可以调大
nf_conntrack_max,或者缩短会话超时时间。
但调大会话表只是治标,根本解决要么是增加翻译器的内存,要么是优化应用减少短连接。我见过一个场景:内部应用每秒钟发起上千个短连接,NAT64会话表几分钟就满了。后来把应用的连接池打开,复用长连接,问题才解决。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查命令 | 解决方向 |
|---|---|---|---|
| ping不通 | 路由缺失 | ip route/ip -6 route | 补路由 |
| ping不通 | 转发未开 | sysctl net.ipv4.ip_forward | 开启转发 |
| ping不通 | 规则未匹配 | nft list ruleset | 检查规则 |
| 大包不通 | MTU/分片 | ping -s 1400 | 调整MTU或开启重组 |
| 新连接失败 | 会话表满 | conntrack -C | 调大上限或优化连接 |
| FTP数据传输失败 | ALG未开 | 抓包看PORT命令 | 开启FTP ALG |
| 语音单通 | SIP ALG未开 | 抓包看SDP | 开启SIP ALG |
| QoS失效 | TOS映射错误 | 抓包看DSCP | 检查翻译器TOS映射 |
5.5 独家避坑技巧
第一个技巧:在翻译器上同时抓两侧的包。只抓一侧往往看不出问题,因为问题可能出在翻译逻辑本身。同时抓包可以对比翻译前后的字段变化,快速定位是哪个字段翻译错了。
第二个技巧:用conntrack的事件模式观察会话建立过程。conntrack -E可以实时显示会话的创建、更新和销毁事件,比看静态表更直观。当会话建立失败时,事件模式会显示是哪一步出了问题。
第三个技巧:测试时先用静态地址,再用动态地址。静态地址翻译(比如1:1的SIIT)排除了端口分配和会话表的干扰,更容易定位地址嵌入的问题。等静态翻译通了,再切换到有状态翻译。
第四个技巧:注意IPv6的临时地址和隐私扩展。很多操作系统默认开启IPv6隐私扩展,源地址会随机变化。这会导致NAT64会话表里出现大量短命会话,加速表满。如果不需要隐私扩展,可以在终端上关闭它,或者配置翻译器使用更长的超时时间。
6. 从协议翻译到工程选型:一些个人体会
地址翻译器这个领域,协议标准很清晰,但工程实现差异极大。同样是NAT64,不同厂商的设备在会话表容量、ALG支持、故障切换行为上可能完全不同。我在选型时一般会关注几个点:会话表是内存还是持久化存储、ALG支持列表是否覆盖业务协议、故障切换时会话是否同步、有没有详细的日志和监控接口。
另外,地址翻译器不是万能的。它解决的是“互通”问题,不解决“性能”问题。翻译本身有开销,有状态翻译的开销更大。如果流量规模很大,翻译器很容易成为瓶颈。在这种情况下,更合理的做法是推动业务系统直接支持双栈,而不是依赖翻译器。翻译器更适合作为过渡期的临时方案,或者用于那些确实无法改造的老旧系统。
最后再分享一个小技巧:在调试翻译器时,把TTL或跳数限制设成一个特殊值(比如64),然后在抓包时过滤这个值,可以快速区分哪些包是翻译后的,哪些是原始包。这个方法在复杂的多跳环境里特别有用,能帮你快速理清包的流向。