土法炼钢兴趣小组的算法知识备份

【网络工程】IP 协议深度解剖:首部、分片与路由

文章导航

分类入口
network
标签入口
#IP#IPv4#TTL#fragmentation#routing#PMTUD#tcpdump

目录

你在排查一个超时问题。traceroute 显示第 5 跳之后就没有响应了,但 ping 目标地址是通的。你把 traceroute 换成 TCP 模式(traceroute -T),突然又通了。为什么?

另一个场景:你的 VPN 用户报告某些网站打不开,但直连没问题。你抓包发现服务端一直在重传同样大小的 IP 包,而且这些包都带着 DF(Don’t Fragment)标志。VPN 隧道的封装开销把有效 MTU 降到了 1400,但服务端还在发 1500 字节的包。

这些问题的根因都藏在 IP 协议首部的几个字段里。IP 是网络层的核心协议,它负责寻址和路由——把包从源送到目的地。但 IP 首部不只是”源地址+目的地址”那么简单,每个字段都有工程含义,理解它们是排查网络问题的基础。

一、IPv4 首部逐字段工程解读

IPv4 首部最小 20 字节,最大 60 字节(含选项)。下面是完整的首部结构:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version|  IHL  |    DSCP   |ECN|         Total Length          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Identification        |Flags|     Fragment Offset     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Time to Live |    Protocol   |        Header Checksum        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       Source Address                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Destination Address                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Options (if IHL > 5)                       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

tcpdump 直接观察一个 IP 包的首部:

# 以十六进制方式抓包,显示 IP 首部细节
tcpdump -i eth0 -nn -X -c 1 icmp
# 输出示例(截取 IP 首部部分):
# 0x0000:  4500 0054 e4a7 4000 4001 ...
#          ^^^^                         Version(4) + IHL(5) = 20 字节首部
#              ^^^^                     DSCP(0) + ECN(0) = 普通服务
#                   ^^^^                Total Length = 0x0054 = 84 字节
#                        ^^^^           Identification = 0xe4a7
#                             ^^^^      Flags(DF=1) + Fragment Offset(0)
#                                  ^^   TTL = 0x40 = 64
#                                    ^^ Protocol = 0x01 = ICMP

下面逐字段分析它们的工程含义。

Version 和 IHL

Version(4 位):IPv4 固定为 4。看起来没什么用,但在排查时有价值——如果抓包看到 Version 不是 4 也不是 6,说明包解析位置偏移了,通常是封装层次没对齐。

IHL(Internet Header Length,4 位):以 4 字节为单位的首部长度。最小值 5(20 字节,无选项),最大值 15(60 字节)。在实际网络中,绝大多数 IP 包的 IHL 都是 5——带选项的包几乎已经绝迹。

# 统计抓包文件中 IP 首部长度的分布
tcpdump -r capture.pcap -nn 'ip[0] & 0x0f != 5' -c 10
# 如果有输出,说明存在带 IP 选项的包——这很罕见,值得关注

DSCP 和 ECN

原始设计中这个字节叫 ToS(Type of Service),现在被重新定义为 DSCP(Differentiated Services Code Point,6 位)和 ECN(Explicit Congestion Notification,2 位)。

DSCP 用于 QoS(Quality of Service)分类。常见的 DSCP 值:

DSCP 值 Per-Hop Behavior 典型用途
0 BE(Best Effort) 普通流量(默认)
46(EF) Expedited Forwarding VoIP 语音、实时视频
34(AF41) Assured Forwarding 41 视频会议
26(AF31) Assured Forwarding 31 关键业务应用
10(AF11) Assured Forwarding 11 低优先级批量传输
8(CS1) Class Selector 1 后台流量(备份等)

工程上的关键问题:DSCP 标记在跨网络边界时经常被重写或清零。 你在源端设置了 EF(Expedited Forwarding),经过运营商网络后可能变成 0。这意味着端到端的 QoS 策略很难在公网上保证,DSCP 的实际价值主要在企业内部网络和数据中心内部。

# 用 tcpdump 观察 DSCP 标记
tcpdump -i eth0 -nn -v 'ip and (ip[1] & 0xfc) != 0' -c 5
# -v 选项显示 TOS/DSCP 字段
# 过滤条件:DSCP 非零的包

# 用 iptables 设置出站包的 DSCP
iptables -t mangle -A OUTPUT -p tcp --dport 5060 -j DSCP --set-dscp-class EF
# 将 SIP 流量标记为 EF(语音优先)

ECN(2 位)是一个极其重要但部署缓慢的机制。它允许路由器在即将发生拥塞时,通过修改 IP 首部的 ECN 字段来通知端点,而不是丢弃数据包。ECN 与 TCP 拥塞控制的交互将在 TCP 工程系列中详细讨论。

