2668 字
约 8 分钟
13
S02-X04 · IPv4 与 IPv6:地址、报文与过渡技术

线上有两类"玄学故障":加了 AAAA 记录后一部分用户打不开;服务明明起着,IPv6 客户端连不上。根都在地址层与套接字层。

内容按四层走:地址怎么表示 → NAT 改了什么 → 报文长什么样 → 双栈机器绑在哪,结论按 JDK 17 + Linux 6.x 核对。


1. 地址结构:分类地址已废弃,一切按 CIDR

IPv4 是 32 位点分十进制。A/B/C 类(按首位划 8/16/24 位网络号)是 1981 年的产物,1993 年 CIDR 落地后在路由层面已不再使用:信息全在"前缀长度"里,192.168.10.37/26 的 /26 才有意义。

前缀 点分掩码 地址总数 可用主机 典型用途
/8 255.0.0.0 16,777,216 16,777,214 云厂商大块分配
/16 255.255.0.0 65,536 65,534 常见分配粒度
/24 255.255.255.0 256 254 单段内网
/30 255.255.255.252 4 2 点对点互联
/31 255.255.255.254 2 2 RFC 3021,两端都可用
/32 255.255.255.255 1 1 主机路由

两处易记反:/31 在 RFC 3021 下不再浪费网络号与广播号;/32 是主机路由而非网段。判同网段只能拿掩码做按位与——10.0.1.5/16 与 10.0.2.5/24 会一方认为同段、一方认为不同,这种不对称是"能 ping 通却连不上"的经典来源。

$ ipcalc 192.168.10.37/26        # 典型输出
Network: 192.168.10.0/26   HostMin: 192.168.10.1   HostMax: 192.168.10.62
Hosts/Net: 62

2. NAT:常态化的代价

IPv4 耗尽后的事实续命方案不是 IPv6 而是 NAT:把"私有地址 + 端口"映射成"公网地址 + 端口"。代价不在性能,在连接语义被改写。

NAT 破坏的东西 表现 后果
端到端可达 外网不能主动连内网主机 需端口映射、反向隧道、中继
端口空间 单 IP 仅 65535 端口,按五元组复用 高并发下连接建立失败
载荷带 IP 的协议 FTP 主动模式、SIP、部分联机协议 对端拿到私有地址,无法回连
分片 改地址必须重算校验和,分片需重组 部分设备直接丢弃
连接跟踪 每条连接占一条 conntrack 表满后新连接被丢
P2P 双方都在 NAT 后 只能 STUN 打洞或 TURN 中继
$ iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE        # 出口 SNAT
$ iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 8080 \
      -j DNAT --to-destination 10.0.0.12:8080                 # 入站端口映射
$ conntrack -C && sysctl net.netfilter.nf_conntrack_max       # 看跟踪表余量
$ dmesg | grep -i "nf_conntrack: table full"

容器里这层是隐藏的:Docker bridge 一层、宿主机再一层,"内网看到的源 IP 全是网关"。要拿真实源地址只能靠 X-Forwarded-For 或 PROXY protocol。


3. IPv6 的表示法:三条硬规则

128 位、8 组、每组 4 位十六进制。写错了是解析失败,不是"容忍":

  1. 每组前导零可省(0db8 → db8);
  2. :: 只能出现一次,2001::db8::1 非法(无法确定各压缩几组);
  3. RFC 5952 规定不得用 :: 压缩单个零组,不得混排两种 v4 写法。
完整形式 规范写法 说明
2001:0db8:0000:0000:0000:0000:0000:0001 2001:db8::1 连续零组压缩
2001:0db8:0000:0001:0000:0000:0000:0001 2001:db8:0:1::1 只压最长零段
2001:0db8:0000:0000:0001:0000:0000:0001 2001:db8::1:0:0:1 等长零段压靠前一段
0000:0000:0000:0000:0000:0000:0000:0000 :: 未指定地址(DAD 期间作源)
0000:0000:0000:0000:0000:ffff:0a00:0105 ::ffff:10.0.1.5 v4 映射,双栈套接字内部用
$ ip -6 addr show dev eth0            # 典型输出
    inet6 2001:db8:1f0a:2c00::5/64 scope global dynamic
    inet6 fe80::a00:27ff:fe4e:66a1/64 scope link

