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