TCP/IP协议栈-ipv4

TCP/IP协议栈-ipv4

ipv4协议是当今基于标准的Internet中的核心协议之一,大部分的流量都是靠它传输的。本章记录IPv4数据包的接收、发送和转发,以及IPv4选项的处理。

IP报头

IP报头

IPv4数据包的开头都是一个IP报头,其长度不少于20字节。如果使用了IP选项,IPv4报头最长可达到60字节。由上图可看出IP报头分为两部分,第一部分为IPv4报头,长20字节;第二部分为IP选项,长度为0-40字节

iphdr

  • ihl:表示Internet报头长度,最短20字节,最长60字节,且必须为4字节的整数倍
  • version:必须为4
  • tos:表示服务类型。
  • id:IPv4报头标识。对SKB进行分段时,所有分段的id值必须相同
  • ttl:存活时间。每经历一个转发节点,ttl将会减1,当ttl值为0时,丢弃数据包,并发回一条ICMPv4超时消息,以避免数据包因某种原因而被无休止转发
  • protocol:数据包第四层协议,IPPROTO_TCP表示TCP,IPPROTO_UDP表示UDP流量
  • check:校验和,长16位
  • saddr:源IPv4地址,长32位
  • daddr:目标IPv4地址,长32位

IPv4初始化

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
static struct packet_type ip_packet_type __read_mostly = {
.type = cpu_to_be16(ETH_P_IP),
.func = ip_rcv,
};

static int __init inet_init(void)
{
struct inet_protosw *q;
struct list_head *r;
int rc = -EINVAL;

BUILD_BUG_ON(sizeof(struct inet_skb_parm) > FIELD_SIZEOF(struct sk_buff, cb));

sysctl_local_reserved_ports = kzalloc(65536 / 8, GFP_KERNEL);
if (!sysctl_local_reserved_ports)
goto out;

rc = proto_register(&tcp_prot, 1);
if (rc)
goto out_free_reserved_ports;

rc = proto_register(&udp_prot, 1);
if (rc)
goto out_unregister_tcp_proto;

rc = proto_register(&raw_prot, 1);
if (rc)
goto out_unregister_udp_proto;

rc = proto_register(&ping_prot, 1);
if (rc)
goto out_unregister_raw_proto;
/*
* Tell SOCKET that we are alive...
*/
(void)sock_register(&inet_family_ops);

#ifdef CONFIG_SYSCTL
ip_static_sysctl_init();
#endif
tcp_prot.sysctl_mem = init_net.ipv4.sysctl_tcp_mem;
/*
* Add all the base protocols.
*/
if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)
pr_crit("%s: Cannot add ICMP protocol\n", __func__);
if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
pr_crit("%s: Cannot add UDP protocol\n", __func__);
if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
pr_crit("%s: Cannot add TCP protocol\n", __func__);
#ifdef CONFIG_IP_MULTICAST
if (inet_add_protocol(&igmp_protocol, IPPROTO_IGMP) < 0)
pr_crit("%s: Cannot add IGMP protocol\n", __func__);
#endif

/* Register the socket-side information for inet_create. */
for (r = &inetsw[0]; r < &inetsw[SOCK_MAX]; ++r)
INIT_LIST_HEAD(r);

for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
inet_register_protosw(q);
/*
* Set the ARP module up
*/
arp_init();
/*
* Set the IP module up
*/
ip_init();
tcp_v4_init();
/* Setup TCP slab cache for open requests. */
tcp_init();
/* Setup UDP memory threshold */
udp_init();
/* Add UDP-Lite (RFC 3828) */
udplite4_register();
ping_init();
/*
* Set the ICMP layer up
*/

if (icmp_init() < 0)
panic("Failed to create the ICMP control socket.\n");

/*
* Initialise the multicast router
*/
#if defined(CONFIG_IP_MROUTE)
if (ip_mr_init())
pr_crit("%s: Cannot init ipv4 mroute\n", __func__);
#endif
/*
* Initialise per-cpu ipv4 mibs
*/
if (init_ipv4_mibs())
pr_crit("%s: Cannot init ipv4 mibs\n", __func__);
ipv4_proc_init();
ipfrag_init();