链路本地地址(fe80::/10)永远存在、无需配置,是邻居发现与 SLAAC 的底座。

3.1 类型与前缀:没有广播

类型 IPv4 对应 IPv6 表现 说明
单播 有 有 一对一
组播 224.0.0.0/4 ff00::/8 接管了广播的职能
任播 靠 BGP 宣告同前缀 同地址多台宣告,路由取最近 一对"最近的一台"
广播 255.255.255.255 不存在 用 ff02::1(all-nodes)替代

常用前缀:::1/128 环回、::/128 未指定(DAD 源地址占位)、fe80::/10 链路本地(邻居发现与下一跳)、fc00::/7 ULA(替代 10/8 与 192.168/16)、2000::/3 全局单播、ff00::/8 组播。广播取消的直接后果是 ARP 没了,改用 NDP,也不再能"广播探测整个网段"。

$ tcpdump -ni eth0 'icmp6 and (ip6[40] == 135 or ip6[40] == 136)'   # NS/NA
$ ip -6 neigh show && ip -6 route show default

4. 报文头:定长 40 字节 vs 变长选项

IPv4 基本头 20 字节(含选项最长 60),每跳都要读选项、重算首部校验和。IPv6 把这两件事一起砍掉:基本头固定 40 字节,可选功能挪进扩展头链,首部校验和取消。

项 IPv4 IPv6
基本头 20 字节(最长 60) 固定 40 字节
地址字段 源 4 + 目的 4 源 16 + 目的 16
首部校验和 有,每跳重算 无
分片 路由器可分片 仅源端分片(Fragment 头)
选项 挤在首部内 扩展头链(Hop-by-Hop/Routing/Fragment)
生存时间 TTL Hop Limit
上层协议 Protocol Next Header
地址解析 ARP + 广播 NDP + 组播

定长头的代价是"新功能都挂扩展头",而不少中间设备对扩展头处理不完整会直接丢包——这是"IPv6 能通但某段不通"的常见根源。


5. 自动配置:SLAAC 与 DHCPv6

维度 SLAAC DHCPv6(stateful)
谁定地址 RA 前缀 + 主机自生成接口标识 服务器分配并记租约
需要服务器 不需要 需要
中心租约表 无 有,可审计
DNS 下发 RDNSS(RFC 8106) option 23
RA 开关 A 标志 M/O 标志
适用 家庭、容器/云网络 企业审计、固定分配
$ sysctl net.ipv6.conf.eth0.use_tempaddr      # 1 = 启用隐私临时地址
$ rdisc6 -1 eth0                              # 抓 RA,看前缀与 A/M/O flags

6. 过渡技术:双栈、隧道、翻译

类别 代表 原理 适用面 主要局限
双栈 主机与网关同时配 v4/v6 两套栈并行、独立路由 最稳,长期方向 仍需 v4 地址,运维两套
隧道 6in4、6rd、DS-Lite、GRE 一种协议封装进另一种 过渡期孤岛互联 MTU 变小、PMTUD 易失效
翻译 NAT64 + DNS64 改写地址/端口跨协议访问 纯 v6 网访问存量 v4 服务 破坏端到端,载荷带 IP 的协议失效
$ dig +short AAAA ipv4only.arpa @10.0.0.53
64:ff9b::c000:aa        # 落在 RFC 6052 前缀 64:ff9b::/96 内即为 DNS64 合成

隧道最常踩 MTU:外层封装吃掉 20~60 字节,内层仍按 1500 发,遇不允许分片的 ICMPv6 被丢就表现为小包通、传文件卡死。


7. 监听绑定:0.0.0.0 和 [::] 不是一回事

