跳到内容

第 4 卷 / 第 9 章 / 第 1 课

9.1 IPv4 首部、CIDR 与最长前缀转发 ​

IP 让数据报跨网络逐跳转发,却不保证送达、顺序、去重或保密。路由器根据目的地址查转发表,选择下一跳。本课先检查一个 IPv4 报文,再用地址集合解释 CIDR 和最长前缀匹配,避免把“路由表里有一行”当成通信已经成功。

1. IPv4 地址用于定位,不等于设备身份 ​

地址配置在接口或逻辑端点上,可以随接入网络改变。一台机器可以有多个地址,多个用户也可能经 NAT 共用一个公网地址。私有地址、链路本地地址、环回、多播和任播部署,都不符合“一台机器一个全球唯一 IP”的简化模型。

文档示例使用 RFC 5737 保留的 192.0.2.0/24、198.51.100.0/24、203.0.113.0/24。这些地址用于说明和隔离实验,不应期待它们在公网可达。IP 地址本身也不能证明用户或设备可信。

2. IPv4 首部有哪些字段 ​

最短首部是 20 字节,IHL 最大可表示 60 字节:

字段作用
VersionIPv4 的版本值为 4;值为 6 的报文应交给 IPv6 解析器
IHL首部长度,以 32 位字为单位
DSCP/ECN服务质量分类与显式拥塞通知
Total Length首部加载荷的总字节数
Identification/Flags/Fragment OffsetIPv4 分片与重组,见 8.3
TTL路由转发时至少减 1,降至 0 时丢弃,避免无限循环
Protocol载荷的协议编号,例如 ICMP=1、TCP=6、UDP=17
Header Checksum只覆盖 IPv4 首部;TTL 改变后需要更新
Source/Destination32 位源地址与目的地址
Options + padding可选字段与填充,使首部按 32 位对齐

已经生成的 IPv4 数据报超过出口 MTU 时,要结合 DF 标志决定分片或丢弃并反馈 ICMP。TCP 分段是发送端在构造报文时的另一层处理,路由器不会替 TCP 重新分段。分片与路径 MTU 发现见8.3。

3. 检查 IHL 和 Total Length ​

下面是 Python 3 的教学解析器,输入从 IPv4 首部开始,不包含以太网头。保存为 ipv4_parse.py 后运行 python3 ipv4_parse.py。末尾构造 IHL=6、载荷为 ping 的测试字节;校验和暂填 0,专门测试长度检查,不是可直接发到网络上的有效 UDP 报文。

python
from dataclasses import dataclass
import ipaddress
import struct

@dataclass(frozen=True)
class IPv4Packet:
    source: ipaddress.IPv4Address
    destination: ipaddress.IPv4Address
    protocol: int
    ttl: int
    header: bytes
    payload: bytes

def parse_ipv4(raw: bytes) -> IPv4Packet:
    if len(raw) < 20:
        raise ValueError("shorter than the minimum IPv4 header")

    version_ihl = raw[0]
    version = version_ihl >> 4
    ihl_words = version_ihl & 0x0F
    if version != 4:
        raise ValueError(f"expected IPv4, got version {version}")
    if ihl_words < 5:
        raise ValueError("invalid IPv4 IHL")

    header_length = ihl_words * 4
    if len(raw) < header_length:
        raise ValueError("truncated IPv4 options/header")

    total_length = struct.unpack("!H", raw[2:4])[0]
    if total_length < header_length:
        raise ValueError("total length is smaller than header length")
    if len(raw) < total_length:
        raise ValueError("truncated IPv4 packet")

    return IPv4Packet(
        source=ipaddress.IPv4Address(raw[12:16]),
        destination=ipaddress.IPv4Address(raw[16:20]),
        protocol=raw[9],
        ttl=raw[8],
        header=raw[:header_length],
        payload=raw[header_length:total_length],
    )

raw = struct.pack(
    "!BBHHHBBH4s4s", 0x46, 0, 28, 1, 0, 64, 17, 0,
    ipaddress.IPv4Address("192.0.2.1").packed,
    ipaddress.IPv4Address("192.0.2.2").packed,
) + b"\x01" * 4 + b"ping"
packet = parse_ipv4(raw)
print(len(packet.header), packet.payload)
try:
    parse_ipv4(raw[:-1])
except ValueError as error:
    print(error)

预期输出:

text
24 b'ping'
truncated IPv4 packet