dev_add_pack(&ip_packet_type);

rc = 0;
out:
return rc;
out_unregister_raw_proto:
proto_unregister(&raw_prot);
out_unregister_udp_proto:
proto_unregister(&udp_prot);
out_unregister_tcp_proto:
proto_unregister(&tcp_prot);
out_free_reserved_ports:
kfree(sysctl_local_reserved_ports);
goto out;
}

方法dev_add_pack()将方法ip_rev指定为IPv4数据包的协议处理程序。这些数据包的以太类型为0x0800(ETH_P_IP),inet_init()执行各种IPv4初始化工作,在引导阶段被占用。

Netfilter

在看ip_rcv之前,先看一下这个Netfilter模块。

linux内核网络之Netfilter

接收IPv4数据包

ip_rcv

以上两个图都是一个意思,可以对比着看。

ip_rcv() 函数验证 IP 分组,比如目的地址是否本机地址,校验和是否正确等。若正确,则交给 netfilter 的NF_IP_PRE_ROUTING 钩子。

到了 ip_rcv_finish() 函数,数据包就要根据 skb 结构的目的或路由信息各奔东西了

  • 判断数据包的去向, ip_local_deliver() 处理到本机的数据分组、 ip_forward() 处理需要转发的数据分组、 ip_mr_input() 转发组播数据包。如果是转发的数据包,还需要找出出口设备和下一跳
  • 分析和处理 IP 选项。(并不是处理所有的 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/*
* Main IP Receive routine.
*/
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
{
const struct iphdr *iph;
u32 len;

/* When the interface is in promisc. mode, drop all the crap
* that it receives, do not try to analyse it.
*/
if (skb->pkt_type == PACKET_OTHERHOST)
goto drop;


IP_UPD_PO_STATS_BH(dev_net(dev), IPSTATS_MIB_IN, skb->len);

if ((skb = skb_share_check(skb, GFP_ATOMIC)) == NULL) {
IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INDISCARDS);
goto out;
}

if (!pskb_may_pull(skb, sizeof(struct iphdr)))
goto inhdr_error;

iph = ip_hdr(skb);

/*
* RFC1122: 3.2.1.2 MUST silently discard any IP frame that fails the checksum.
*
* Is the datagram acceptable?
*
* 1. Length at least the size of an ip header
* 2. Version of 4
* 3. Checksums correctly. [Speed optimisation for later, skip loopback checksums]
* 4. Doesn't have a bogus length
*/
//这边判断报头长度以4字节为单位,最短20字节,故ihl最小为5,version必须为4
if (iph->ihl < 5 || iph->version != 4)
goto inhdr_error;

if (!pskb_may_pull(skb, iph->ihl*4))
goto inhdr_error;

iph = ip_hdr(skb);
//检查校验和,如果不正确,丢弃数据包
if (unlikely(ip_fast_csum((u8 *)iph, iph->ihl)))
goto inhdr_error;

len = ntohs(iph->tot_len);
if (skb->len < len) {
IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INTRUNCATEDPKTS);
goto drop;
} else if (len < (iph->ihl*4))
goto inhdr_error;

/* Our transport medium may have padded the buffer out. Now we know it
* is IP we can trim to the true length of the frame.
* Note this now means skb->len holds ntohs(iph->tot_len).
*/
if (pskb_trim_rcsum(skb, len)) {
IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INDISCARDS);
goto drop;
}

/* Remove any debris in the socket control block */
memset(IPCB(skb), 0, sizeof(struct inet_skb_parm));

/* Must drop socket now because of tproxy. */
skb_orphan(skb);
//调用NF_HOOK宏,数据包由Netfilter子系统接管,调用方法ip_rcv_finish
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
ip_rcv_finish);

inhdr_error:
IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INHDRERRORS);
drop:
kfree_skb(skb);
out:
return NET_RX_DROP;
}