# 检查系统是否启用了 ECN
sysctl net.ipv4.tcp_ecn
# 0 = 禁用(默认)
# 1 = 启用(主动请求和被动接受)
# 2 = 只被动接受(推荐的保守设置)

Total Length

16 位,表示整个 IP 数据报(首部 + 数据)的字节数。最大值 65535。

工程上的意义:Total Length 是分片重组的关键。 接收端通过所有分片的 Fragment Offset 和 Total Length 来判断是否收集齐了所有分片。

一个常被忽视的点:以太网帧有最小长度限制(64 字节,含 FCS)。 如果 IP 包太小(比如一个只有 20 字节首部的 ACK),以太网层会添加填充(padding)到最小帧长度。这个 padding 会出现在 IP 层的 Total Length 之后——IP 层通过 Total Length 知道真正的数据在哪里结束,忽略尾部的 padding。

# 观察小包的以太网 padding
tcpdump -i eth0 -nn -e -X 'tcp[tcpflags] == tcp-ack and len == 0' -c 1
# -e 显示链路层信息,可以看到帧长度 > IP Total Length

Identification、Flags 和 Fragment Offset

这三个字段共同控制 IP 分片与重组。它们是本文的重点之一,将在下一节详细讨论。

TTL(Time to Live)

8 位,名字叫”生存时间”,但实际语义是跳数限制(Hop Limit)。每经过一个路由器减 1,到 0 时路由器丢弃该包并返回 ICMP Time Exceeded(type 11)消息。

TTL 保护什么? 防止路由环路导致的包在网络中无限循环。没有 TTL,一个路由配置错误就能让一个包永远在两个路由器之间弹来弹去,消耗带宽和处理资源。

不同操作系统的默认 TTL 不同,这可以用于操作系统指纹识别(OS Fingerprinting):

操作系统 默认 TTL
Linux 64
Windows 128
macOS / iOS 64
Cisco IOS 255
FreeBSD 64
Solaris 255
# 查看和修改 Linux 的默认 TTL
sysctl net.ipv4.ip_default_ttl
# 默认 64

# 通过 TTL 估算距离
ping -c 1 目标IP
# 如果收到的 TTL=52,初始 TTL 可能是 64,经过了 12 跳
# 如果收到的 TTL=117,初始 TTL 可能是 128(Windows),经过了 11 跳

TTL 的工程应用

  1. traceroute 的原理:发送 TTL=1, 2, 3, … 的包,收集每一跳返回的 ICMP Time Exceeded 消息,从中提取路由器 IP 地址
  2. 多播范围限制:TTL=1 限制多播在本地子网内,TTL=32 限制在本站点内
  3. 安全防护:BGP 使用 TTL Security(GTSM,RFC 5082),要求 BGP 包的 TTL ≥ 254(即最多经过 1 跳),防止远程攻击者伪造 BGP 包
# 用不同 TTL 值追踪路由路径
traceroute -m 30 目标IP
# -m 30 设置最大 TTL 为 30

# 观察 TTL 在每一跳的变化
tcpdump -i eth0 -nn -v 'icmp[0] == 11' -c 5
# 捕获 ICMP Time Exceeded 消息

Protocol

8 位,标识上层协议。常见值:

协议号 协议 说明
1 ICMP 网络控制消息
2 IGMP 多播组管理
6 TCP 传输控制协议
17 UDP 用户数据报协议
47 GRE 通用路由封装(隧道)
50 ESP IPsec 加密载荷
51 AH IPsec 认证头
89 OSPF 开放最短路径优先(路由协议)
132 SCTP 流控制传输协议

在排查防火墙问题时,Protocol 字段非常重要——防火墙规则可能只放行 TCP(6)和 UDP(17),忘记放行 GRE(47)或 ESP(50)就会导致 VPN 隧道不通。

# 统计抓包文件中的协议分布
tcpdump -r capture.pcap -nn -q 2>/dev/null | awk '{print $3}' | sort | uniq -c | sort -rn | head
# 或者直接按协议号过滤
tcpdump -i eth0 -nn 'ip proto 47' -c 5   # 只抓 GRE 包
tcpdump -i eth0 -nn 'ip proto 50' -c 5   # 只抓 ESP 包

Header Checksum

16 位,只校验 IP 首部(不包括数据部分)。每经过一个路由器都要重新计算,因为 TTL 变了。

一个重要的工程现实:现代网卡几乎都支持 IP 校验和卸载(Checksum Offload)。 这意味着在发送端用 tcpdump 抓包时,你可能看到 IP 首部校验和为 0 或者不正确——这不是 bug,而是网卡还没来得及计算(抓包发生在网卡计算校验和之前)。

# 查看网卡校验和卸载设置
ethtool -k eth0 | grep checksum
# 输出示例:
# rx-checksumming: on
# tx-checksumming: on
#   tx-checksum-ipv4: on
#   tx-checksum-ipv6: on

