文章目录
- 1. 前言
- 2. Path MTU Discovery(PMTUD) 协议
- 2.1 PMTUD 发现最小 MTU 的过程
- 3. Linux 的 PMTUD 简析
- 3.1 创建 socket 时初始化 PMTUD 模式
- 3.2 数据发送时 PMTUD 相关处理
- 3.2.1 源头主机发送过程中 PMTUD 处理
- 3.2.2 转发过程中 PMTUD 处理
- 4. PMTUD 观察
- 5. 参考链接
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
,而中间的路由设备的 MTU
为 576
,也就是说,数据经过的各种传输媒介,它们各自可能拥有不同的 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 30 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)
协议只适用于 TCP
和 UDP
协议。另外,并非所有设备都支持 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
。
本文只讨论 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;...
当然,内核也提供了接口修改 socket
的 PMTUD
的配置。如:
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.cip_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); /* 将数据包传递给网络设备 */...
接收端设备收到数据后,如果发现大于自己的 MTU
,且设置了 DF(Don't Fragment)
标记,则回送 Type=3,Code=4 的 ICMP 消息
:
// net/ipv4/ip_output.cip_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.hstruct 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;__be16 mtu; // type=3, code=4 时回送接收端的 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;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);...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.cint 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.0inet6 addr: fe80::bbc7:b835:be2a:a578/64 Scope:LinkUP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1RX packets:2077 errors:0 dropped:0 overruns:0 frame:0TX packets:775 errors:0 dropped:0 overruns:0 carrier:0collisions:0 txqueuelen:1000 RX bytes:1684142 (1.6 MB) TX bytes:74056 (74.0 KB)
用 ping
发送超过 MTU
的数据包,且禁止 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.com1?: [LOCALHOST] pmtu 15001: 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 14923: 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 reply8: 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
的观察一下抓到的数据包:
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