IP 协议:大蓝图

附一张在其它地方看到的类似图

IP 协议的一些任务:

  • 健康检查

    • 校验和不正确
    • 一个报头字段的值超过范围
  • Netfilter 防火墙子系统

  • 处理选项

    • IP 协议包含一些应用程序可以使用的选项
  • 分段 / 重组

  • 接收,传输以及转发操作

IP 报头

版本 (Version)

  • IPv4
  • IPv6

报头长度 (Header Length, IHL)

  • 以 32 位为单位

服务类型 (Type of Service, TOS)

  • 8位,由 3 个字段组成

总长度(Total Length)

  • 包括报头,以字节为单位

识别 (Identification)

  • packet 标识符

DF (Don’t Fragment) 不分段

MF (More Fragments) 还有其它分段

片段偏移量 (Fragment Offset)

  • 由 IP 协议的分段/重组功能使用

存活时间 (Time To Live, TTL)

  • 跳点技术

  • 每个路由器在转发时都应该递减此字段,当 TTL 为零时,丢弃该packet

  • 多数时候使用默认值 64

  • packet 被丢弃时,发送方会收到一条 ICMP 消息

协议 (Protocol)

  • L4 的协议标识符

  • cat /etc/protocols

报头校验和 (Header Checksum)

  • 确保 IP 报头在传输之后依然准确,但不包括packet 的有效载荷

  • 必要时,L4 协议负责检查内容

选项 (Options)

  • 最大为 40 字节

  • 报头长度是一个 4 位的值,可表示 60 字节,IP 报头占 20 字节

IP 选项

  • 多字节选项内的 option_data 并没有从 32 位边界起算

  • 每个选项都有一个 8 位字段,名为 type,可进一步拆成三个子字段

  • 当 copied 被设定,若packet 需要分段,IP 层就必须把该选项拷贝至每个片段

include/linux/ip.h 中可以找到这些选项类型的定义,以及一些存取其子字段的宏:

1
2
3
#define	IPOPT_COPIED(o)		((o)&IPOPT_COPY)
#define IPOPT_CLASS(o) ((o)&IPOPT_CLASS_MASK)
#define IPOPT_NUMBER(o) ((o)&IPOPT_NUMBER_MASK)

End of Option List 和 No Operation 选项

  • 不含选项的 IP 报头的大小是 20 字节

  • 当 IP 选项的大小不是 4 字节的倍数时,传送者会以 IPOPT_END 选项使其对齐 4 字节边界

  • IPOPT_NOOP 选项可用于填补选项之间的空白

Source Route 选项

  • 让传送者指定packet 到其接收者所走的路径。

  • 多字节选项,发送节点会列出后续跳点所用的 IP 地址

两种类型:

  1. 严格 (strict): 传送者必须列出路径上每台路由器的 IP 地址,沿途都不能做修改

  2. 松散 (loose): 一台中间路由器可以使用另一台路由器(不在列表中的)作为通向列表中下一个路由器的路径;传送者所指定的所有路由器还是必须按其指定的次序使用

严格:R_1, R_2, R_3

松散: R_1, R_3(如果 R_2 失败,可改用 R_2b)

Record Route 选项

请求发送发和目的地之间的路由器存储它们用于转发packet 时,所用的外出接口的 IP 地址。

TimeStamp 选项

最右边的 4 位代表的是可以改变此选项效应的子命令:

  • RECORD TIMESTAMPS: 每个路由器会记录其收到packet 的时间

  • RECORD ADDRESSES AND TIMESTAMPS: 存储接收接口的 IP 地址

  • RECORD TIMESTAMPS ONLY AT THE PRESPECIFIFD SYSTEMS: 只针对传送者选择的一些特定 IP 地址记录接收时间

时间以毫秒,存储在一个 32 位变量内。

Router Alert 选项

最后两个字节只有一个指定值,零。

路由器应该检查该packet ,携带其他值的packet 是非法的,应该被丢弃,同时产生 ICMP 错误消息返回给产生这种packet 的发送方。

packet 的分段 / 重组