# 如果在发送端看到校验和错误,先检查是否是卸载导致的
# 关闭卸载后再抓包(仅调试用,别在生产环境持续关闭)
# ethtool -K eth0 tx off

源地址和目标地址

各 32 位。看起来简单直白,但有几个工程细节值得注意:

  1. 源地址选择:当一台多网卡的 Linux 主机发送包时,源 IP 地址的选择遵循路由表的 src 指示。如果路由条目没有指定 src,内核会选择出口网卡上”最合适”的 IP(通常是与目标同一子网的地址,或者主要地址)。这个行为在多网卡服务器上经常导致困惑。

  2. NAT 对源地址的影响:经过 SNAT(包括 MASQUERADE)后,包的源地址被改写。在排查时需要注意抓包的位置——在 NAT 之前和之后看到的源地址不同。

# 查看路由表中的源地址选择
ip route get 8.8.8.8
# 输出示例:
# 8.8.8.8 via 10.0.0.1 dev eth0 src 10.0.0.100 uid 0
#                                     ^^^^^^^^^^^^ 出站包会使用这个源 IP

# 查看所有网卡的 IP 地址
ip -4 addr show | grep inet

二、IP 分片:为什么它在现代网络中几乎是 bug

IP 协议设计之初就支持分片(Fragmentation):当一个 IP 数据报大于链路的 MTU 时,路由器可以把它拆成多个小的分片(Fragment),每个分片独立传输,到达目的地后重组。

这个设计在 1981 年看起来很合理——让 IP 层透明地处理不同链路的 MTU 差异。但在 40 多年后的今天,IP 分片在生产环境中几乎总是有害的。

分片的机制

三个首部字段控制分片行为:

# 故意制造一个需要分片的 IP 包(关闭 DF 位)
ping -M dont -s 4000 -c 1 目标IP
# -M dont 清除 DF 位,允许路由器分片
# -s 4000 发送 4000 字节的 ICMP payload

# 观察分片
tcpdump -i eth0 -nn -v 'ip[6] & 0x20 != 0 or (ip[6:2] & 0x1fff) != 0' -c 10
# 过滤条件:MF 标志=1 或 Fragment Offset > 0

为什么 IP 分片是有害的

分片在工程上有五个严重问题:

1. 任何一个分片丢失,整个数据报作废。 IP 层不提供分片级的重传机制。如果一个 4000 字节的数据报被分成 3 个分片,丢了第 2 个分片,接收端必须等待重组超时后丢弃已收到的分片。重传由上层(TCP)负责,而 TCP 会重传整个段(不只是丢失的分片),导致带宽浪费。

2. 分片重组消耗接收端资源。 接收端需要分配内存缓冲区保存不完整的分片集合,并维护重组定时器。这是 DDoS 攻击的攻击面——攻击者可以发送大量不完整的分片,耗尽接收端的重组缓冲区。

3. 分片破坏了中间设备的过滤能力。 防火墙、NAT 设备需要检查 TCP/UDP 端口号来做过滤——但端口号只在第一个分片中(因为传输层首部在数据报的开头)。后续分片没有端口信息,防火墙要么放行(安全风险),要么丢弃(功能故障),要么维护分片重组状态(性能开销)。

4. 分片增加了乱序的概率。 不同分片可能走不同的路径(ECMP 场景),到达顺序可能和发送顺序不同,增加了重组的复杂性和延迟。

5. Identification 字段只有 16 位。 在高速网络中,Identification 可能在几秒内就回绕(wrap around),导致不同数据报的分片被错误地混合重组。RFC 6864 明确指出这个问题,并建议避免分片。

# 检查系统的分片重组统计
cat /proc/net/snmp | grep -A 1 "^Ip:"
# 关注以下字段:
# ReasmReqds  - 收到需要重组的分片数
# ReasmOKs    - 成功重组的数据报数
# ReasmFails  - 重组失败的数据报数
# FragCreates - 本机创建的分片数
# FragOKs     - 本机成功分片的数据报数
# FragFails   - 本机分片失败的次数(因为设了 DF)

# 用 nstat 看增量统计
nstat -z | grep -iE "frag|reasm"
# IpReasmTimeout   - 重组超时的次数
# IpReasmReqds     - 收到的分片数
# IpReasmOKs       - 重组成功
# IpReasmFails     - 重组失败

DF 位与 PMTUD 的交互

现代 TCP 栈几乎都会在 IP 首部设置 DF 位——不允许路由器分片,而是通过 PMTUD 在源端就把包大小控制在路径 MTU 以内。 这是避免 IP 分片的正确做法。

DF 位和 PMTUD 的完整交互流程:

