Linux内核分析 - 网络[八]:IP协议

内核版本: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;

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值