Linux 网络:PMTUD 协议简介

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. Path MTU Discovery(PMTUD) 协议

在说明 Path MTU Discovery(PMTUD) 之前,先得说说 MTU(Maximum Transmission Unit) 。什么是 MTU(Maximum Transmission Unit)MTU 是网卡的最大传输单元,即网卡一次最多传输数据的字节数,这是一个网卡硬件的参数。当数据从 IP 层 向下传递到 数据链路层 时,如果发现 IP 数据包的长度 大于 网卡的 MTU ,就需要将 IP 数据包 进程 分片(在 IP 协议没有设置 DF 标志位时),以适应网卡的 MTU。需要知道的是,MTU 限定的是 IP 层 向下传递数据的最大长度,这并不包含以太网帧头和帧尾长度在内。
说完了 MTU ,接下来说说本篇的主角 Path MTU Discovery(PMTUD) 。数据在传输过程中,可能经过多个各种类型的网络数据的传输介质,如交换机、路由器等,下图给出一个简单的示例:
在这里插入图片描述
从上图中看到,数据的源头和目的设备的 MTU 均为 1500,而中间的路由设备的 MTU576 ,也就是说,数据经过的各种传输媒介,它们各自可能拥有不同的 MTU 值,这就意味着数据帧经过不同 MTU 的设备时,要进行分片(从 更大 MTU 设备 到 更小 MTU 的设备)、组包(从更小 MTU 设备 到 更大 MTU 设备)。这样的不停分片、组包,需要开辟额外的缓存进行数据排队,通常来说对于网络传输效率是不利的(尤其是交换机这类设备),更不要说丢包等情形的处理。为了适应这种不同设备具有 MTU 的情形,引入了 Path MTU Discovery(PMTUD) 协议,协议 RFC 编号为 RFC1191 ,该协议用来发现网络数据传输整个路径中的 最小 MTU,然后数据传输路径中所有设备使用这个 最小 MTU 来传输数据,因此所有的 IP 数据 都可以不用进行分片,以期达到更大的传输效率。这个 最小 MTU 有个名目,叫做 PMTU(Path MTU)

2.1 PMTUD 发现最小 MTU 的过程