分段后的 IP packet 通常会由目的主机重组,但,必须查看整个 IP packet 的中间设备也得重组,如防火墙和 NAT 路由器。

分段对较高分层的效应

对packet 进行分段和重组都会用到 CPU 时间和内存。分段也会消耗传输用的带宽,因为每个分段都必须包含 L2 和 L3 报头。

许多应用程序会通过考虑下列因素避免分段:

  • 内核可以使用一个名为路径 MTU 发现(path MTU discovery)的功能,发现可以使用的最大packet 尺寸。

  • MTU 可以设成一个相当安全的小值 576。

分段/重组所用的 IP 报头字段

DF (Don’t Fragment) 不分段

  • 如果路径上的packet 超出某个连结的 MTU,就会被丢弃

MF (More Fragments) 还有其它分段

  • 当一个节点把一个packet 分段时,在每个片段中将此标志设为 TRUE,最后一个除外

  • 当接收者接收由此packet 所建的最后一个片段时,就会知道原本没分段的packet 的尺寸

片段偏移量 (Fragment Offset)

  • 由 IP 协议的分段/重组功能使用

  • 代表该片段要放在原本 IP packet 中的位置偏移量

  • 这是一个 13位字段

  • 偏移量 0 是指该片段是此packet 内第一个片段。第一个片段包含和整个原有packet 相关的报头信息

ID

  • IP packet ID,对一个IP packet 的所有片段是相同的。

  • Linux 会把最近一个所用的 ID 存储在一个名为 inet_peer 的结构中,此结构中还存储了其正在通信的远程主机的信息

分段/重组的问题实例

重新传输

每个传送者只会等待,直到较高分层通知其重传整个封包

重传的数据包不会复用原始数据包的标识(ID)。不过,主机仍有可能接收到具有相同数据包标识(ID)的同一 IP 分片的多个副本,因此主机必须能够处理这种情况。

由于 IP 是无连接协议,它不提供流量控制,丢包问题需要由上层协议(或应用程序)来处理。

假设上层通过某种方式检测到部分数据丢失(例如,因未收到确认而超时的计时器),并尝试重传。由于无法仅选择性地重传丢失的分片,传输层(L4)协议必须重传整个 IP 数据包。

每次重传都可能导致一些特殊情况,这些情况需要由接收方处理(有时,当中介路由器实现了某些需要对数据包进行重组的防火墙功能时,也需要由这些路由器处理)。以下是其中一些情况:

  • 重叠

    • 一个片段内含的某些数据可能已经在前一个封包内抵达了。
  • 重复

    • 两个片段完全相同
  • 重组完成后又接收到

    • 在这种情况下,IP 层会将该分片视为一个新 IP 数据包的首个分片。
    • 如果未能收到所有新分片,IP 层会在垃圾回收过程中直接清理重复数据;否则,它会重新组装整个数据包,而识别该数据包为重复包则是上层协议的工作。

把片段和其 IP 封包相关联

要识别一个片段所属的 IP 封包,内核会考虑下列参数:

  • 源 IP 和目的 IP

  • IP packet ID

  • L4 协议

IP ID 生成范例

Linux 内核不是使用一个全局 IP ID,而是为每个目的 IP 地址设置不同的计数器。

  • 通往相同目的地的所有数据流都共享 IP ID
  • 把通往目的地的数据看成一个整体,封包就有连续的 ID,但每个应用程序的数据流则不会有连续的 ID

无法解决的重组问题范例:NAT

当两个由 R 所传输的 IP 封包在抵达服务器 S 前被分段是,服务器 S 接收的那些片段都拥有相同发送方和目的 IP 地址以及相同的 IP ID,会试着将它们都放在一起,且可能把两个不同的 IP 封包的片段混在一起。

路径 MTU 发现

  • 用于发现封包传输至目的地址而不用被分段的最大尺寸,该参数称为路径 MTU (Path MTU, PMTU)

  • 由于每个目的 IP 地址会使用不同的 PMTU,其值会暂存在相关的路由表缓存项目中

  • LAN 上的所有设备(彼此间没有路由器)共享相同 MTU 以正确运作。

  • 如果设备没有直接连接,或 PMTU 功能被关闭,PMTU 默认设成 576