ip_rcv_finish

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
static int ip_rcv_finish(struct sk_buff *skb)
{
const struct iphdr *iph = ip_hdr(skb);
struct rtable *rt;

if (sysctl_ip_early_demux && !skb_dst(skb)) {
const struct net_protocol *ipprot;
int protocol = iph->protocol;

ipprot = rcu_dereference(inet_protos[protocol]);
if (ipprot && ipprot->early_demux) {
ipprot->early_demux(skb);
/* must reload iph, skb->head might have changed */
iph = ip_hdr(skb);
}
}

/*
* Initialise the virtual path cache for the packet. It describes
* how the packet travels inside Linux networking.
*/
//skb->dst 包含了数据分组到达目的地的路由信息,如果没有,则需要查找路由,如果最后结果显示目的地不可达,那么就丢弃该数据包
if (!skb_dst(skb)) {
//在路由选择子系统中进行查找
int err = ip_route_input_noref(skb, iph->daddr, iph->saddr,
iph->tos, skb->dev);
if (unlikely(err)) {
if (err == -EXDEV)
NET_INC_STATS_BH(dev_net(skb->dev),
LINUX_MIB_IPRPFILTER);
goto drop;
}
}

#ifdef CONFIG_IP_ROUTE_CLASSID
if (unlikely(skb_dst(skb)->tclassid)) {
struct ip_rt_acct *st = this_cpu_ptr(ip_rt_acct);
u32 idx = skb_dst(skb)->tclassid;
st[idx&0xFF].o_packets++;
st[idx&0xFF].o_bytes += skb->len;
st[(idx>>16)&0xFF].i_packets++;
st[(idx>>16)&0xFF].i_bytes += skb->len;
}
#endif
//检查报头长度,如果大于5说明含有选项,调用ip_rcv_options进行处理
if (iph->ihl > 5 && ip_rcv_options(skb))
goto drop;

rt = skb_rtable(skb);
//如果路由选择的类型是组播或广播,将更新IPSTATS_MIB_INMCAST或IPSTATS_MIB_INBCAST
if (rt->rt_type == RTN_MULTICAST) {
IP_UPD_PO_STATS_BH(dev_net(rt->dst.dev), IPSTATS_MIB_INMCAST,
skb->len);
} else if (rt->rt_type == RTN_BROADCAST)
IP_UPD_PO_STATS_BH(dev_net(rt->dst.dev), IPSTATS_MIB_INBCAST,
skb->len);

return dst_input(skb);

drop:
kfree_skb(skb);
return NET_RX_DROP;
}

/* Input packet from network to transport. */
static inline int dst_input(struct sk_buff *skb)
{
return skb_dst(skb)->input(skb);
}

下面就开始介绍这个skb_dst(skb)->input函数是怎么注册的。

方法ip_rcv也是组播数据包的处理程序。在完成一些完整性检查后,它会调用方法ip_rcv_finish,后者调用ip_route_input_noref执行路由选择子系统查找。首先调用ip_check_mc_rcu来检查当前机器是否属于目标组播地址指定的组播组。如果是这样的组播组或当前机器为组播路由器(设置了CONFIG_IP_MROUTE),就调用ip_route_input_mc

ip_route_input_noref

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
27
28
29
30
31
32
int ip_route_input_noref(struct sk_buff *skb, __be32 daddr, __be32 saddr,
u8 tos, struct net_device *dev)
{
//...
if (ipv4_is_multicast(daddr)) {
struct in_device *in_dev = __in_dev_get_rcu(dev);

if (in_dev) {
//检查当前机器是否属于目标组播地址指定的组播组
int our = ip_check_mc_rcu(in_dev, daddr, saddr,
ip_hdr(skb)->protocol);
if (our
#ifdef CONFIG_IP_MROUTE
||
(!ipv4_is_local_multicast(daddr) &&
IN_DEV_MFORWARD(in_dev))
#endif
) {
//our==1,说明当前机器是目标组播组
int res = ip_route_input_mc(skb, daddr, saddr,
tos, dev, our);
rcu_read_unlock();
return res;
}
}
rcu_read_unlock();
return -EINVAL;
}
res = ip_route_input_slow(skb, daddr, saddr, tos, dev);
rcu_read_unlock();
return res;
}