上面说了,Path MTU Discovery(PMTUD) 用来发现传输路径中的 最小 MTU ,那是如何发现的呢?过程也不复杂,就是在传输 IP 数据 的时候发送端设置 DF(Don't Fragment) 标记,如下图:
在这里插入图片描述
然后在数据经过的传输路径中的任何设备,如果发现接收的 IP 数据的长度超过自己的 MTU ,同时 IP 数据设置了 DF 标记,则改设备回复发送端一个 Type=3,Code=4 的 ICMP 消息,表示 Destination Unreachable Message, fragmentation needed and DF set ,告知发送端数据太长被丢弃,需要进行分片,回送的 ICMP 消息也会带上接收端的自己 MTU发送端接收到 ICMP 消息后,缓存接收端回送的 MTU 值,然后调整数据长度重新进行发送。ICMP 协议数据格式如下:

           0               1               2               3
    0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |     Type      |     Code      |          Checksum             |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                             unused                            |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |      Internet Header + 64 bits of Original Data Datagram      |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

更多关于 ICMP(Internet Control Message Protocol) 的细节可参考 RFC792
应该了解的是,Path MTU Discovery(PMTUD) 协议只适用于 TCPUDP 协议。另外,并非所有设备都支持 PMTUD 协议不支持 PMTUD 协议的设备,在收到大于自身 MTU 长度、且设置了 DF 标志的 IP 数据时,回送给发送端的 ICMP 消息里不会包含其 MTU 值,此时发送端需要采取某种策略来处理这种情形。RFC1191 协议的章节 5 对此给出了一些建议性的方案,但这些建议并非 RFC1191 协议的一部分,感兴趣的读者可自行阅读RFC1191 协议的章节 5,以了解更多的相关细节。

3. Linux 的 PMTUD 简析

首先,本文分析以 Linux 4.14 内核代码为背景进行分析。Linux 下默认开启 Path MTU Discovery(PMTUD) 功能。另外,可以通过文件节点 /proc/sys/net/ipv4/ip_no_pmtu_disc 来开启或关闭 Path MTU Discovery(PMTUD) :向文件写 0 开启 PMTUD,写非零值(1-3)关闭 PMTUD 。更多关于 /proc/sys/net/ipv4/ip_no_pmtu_disc 配置的细节,可参考内核文档 Documentation/networking/ip-sysctl.txt
本文只讨论 IPv4 协议栈下 Path MTU Discovery(PMTUD) 开启的情形,对其它情形感兴趣的读者可自行阅读源码进行分析。

3.1 创建 socket 时初始化 PMTUD 模式

socket()
	...
	inet_create() // net/ipv4/af_inet.c
		...
		if (net->ipv4.sysctl_ip_no_pmtu_disc)
			...
		else /* 开启 PMTUD 的情形 */
			inet->pmtudisc = IP_PMTUDISC_WANT;
		...

当然,内核也提供了接口修改 socketPMTUD 的配置。如:

on = IP_PMTUDISC_PROBE;
setsockopt(fd, IPPROTO_IP, IP_MTU_DISCOVER, &on, sizeof(on));

3.2 数据发送时 PMTUD 相关处理

要发送的数据,当前可能有两种情形:

情形1:当前数据正从源头主机往外发送
情形2:当前数据正经过某中间设备(譬如路由器)往外转发

下面分别对这两种情形下,和 PMTUD 协议相关的处理部分。

3.2.1 源头主机发送过程中 PMTUD 处理

// net/ipv4/ip_output.c

ip_queue_xmit()
	...
packet_routed:
	if (ip_dont_fragment(sk, &rt->dst) && !skb->ignore_df) /* 不允许对 IP 数据分片 */
		iph->frag_off = htons(IP_DF); /* 标记 DF */
	else
		...
	...
	res = ip_local_out(net, sk, skb); /* 将数据包传递给网络设备 */
		...
		ip_finish_output()
	...

接收端设备收到数据后,如果发现大于自己的 MTU ,且设置了 DF(Don't Fragment) 标记,则回送 Type=3,Code=4 的 ICMP 消息

// net/ipv4/ip_output.c

ip_finish_output()
	...
	/* 包长度大于本机 MTU, 进行分片处理 */
	if (skb->len > mtu || (IPCB(skb)->flags & IPSKB_FRAG_PMTU))
		return ip_fragment(net, sk, skb, mtu, ip_finish_output2);
			struct iphdr *iph = ip_hdr(skb);

			if ((iph->frag_off & htons(IP_DF)) == 0) /* 允许 IP 数据 分片 */
				...

			/* 不允许 IP 数据 分片(设置了 IP_DF 标记) */
			if (unlikely(!skb->ignore_df ||
					(IPCB(skb)->frag_max_size &&
					IPCB(skb)->frag_max_size > mtu)/*IP 分片 的 长度大于 MTU*/)) {
				IP_INC_STATS(net, IPSTATS_MIB_FRAGFAILS);
				/*
				 * IP 分片长度 超过 MTU && 禁止分片, 
				 * 则给本地 socket 发送 ICMP 的 {ICMP_DEST_UNREACH,ICMP_FRAG_NEEDED} 包,
				 * 告知其包将不被发送 (IP 数据 由本地 socket 往外发送,发不出去就回送
				 * 给 socket 回 ICMP 的 {ICMP_DEST_UNREACH,ICMP_FRAG_NEEDED} 包 告知 socket).
				 */
				icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED, htonl(mtu));
				kfree_skb(skb);
				return -EMSGSIZE;
			}
	...

发送端收到 Type=3,Code=4 的 ICMP 消息 后更新缓存 PMTU 值:

// include/uapi/linux/icmp.h

struct icmphdr { /* ICMP 数据报头部格式: https://datatracker.ietf.org/doc/html/rfc792 */
	__u8  type;
	__u8  code;
	__sum16 checksum;
	union {
		struct {
			__be16 id;
			__be16 sequence;
		} echo;
		__be32 gateway;
		struct {
			__be16 __unused;
			/*
			 * . 对于支持 PMTUD 的设备, 收到大于自身 MTU 长度、且设置了 DF 
			 *   标记的数据, 回送发送端 type=3, code=4 类型的 ICMP 消息, 消
			 *   息中 @mtu 设置为接收设备的 MTU.
			 * . 对于不支持 PMTUD 的设备, @mtu 设置为 0 .
			 * 更多细节请参考 RFC1191 .
			 */
			__be16 mtu;
		} frag;
		__u8 reserved[4];
	} un;
};

// net/ipv4/icmp.c
static bool icmp_unreach(struct sk_buff *skb)
{
	const struct iphdr *iph;
	struct icmphdr *icmph;
	...

	icmph = icmp_hdr(skb);
	iph   = (const struct iphdr *)skb->data;
	
	...

	switch (icmph->type) {
	case ICMP_DEST_UNREACH:
		switch (icmph->code & 15) {
		...
		case ICMP_FRAG_NEEDED:
			switch (net->ipv4.sysctl_ip_no_pmtu_disc) {
			...
			case 0:
				info = ntohs(icmph->un.frag.mtu); /* 解析 接收端回传 的 MTU */
			}
		}
	}
	
	...
	
	icmp_socket_deliver(skb, info);
		...
		ipprot = rcu_dereference(inet_protos[protocol]);
		if (ipprot && ipprot->err_handler)
			ipprot->err_handler(skb, info); /* tcp_v4_err() */
				tcp_v4_err()
	...
}

// net/ipv4/tcp_ipv4.c
void tcp_v4_err(struct sk_buff *icmp_skb, u32 info)
{
	...
	const int type = icmp_hdr(icmp_skb)->type;
	const int code = icmp_hdr(icmp_skb)->code;
	...

	switch (type) {
	...
	case ICMP_DEST_UNREACH:
		...
		if (code == ICMP_FRAG_NEEDED) { /* PMTU discovery (RFC1191) */
			...

			tp->mtu_info = info; /* 接收端 回送 的 MTU */
			if (!sock_owned_by_user(sk)) {
				tcp_v4_mtu_reduced(sk);
			} else {
				...
			}
			goto out;
		}
	...
	}
	...
out:
	...	
}

void tcp_v4_mtu_reduced(struct sock *sk)
{
	...
	u32 mtu;

	...
	mtu = tcp_sk(sk)->mtu_info; /* 接收端 回送 的 MTU */
	dst = inet_csk_update_pmtu(sk, mtu); /* 更新缓存 PMTU */

	...
	
	mtu = dst_mtu(dst);
	
	if (inet->pmtudisc != IP_PMTUDISC_DONT &&
	    ip_sk_accept_pmtu(sk) &&
	    inet_csk(sk)->icsk_pmtu_cookie > mtu) {
	    tcp_sync_mss(sk, mtu); /* MSS 同步 */

		/* Resend the TCP packet because it's
		 * clear that the old packet has been
		 * dropped. This is the new "fast" path mtu
		 * discovery.
		 */
		tcp_simple_retransmit(sk); /* 数据重传 */
	}
}

3.2.2 转发过程中 PMTUD 处理

// net/ipv4/ip_forward.c

int ip_forward(struct sk_buff *skb)
{
	...

	IPCB(skb)->flags |= IPSKB_FORWARDED;
	mtu = ip_dst_mtu_maybe_forward(&rt->dst, true);
	if (ip_exceeds_mtu(skb, mtu)) { /* 转发的 @skb 的 数据长度 超过 MTU */
		IP_INC_STATS(net, IPSTATS_MIB_FRAGFAILS);
		/* 
		 * 当前 @skb 正经过 【交换机】 或 【路由器 上】 进行 转发, 当
		 * 【 @skb 的 数据长度 超过 MTU 】 && 【 数据源头设定不允许分片(DF=1) 】 
		 * 时, 给数据发送源头回送 ICMP 包 {ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED}
		 * 数据将被丢弃.
		 */
		icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED, htonl(mtu));
		goto drop;
	}
}