sequenceDiagram
    participant S as 发送端
    participant R as 中间路由器<br>MTU=1400
    participant D as 接收端

    Note over S: TCP MSS=1460<br>IP包大小=1500<br>DF=1

    S->>R: IP包(1500字节,DF=1)
    R-->>S: ICMP Frag Needed<br>Next-Hop MTU=1400

    Note over S: 更新 PMTU=1400<br>TCP MSS 调整为 1360

    S->>R: IP包(1400字节,DF=1)
    R->>D: IP包(1400字节)
    D->>S: ACK
# 查看内核缓存的 PMTU
ip route get 目标IP
# 如果有 PMTU 信息,会显示 mtu xxx

# 查看 PMTU 缓存的详细信息
ip route show cache | grep "mtu"

# 清除 PMTU 缓存(强制重新探测)
ip route flush cache

当 PMTUD 失败时(ICMP 被防火墙拦截),就会出现 PMTUD 黑洞——上一篇文章详细讨论了这个问题。解决方案包括 MSS Clamping、PLPMTUD 和手动降低 MTU。

分片与非分片的行为对比

一个具体的例子帮助理解分片的代价。假设发送一个 3000 字节的 UDP 数据报,链路 MTU 为 1500:

属性 不分片(DF=1) 分片(DF=0)
发送的 IP 包数 1 个(被路由器丢弃) 3 个分片
分片 1 1500 字节(含 20B IP首部 + 8B UDP首部 + 1472B 数据,MF=1,Offset=0)
分片 2 1500 字节(含 20B IP首部 + 1480B 数据,MF=1,Offset=185)
分片 3 88 字节(含 20B IP首部 + 48B 数据,MF=0,Offset=370)
IP 首部开销 20 字节 60 字节(3×20)
丢失一个分片的后果 源端收到 ICMP 错误 整个 3000 字节数据报丢失,需等待重组超时
防火墙能否过滤 可以(首部完整) 只能过滤第 1 个分片(有端口号),后续分片无法单独过滤

注意 Fragment Offset 的单位是 8 字节。1472 / 8 = 184,所以第二个分片的 Offset 是 185(从 0 开始)。只有第一个分片包含 UDP 首部,后续分片只有 IP 首部和数据。

什么场景下仍然会出现分片

尽管 TCP 使用 PMTUD 避免了分片,但以下场景仍然可能产生分片:

  1. UDP 大包:UDP 不做分段,应用发送大于 MTU 的 UDP 数据报时,IP 层会分片(除非应用设置了 DF 位)。DNS 查询超过 512 字节时就可能出现这个问题
  2. IPsec 隧道:ESP 封装增加了开销,如果内部 IP 包大小 + ESP 开销 > 外部 MTU,且内部包设置了 DF,IPsec 网关需要特殊处理
  3. GRE 隧道:和 IPsec 类似的封装开销问题
  4. NFS over UDP:传统的 NFS v3 over UDP 使用 8192 字节的 rsize/wsize,会产生大量分片
# 实时监控分片统计的变化
watch -n 1 'nstat -z 2>/dev/null | grep -iE "frag|reasm"'
# 如果 FragCreates 持续增长,说明本机在产生分片——需要排查原因

# 查找哪些进程在发送大 UDP 包
ss -unp | head -20
# 结合 tcpdump 确认是否有 UDP 分片
tcpdump -i eth0 -nn 'udp and (ip[6] & 0x20 != 0)' -c 5

分片攻击与防御

IP 分片是经典的攻击向量:

Teardrop 攻击:发送重叠的分片(Fragment Offset 计算错误),导致接收端在重组时内存溢出或崩溃。现代操作系统已经修复了这个漏洞,但它展示了分片实现的复杂性。

分片洪泛攻击:发送大量第一个分片(有传输层端口信息),但故意不发送后续分片。接收端分配内存等待重组,最终耗尽资源。

# 内核的分片重组相关参数
sysctl net.ipv4.ipfrag_time
# 分片重组超时时间(秒),默认 30

sysctl net.ipv4.ipfrag_high_thresh
# 重组缓冲区上限(字节),默认 4194304 (4MB)

sysctl net.ipv4.ipfrag_low_thresh
# 当缓冲区超过 high_thresh 后,清理到 low_thresh
# 默认 3145728 (3MB)

# 在高风险环境中,可以降低超时时间和缓冲区大小
sysctl -w net.ipv4.ipfrag_time=15
sysctl -w net.ipv4.ipfrag_high_thresh=2097152

三、IP 选项的安全风险

IPv4 首部支持可变长度的选项字段(IHL > 5 时存在)。虽然设计初衷是好的——提供路由记录、时间戳、源路由等功能——但在实际网络中,IP 选项几乎不被使用,而且是安全风险。

常见 IP 选项

选项类型 名称 功能 安全风险
0x07 Record Route 记录经过的每一跳路由器地址 暴露网络拓扑
0x44 Timestamp 记录每一跳的时间戳 暴露网络拓扑和时钟信息
0x83 Loose Source Route 指定包必须经过的路由器(可以有其他中间路由器) 绕过基于源地址的访问控制
0x89 Strict Source Route 指定包必须严格经过的路由器序列 同上,更严格

