路径MTU发现是用来确定到达目的地的路径中最大传输单元(MTU)的大小。通过在IP报头中设置不分片DF(Don't Fragment)标志来探测路径中的MTU值, 如果路径中设备的MTU值小于此报文长度,并且发现DF标志,就会发回一个Internet控制消息协议(ICMP)(类型3、代码4需要分片的消息ICMP_FRAG_NEEDED),消息中包含它可接受的MTU值。
#define IP_PMTUDISC_DONT 0 /* Never send DF frames */
#define IP_PMTUDISC_WANT 1 /* Use per route hints */
#define IP_PMTUDISC_DO 2 /* Always DF */
#define IP_PMTUDISC_PROBE 3 /* Ignore dst pmtu */
#define IP_PMTUDISC_INTERFACE 4 /* 使用出接口的设备MTU值 */
#define IP_PMTUDISC_OMIT 5 /* 忽略DF位 */
IP_PMTUDISC_DONT策略表示从不设置DF位,即不进行PMTU发现(参见函数ip_dont_fragment)。
IP_PMTUDISC_WANT策略根据路由中表项是否锁定了MTU,来决定是否设置DF位,如锁定,不设置DF位。
IP_PMTUDISC_DO策略总是设置DF位,除非内核设置了忽略df(ignore_df),参见以下内容。
IP_PMTUDISC_INTERFACE策略不设置DF位,不发送设置了DF位并且长度超过出接口设备MTU的数据包。
IP_PMTUDISC_OMIT策略与IP_PMTUDISC_INTERFACE策略含义相同,唯一区别在于,即使数据包设置了DF位,内核也会对长度超过出接口设备MTU的数据包进行分片处理并发送。
通过porc文件ip_no_pmtu_disc 设置pmtu的全局默认策略(/proc/sys/net/ipv4/ip_no_pmtu_disc),在sock创建时根据此值初始化pmtudisc变量。内核初始将其设置为0,即系统缺省的pmtu策略为IP_PMTUDISC_WANT,尝试进行pmtu发现。如果置0,不进行pmtu发现(IP_PMTUDISC_DONT),系统不发送IP头带有DF标志的报文。
static int inet_create(struct net *net, struct socket *sock, int protocol, int kern)
{
struct inet_sock *inet;
//初始化sock中的pmtudisc值。
if (net->sysctl_ip_no_pmtu_disc)
inet->pmtudisc = IP_PMTUDISC_DONT;
else
inet->pmtudisc = IP_PMTUDISC_WANT;
}
如前所述IP报头的DF标志,用于PMTU发现,由ip_queue_xmit函数可知,在满足以下两个条件之一,
1)pmtudisc等于IP_PMTUDISC_DO;
2)或者pmtudisc等于IP_PMTUDISC_WANT,并且mtu没有在路由表中锁定。
而且内核没有设置忽略DF的条件下,设置IP报头的DF标志位,进行PMTU发现操作。IP_PMTUDISC_DO为使能PMTU发现策略,IP_PMTUDISC_WANT会根据mtu是否锁定进行pmtu发现。
路径发现策略pmtudisc也可设置为IP_PMTUDISC_PROBE,此策略下只有在发送聚合了的分片报文的情况下设置DF位,此发送流程不经过ip_queue_xmit,所以不与前面两个策略冲突。
int ip_dont_fragment(struct sock *sk, struct dst_entry *dst)
{
return inet_sk(sk)->pmtudisc == IP_PMTUDISC_DO ||
(inet_sk(sk)->pmtudisc == IP_PMTUDISC_WANT && !(dst_metric_locked(dst, RTAX_MTU)));
}
int ip_queue_xmit(struct sk_buff *skb, struct flowi *fl)
{
if (ip_dont_fragment(sk, &rt->dst) && !skb->ignore_df)
iph->frag_off = htons(IP_DF);
else
iph->frag_off = 0;
}
struct sk_buff *__ip_make_skb(...)
{
if (inet->pmtudisc == IP_PMTUDISC_DO ||
inet->pmtudisc == IP_PMTUDISC_PROBE ||
(skb->len <= dst_mtu(&rt->dst) && ip_dont_fragment(sk, &rt->dst)))
df = htons(IP_DF);
}
sock结构体中pmtudisc变量可通过setsockopt系统调用进行设置。用户也可使用ip命令对MTU值进行锁定,不允许进行修改,如下命令,锁定lock到网关192.168.1.1的mtu值为1300字节大小:
static int do_ip_setsockopt(...)
{
struct inet_sock *inet = inet_sk(sk);
case IP_MTU_DISCOVER:
if (val < IP_PMTUDISC_DONT || val > IP_PMTUDISC_OMIT)
goto e_inval;
inet->pmtudisc = val;
}
ip route add 0.0.0.0 via 192.168.1.1 mtu lock 1300
路径mtu发现策略设置为IP_PMTUDISC_INTERFACE或者IP_PMTUDISC_OMIT的时候,内核都不会保存ICMP消息中发送的新MTU值,ipv4_sk_update_pmtu函数判断之后直接返回。需要注意的是,除去这两种PMTU策略外,其它情况下,内核还是会保存通过ICMP接收到的新MTU值,这样在用户之后修改pmtu策略后能马上生效。另外在策略配置为非IP_PMTUDISC_DONT时,设置sock错误标志。
static inline bool ip_sk_accept_pmtu(const struct sock *sk)
{
return inet_sk(sk)->pmtudisc != IP_PMTUDISC_INTERFACE &&
inet_sk(sk)->pmtudisc != IP_PMTUDISC_OMIT;
}
void ipv4_sk_update_pmtu(struct sk_buff *skb, struct sock *sk, u32 mtu)
{
if (!ip_sk_accept_pmtu(sk))
goto out;
}
void __udp4_lib_err(struct sk_buff *skb, u32 info, struct udp_table *udptable)
{
case ICMP_DEST_UNREACH:
if (code == ICMP_FRAG_NEEDED) { /* Path MTU discovery */
ipv4_sk_update_pmtu(skb, sk, info);
if (inet->pmtudisc != IP_PMTUDISC_DONT) {
err = EMSGSIZE;
harderr = 1;
break;
}
}
}
在ip_queue_xmit函数中看到,如果skb->ignore_df为真,就会清除IP报头的DF位,ignore_df变量有函数ip_sk_ignore_df赋值。当pmtudisc策略设置成IP_PMTUDISC_DONT、IP_PMTUDISC_WANT或者IP_PMTUDISC_OMIT的时候,ignore_df变量为真,内核将会在发出的报文中清除DF标志位。
static inline bool ip_sk_ignore_df(const struct sock *sk)
{
return inet_sk(sk)->pmtudisc < IP_PMTUDISC_DO ||
inet_sk(sk)->pmtudisc == IP_PMTUDISC_OMIT;
}
在分片处理函数中,对于本机产生的报文,在发送时即便设置了不允许分片DF标志位,只要ignore_df为真,强制进行分片。对于转发的报文,如果设置了DF位,同时接收的时候就是一个已经经过分片的报文,内核进行了重组,但是原始报文的最大分片值大于出接口的mtu值,此时不分片,回复ICMP消息ICMP_FRAG_NEEDED,告知对方内核的MTU值。
static int ip_fragment(...)
{
if ((iph->frag_off & htons(IP_DF)) == 0)
return ip_do_fragment(sk, skb, output);
if (unlikely(!skb->ignore_df ||
(IPCB(skb)->frag_max_size && IPCB(skb)->frag_max_size > mtu))) {
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED, htonl(mtu));
return -EMSGSIZE;
}
return ip_do_fragment(sk, skb, output);
}
使用ping命令即可测试PMTU策略:
ping
-M pmtudisc_opt
Select Path MTU Discovery strategy. pmtudisc_option may be
either do (prohibit fragmentation, even local one), want (do PMTU
discovery, fragment locally when packet size is large), or dont
(do not set DF flag).
例如发送长度超过超过MTU值(1500)的数据包,并且设置IP头的DF位,系统提示message too long:
ping -c 3 -s 1473 -M do 192.168.1.133
PING 192.168.1.133 (192.168.1.133) 1473(1501) bytes of data.
ping: local error: Message too long, mtu=1500
ping: local error: Message too long, mtu=1500
ping: local error: Message too long, mtu=1500
--- 192.168.1.133 ping statistics ---
3 packets transmitted, 0 received, +3 errors, 100% packet loss, time 1999ms
内核版本
linux-3.10.0