数据发送源收到 Type=3,Code=4 的 ICMP 消息 后的处理和 3.2.1 处理一样。

4. PMTUD 观察

ifconfig 等工具可看到网卡配置的 MTU

$ ifconfig ens33
ens33     Link encap:Ethernet  HWaddr 00:0c:29:4f:b1:e7  
          inet addr:192.168.0.9  Bcast:192.168.0.255  Mask:255.255.255.0
          inet6 addr: fe80::bbc7:b835:be2a:a578/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:2077 errors:0 dropped:0 overruns:0 frame:0
          TX packets:775 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:1684142 (1.6 MB)  TX bytes:74056 (74.0 KB)

ifconfig 的输出可以看到网卡 ens33MTU1500 。用 ping 发送超过 MTU 的数据包(-s 2000 指定数据长度为 2000),且通过 -M do 禁止 IP 分片

$ ping www.baidu.com -s 2000 -M do
PING www.baidu.com (183.2.172.185) 2000(2028) bytes of data.
ping: local error: Message too long, mtu=1500

我们可以通过 tracepath 工具来跟踪数据发送超 MTU 时接收设备回送的 ICMP 包:

$ tracepath www.baidu.com
 1?: [LOCALHOST]                                         pmtu 1500
 1:  192.168.0.1                                          43.888ms 
 1:  192.168.0.1                                           2.902ms 
 2:  192.168.1.1                                          37.109ms 
 3:  192.168.1.1                                         117.816ms pmtu 1492
 3:  100.64.0.1                                           33.586ms 
 4:  61.146.242.189                                       33.665ms 
 5:  177.107.38.59.broad.fs.gd.dynamic.163data.com.cn     39.025ms 
 6:  113.96.5.38                                          54.439ms 
 7:  no reply
 8:  121.14.67.174                                        64.413ms 
 9:  182.61.216.71                                        39.233ms

tcpdump 工具抓回送的 ICMP 包:

$ sudo tcpdump icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens33, link-type EN10MB (Ethernet), capture size 262144 bytes
......
16:17:26.350958 IP 192.168.1.1 > 192.168.0.9: ICMP time exceeded in-transit, length 556
16:17:26.421870 IP 192.168.1.1 > 192.168.0.9: ICMP 183.2.172.185 unreachable - need to frag (mtu 1492), length 556

再用 WireShark 来观察一下 tcpdump 抓到的数据包(tcpdump 抓包时可通过 -w 参数将抓包数据写入文件):
在这里插入图片描述

5. 参考资料

[1] https://packetlife.net/blog/2008/aug/18/path-mtu-discovery/#:~:text=RFC%201191%20defines%20path%20MTU%20discovery%2C%20a%20simple,of%20the%20ICMP%20Destination%20Unreachable%20message%2C%20Fragmentation%20Needed.
[2] https://www.rfc-editor.org/rfc/rfc1191
[3] https://datatracker.ietf.org/doc/html/rfc792

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值