內核版本: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分片超時時進行處理。
[cpp] view plaincopy 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);
net_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;