首部占 24 字节,固定跳过 20 字节会把选项误当载荷;截短一个字节则触发错误。生产解析器还需检查首部校验和、分片标志、选项和抓包截断策略。这里返回的是当前分片的载荷,没有执行重组,也没有验证 UDP/TCP。超出 Total Length 的尾随字节被忽略。

4. CIDR 前缀定义一个地址集合 ​

192.0.2.0/24 固定前 24 位,剩余 8 位可变。IPv4 前缀长度范围为 0–32,不再依赖 A/B/C 类地址边界。下面的 Python 3 示例把主机地址归入所在子网:

python
import ipaddress

network = ipaddress.ip_network("192.0.2.14/28", strict=False)
print(network.network_address)    # 192.0.2.0
print(network.broadcast_address)  # 192.0.2.15
print(network.num_addresses)      # 16
print(list(network.hosts()))

前三行依次输出 192.0.2.0、192.0.2.15 和 16;最后一行列出 .1 到 .14 共 14 个主机地址。strict=False 允许输入带有主机位的地址,并归一化为网络前缀。

“每个子网都减掉网络地址和广播地址”有例外:

  • RFC 3021 允许点到点链路把 /31 的两个地址都用于端点;
  • /32 表示单个地址,常用于环回端点或主机路由;
  • 云平台还可能保留额外地址,可用数量要按平台规则计算。

IPv6 没有广播地址,不能直接套用 IPv4 的计算口诀。

5. 最长前缀匹配选择转发表项 ​

假设转发信息库(FIB)包含以下三项:

text
0.0.0.0/0        -> gateway A
203.0.113.0/24   -> gateway B
203.0.113.128/25 -> gateway C

203.0.113.200 同时匹配三项,最长的 /25 最具体,所以选择 C。没有更具体的匹配时,才使用默认路由 /0。下面是可直接运行的 Python 3 示例,断言通过时没有输出:

python
import ipaddress

routes = [
    (ipaddress.ip_network("0.0.0.0/0"), "gateway-a"),
    (ipaddress.ip_network("203.0.113.0/24"), "gateway-b"),
    (ipaddress.ip_network("203.0.113.128/25"), "gateway-c"),
]

def longest_prefix_match(address: str) -> str:
    target = ipaddress.ip_address(address)
    matches = [(prefix.prefixlen, next_hop) for prefix, next_hop in routes
               if target in prefix]
    if not matches:
        raise LookupError("no route")
    return max(matches, key=lambda match: match[0])[1]

assert longest_prefix_match("203.0.113.200") == "gateway-c"
assert longest_prefix_match("198.51.100.8") == "gateway-a"

示例对每个前缀只配置一个下一跳,线性扫描的复杂度为 O(n),不实现同前缀选优、策略路由或等价多路径。实际路由器先在路由信息库(RIB)中处理协议和静态配置产生的候选,再把活动转发表项写入 FIB。最长前缀匹配负责逐包查找,不代替 OSPF/BGP 的路由选优。

得到出口与下一跳后,在需要邻居解析的链路上再做 ARP 或 IPv6 的 NDP。跨互联网转发不会一直携带远端主机的 MAC 地址。

6. 路由聚合也是转发责任 ​

192.0.2.0/25 与 192.0.2.128/25 可以聚合成 192.0.2.0/24,减少上游表项。聚合发布后,即使其中一半暂时不可达,上游仍会把流量送来。聚合点通常需要丢弃路由和成员路由的健康策略,避免把无法细分的流量沿默认路由又送回上游。

聚合只适用于对齐、连续的地址范围,任意四个 /24 不能直接变成一个 /22。ipaddress.collapse_addresses() 可以计算保持相同地址集合的最简前缀集合,但是否发布聚合,还要看实际可达性与策略。省下一行路由,转发责任并不会跟着消失。

7. 验收问题 ​

  1. 解析器为什么检查 IHL,而不能固定跳过 20 字节?
  2. /31 和 /32 为什么不能套“减掉网络与广播地址”的口诀?
  3. RIB 候选选优与 FIB 最长前缀查找有什么区别?
  4. /26 与 /25 都匹配目的地址时,哪一项更具体?
  5. 聚合范围内某条成员路由消失后,流量仍可能被送到哪里?

下一课进入9.2 ICMP、ping 与 traceroute 的证据边界。

参考 ​

Built with VitePress | Software Systems Atlas