如何运作,利用 IP 报头中用于处理分段/重组的字段以及相关 ICMP 消息

  • 如果你发送一个在头部设置了 DF(不分片)标志的 IP 数据包,且没有收到任何报错,这意味着在通往目的地的路径上没有发生分片,并且你使用的路径 MTU(PMTU)是可行的。

  • 但这并不代表你正在使用最优的数据包大小。你完全有可能增大 PMTU,同时仍然不会触发分片。

  • 如果你将探测包的大小增加到最优值,当超过实际路径 MTU(PMTU)时,你会收到一条 ICMP 消息通知。该 ICMP 消息会包含触发报错的设备的 MTU 值,以便内核据此更新本地的路径 MTU。

Linux 可以配置下列方式来处理路径 MTU 发现功能:

IP_PMTUDIISC_DONT

  • 绝不传送报头中设定了 DF 标志的 IP 封包。不使用路径 MTU 发现功能。

IP_PMTUDISC_DO

  • 总是在本地节点所产生的封包的报头中设定 DF 标志,试图在每次传输时都找出最佳的 PMTU

IP_PMTUDISC_WANT

  • 按每条路径决定是否使用路径 MTU 发现功能。默认行为。

通过 ip route 命令新增路径时,路径的 PMTU 也可手动设定。

即便开启路径 MTU 发现功能,依然可以锁定当前 PMTU,使其无法被更改。

  1. 使用 ip route 设定 PMTU 时,以 lock 关键词锁定
1
ip route add 10.10.1.0/24 via 100.100.100.1 mtu lock 750
  1. 如果因接收到 ICMP FRAGMENTATION NEEDED 消息使得你要用的 PMTU 小于最小容许值,则 PMTU 会设成该最小值,然后锁住
    • 最小值可通过 /proc/sys/net/ipv4/route/min_pmtu 文件配置
    • PMTU 不能设成低于 68 的值

Linux 的 ip_dont_fragment 函数会以以上描述的思路决定当封包超过 PMTU 时是否应该分段。

校验和 checksum

  • 一种冗余类型,网络协议用它发现传输错误

  • IP 协议的校验只包括 IP 报头。

  • 多数 L4 协议的校验和则包括报头和数据。

传输封包前,传送者会计算一个小而长度固定的字段(校验和),其中包含数据的某种 hash 值。

IP 协议所用的校验和算法很简单,只涉及总和以及 1 的补码

可靠的健康检查需要依靠 L2 CRC 或者 SSL/IPSec MACs(Message Authentication Codes, 消息鉴定码)

大多数 L2 和 L4 协议都会提供校验工作,IPv6 已把校验工作删除。

在 IPv4 中,IP 校验和是一个 16 位字段,覆盖整个 IP 头部(包括选项字段)。

  1. 校验和首先由数据包的源端计算,然后在通往目的地的路径上逐跳更新,以反映每个路由器对头部所做的修改。

  2. 在更新校验和之前,每一跳都必须先通过将数据包中包含的校验和与本地计算的校验和进行比较,来检查数据包的完整性。

  3. 如果完整性检查失败,数据包将被丢弃,但不会生成 ICMP 消息:传输层(L4)协议会处理这种情况(例如,通过计时器,若在指定时间内未收到确认,将强制重传)。

一些触发 校验和 更换需求的情况:

  • 递减 TTL

    • 路由器转发封包前,必须递减其 IP 报头中的 TTL
    • TTL 是由 ip_decrease_ttl 递减
  • 封包调整(包括 NAT)

    • 改变一个或多个 IP 报头字段的所有功能都会强制校验和重新计算
  • IP 选项的处理

    • 选项是报头的一部分
  • 分段

    • 每个片段有不同的报头,大多数字段不变,但 offset 不同

用于 校验和 计算的 API

相关文件 include/asm-xx/checksum.h

IP 专用函数

  • 对要被传输的封包计算 IP 报头的校验和是,iphdr->check 的值应该先变为零
