歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux綜合 >> Linux內核 >> Linux內核分析 - 網絡[八]:IP協議

Linux內核分析 - 網絡[八]:IP協議

日期:2017/3/3 16:38:17   编辑:Linux內核

內核版本: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;
Copyright © Linux教程網 All Rights Reserved