内核版本:2.6.34
这篇是关于IP层协议接收报文时的处理,重点说明了路由表的查找,以及IP分片重组。
ip_rcv进入IP层报文接收函数
丢弃掉不是发往本机的报文,skb->pkt_type在网卡接收报文处理以太网头时会根据dst mac设置,协议栈的书会讲不是发往本机的广播报文会在二层被丢弃,实际上丢弃是发生在进入上层之初。
if (skb->pkt_type == PACKET_OTHERHOST) goto drop;
在取IP报头时要注意可能带有选项,因此报文长度应当以iph->ihl * 4为准。这里就需要尝试两次,第一次尝试sizeof(struct iphdr),只是为了确保skb还可以容纳标准的报头(即20字节),然后可以ip_hdr(skb)得到报头;第二次尝试ihl * 4,这才是报文的真正长度,然后重新调用ip_hdr(skb)来得到报头。两次尝试pull后要重新调用ip_hdr()的原因是pskb_may_pull()可能会调用__pskb_pull_tail()来改现现有的skb结构。
if (!pskb_may_pull(skb, sizeof(struct iphdr))) goto inhdr_error; iph = ip_hdr(skb); …… if (!pskb_may_pull(skb, iph->ihl*4)) goto inhdr_error; iph = ip_hdr(skb);
获取到IP报头后经过一些检查,获取到报文的总长度len = iph->tot_len,此时调用pskb_trim_rcsum()去除多余的字节,即大于len的。
if (pskb_trim_rcsum(skb, len)) { IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INDISCARDS); goto drop; }
然后调用ip_rcv_finish()继续IP层的处理,ip_rcv()可以看成是查找路由前的IP层处理,接下来的ip_rcv_finish()会查找路由表,两者间调用插入的netfilter(关于NetFilter,参考前篇http://blog.csdn.net/qy532846454/article/details/6605592)。
return NF_HOOK(PF_INET, NF_INET_PRE_ROUTING, skb, dev, NULL, ip_rcv_finish);
进入ip_rcv_finish函数
ip_rcv_finish()主要工作是完成路由表的查询,决定报文经过IP层处理后,是继续向上传递,还是进行转发,还是丢弃。
刚开始没有进行路由表查询,所以还没有相应的路由表项:skb_dst(skb) == NULL。则在路由表中查找ip_route_input(),关于内核的路由表,可以参见前篇http://blog.csdn.net/qy532846454/article/details/6726171:
if (skb_dst(skb) == NULL) { int err = ip_route_input(skb, iph->daddr, iph->saddr, iph->tos, skb->dev); if (unlikely(err)) { if (err == -EHOSTUNREACH) IP_INC_STATS_BH(dev_net(skb->dev), IPSTATS_MIB_INADDRERRORS); else if (err == -ENETUNREACH) IP_INC_STATS_BH(dev_net(skb->dev), IPSTATS_MIB_INNOROUTES); goto drop; } }
通过路由表查找,我们知道:
- 如果是丢弃的报文,则直接drop;
- 如果是不能接收或转发的报文,则input = ip_error
- 如果是发往本机报文,则input = ip_local_deliver;
- 如果是广播报文,则input = ip_local_deliver;
- 如果是组播报文,则input = ip_local_deliver;
- 如果是转发的报文,则input = ip_forward;
在ip_rcv_finish()最后,会调用查找到的路由项_skb_dst->input()继续向上传递:
return dst_input(skb);
具体看下各种情况下的报文传递,如果是丢弃的报文,则报文被释放,并从IP协议层返回,完成此次报文传递流程。
drop: kfree_skb(skb); return NET_RX_DROP;
如果是不能处理的报文,则执行ip_error,根据error类型发送相应的ICMP错误报文。
static int ip_error(struct sk_buff *skb) { struct rtable *rt = skb_rtable(skb); unsigned long now; int code; switch (rt->u.dst.error) { case EINVAL: default: goto out; case EHOSTUNREACH: code = ICMP_HOST_UNREACH; break; case ENETUNREACH: code = ICMP_NET_UNREACH; IP_INC_STATS_BH(dev_net(rt->u.dst.dev), IPSTATS_MIB_INNOROUTES); break; case EACCES: code = ICMP_PKT_FILTERED; break; } now = jiffies; rt->u.dst.rate_tokens += now - rt->u.dst.rate_last; if (rt->u.dst.rate_tokens > ip_rt_error_burst) rt->u.dst.rate_tokens = ip_rt_error_burst; rt->u.dst.rate_last = now; if (rt->u.dst.rate_tokens >= ip_rt_error_cost) { rt->u.dst.rate_tokens -= ip_rt_error_cost; icmp_send(skb, ICMP_DEST_UNREACH, code, 0); } out: kfree_skb(skb); return 0; }
如果是主机可以接收报文,则执行ip_local_deliver。ip_local_deliver在向上传递前,会对分片的IP报文进行组包,因为IP层协议会对过大的数据包分片,在接收时,就要进行重组,而重组的操作就是在这里进行的。IP报头的16位偏移字段frag_off是由3位的标志(CE,DF,MF)和13的偏移量组成。如果收到了分片的IP报文,如果是最后一片,则MF=0且offset!=0;如果不是最后一片,则MF=1。
在这种情况下会执行ip_defrag来处理分片的IP报文,如果不是最后一片,则将该报文添加到ip4_frags中保留下来,并return 0,此次数据包接收完成;如果是最后一片,则取出之前收到的分片重组成新的skb,此时ip_defrag返回值为0,skb被重置为完整的数据包,然后继续处理,之后调用ip_local_deliver_finish处理重组后的数据包。
if (ip_hdr(skb)->frag_off & htons(IP_MF | IP_OFFSET)) { if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER)) return 0; }
下面来看下ip_defrag()函数,主体就是下面的代码段。它首先用ip_find()查找IP分片,并返回(如果没有则创建),然后用ip_frag_queue()将新分片加入,关于IP分片的处理,在后面的IP分片中有详细描述。
if ((qp = ip_find(net, ip_hdr(skb), user)) != NULL) { int ret; spin_lock(&qp->q.lock); ret = ip_frag_queue(qp, skb); spin_unlock(&qp->q.lock); ipq_put(qp); return ret; }
然后会调用ip_local_deliver_finish()完成IP协议层的传递,两者调用间依然有netfilter,这是查找完路由表继续向上传递的中间点。
NF_HOOK(PF_INET, NF_INET_LOCAL_IN, skb, skb->dev, NULL, ip_local_deliver_finish);
在ip_local_deliver_finish()中会完成IP协议层处理,再交由上层协议模块处理:ICMP、IGMP、UDP、TCP。在ip_local_deliver_finish函数中,由于IP报头已经处理完,剔除IP报头,并设置skb->transport_header指向传输层协议报头位置。
__skb_pull(skb, ip_hdrlen(skb)); skb_reset_transport_header(skb);
protocol是IP报头中的的上层协议号,以它在inet_protos哈希表中查找处理protocol的协议模块,取出得到ipprot。
hash = protocol & (MAX_INET_PROTOS - 1); ipprot = rcu_dereference(inet_protos[hash]);
而关于inet_protos,它的数据结构是哈希表,用来存储IP层上的协议,包括传输层协议和3.5层协议,它在IP协议模块加载时被添加。
if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0) printk(KERN_CRIT "inet_init: Cannot add ICMP protocol\n"); if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0) printk(KERN_CRIT "inet_init: Cannot add UDP protocol\n"); if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0) printk(KERN_CRIT "inet_init: Cannot add TCP protocol\n"); #ifdef CONFIG_IP_MULTICAST if (inet_add_protocol(&igmp_protocol, IPPROTO_IGMP) < 0) printk(KERN_CRIT "inet_init: Cannot add IGMP protocol\n"); #endif
然后通过调用handler交由上层协议处理,至此,IP层协议处理完成。
ret = ipprot->handler(skb);
IP分片
在收到IP分片时,会暂时存储到一个哈希表ip4_frags中,它在IP协议模块加载时初始化,inet_init() -> ipfrag_init()。要留意的是ip4_frag_match用于匹配IP分片是否属于同一个报文;ip_expire用于在IP分片超时时进行处理。
void __init ipfrag_init(void) { ip4_frags_ctl_register(); register_pernet_subsys(&ip4_frags_ops); ip4_frags.hashfn = ip4_hashfn; ip4_frags.constructor = ip4_frag_init; ip4_frags.destructor = ip4_frag_free; ip4_frags.skb_free = NULL; ip4_frags.qsize = sizeof(struct ipq); ip4_frags.match = ip4_frag_match; ip4_frags.frag_expire = ip_expire; ip4_frags.secret_interval = 10 * 60 * HZ; inet_frags_init(&ip4_frags); }
当收到一个IP分片,首先用ip_find()查找IP分片,实际上就是从ip4_frag表中取出相应项。这里的哈希值是由IP报头的(标识,源IP,目的IP,协议号)得到的。
hash = ipqhashfn(iph->id, iph->saddr, iph->daddr, iph->protocol); q = inet_frag_find(&net->ipv4.frags, &ip4_frags, &arg, hash);
inet_frag_find实现直正的查找
根据hash值取得ip4_frag->hash[hash]项 – inet_frag_queue,它是一个队列,然后遍历该队列,当net, id, saddr, daddr, protocol, user相匹配时,就是要找的IP分片。如果没有匹配的,则调用inet_frag_create创建它。
struct inet_frag_queue *inet_frag_find(struct netns_frags *nf, struct inet_frags *f, void *key, unsigned int hash) __releases(&f->lock) { struct inet_frag_queue *q; struct hlist_node *n; hlist_for_each_entry(q, n, &f->hash[hash], list) { if (q->net == nf && f->match(q, key)) { atomic_inc(&q->refcnt); read_unlock(&f->lock); return q; } } read_unlock(&f->lock); return inet_frag_create(nf, f, key); }
inet_frag_create创建一个IP分片队列ipq,并插入相应队列中。
首先分配空间,真正分配空间的是inet_frag_alloc中的q = kzalloc(f->qsize, GFP_ATOMIC);其中f->qsize = sizeof(struct ipq),也就是说分配了ipq大小空间,但返回的却是struct inet_frag_queue q结构,原因在于inet_frag_queue是ipq的首个属性,它们两者的联系如下图。
static struct inet_frag_queue *inet_frag_create(struct netns_frags *nf, struct inet_frags *f, void *arg) { struct inet_frag_queue *q; q = inet_frag_alloc(nf, f, arg); if (q == NULL) return NULL; return inet_frag_intern(nf, q, f, arg); }
在分配并初始化空间后,由inet_frag_intern完成插入动作,首先还是根据(标识,源IP,目的IP,协议号)先成hash值,这里的qp_in即之前的q。
hash = f->hashfn(qp_in);
然后新创建的队列qp(即上面的qp_in)插入到hash表(即ip4_frags->hash)和net->ipv4.frags中,并增加队列qp的引用计数,net中的队列nqueues统计数。至此,IP分片的创建过程完成。
atomic_inc(&qp->refcnt); hlist_add_head(&qp->list, &f->hash[hash]); list_add_tail(&qp->lru_list, &nf->lru_list); nf->nqueues++;
ip_frag_queue实现将IP分片加入队列中
首先获取该IP分片偏移位置offset,和IP分片偏移结束位置end,其中skb->len – ihl表示IP分片的报文长度,三者间关系即为end = offset + skb->len – ihl。
offset = ntohs(ip_hdr(skb)->frag_off); flags = offset & ~IP_OFFSET; offset &= IP_OFFSET; offset <<= 3; /* offset is in 8-byte chunks */ ihl = ip_hdrlen(skb); /* Determine the position of this fragment. */ end = offset + skb->len - ihl;
如果该IP分片是最后一片(MF=0,offset!=0),即设置q.last_iin |= INET_FRAG_LAST_IN,表示收到了最后一个分片,qp->q.len = end,此时q.len是整个IP报文的总长度。
if ((flags & IP_MF) == 0) { if (end < qp->q.len || ((qp->q.last_in & INET_FRAG_LAST_IN) && end != qp->q.len)) goto err; qp->q.last_in |= INET_FRAG_LAST_IN; qp->q.len = end; }
如果该IP分片不是最后一片(MF=1),当end不是8字节倍数时,通过end &= ~7处理为8字节整数倍(但此时会忽略掉多出的字节,如end=14 => end=8);然后如果该分片更靠后,则q.len = end。
else { if (end&7) { end &= ~7; if (skb->ip_summed != CHECKSUM_UNNECESSARY) skb->ip_summed = CHECKSUM_NONE; } if (end > qp->q.len) { /* Some bits beyond end -> corruption. */ if (qp->q.last_in & INET_FRAG_LAST_IN) goto err; qp->q.len = end; } }
查找q.fragments链表,找到该IP分片要插入的位置,这里的q.fragments就是struct sk_buff类型,即各个IP分片skb都会插入到该链表中,插入的位置按偏移顺序由小到大排列,prev表示插入的前一个IP分片,next表示插入的后一个IP分片。
prev = NULL; for (next = qp->q.fragments; next != NULL; next = next->next) { if (FRAG_CB(next)->offset >= offset) break; /* bingo! */ prev = next; }
然后将skb插入到链表中,要注意fragments为空和不为空的情形,在下图中给出。
skb->next = next; if (prev) prev->next = skb; else qp->q.fragments = skb;
增加q.meat计数,表示已收到的IP分片的总长度;如果offset为0,则表明是第一个IP分片,设置q.last_in |= INET_FRAG_FIRST_IN。
qp->q.meat += skb->len; if (offset == 0) qp->q.last_in |= INET_FRAG_FIRST_IN;
最后当满足一定条件时,进行IP重组。当收到了第一个和最后一个IP分片,且收到的IP分片的最大长度等于收到的IP分片的总长度时,表明所有的IP分片已收集齐,调用ip_frag_reasm重组包。具体的,当收到第一个分片(offset=0且MF=1)时设置q.last_in |= INET_FRAG_FIRST_IN;当收到最后一个分片(offset != 0且MF=0)时设置q.last_in |= INET_FRAG_LAST_IN。meat和len的区别在于,IP是不可靠传输,到达的IP分片不能保证顺序,而meat表示到达IP分片的总长度,len表示到达的IP分片中偏移最大的长度。所以当满足上述条件时,IP分片一定是收集齐了的。
if (qp->q.last_in == (INET_FRAG_FIRST_IN | INET_FRAG_LAST_IN) && qp->q.meat == qp->q.len) return ip_frag_reasm(qp, prev, dev);
以下图为例,原始IP报文分成了4片发送,假设收到了1, 3, 4分片,则此时q.last_in = INET_FRGA_FIRST_IN | INET_FRAG_LAST_IN,q.meat = 30,q.len = 50。表明还未收齐IP分片,等待IP分片2的到来。
这里还有一些特殊情况需要处理,它们可能是重新分片或传输时错误造成的,那就是IP分片互相间有重叠。为了避免这种情况发生,在插入IP分片前会处理掉这些重叠。
第一种重叠是与前个分片重叠,即该分片的的偏移是从前个分片的范围内开始的,这种情况下i表示重叠部分的大小,offset+=i则将该分片偏移后移i个长度,从而与前个分片隔开,而且减少len,pskb_pull(skb, i),见下图图示。
if (prev) { int i = (FRAG_CB(prev)->offset + prev->len) - offset; if (i > 0) { offset += i; err = -EINVAL; if (end <= offset) goto err; err = -ENOMEM; if (!pskb_pull(skb, i)) goto err; if (skb->ip_summed != CHECKSUM_UNNECESSARY) skb->ip_summed = CHECKSUM_NONE; } }
第二种重叠是与后个分片重叠,即该分片的的结束位置在后个分片的范围内,这种情况下i表示重叠部分的大小。后片重叠稍微复杂点,被i重叠的部分都要删除掉,如果i比较大,超过了分片长度,则整个分片都被覆盖,从q.fragments链表中删除。使用while处理i覆盖多个分片的情况。
while (next && FRAG_CB(next)->offset < end)
当整个分片被覆盖掉,从q.fragments中删除,并且由于减少了分片总长度,所以q.meat要减去删除分片的长度。
else { struct sk_buff *free_it = next; next = next->next; if (prev) prev->next = next; else qp->q.fragments = next; qp->q.meat -= free_it->len; frag_kfree_skb(qp->q.net, free_it, NULL); }
当只覆盖分片一部分时,offset+=i则将后个分片偏移后移i个长度,从而与该分片隔开,同时这样相当于减少了IP分片的长度,所以q.meat -= i;见下图图示,
if (i < next->len) { if (!pskb_pull(next, i)) goto err; FRAG_CB(next)->offset += i; qp->q.meat -= i; if (next->ip_summed != CHECKSUM_UNNECESSARY) next->ip_summed = CHECKSUM_NONE; break; }
ip_frag_reasm函数实现IP分片的重组
ip_frag_reasm传入的参数是prev,而重组完成后ip_defrag会将skb替换成重组后的新的skb,而在之前的操作中,skb插入了qp->q.fragments中,并且prev->next即为skb,因此第一步就是让skb变成qp->q.fragments,即IP分片的头部。
if (prev) { head = prev->next; fp = skb_clone(head, GFP_ATOMIC); if (!fp) goto out_nomem; fp->next = head->next; prev->next = fp; skb_morph(head, qp->q.fragments); head->next = qp->q.fragments->next; kfree_skb(qp->q.fragments); qp->q.fragments = head; }
下面图示说明了上面代码段作用,skb是IP分片3,通过skb_clone拷贝一份3_copy替代之前的分片3,再通过skb_morph拷贝q.fragments到原始IP分片3,替代分片1,并释放分片1:
获取IP报头长度ihlen,head就是ip_defrag传入参数中的skb,并且它已经成为了IP分片队列的头部;len为整个IP报头+报文的总长度,qp->q.len是未分片前IP报文的长度。
ihlen = ip_hdrlen(head); len = ihlen + qp->q.len;
此时head就是skb,并且它的skb->data存储了第一个IP分片的内容,其它IP分片的内容将存储在紧接skb的空间 – frag_list;skb_push将skb->data回归原位,即未处理IP报头前的位置,因为之前的IP分片处理会调用skb_pull移走IP报头,将它回归原位是因为skb即将作为重组后的报文而被处理,那里会真正的skb_pull移走IP报头,再交由上层协议处理。
skb_shinfo(head)->frag_list = head->next; skb_push(head, head->data - skb_network_header(head));
上面所说的frag_list是struct skb_shared_info的一个属性,在分配skb时分配在其后空间,通过skb_shinfo(skb)进行引用。下面分配skb大小size和skb_shared_info大小的代码摘自[net/core/skbuff.c]
size = SKB_DATA_ALIGN(size); data = kmalloc_node_track_caller(size + sizeof(struct skb_shared_info), gfp_mask, node);
这里要弄清楚sk_buff中线性存储区和paged buffer的区别,线性存储区就是存储报文,如果是分片后的,则只是第一个分片的内容;而paged buffer则存储其余分片的内容。而skb->data_len则表示paged buffer中内容长度,而skb->len则是paged buffer + linear buffer。下面这段代码就是根据余下的分片增加data_len和len计数。
for (fp=head->next; fp; fp = fp->next) { head->data_len += fp->len; head->len += fp->len; …… }
IP分片已经重组完成,分片从q.fragments链表移到了frag_list上,因此head->next和qp->q.fragments置为NULL。偏移量frag_off置0,总长度tot_len置为所有分片的长度和,这样,skb就相当于没有分片的完整的大数据包,继续向上传递。
head->next = NULL; head->dev = dev; …… iph = ip_hdr(head); iph->frag_off = 0; iph->tot_len = htons(len); IP_INC_STATS_BH(net, IPSTATS_MIB_REASMOKS); qp->q.fragments = NULL;