Linux下IP――分片與重組――詳解

LinuxIP――分片與重組――詳解

原理介紹 爲一個數據包片再次分片

    爲數據包分片和爲數據包片再次分片之間的細微差別就在於網關處理MF比特的不同。但一個網關爲原來爲分片的數據包分片時,除了末尾的數據包片,它將其餘所有分片上的MF比特都置爲一,最後一片爲0。然而,當網關爲一個非末尾的數據包片再次分片時,它會把生成的所有子分片中的MF比特全部設置爲1,因爲所有這些子分片都不可能是整個數據包的末尾的數據包片。

    對於分片,需要拷貝IP首部和選項,以及數據。而選項的拷貝要注意:根據協議標準,某些選項只應當出現在的一個數據包片中,而其他一些則必須出現在所有的數據包中。

數據包重組 數據結構

    爲了使數據包的重組效率更高,用於保存數據包的數據結構必須能夠做到:

爲構成某一個特定數據包的一組數據包片快速定位; 在一組數據包片中快速插入新的數據包片; 有效地判斷一個完整的數據包是否已經全部抵達; 具有數據包片超時機制(ip_expire),並且,如果在重組完成之前定時器溢出,則刪除數據包片。 互斥操作

    重組程序代碼使用了一個互斥信號量。Ipfrag_lock

在鏈表中加入一個數據包片

    查找方式:鏈表的線性查找

溢出時的丟棄

    分片列表空間以全滿的情況下:丟棄對應的數據包的所有分片。Ip_evictor

測試是否組成一個完整的數據包ip_frag_queue

    判斷IP_MF位是否爲0

將數據包片組裝成完整的數據包LAST_INip_frag_reasm 數據包片鏈表的維護管理

爲了使丟失數據包片的數據包不再浪費存儲資源 ,並防止因爲標示符字段的重新使用而給IP帶來混亂,但已經不可能再受到剩餘數據包片時,IP必須定期檢查數據包片列表。

Ipq_unlink

Ipq_put

Ipq_kill

Ipqhashfn

Linux下的實現 IP分片

如何提高分片處理的效率

ip_fragment(非UDP使用) 典型調用者

    ip_sendà ip_fragment(skb, ip_finish_output);一般從轉發來

    ip_queue_xmit2à ip_fragment(skb, skb->dst->output)一般從TCP

    因爲IP報太大而將其分片以適合於一個幀的傳輸。

處理過程

    獲取外出設備(由skb決定)

          dev = rt->u.dst.dev;    出口路由設備

    !!!skb->dst=rt=rt->u.dstàdst_entry

    IP包頭

          raw = skb->nh.raw;

          iph = (struct iphdr*)raw;   IP

    設定開始值

          hlen=IP頭長

          left = ntohs(iph->tot_len) - hlen; 包總長度減去IP頭長度――需要分片的數據長度

          mtu = rt->u.dst.pmtu - hlen;  物理MTU減去IP頭長度――除去IP頭的分片長度

          ptr = raw + hlen;    取數據區指針

    將數據包分片

分片算法很簡單,但由於對sk_buff結構和鏈的操作時的實現非常複雜。 如果DF比特禁止分片,則ip_output丟棄分組並返回錯誤消息。 如果該數據包是在本地生成的,則傳輸層協議把該錯誤傳回該進程 如果分組是被轉發的,則ip_forward生成一個ICMP目的不可達差錯報文,並指出不分片就我發轉發該分組。 路徑發現機制?該算法用來搜索到目的主機的路徑,並發現所有中間網絡支持的最大傳送單元MTU 新的分片包含:IP首部、某些原始分組中的選項以及最多len的長度的數據。 Linux下沒有分片隊列,分一個片就發一個分片包。

          offset = (ntohs(iph->frag_off) & IP_OFFSET) << 3;

    取出偏移位(13位),並乘8算出總字節數――算該包的偏移字節數

          not_last_frag = iph->frag_off & htons(IP_MF);