bind(0.0.0.0:8080) 只接受 IPv4;bind([::]:8080) 在 IPV6_V6ONLY=0 时接受 IPv6 和 IPv4(v4 客户端以 ::ffff:a.b.c.d 映射地址进来)。默认值来自 net.ipv6.bindv6only。

$ ss -lntp | grep 8080
LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("java",pid=5123,fd=88))   # 仅 v4
LISTEN 0 4096    [::]:8080    [::]:* users:(("java",pid=5123,fd=88))   # 双栈
绑定写法 实际接受 依赖 现象
0.0.0.0 仅 IPv4 — IPv6 客户端 Connection refused
[::] 且 V6ONLY=0 v4 + v6 bindv6only=0(多数发行版默认) 两者都通
[::] 且 V6ONLY=1 仅 v6 bindv6only=1 或显式 setsockopt v4 连不上
127.0.0.1 仅 v4 回环 — localhost 解析到 ::1 时本机也连不上
[::1] 仅 v6 回环 — 镜像问题
# Spring Boot 3.3.x
server:
  port: 8080
  address: "::"        # 双栈;只要 v4 写 0.0.0.0
server {
    listen 80;
    listen [::]:80;          # nginx 不会自动继承,必须显式写
    # 或一条:listen [::]:80 ipv6only=off;
}

-Djava.net.preferIPv4Stack=true 让 InetAddress 只返回 v4 结果,长期用等于放弃 v6。


8. 排错:五种典型现象

现象 原因 定位 处理
移动网络用户打不开,桌面正常 AAAA 指向未监听 v6 的地址 dig +short AAAA 域名、curl -6 补 v6 监听或暂撤 AAAA
curl http://[::1]:8080 拒绝 只绑了 v4 ss -lntp 改 address: "::"
放行 v4 没放行 v6 nftables/ip6tables 是独立规则集 nft list ruleset 两套规则同步生成
内网 v6 通、跨段不通 网关未下发前缀路由或 RA ip -6 route、ping -6 fe80::1%eth0 补路由与 RA
校验把合法地址判非法 手写正则漏 ::/单零组/映射地址 拿 2001:db8::1、::ffff:10.0.1.5 试 用库解析,不用正则

校验那条:手写正则几乎必错,要覆盖压缩、大小写、映射地址、zone id。

import ipaddress
ipaddress.ip_address("2001:db8::1").version      # 6
ipaddress.ip_address("2001:db8::1::2")           # ValueError:`::` 两次
ipaddress.ip_address("::ffff:10.0.1.5").version  # 6

9. 边界:什么情况不值得动

场景 更合适的做法 理由
端到端直连(P2P、VoIP、联机) 双栈 + STUN,TURN 兜底 打洞成功率不是 100%
大量存量 v4-only 设备 双栈并保留 NAT44 翻译层破坏载荷带 IP 的协议
只需内网隔离 ULA + 不设默认路由 比 NAT 彻底,不需改写地址
当前无 v6 需求 维持 v4,但把监听改 :: 成本为零,避免迁移才发现问题

一句话:IPv4 靠 NAT 掩盖问题,IPv6 把问题还原成"每台机器都有地址",所有依赖"地址不可达"的假设都要重查。


10. 自测

  1. 10.0.1.5/16 与 10.0.2.5/24 互相认为对方在同网段吗?这种不对称会引发什么现象?
  2. NAT 让 P2P 变难的根本原因是什么?STUN 在什么条件下才有效?
  3. 2001:db8::1:0:0:1 展开是哪 8 组?2001::db8::1 为什么非法?
  4. IPv6 没有广播,"找网段内所有主机"靠什么?ARP 被什么替代?
  5. IPv6 取消首部校验和,可靠性由谁保证?会带来什么新的丢包风险?
  6. 服务绑 0.0.0.0:8080,curl http://[::1]:8080 会怎样?改哪项配置能同时支持两者?
S02-X04 · IPv4 与 IPv6:地址、报文与过渡技术
https://www.clxhxhhr.top/posts/5794/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。