1
2
3
4
// iph: 报头的指针
// ihl: 报头的长度
static inline
unsigned short ip_fast_csum(unsigned char *iph, unsigned int ihl)

校验和算法有一个有趣的特性,如果校验和正确,那么转发或接收节点对整个头部运行该算法(保持原始的 iphdr->check 字段不变),得到的结果将是零。这种检查数据损坏的方式,比更直观的先将 iphdr->check 字段清零再重新计算的方法要快。

用于计算或更新 IP 校验和 的主要函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 计算校验和 的通用函数。输入参数是一个任意大小的缓冲区
unsigned short ip_compute_csum(unsigned char * buff, int len);

// 指定 IP 报头和长度后,计算并返回 IP 校验和
// 可用于验证输入封包,计算外出封包的校验和
unsigned short ip_fast_csum(unsigned char *iph, unsigned int ihl);

// 计算外出封包的 IP 校验和
__inline__ void ip_send_check(struct iphdr *iph)
{
iph->check = 0;
iph->check = ip_fast_csum((unsigned char *)iph, iph->ihl);
}


// 对 IP 校验和进行增值更新
// ip_forward 中调用
int ip_decrease_ttl(struct iphdr *iph)
{
u32 check = iph->check;
check += htons(0x0100);
iph->check = check + (check>=0xFFFF);
return --iph->ttl;
}

checksum.h 中的通用函数,几乎都是由 L4 协议使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 通用校验和计算函数,定义在 net/core/skbuff.c 中
// 几乎都是由 L4 协议在特定情况下使用
unsigned int skb_checksum(const struct sk_buff *skb, int offset,
int len, unsigned int csum);

// 把32位值的最高 16 位对折到最低 16 位,然后求出输出值的补码
// 此预算通常是校验和计算的最后阶段
unsigned int csum_fold(unsigned int sum);

// 这个函数系列所计算的校验和会缺少 csum_fold 所做的最后对折步骤
unsigned int csum_partial[_xxx](const unsigned char *buff, unsigned len, unsigned int sum);

unsigned int csum_block_add(unsigned int csum, unsigned int csum2, int offset);

unsigned int csum_block_sub(unsigned int csum, unsigned int csum2, int offset);

// 入口封包:使其 L4 硬件校验和失效
// 出口封包:计算 L4 校验和
int skb_checksum_help(struct sk_buff *skb);

// 对 TCP 和 UDP 伪报头计算校验和
unsigned short int csum_tcpudp_magic(unsigned long saddr,
unsigned long daddr,
unsigned short len,
unsigned short proto,
unsigned int sum);

对 L4 校验和 所做的修改

TCP 和 UDP 协议所计算的 校验和 会包括其报头、有效载荷以及伪报头。

伪报头中的字段是从 IP 报头中取来,IP 报头中出现的一些信息最后会整合至 L4 校验和内。

伪报头只是为了计算校验和而定义,并不存在于在网络中传送的封包内。

对 IP 层次所做的修改会使得 L4 校验和 失效。

已接收数据帧中的硬件计算的 L4 校验和 失效的几种情况:

  • 当入站二层帧为达到最小帧长度而包含填充字段,但网卡在计算校验和时,未能智能地将该填充字段排除在外,就会出现校验和不匹配的情况。

  • 当入站的 IP 分片与此前已接收的分片发生重叠时。

  • 当入站 IP 数据包使用 IPsec 协议簇中的任一协议时,网卡无法正确计算出传输层校验和 —— 因为此时传输层头部与载荷数据已被执行压缩、摘要计算或加密操作。相关实现示例可参考 net/ipv4/esp4.c 文件中的 esp_input 函数。

  • 由于网络地址转换(NAT)或 IP 层的其他类似干预操作,该校验和需要重新计算。相关实现可参见 net/ipv4/netfilter/ip_nat_standalone.c 文件中的 ip_nat_fn 函数。

skb->ip_summed 字段实际作用于传输层(L4)校验和。当 IP 层检测到有操作导致传输层校验和失效时(例如,对伪首部中的某个字段进行了修改),会对该字段的值进行相应调整。