源路由(Source Routing) 是最危险的选项。它允许发送者指定包的转发路径,绕过正常的路由决策。攻击者可以利用这个功能:

# 检查系统是否接受源路由(应该禁止)
sysctl net.ipv4.conf.all.accept_source_route
# 0 = 不接受(推荐)
# 1 = 接受

# 确保禁用源路由
sysctl -w net.ipv4.conf.all.accept_source_route=0
sysctl -w net.ipv4.conf.default.accept_source_route=0

# 绝大多数互联网路由器和防火墙都会丢弃带源路由选项的包
# 但在内部网络中,最好在主机层面也禁用

IP 选项对性能的影响

带选项的 IP 包需要路由器进行额外处理。在现代高速路由器中,普通包走快速转发路径(Fast Path / ASIC 硬件转发),而带选项的包必须走慢速路径(Slow Path / CPU 处理)。这意味着带选项的包不仅自身处理慢,还可能影响路由器的整体性能。

这也是为什么很多运营商网络直接丢弃带 IP 选项的包——不是因为它们不理解这些选项,而是因为这些包必须走 CPU 路径,构成了潜在的 DoS 向量。

四、路由表查找:最长前缀匹配

IP 协议的另一个核心功能是路由——决定一个包应该从哪个接口、发给哪个下一跳路由器。路由表查找使用最长前缀匹配(Longest Prefix Match, LPM) 算法。

路由表基础

# 查看路由表
ip route show
# 输出示例:
# default via 10.0.0.1 dev eth0
# 10.0.0.0/24 dev eth0 proto kernel scope link src 10.0.0.100
# 172.16.0.0/16 via 10.0.0.254 dev eth0
# 172.16.1.0/24 via 10.0.0.253 dev eth0
# 192.168.0.0/16 via 10.0.0.252 dev eth0

# 查询特定目标的路由决策
ip route get 172.16.1.50
# 输出示例:
# 172.16.1.50 via 10.0.0.253 dev eth0 src 10.0.0.100
# 注意:匹配的是 172.16.1.0/24(/24 比 /16 更长),而不是 172.16.0.0/16

最长前缀匹配的工程含义

当多条路由都匹配目标地址时,选择前缀最长(子网掩码最具体)的那条。这个规则有重要的工程含义:

  1. 更具体的路由优先:/32(单个主机)优先于 /24(子网),/24 优先于 /16,/16 优先于 default(/0)
  2. 黑洞路由用于安全:添加一条 blackhole 路由可以丢弃特定目标的流量
  3. 路由泄漏事故:如果有人错误地宣告了一条更具体的路由(比如 /24),它会劫持本应走更粗路由(/16)的流量——这是 BGP 路由泄漏事故的核心机制
# 添加不同精度的路由,观察最长前缀匹配
ip route add 10.1.0.0/16 via 192.168.1.1    # 粗路由
ip route add 10.1.2.0/24 via 192.168.1.2    # 精确路由
ip route add 10.1.2.5/32 via 192.168.1.3    # 主机路由

# 测试不同目标地址的匹配结果
ip route get 10.1.3.1      # 匹配 10.1.0.0/16 → via 192.168.1.1
ip route get 10.1.2.100    # 匹配 10.1.2.0/24 → via 192.168.1.2
ip route get 10.1.2.5      # 匹配 10.1.2.5/32 → via 192.168.1.3

# 添加黑洞路由
ip route add blackhole 10.99.0.0/16
# 发往 10.99.x.x 的包将被直接丢弃

Linux 路由表的实现

Linux 内核使用 FIB(Forwarding Information Base) 来存储路由表。FIB 的查找算法经历了多次演进:

# 查看 FIB 的统计信息
cat /proc/net/fib_trie | head -20
# 显示 trie 数据结构的节点信息

# 查看 FIB 的统计摘要
cat /proc/net/fib_triestat
# 显示 trie 的深度、节点数等统计

# 查看路由缓存命中率
cat /proc/net/stat/rt_cache

策略路由

标准路由只看目标地址,但有时候需要根据源地址、入口接口、TOS 标记等做出不同的路由决策——这就是策略路由(Policy Routing)

Linux 通过多个路由表(编号 0-255)和路由规则(ip rule)来实现策略路由:

# 查看路由规则
ip rule show
# 输出示例:
# 0:      from all lookup local
# 32766:  from all lookup main
# 32767:  from all lookup default

# 创建自定义路由表(编号 100)
ip route add default via 10.0.1.1 table 100

# 添加策略:来自 192.168.1.0/24 的流量走路由表 100
ip rule add from 192.168.1.0/24 table 100

