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;


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章