ip_route_input_mc

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
static int ip_route_input_mc(struct sk_buff *skb, __be32 daddr, __be32 saddr,
u8 tos, struct net_device *dev, int our)
{
struct rtable *rth;
struct in_device *in_dev = __in_dev_get_rcu(dev);
//...
rth = rt_dst_alloc(dev_net(dev)->loopback_dev,
IN_DEV_CONF_GET(in_dev, NOPOLICY), false, false);
if (!rth)
goto e_nobufs;

#ifdef CONFIG_IP_ROUTE_CLASSID
rth->dst.tclassid = itag;
#endif
//设置dst.output函数
rth->dst.output = ip_rt_bug;

rth->rt_genid = rt_genid(dev_net(dev));
rth->rt_flags = RTCF_MULTICAST;
rth->rt_type = RTN_MULTICAST;
rth->rt_is_input= 1;
rth->rt_iif = 0;
rth->rt_pmtu = 0;
rth->rt_gateway = 0;
rth->rt_uses_gateway = 0;
INIT_LIST_HEAD(&rth->rt_uncached);
if (our) {
//设置dst.input函数为ip_local_deliver
rth->dst.input= ip_local_deliver;
rth->rt_flags |= RTCF_LOCAL;
}
//此宏会检查procfs组播转发条目
#ifdef CONFIG_IP_MROUTE
if (!ipv4_is_local_multicast(daddr) && IN_DEV_MFORWARD(in_dev))
//将dst.input函数设置为ip_mr_input
rth->dst.input = ip_mr_input;
#endif
RT_CACHE_STAT_INC(in_slow_mc);

skb_dst_set(skb, &rth->dst);
return 0;
}

所以如果是组播数据包就会调用ip_mr_input。然后调用ip_mr_forward。此函数执行一些检查并最终调用ipmr_queue_xmit。在方法ipmr_queue_xmit中,调用方法ip_decrease_ttl将ttl减1,并更新校验和。最后调用NF_HOOKNF_INET_FORWARD来调用方法ipmr_forward_finish

ip_mr_input流程

ip_route_input_slow

介绍几个关键的数据结构:

struct flowi4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct flowi4 {
struct flowi_common __fl_common;
#define flowi4_oif __fl_common.flowic_oif
#define flowi4_iif __fl_common.flowic_iif
#define flowi4_mark __fl_common.flowic_mark
#define flowi4_tos __fl_common.flowic_tos
#define flowi4_scope __fl_common.flowic_scope
#define flowi4_proto __fl_common.flowic_proto
#define flowi4_flags __fl_common.flowic_flags
#define flowi4_secid __fl_common.flowic_secid

/* (saddr,daddr) must be grouped, same order as in IP header */
__be32 saddr;
__be32 daddr;

union flowi_uli uli;
#define fl4_sport uli.ports.sport
#define fl4_dport uli.ports.dport
#define fl4_icmp_type uli.icmpt.type
#define fl4_icmp_code uli.icmpt.code
#define fl4_ipsec_spi uli.spi
#define fl4_mh_type uli.mht.type
#define fl4_gre_key uli.gre_key
} __attribute__((__aligned__(BITS_PER_LONG/8)));

flowi4对象包含了对IPv4路由选择查找过程中至关重要的字段,比如目标地址dst、原地址src、服务类型tos等。在fib_lookup中执行查找前对其进行初始化。

struct rtable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct rtable {
struct dst_entry dst;

int rt_genid;
unsigned int rt_flags;//rtable的标志
__u16 rt_type;
__u8 rt_is_input;//对于输入路由为1
__u8 rt_uses_gateway;//下一条为网关为1,直连路由为0

int rt_iif;
/* Info on neighbour */
__be32 rt_gateway;

/* Miscellaneous cached information */
u32 rt_pmtu;

struct list_head rt_uncached;
};
struct dst_entry {
//...
int (*input)(struct sk_buff *);
int (*output)(struct sk_buff *);
//...
}

rtable结构中内嵌了一个dst_entry结构体,dst_entry中有着两个相当重要的函数。也就是dst.input以及dst.output。在进行路由选择查找时,会根据路由选择查找结果将这两个函数设置为合适的处理程序。

  1. 对于目的地为当前主机的单播数据包:dst.input=ip_local_deliver
  2. 对于需要转发的单播数据包:dst.input=ip_forward
  3. 对于当前主机生成并要向外发送的数据包:dst.output=ip_output
  4. 对于组播数据包:dst.input=ip_mr_input
  5. 有些情况:dst.input=ip_error