# 一个常见场景:多 ISP 出口
# 来自内网 A 的流量走 ISP 1,来自内网 B 的流量走 ISP 2
ip route add default via 10.0.1.1 table 100  # ISP 1
ip route add default via 10.0.2.1 table 200  # ISP 2
ip rule add from 192.168.1.0/24 table 100    # 内网 A → ISP 1
ip rule add from 192.168.2.0/24 table 200    # 内网 B → ISP 2

ECMP(等价多路径路由)

当到达同一个目标有多条等价路径时,Linux 可以进行负载均衡:

# 添加 ECMP 路由
ip route add 10.2.0.0/16 \
    nexthop via 192.168.1.1 weight 1 \
    nexthop via 192.168.1.2 weight 1

# 查看 ECMP 路由的实际选路
ip route get 10.2.0.1
ip route get 10.2.0.2
# 不同的目标地址可能走不同的下一跳

# ECMP 的哈希算法选择
sysctl net.ipv4.fib_multipath_hash_policy
# 0 = L3(源/目标 IP),默认值
# 1 = L4(源/目标 IP + 源/目标端口),推荐
# 2 = L3 或内层 L3(用于隧道)
# 建议设置为 1,基于 L4 哈希可以更均匀地分布流量
sysctl -w net.ipv4.fib_multipath_hash_policy=1

ECMP 的工程注意事项

  1. 流粘性(Flow Affinity):同一条 TCP 连接的所有包应该走同一个路径,否则会导致乱序。L4 哈希可以保证这一点
  2. 路径故障检测:当一条路径故障时,相关的流会被重新分配到其他路径。这个切换过程中可能有短暂丢包
  3. 哈希不均衡:如果流的数量少或者源/目标地址分布不均匀,ECMP 的负载可能严重不均衡。实际生产中需要监控各路径的流量分布
# 监控各下一跳的实际使用情况
# 查看路由统计(需要启用路由统计计数器)
ip -s route show 10.2.0.0/16

# 用 conntrack 观察连接分布在哪些路径上
conntrack -L 2>/dev/null | awk '{print $0}' | head -20

五、IP 地址工程:特殊地址与规划

IP 地址不只是”四个数字用点隔开”。有一些特殊地址和地址范围需要工程师了解:

特殊地址

地址/范围 用途 RFC 工程注意
0.0.0.0/8 “本网络” RFC 1122 作为源地址:DHCP 发现阶段使用
10.0.0.0/8 私有地址 A 类 RFC 1918 最大的私有地址块,常用于数据中心
100.64.0.0/10 CGN 共享地址 RFC 6598 运营商 NAT 内部使用,不可在公网路由
127.0.0.0/8 环回地址 RFC 1122 整个 /8 都是环回,不只是 127.0.0.1
169.254.0.0/16 链路本地 RFC 3927 DHCP 失败时的自动配置地址
172.16.0.0/12 私有地址 B 类 RFC 1918 172.16.0.0 到 172.31.255.255
192.168.0.0/16 私有地址 C 类 RFC 1918 最常见的家庭/小型网络地址
198.18.0.0/15 基准测试 RFC 2544 专用于网络设备性能测试
224.0.0.0/4 组播 RFC 5771 224.0.0.0 到 239.255.255.255
255.255.255.255 有限广播 RFC 919 本地网络广播,不可路由
# 检查一个地址是否是私有地址的快速方法
# (在脚本中有用,比如判断是否需要做 NAT)
ip_is_private() {
    local ip="$1"
    echo "$ip" | grep -qE '^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)'
}

# 查看本机的链路本地地址(IPv4 和 IPv6 都有)
ip addr show | grep "169.254\|fe80"

# 环回地址的"冷知识":整个 127.0.0.0/8 都是环回
ping -c 1 127.0.0.1    # 通
ping -c 1 127.1.2.3    # 也通
ping -c 1 127.255.255.254  # 还是通

子网规划的工程考虑

子网划分不只是数学计算。工程上需要考虑:

  1. 预留增长空间:/24 有 254 个可用地址,看起来够用,但加上管理地址、VIP、负载均衡器等,消耗很快。建议至少预留 50% 的增长空间
  2. 汇总性(Summarizability):相邻子网应该可以汇总成更大的路由前缀,减少路由表条目。规划时按 2 的幂次分配连续地址块
  3. 隔离性:不同安全域使用不同子网,便于防火墙规则编写。不要为了”节省地址”把不同安全等级的服务放在同一个子网
# 计算子网信息的工具
ipcalc 192.168.1.0/24
# 输出网络地址、广播地址、可用主机范围等

# 或者用 Python 的 ipaddress 模块
python3 -c "
import ipaddress
net = ipaddress.ip_network('10.0.0.0/22')
print(f'Network:   {net.network_address}')
print(f'Broadcast: {net.broadcast_address}')
print(f'Hosts:     {net.num_addresses - 2}')
print(f'Netmask:   {net.netmask}')
# 拆分成 4 个 /24
for subnet in net.subnets(new_prefix=24):
    print(f'  Subnet: {subnet}')