取出MF位(第14位)

    循環進行分片:

          while(left > 0) {

                 len = left;

                 /* IF: it doesn't fit, use 'mtu' - the data space left */

                 if (len > mtu)    如果剩下的數據left還比MTU大,則以MTU爲分片的數據長度;否則,就用left作爲數據長度(對於最後一片)

                        len = mtu;   

                 /* IF: we are not sending upto and including the packet end

                    then align the next start on an eight byte boundary下一個開始出是八字節的邊界對齊 */

                 if (len < left) {   len<mtu時,即最後一個分片長度小於MTU,則不需要再取8字節的整數倍

                        len &= ~7;   8字節的整數倍

                 }

    分配緩衝區新的分片包的sk_buff,大小:硬件幀長+分片長+IP頭長

    填充分片

包類型:本機、廣播、多播、其他主機、外出、迴環、快速路由 包優先級 留出幀頭的空間 指定IPTCP的原始指針raw

                 /*

                  * Charge the memory for the fragment to any owner

                  * it might possess

                  */

如果該包有sock,在包的sock註冊該分片的所有者。 拷貝目的地址,增加引用計數。 拷貝出口設備。 拷貝IP 拷貝IP報塊(只有分片長度MTU大小),並減小總的包長度left-len,以便下次分片。Left記錄了剩餘數據量。

    裝填新的IP

定位新分片包的IP 設置分片的偏移值(對於第一個分片該值就是原IP包的偏移值)――offset記錄了分片的偏移量。此時標誌位爲空。iph->frag_off = htons((offset >> 3));  

    如果偏移爲0――表明該包第一次被分片

                 if (offset == 0)                      

    表明該IP包是第一次分片要在第一片中填入一些不允許在其他分片中出現的選項,爲了提高效率(選項一般放在分片的第一個包中。

                        ip_options_fragment(skb); 

    對於多充分片(not_last_frag=1表示該包就是一個分片包)――對分片包再次分片時,需要保持MF1

                 if (left > 0 || not_last_frag)

                        iph->frag_off |= htons(IP_MF);          設置MF1

    移動原IP包的數據指針ptr

    移動分片偏移指針offset

                 ptr += len;        移動IP包數據指針

                 offset += len;        移動分片指針

    如果配置了防火牆,則進行防火牆值的設置。

    發送該分片

計算總包長 爲分片包重新產生校樣和 發送分片ip_finish_output

    }循環直到數據分片結束(left=0

UDP的分片(待續) IP重組 ip_defrag

    衆所周知,網絡數據報在linux的網絡堆棧中是以sk_buff的結構傳送的,ip_defrag()的功能就是接受分片的數據包(sk_buff),並試圖進行組合,當完整的包組合好時,將新的sk_buff返還,否則返回一個空指針。

典型調用者

    if (iph->frag_off & htons(IP_MF|IP_OFFSET))判斷是否是分片

    ip_local_deliveràip_defrag(skb)

關鍵數據結構(2.4系列) ipq

    這些分片形成一個雙向鏈表(在linux內核中,若需要使用鏈表,除非有特殊需要,否則推薦雙向鏈表,見document/CodingStyle,表示一個未組裝完的分片隊列(屬於一個ip包)。這個鏈表的頭指針要放在ipq結構中:

    /* Describe an entry in the "incomplete datagrams" queue. */

    struct ipq {

          struct ipq *next;  /* linked list pointers   */

          u32  saddr;

          u32  daddr;

          u16  id;

          u8  protocol;

          u8  last_in;

    #define COMPLETE  4

    #define FIRST_IN  2

    #define LAST_IN   1 

          struct sk_buff *fragments; /* linked list of received fragments */

          int  len;  /* total length of original datagram */

          int  meat;  保留現有的分片長度的累加值

          spinlock_t lock;

          atomic_t refcnt;

          struct timer_list timer; /* when will this queue expire?  */

          struct ipq **pprev;

          int  iif;  /* Device index - for icmp replies */

    };

    注意每個ipq保留了一個定時器(即struct timer_list timer;)。 

    Ipq利用一個HASH表來構建分片鏈。

    hash表:

    #define IPQ_HASHSZ   64

    struct ipq *ipq_hash[IPQ_HASHSZ];

    #define ipqhashfn(id, saddr, daddr, prot) /標識、源地址、目的地址、協議

        ((((id) >> 1) ^ (saddr) ^ (daddr) ^ (prot)) & (IPQ_HASHSZ - 1)) 
 

    每個IP包用如下四元組表示:(id,saddr,daddr,protocol,四個值都相同的碎片散列到一個IPQ鏈中,即可組裝成一個完整的IP包。

FRAG_CB

    #define FRAG_CB(skb) ((struct ipfrag_skb_cb*)((skb)->cb))

    cb是一塊控制緩衝區。它提供給每一層存放私有的數據。如果你需要將它們保持到其他層,則必須進行克隆skb_clone

          char  cb[48]; 

ipfrag_skb_cb

    struct ipfrag_skb_cb

    {

          struct inet_skb_parm h;

          int   offset;

    };

inet_skb_parm

    struct inet_skb_parm

    {

          struct ip_options opt;  /* Compiled IP options  */

          unsigned char  flags; 

    #define IPSKB_MASQUERADED 1

    #define IPSKB_TRANSLATED 2

    #define IPSKB_FORWARDED  4

    };

函數說明 當內核接收到本地的IP, 在傳遞給上層協議處理之前, 先進行碎片重組.IP包分片之間的標識號(id)是相同的. IP包片偏量(frag_off)14(IP_MF)1, 表示該IP包有後繼分片. 片偏移量的低13位則爲該分片在完整數據包中的偏移量, 8字節爲單位. IP_MF位爲0, 表示IP包是最後一塊碎片. 如果60120秒後(IP_FRAG_TIME常量指定。(30HZ))重組隊列內包未到齊, 則重組過程失敗, 重組隊列被釋放, 同時向發送方以ICMP協議通知失敗信息. 重組隊列的內存消耗不得大於256k(sysctl_ipfrag_high_thresh), 否則將會調用(ip_evictor)釋放每支散列尾端的重組隊列. 所有IP實現必須能給重裝最多576字節的數據報。 可能會有分片重疊。分片重疊的處理。 爲了防止因保留分片而造成內存消耗過大,linux設置了界限來防止這種情況,如果超過了內存使用的上限,則清空內存中最老的隊列(ipq.所用內存的大小保存在變量ip_frag_mem中,當然,對它的讀寫都應是“原子”操作(atomic_subatomic_addatomic_readetc)。其定義在文件ip_fragment.c前部 2.4的分片組裝代碼的流程與2.2系列基本相同,不同的是將函數的分工變化了。由於原ipfrag結構保留的結構均可在skbuff中得到,在2.4中將此結構取消了,並對ipq結構做了一些修改。其他主要變化有:

    1ip_defrag分成了ip_defragip_frag_queue兩部分。

    2ip_glue換名成ip_frag_reasm,流程基本未動。

3)現在ipq中用meat保留現有的分片長度的累加值(已經解決重疊),如果此值到達總長度,則意味着所有的分片到達,因此取消了ip_done函數,不必每次遍歷一次鏈表,因此在效率上有了較大的提高,抗小碎片攻擊的能力得到加強。 

    ip_findàip_frag_createàip_frag-internà IP_FRAG_TIME

流程

    struct sk_buff *ip_defrag(struct sk_buff *skb)

    {

    如果用於分片處理的內存空間大於系統規定的最大值256k,那麼要進行清洗ip_evictor 

    指定IP包對應設備dev

    根據HASH值,定位該包在分片鏈中的位置:

如果有分片鏈,說明已有其他分片到達,???如果非正常順序到達呢?

    將該分片插入到對應的分片隊列中,

根據該包的標誌位與偏移量:如果是最後一個分片包(但不一定分片完全到齊),設置分片隊列長度爲原包的長度;如果是比當前分片靠後的包,改變分片隊列的長度;如果是靠前的包。 調整分片包的skb指針:使skb->data指向ip載荷(數據區),skb->tail指向ip載荷尾。即:skb->data-skb->tailip載荷區。如此以便將該sk_buff加入該分片鏈,並在重組時很容易拼接分片。 掃描重組隊列中的包片段,找到分片鏈中該分片的前一個分片:利用偏移量找offset 如果該分片不是第一個分片(prev!=null),先消除與前一分片重疊:求prev尾部與當前偏移之差,(該偏移包,不一定就是緊接着的那一個包);再消除與後一分片的重疊,求當前偏移與next重疊之差,如果當前包尾部小於後一包尾部,後一包起點後移,後一分片交疊部分清除,減少分片統計總長度meat;如果後一分片完全包含在此分片中,清除它next,減少分片統計總長度meat 將該分片插入到分片隊列中:設置設備,增加meat分片總長度,如果偏移爲0則說明是第一個包,置上FIRST_IN標誌。 檢查該分片是否是最後一個分片(分片是否都到齊了,包長度),是:進行分片重組ip_frag_reasm 重組前,先刪除該分片隊列。 爲新的包分配sk_buff結構,填入相應的值:設置新的IP總長度(不能超過65535字節);幀頭位置、IP頭位置、選項數據 拷貝原始的IP頭(分片隊列的第一個分片中有記錄)到新的skb結構 循環拷貝:將分片鏈上的分片skb數據拷貝到新的skb結構中。 增加校樣值。 設置目的地址(克隆)、包類型、協議、設備。 進行防火牆處理。 重新設置IP頭、將3位標誌和13位偏移設置爲0、計算總長度。 返回新的IP包。 如果是第一個分片(找不到),就創建新的入口項插到分片隊列頭ip_frag_create ip_evictor

    當分片所用的內存超過一定的上限時(sysctl_ipfrag_high_thresh)會調用ip_evicator以釋放內存。

    ip_evicator會找尋可清空的IPQ,並將其清空,直到到達到可用的下限(sysctl_ipfrag_low_thresh)。 

    這個值在ip_fragment.c中按如下定義:

    int sysctl_ipfrag_high_thresh = 256*1024;

    int sysctl_ipfrag_low_thresh = 192*1024; 

    同樣,用sysctl -a可可看到這兩參數,同時可以動態修改。

    sysctl -a

    ......

    net.ipv4.ipfrag_low_thresh = 196608

    net.ipv4.ipfrag_high_thresh = 262144

    ......

    理論上ip_evicator應該採用LRU算法,將最古老的IPQ清除。但目前linux(包括2.4.0)沒有實現此功能,只是將hash表按次序清空,這樣的好處是簡單易行。 

    Memory limiting on fragments.  Evictor trashes(丟棄) the oldest * fragment queue until we are back under the low threshold.

    Ip_evictor函數遍歷分片隊列,同時丟棄到目前爲止已經收集到的分片,直到所使用的總的內存量小於規定的限制時爲止。只要佔用的內存大於對他的內存限制值,這個函數就調用Ip_free函數。當分片隊列爲空並且內存閥值也超過時,Ip_evictor函數可以引起內核的恐慌。

    LRU算法,全局變量ipq_hash[64]鏈表,越靠近鏈尾越老引用計數越大越不容易被洗掉

    兩重循環每次洗掉時間最老引用計數最少的分片,直到總佔用內存降到sysctl_ipfrag_low_thresh1921024

ip_frag_create

    初始化一個IP分片隊列,包括了定時器,處理函數爲IP_expire。用ip_frag_intern建立鏈表頭和插入時間鏈。

ip_frag_intern

    ip_frag_nqueues++

發佈了42 篇原創文章 · 獲贊 4 · 訪問量 20萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章