发送IPv4数据包

IPv4为它上面的层(传输层L4)提供了将数据包交给数据链路层L2的发送方式。从L4发送数据包的方法有2个:一个是ip_queue_xmit(),另一个是ip_append_data()。有时候还直接调用dst_output(),例如在使用套接字选项IP_HDRINCL的原始套接字发送数据包时,不需要准备IPv4的报头。

ip_queue_xmit

rtable对象为路有选择子系统查找结果。如果rtable实例为NULL,需要执行路由选择子系统查找的情形。接下去使用函数ip_route_output_ports()在路由子系统中执行查找。如果查找失败,丢弃该数据包。

L3报头的生成

在L3报头添加后,就可以通过ip_local_out()进行发送了

上面就是NF_INET_LOCAL_OUT这个hook点的挂载,我们可以搜一下dst.output在哪些地方被赋值!

无论走的那一条,他们都调用的同一个NF_HOOK_COND宏,来挂载NF_INET_POST_ROUTING.

回调函数output是要使用的传输方法。从ip_finish_output调用方法ip_fragment时,被设置为ip_finish_output2
ip_fragment是数据包分段的函数。下面就开始介绍分段。

分段

发送长于出栈网卡MTU的数据包时,需要将其划分为较小的片段,这是通过ip_fragment完成的。而当收数据包时,需要将这些小数据包重组成一个数据包,这是通过ip_defrag完成的。

判断是否可分段

这个之前也提到过ip_options_build的最后一个参数就是用来判断是否可分段,如果不可分段,就向发送方返回一个ICMPv4`项目不可达`的消息

快速路径

  1. 调用skb_has_frag_list判断是否采用快速路径处理
  2. 为第一个分段创建IPv4报头。这个报头的frag_off被设置为htons(IP_MF),frag_off字段长16位,其中后13位为分段偏移量,前3位为标志。第一个分段偏移量为0,标志位IP_MF。除最后一个分段外,标志位都为IP_MF,最后一个分段,不设置标志位
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
if (skb_has_frag_list(skb)) {
struct sk_buff *frag, *frag2;
int first_len = skb_pagelen(skb);
//...

/* Everything is OK. Generate! */

err = 0;
offset = 0;
frag = skb_shinfo(skb)->frag_list;
//调用skb_frag_list_init将skb_fshinfo(skb)->frag_list设置为NULL
skb_frag_list_init(skb);
skb->data_len = first_len - skb_headlen(skb);
skb->len = first_len;
iph->tot_len = htons(first_len);
//设置第一个分段标志位
iph->frag_off = htons(IP_MF);
//重新计算校验和
ip_send_check(iph);

for (;;) {
/* Prepare header of the next frame,
* before previous one went down. */
if (frag) {
frag->ip_summed = CHECKSUM_NONE;
skb_reset_transport_header(frag);
__skb_push(frag, hlen);
//设置L3报头,使其只想skb->data
skb_reset_network_header(frag);
//将这个报头复制到L3报头中
memcpy(skb_network_header(frag), iph, hlen);
//初始化下一个分段的报头
iph = ip_hdr(frag);
iph->tot_len = htons(frag->len);
//将各个SKB字段(pkt_type、priority、protocol)复制到frag中
ip_copy_metadata(frag, skb);
if (offset == 0)
ip_options_fragment(frag);
offset += skb->len - hlen;
//frag_off字段必须以8字节为单位,所以除以8
iph->frag_off = htons(offset>>3);
//这边就是循环设置frag_off标志位为IP_MF,除最后一个分段外
if (frag->next != NULL)
iph->frag_off |= htons(IP_MF);
/* Ready, complete checksum */
ip_send_check(iph);
}
//调用output回调函数发送分段
err = output(skb);

if (!err)
IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGCREATES);
if (err || !frag)
break;

skb = frag;
frag = skb->next;
skb->next = NULL;
}

if (err == 0) {
IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGOKS);
return 0;
}
//如果有一次调用output时失败,就释放所有的skb
while (frag) {
skb = frag->next;
kfree_skb(frag);
frag = skb;
}
IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGFAILS);
return err;