"

六、IP 层的排查工具集

把 IP 层的排查工具整理成系统化的工具集,覆盖地址、路由、分片、TTL 各个方面:

地址与接口

# 完整的接口信息(地址、MTU、状态、统计)
ip -s -d addr show

# 查看 ARP/邻居缓存
ip neigh show

# 查看接口的 IP 配置细节
ip -4 addr show dev eth0

路由诊断

# 查看完整路由表(包括所有路由表)
ip route show table all

# 追踪特定目标的路由决策过程
ip route get 目标IP

# 实时监控路由变化
ip monitor route
# 在另一个终端添加/删除路由,这里会实时显示变化

分片监控

# IP 层统计(含分片数据)
nstat -z | grep -iE "^Ip"

# 实时监控分片创建
watch -d -n 1 'cat /proc/net/snmp | grep "^Ip:" | tail -1 | tr " " "\n" | paste - - | grep -iE "frag|reasm"'

# 抓分片包
tcpdump -i eth0 -nn '(ip[6] & 0x20 != 0) or ((ip[6:2] & 0x1fff) != 0)' -c 20

TTL 分析

# 查看路径上每一跳的 TTL 变化
traceroute -I 目标IP     # ICMP 模式
traceroute -T 目标IP     # TCP 模式(绕过某些防火墙)
traceroute -U 目标IP     # UDP 模式(默认)

# 用 mtr 持续监控路径(综合了 ping 和 traceroute)
mtr --report -c 100 目标IP
# 输出每一跳的丢包率、延迟统计

# 用 tcpdump 观察收到包的 TTL 分布
tcpdump -i eth0 -nn -v 'src host 目标IP' -c 20 2>/dev/null | grep -oP 'ttl \d+' | sort | uniq -c

IP 层性能诊断

# 查看内核 IP 层的详细统计
cat /proc/net/snmp | grep "^Ip:"
# 关键指标:
# InReceives    - 收到的 IP 包总数
# InHdrErrors   - 首部错误(校验和错误、版本错误等)
# InAddrErrors  - 地址错误(目标地址不是本机且不转发)
# ForwDatagrams - 转发的数据报数
# InDiscards    - 因资源不足被丢弃
# OutNoRoutes   - 找不到路由的包数

# 如果 InHdrErrors 持续增长,说明有坏包到达
# 如果 OutNoRoutes 增长,说明有路由配置问题
# 如果 ForwDatagrams > 0 而你不是路由器,检查 ip_forward 配置

# 检查 IP 转发是否启用
sysctl net.ipv4.ip_forward
# 除非你的机器是路由器/网关/容器宿主机,否则应该是 0

路由排查实战案例

一个真实场景:服务 A(10.1.1.100)无法访问服务 B(10.2.1.50),但可以 ping 通 10.2.1.1(服务 B 所在子网的网关)。

# 1. 在服务 A 上检查路由
ip route get 10.2.1.50
# 输出:10.2.1.50 via 10.1.1.1 dev eth0 src 10.1.1.100
# 路由正常,下一跳是 10.1.1.1

# 2. 从服务 A ping 服务 B
ping -c 3 10.2.1.50
# 无响应

# 3. 用 traceroute 看路径
traceroute -n 10.2.1.50
# 1  10.1.1.1   1ms
# 2  10.0.0.1   2ms
# 3  10.2.1.1   3ms
# 4  * * *         ← 最后一跳没有响应
# 路径到了 10.2.1.1(服务 B 的子网网关)就断了

# 4. 检查服务 B 的路由表
# 在服务 B 上执行:
ip route get 10.1.1.100
# 输出:10.1.1.100 via 10.2.1.254 dev eth0 src 10.2.1.50
# 注意!服务 B 的默认网关是 10.2.1.254,不是 10.2.1.1
# 回程路由走了不同的路径!

# 5. 在 10.2.1.254 上检查
# 发现 10.2.1.254 没有到 10.1.1.0/24 的路由
# 回程包被丢弃 → 非对称路由导致的单向不通

# 6. 解决:在 10.2.1.254 上添加回程路由
# ip route add 10.1.1.0/24 via 10.2.1.1
# 或者在服务 B 上修改默认网关为 10.2.1.1

经验:单向不通往往是回程路由的问题。在排查时,不只要检查去程路径,还要检查回程路径——两个方向的路由可能完全不同。

IPv4 vs IPv6 首部对比

作为下一篇 IPv6 文章的预告,列出两者的关键差异:

特性 IPv4 IPv6
首部长度 20-60 字节(可变) 40 字节(固定)
地址长度 32 位 128 位
首部校验和 无(交给传输层)
分片 路由器和端点都可以分片 只有源端可以分片
选项 首部内可变长度选项 扩展头链
TTL/Hop Limit TTL(Time to Live) Hop Limit(语义更准确)
广播 支持 不支持(用组播替代)
ARP 需要 ARP 协议 用 NDP(ICMPv6)替代

七、IP 协议的设计权衡与局限性

回顾 IPv4 首部的设计,有几个值得思考的设计权衡:

Identification 字段太窄:16 位在高速网络中不够用。一个 10Gbps 的链路以最小帧(64 字节)发送时,每秒产生约 1500 万个包——16 位 ID 在 4 毫秒内就会回绕。这使得分片重组在高速网络中不可靠,也是 IPv6 将分片从路由器移到端点的原因之一。

首部校验和是冗余的:每一跳都要重新计算 Header Checksum(因为 TTL 变了),消耗了路由器的 CPU。而 TCP/UDP 有自己的校验和覆盖数据完整性,以太网有 FCS 校验帧完整性。IPv6 干脆取消了首部校验和,把完整性保护交给链路层和传输层。

选项字段的设计过于灵活:可变长度的选项使得路由器必须解析首部长度才能找到数据开始的位置,这对硬件转发不友好。IPv6 改用了扩展头链,每个扩展头有固定的”下一个头”字段,路由器只需要处理逐跳选项头。

地址空间不够:这是最明显的问题。32 位地址最多 42 亿个,减去各种保留地址和低效分配,实际可用远少于此。NAT 虽然缓解了地址耗尽,但引入了新的工程复杂性(NAT 穿越、端到端连通性丧失、应用层协议适配)。IPv6 用 128 位地址彻底解决了这个问题——下一篇文章将详细讨论。

八、结论

IPv4 首部只有 20 字节(通常),但每个字段都携带了工程信息。掌握这些字段的含义,在排查网络问题时能事半功倍。

四个核心要点:

  1. TTL 是你的路径探测器。 通过 traceroute 递增 TTL,你可以看到包经过的每一跳。通过收到的包的 TTL 值,你可以估算对端的操作系统和距离。异常的 TTL(比如突然从 64 变成 128)可能暗示路径变化。

  2. DF 位是现代网络的”请勿分片”标识。 TCP 栈设置 DF 位,依靠 PMTUD 在源端控制包大小。如果你看到 IP 分片(特别是 TCP 的 IP 分片),那几乎一定是配置错误。唯一合理的分片出现在 UDP 大包(DNS、NFS)和某些隧道场景。

  3. 路由查找遵循最长前缀匹配。 这意味着一条 /24 路由会覆盖 /16 路由,一条主机路由(/32)会覆盖一切。理解这个规则对于排查路由问题、理解 BGP 路由泄漏事故、设计子网规划都至关重要。

  4. IP 选项在现代网络中是安全隐患。 源路由(Source Routing)必须禁用,Record Route 和 Timestamp 也不应启用。大多数运营商网络会丢弃带 IP 选项的包。

一个经验法则:如果你在排查网络问题时感到困惑,抓一个包看看 IP 首部。 tcpdump -nn -v 会告诉你 TTL、DF 位、分片信息、协议号——这些信息往往比应用层日志更直接。


上一篇:以太网工程:帧结构、MTU 与 Jumbo Frame

下一篇:IPv6 工程实践:从双栈到纯 IPv6 迁移

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-14 · network

【网络工程】以太网工程:帧结构、MTU 与 Jumbo Frame

以太网是局域网的事实标准,但大多数工程师只知道'网线插上就能用'。这篇文章拆解以太网帧的真实结构、MTU 与 MSS 的关系、PMTUD 黑洞的成因与排查、VLAN 标签对帧大小的影响,以及数据中心 Jumbo Frame 的性能收益与部署陷阱。理解链路层,才能排查那些'莫名其妙的丢包'。

2025-07-28 · network

【网络工程】DNS 解析链路:递归、迭代与缓存层级

一个域名从浏览器到最终 IP 地址,经历了浏览器缓存、操作系统缓存、Stub Resolver、递归解析器、权威服务器多层解析和缓存。本文完整追踪 DNS 解析的每一跳,剖析递归与迭代查询的差异,详解各级缓存的 TTL 行为和否定缓存的工程影响,分析公共 DNS、运营商 DNS 与自建 DNS 的选型决策。

2025-07-29 · network

【网络工程】DNS 性能优化:预取、TTL 策略与本地缓存

DNS 解析延迟直接影响用户体验和服务可用性。本文从浏览器 DNS Prefetch、服务端预解析、TTL 策略设计、本地 DNS 缓存部署(systemd-resolved / dnsmasq / CoreDNS)四个维度,系统性地分析 DNS 性能优化的工程实践,包含延迟量化、缓存命中率提升和故障切换加速的完整方案。


By .