慢速路径

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
slow_path:
/* for offloaded checksums cleanup checksum before fragmentation */
if ((skb->ip_summed == CHECKSUM_PARTIAL) && skb_checksum_help(skb))
goto fail;
iph = ip_hdr(skb);

left = skb->len - hlen; /* Space per frame */
ptr = hlen; /* Where to start from */

/* for bridged IP traffic encapsulated inside f.e. a vlan header,
* we need to make room for the encapsulating header
*/
ll_rs = LL_RESERVED_SPACE_EXTRA(rt->dst.dev, nf_bridge_pad(skb));

/*
* Fragment the datagram.
*/

offset = (ntohs(iph->frag_off) & IP_OFFSET) << 3;
not_last_frag = iph->frag_off & htons(IP_MF);

/*
* Keep copying data until we run out.
*/

while (left > 0) {
len = left;
/* IF: it doesn't fit, use 'mtu' - the data space left */
if (len > mtu)
len = mtu;
/* IF: we are not sending up to and including the packet end
then align the next start on an eight byte boundary */
if (len < left) {
len &= ~7;
}
/*
* Allocate buffer.
*/

if ((skb2 = alloc_skb(len+hlen+ll_rs, GFP_ATOMIC)) == NULL) {
NETDEBUG(KERN_INFO "IP: frag: no memory for new fragment!\n");
err = -ENOMEM;
goto fail;
}

/*
* Set up data on packet
*/
//生成数据包数据
ip_copy_metadata(skb2, skb);
skb_reserve(skb2, ll_rs);
skb_put(skb2, len + hlen);
skb_reset_network_header(skb2);
skb2->transport_header = skb2->network_header + hlen;

/*
* Charge the memory for the fragment to any owner
* it might possess
*/
//为分段分配的内存归占有者占用
if (skb->sk)
skb_set_owner_w(skb2, skb->sk);

/*
* Copy the packet header into the new buffer.
*/
//拷贝数据包报头复制到新缓冲区
skb_copy_from_linear_data(skb, skb_network_header(skb2), hlen);

/*
* Copy a block of the IP datagram.
*/
//复制IP数据块
if (skb_copy_bits(skb, ptr, skb_transport_header(skb2), len))
BUG();
left -= len;

/*
* Fill in the new header fields.
*/
//填充新的报头字段
iph = ip_hdr(skb2);
iph->frag_off = htons((offset >> 3));

/* ANK: dirty, but effective trick. Upgrade options only if
* the segment to be fragmented was THE FIRST (otherwise,
* options are already fixed) and make it ONCE
* on the initial skb, so that all the following fragments
* will inherit fixed options.
*/
if (offset == 0)
ip_options_fragment(skb);

/*
* Added AC : If we are fragmenting a fragment that's not the
* last fragment then keep MF on each bit
*/
if (left > 0 || not_last_frag)
iph->frag_off |= htons(IP_MF);
ptr += len;
offset += len;

/*
* Put this fragment into the sending queue.
*/
iph->tot_len = htons(len + hlen);

ip_send_check(iph);

err = output(skb2);
if (err)
goto fail;

IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGCREATES);
}
consume_skb(skb);
IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGOKS);
return err;

fail:
kfree_skb(skb);
IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGFAILS);
return err;
}

重组

重组指的是将数据包的所有分段重组为一个缓冲区,这些分段的IPv4报头的id都相同。在接收路径中,处理重组的主方法为ip_defrag。在ip_local_deliver中就调用了ip_is_fragment来检查数据包是否经过分段。如果是就调ip_defrag

ip_is_fragment返回true的条件:

  1. 设置IP_MF标志
  2. 分段偏移量不为0

ip_defrag

ip_defrag

ip_frag_queue

接下来就是找出分段的添加位置。方法是:查找分段偏移量后面的第一个位置(因为分段链表是按偏移量排序的)

插入所有的分段到分段链表

最后调用ip_frag_reasm进行重组

总结

本篇介绍了IPv4的相关linux网络协议栈,包括IPv4数据包如何创建、IPv4报头的结构和IP选项以及如何处理IPv4报头。还有数据包的发送和接收。数据包的分段以及重组。