天天看點

Netfilter 連接配接跟蹤與狀态檢測的實作ifdef CONFIG_PROC_FSendif

核心版本:2.6.12

本文隻是一部份,詳細分析了連接配接跟蹤的基本實作,對于ALG部份,還沒有寫,在整理筆記,歡迎大家提意見,批評指正。

1.什麼是連接配接跟蹤

連接配接跟蹤(CONNTRACK),顧名思義,就是跟蹤并且記錄連接配接狀态。Linux為每一個經過網絡堆棧的資料包,生成一個新的連接配接記錄項(Connection entry)。此後,所有屬于此連接配接的資料包都被唯一地配置設定給這個連接配接,并辨別連接配接的狀态。連接配接跟蹤是防火牆子產品的狀态檢測的基礎,同時也是位址轉換中實作SNAT和DNAT的前提。

那麼Netfilter又是如何生成連接配接記錄項的呢?每一個資料,都有“來源”與“目的”主機,發起連接配接的主機稱為“來源”,響應“來源”的請求的主機即為目的,所謂生成記錄項,就是對每一個這樣的連接配接的産生、傳輸及終止進行跟蹤記錄。由所有記錄項産生的表,即稱為連接配接跟蹤表。

2.連接配接跟蹤表

Netfilter使用一張連接配接跟蹤表,來描述整個連接配接狀态,這個表在實作算法上采用了hash算法。我們先來看看這個hash 表的實作。

整個hash表用全局指針ip_conntrack_hash 指針來描述,它定義在ip_conntrack_core.c中:

struct list_head *ip_conntrack_hash;

這個hash表的大小是有限制的,表的大小由ip_conntrack_htable_size 全局變量決定,這個值,使用者态可以在子產品插入時傳遞,預設是根據記憶體大小計算出來的。

每一個hash節點,同時又是一條連結清單的首部,是以,連接配接跟蹤表就由ip_conntrack_htable_size 條連結清單構成,整個連接配接跟蹤表大小使用全局變量ip_conntrack_max描述,與hash表的關系是ip_conntrack_max = 8 * ip_conntrack_htable_size。

連結清單的每個節點,都是一個struct ip_conntrack_tuple_hash 類型:

/ Connections have two entries in the hash table: one for each way /

struct ip_conntrack_tuple_hash

{

struct list_head list;

    struct ip_conntrack_tuple tuple;           

};

這個結構有兩個成員,list 成員用于組織連結清單。多元組(tuple) 則用于描述具體的資料包。

每個資料包最基本的要素,就是“來源”和“目的”,從Socket套接字角度來講,連接配接兩端用“位址+端口”的形式來唯一辨別一個連接配接(對于沒有端口的協定,如ICMP,可以使用其它辦法替代),是以,這個資料包就可以表示為“來源位址/來源端口+目的位址/目的端口”,Netfilter用結構struct ip_conntrack_tuple 結構來封裝這個“來源”和“目的”,封裝好的struct ip_conntrack_tuple結構節點在核心中就稱為“tuple”。最終實作“封裝”,就是根據來源/目的位址、端口這些要素,來進行一個具體網絡封包到tuple的轉換。結構定義如下:

/* The protocol-specific manipulable parts of the tuple: always in

network order! */

union ip_conntrack_manip_proto

/* Add other protocols here. */
    u_int16_t all;

    struct {
            u_int16_t port;
    } tcp;
    struct {
            u_int16_t port;
    } udp;
    struct {
            u_int16_t id;
    } icmp;
    struct {
            u_int16_t port;
    } sctp;           

/ The manipulable part of the tuple. /

struct ip_conntrack_manip

u_int32_t ip;
    union ip_conntrack_manip_proto u;           

/ This contains the information to distinguish a connection. /

struct ip_conntrack_tuple

struct ip_conntrack_manip src;

    /* These are the parts of the tuple which are fixed. */
    struct {
            u_int32_t ip;
            union {
                    /* Add other protocols here. */
                    u_int16_t all;

                    struct {
                            u_int16_t port;
                    } tcp;
                    struct {
                            u_int16_t port;
                    } udp;
                    struct {
                            u_int8_t type, code;
                    } icmp;
                    struct {
                            u_int16_t port;
                    } sctp;
            } u;

            /* The protocol. */
            u_int8_t protonum;

            /* The direction (for tuplehash) */
            u_int8_t dir;
    } dst;           

struct ip_conntrack_tuple 中僅包含了src、dst兩個成員,這兩個成員基本一緻:包含ip以及各個協定的端口,值得注意的是,dst成員中有一個dir成員,dir是direction 的縮寫,辨別一個連接配接的方向,後面我們會看到它的用法。

tuple 結構僅僅是一個資料包的轉換,并不是描述一條完整的連接配接狀态,核心中,描述一個包的連接配接狀态,使用了struct ip_conntrack 結構,可以在ip_conntrack.h中看到它的定義:

struct ip_conntrack

……
    /* These are my tuples; original and reply */
    struct ip_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX];           

這裡僅僅是分析hash表的實作,是以,我們僅需注意struct ip_conntrack結構的最後一個成員tuplehash,它是一個struct ip_conntrack_tuple_hash 類型的數組,我們前面說了,該結構描述連結清單中的節點,這個數組包含“初始”和“應答”兩個成員(tuplehash[IP_CT_DIR_ORIGINAL]和tuplehash[IP_CT_DIR_REPLY]),是以,當一個資料包進入連接配接跟蹤子產品後,先根據這個資料包的套接字對轉換成一個“初始的”tuple,指派給tuplehash[IP_CT_DIR_ORIGINAL],然後對這個資料包“取反”,計算出“應答”的tuple,指派給tuplehash[IP_CT_DIR_REPLY],這樣,一條完整的連接配接已經躍然紙上了。

最後一要注意的問題,就是對于每一條連接配接,尋找連結清單在hash表的入口,也就是如計算hash值。我們關心的是一條連接配接,連接配接是由“請求”和“應答”的資料包組成,資料包會被轉化成tuple,是以,hash值就是根據tuple,通過一定的hash算法實作,這樣,整個hash表如下圖所示:

如圖,小結一下:

n 整個hash表用ip_conntrack_hash 指針數組來描述,它包含了ip_conntrack_htable_size個元素,使用者态可以在子產品插入時傳遞,預設是根據記憶體大小計算出來的;

n 整個連接配接跟蹤表的大小使用全局變量ip_conntrack_max描述,與hash表的關系是ip_conntrack_max = 8 * ip_conntrack_htable_size;

n hash連結清單的每一個節點是一個struct ip_conntrack_tuple_hash結構,它有兩個成員,一個是list,一個是tuple;

n Netfilter将每一個資料包轉換成tuple,再根據tuple計算出hash值,這樣,就可以使用ip_conntrack_hash[hash_id]找到hash表中連結清單的入口,并組織連結清單;

n 找到hash表中連結清單入口後,如果連結清單中不存在此“tuple”,則是一個新連接配接,就把tuple插入到連結清單的合适位置;

n 圖中兩個節點tuple[ORIGINAL]和tuple[REPLY],雖然是分開的,在兩個連結清單當中,但是如前所述,它們同時又被封裝在ip_conntrack結構的tuplehash數組中,這在圖中,并沒有标注出來;

n 連結清單的組織采用的是雙向連結清單,上圖中沒有完整表示出來;

當然,具體的實作要稍微麻煩一點,主要展現在一些複雜的應用層協定上來,例如主動模式下的FTP協定,伺服器在連接配接建立後,會主動打開高端口與用戶端進行通訊,這樣,由于端口變換了,我們前面說的連接配接表的實作就會遇到麻煩。Netfilter為這些協定提供了一個巧秒的解決辦法,我們在本章中,先分析連接配接跟蹤的基本實作,然後再來分析Netfilter對這些特殊的協定的支援的實作。

3.連接配接跟蹤的初始化

3.1 初始化函數

ip_conntrack_standalone.c 是連接配接跟蹤的主要子產品:

static int __init init(void)

return init_or_cleanup(1);           

}

複制代碼

初始化函數進一步調用init_or_cleanup() 進行子產品的初始化,它主要完成hash表的初始化等三個方面的工作:

static int init_or_cleanup(int init)

/*初始化連接配接跟蹤的一些變量、資料結構,如初始化連接配接跟蹤表的大小,Hash表的大小等*/
    ret = ip_conntrack_init();
    if (ret < 0)
            goto cleanup_nothing;
           

/建立proc 檔案系統的對應節點/

ifdef CONFIG_PROC_FS

……           

endif

/為連接配接跟蹤注冊Hook /

ret = nf_register_hook(&ip_conntrack_defrag_ops);
    if (ret < 0) {
            printk("ip_conntrack: can't register pre-routing defrag hook.\n");
            goto cleanup_proc_stat;
    }
    ……           

3.2 ip_conntrack_init

ip_conntrack_init 函數用于初始化連接配接跟蹤的包括hash表相關參數在内一些重要的變量:

/使用者态可以在子產品插入的時候,可以使用hashsize參數,指明hash 表的大小/

static int hashsize;

module_param(hashsize, int, 0400);

int __init ip_conntrack_init(void)

unsigned int i;
    int ret;

    /* 如果子產品指明了hash表的大小,則使用指定值,否則,根據記憶體的大小,來計算一個預設值. ,hash表的大小,是使用全局變量ip_conntrack_htable_size 來描述*/
    if (hashsize) {
            ip_conntrack_htable_size = hashsize;
    } else {
            ip_conntrack_htable_size
                    = (((num_physpages << PAGE_SHIFT) / 16384)
                       / sizeof(struct list_head));
            if (num_physpages > (1024 * 1024 * 1024 / PAGE_SIZE))
                    ip_conntrack_htable_size = 8192;
            if (ip_conntrack_htable_size < 16)
                    ip_conntrack_htable_size = 16;
    }
           

/根據hash表的大小,計算最大的連接配接跟蹤表數/

ip_conntrack_max = 8 * ip_conntrack_htable_size;

    printk("ip_conntrack version %s (%u buckets, %d max)"
           " - %Zd bytes per conntrack\n", IP_CONNTRACK_VERSION,
           ip_conntrack_htable_size, ip_conntrack_max,
           sizeof(struct ip_conntrack));
               

/注冊socket選項/

ret = nf_register_sockopt(&so_getorigdst);
    if (ret != 0) {
            printk(KERN_ERR "Unable to register netfilter socket option\n");
            return ret;
    }

    /* 初始化記憶體配置設定辨別變量 */
    ip_conntrack_vmalloc = 0; 

    /*為hash表配置設定連續記憶體頁*/
    ip_conntrack_hash 
            =(void*)__get_free_pages(GFP_KERNEL, 
                                     get_order(sizeof(struct list_head)
                                               *ip_conntrack_htable_size));
    /*配置設定失敗,嘗試調用vmalloc重新配置設定*/           

if (!ip_conntrack_hash) {

ip_conntrack_vmalloc = 1;
            printk(KERN_WARNING "ip_conntrack: falling back to vmalloc.\n");
            ip_conntrack_hash = vmalloc(sizeof(struct list_head)
                                        * ip_conntrack_htable_size);
    }
    /*仍然配置設定失敗*/
    if (!ip_conntrack_hash) {
            printk(KERN_ERR "Unable to create ip_conntrack_hash\n");
            goto err_unreg_sockopt;
    }

    ip_conntrack_cachep = kmem_cache_create("ip_conntrack",
                                            sizeof(struct ip_conntrack), 0,
                                            0, NULL, NULL);
    if (!ip_conntrack_cachep) {
            printk(KERN_ERR "Unable to create ip_conntrack slab cache\n");
            goto err_free_hash;
    }

    ip_conntrack_expect_cachep = kmem_cache_create("ip_conntrack_expect",
                                    sizeof(struct ip_conntrack_expect),
                                    0, 0, NULL, NULL);
    if (!ip_conntrack_expect_cachep) {
            printk(KERN_ERR "Unable to create ip_expect slab cache\n");
            goto err_free_conntrack_slab;
    }

    /* Don't NEED lock here, but good form anyway. */
    WRITE_LOCK(&ip_conntrack_lock);
               

/ 注冊協定。對不同協定,連接配接跟蹤記錄的參數不同,是以不同的協定定義了不同的 ip_conntrack_protocol結構來處理與協定相關的内容。這些結構被注冊到一個全局的連結清單中,在使用時根據協定去查找,并調用相應的處理函數來完成相應的動作。/

for (i = 0; i < MAX_IP_CT_PROTO; i++)
            ip_ct_protos[i] = &ip_conntrack_generic_protocol;
    ip_ct_protos[IPPROTO_TCP] = &ip_conntrack_protocol_tcp;
    ip_ct_protos[IPPROTO_UDP] = &ip_conntrack_protocol_udp;
    ip_ct_protos[IPPROTO_ICMP] = &ip_conntrack_protocol_icmp;
    WRITE_UNLOCK(&ip_conntrack_lock);
    
    /*初始化hash表*/
    for (i = 0; i < ip_conntrack_htable_size; i++)
            INIT_LIST_HEAD(&ip_conntrack_hash[i]);

    /* For use by ipt_REJECT */
    ip_ct_attach = ip_conntrack_attach;

    /* Set up fake conntrack:
        - to never be deleted, not in any hashes */
    atomic_set(&ip_conntrack_untracked.ct_general.use, 1);
    /*  - and look it like as a confirmed connection */
    set_bit(IPS_CONFIRMED_BIT, &ip_conntrack_untracked.status);

    return ret;
           

err_free_conntrack_slab:

kmem_cache_destroy(ip_conntrack_cachep);           

err_free_hash:

free_conntrack_hash();           

err_unreg_sockopt:

nf_unregister_sockopt(&so_getorigdst);

    return -ENOMEM;           

富貴論壇

這個函數中,有兩個重點的地方值得注意,一個是hash表的相關變量的初始化、記憶體空間的分析等等,另一個是協定的注冊。

連接配接跟蹤由于針對每種協定的處理,都有些細微不同的地方,舉個例子,我們前面講到資料包至tuple的轉換,TCP的轉換與ICMP的轉換肯定不同的,因為ICMP連端口的概念也沒有,是以,對于每種協定的一些特殊處理的函數,需要進行封裝,struct ip_conntrack_protocol 結構就實作了這一封裝,在初始化工作中,針對最常見的TCP、UDP和ICMP協定,定義了ip_conntrack_protocol_tcp、ip_conntrack_protocol_udp和ip_conntrack_protocol_icmp三個該類型的全局變量,初始化函數中,将它們封裝至ip_ct_protos 數組,這些,在後面的資料包處理後,就可以根據包中的協定值,使用ip_ct_protos[協定值],找到注冊的協定節點,就可以友善地調用協定對應的處理函數了,我們在後面将看到這一調用過程。

3.2 鈎子函數的注冊

init_or_cleanup 函數在建立/proc檔案系統完成後,會調用nf_register_hook 函數注冊鈎子,進行連接配接跟蹤,按優先級和Hook不同,注冊了多個鈎子:

ret = nf_register_hook(&ip_conntrack_defrag_ops);
    if (ret < 0) {
            printk("ip_conntrack: can't register pre-routing defrag hook.\n");
            goto cleanup_proc_stat;
    }
    ret = nf_register_hook(&ip_conntrack_defrag_local_out_ops);
    if (ret < 0) {
            printk("ip_conntrack: can't register local_out defrag hook.\n");
            goto cleanup_defragops;
    }
    ……           

整個Hook注冊好後,如下圖所示:

上圖中,粗黑體辨別函數就是連接配接跟蹤注冊的鈎子函數,除此之外,用于處理分片包和處理複雜協定的鈎子函數在上圖中沒有辨別出來。處理分片包的鈎子用于重組分片,用于保證資料在進入連接配接跟蹤子產品不會是一個分片資料包。例如,在資料包進入NF_IP_PRE_ROUTING Hook點,主要的連接配接跟蹤函數是ip_conntrack_in,然而,在它之前,還注冊了ip_conntrack_defrag,用于處理分片資料包:

static unsigned int ip_conntrack_defrag(unsigned int hooknum,

struct sk_buff **pskb,
                                    const struct net_device *in,
                                    const struct net_device *out,
                                    int (*okfn)(struct sk_buff *))           
/* Gather fragments. */
    if ((*pskb)->nh.iph->frag_off & htons(IP_MF|IP_OFFSET)) {
            *pskb = ip_ct_gather_frags(*pskb,
                                       hooknum == NF_IP_PRE_ROUTING ? 
                                       IP_DEFRAG_CONNTRACK_IN :
                                       IP_DEFRAG_CONNTRACK_OUT);
            if (!*pskb)
                    return NF_STOLEN;
    }
    return NF_ACCEPT;           

對于我們本章的分析而言,主要是以“Linux做為一個網關主機,轉發過往資料”為主線,更多關注的是在NF_IP_PRE_ROUTING和NF_IP_POSTROUTING兩個Hook點上注冊的兩個鈎子函數ip_conntrack_in和ip_refrag(這個函數主要執行的是ip_confirm函數)。

鈎子的注冊的另一個值得注意的小問題,就是鈎子函數的優先級,NF_IP_PRE_ROUTING上的優先級是NF_IP_PRI_CONNTRACK ,意味着它的優先級是很高的,這也意味着每個輸入資料包首先被傳輸到連接配接跟蹤子產品,才會進入其它優先級較低的子產品。同樣地,NF_IP_POSTROUTING上的優先級為NF_IP_PRI_CONNTRACK_CONFIRM,優先級是很低的,也就是說,等到其它優先級高的子產品處理完成後,才會做最後的處理,然後将資料包送出去。

4.ip_conntrack_in

資料包進入Netfilter後,會調用ip_conntrack_in函數,以進入連接配接跟蹤子產品,ip_conntrack_in 主要完成的工作就是判斷資料包是否已在連接配接跟蹤表中,如果不在,則為資料包配置設定ip_conntrack,并初始化它,然後,為這個資料包設定連接配接狀态。

/ Netfilter hook itself. /

unsigned int ip_conntrack_in(unsigned int hooknum,

struct sk_buff **pskb,
                         const struct net_device *in,
                         const struct net_device *out,
                         int (*okfn)(struct sk_buff *))           
struct ip_conntrack *ct;
    enum ip_conntrack_info ctinfo;
    struct ip_conntrack_protocol *proto;
    int set_reply;
    int ret;

    /* 判斷目前資料包是否已被檢查過了 */
    if ((*pskb)->nfct) {
            CONNTRACK_STAT_INC(ignore);
            return NF_ACCEPT;
    }
           

/ 分片包當會在前一個Hook中被處理,事實上,并不會觸發該條件 /

if ((*pskb)->nh.iph->frag_off & htons(IP_OFFSET)) {
            if (net_ratelimit()) {
            printk(KERN_ERR "ip_conntrack_in: Frag of proto %u (hook=%u)\n",
                   (*pskb)->nh.iph->protocol, hooknum);
            }
            return NF_DROP;
    }
           

/ 将目前資料包設定為未修改 /

(*pskb)->nfcache |= NFC_UNKNOWN;
           

/根據目前資料包的協定,查找與之相應的struct ip_conntrack_protocol結構/

proto = ip_ct_find_proto((*pskb)->nh.iph->protocol);

    /* 沒有找到對應的協定. */
    if (proto->error != NULL 
        && (ret = proto->error(*pskb, &ctinfo, hooknum)) <= 0) {
            CONNTRACK_STAT_INC(error);
            CONNTRACK_STAT_INC(invalid);
            return -ret;
    }
           

/在全局的連接配接表中,查找與目前包相比對的連接配接結構,傳回的是struct ip_conntrack 類型指針,它用于描述一個資料包的連接配接狀态*/

if (!(ct = resolve_normal_ct(*pskb, proto,&set_reply,hooknum,&ctinfo))) {
            /* Not valid part of a connection */
            CONNTRACK_STAT_INC(invalid);
            return NF_ACCEPT;
    }

    if (IS_ERR(ct)) {
            /* Too stressed to deal. */
            CONNTRACK_STAT_INC(drop);
            return NF_DROP;
    }

    IP_NF_ASSERT((*pskb)->nfct);
           

/Packet函數指針,為資料包傳回一個判斷,如果資料包不是連接配接中有效的部分,傳回-1,否則傳回NF_ACCEPT。/

ret = proto->packet(ct, *pskb, ctinfo);
    if (ret < 0) {
            /* Invalid: inverse of the return code tells
             * the netfilter core what to do*/
            nf_conntrack_put((*pskb)->nfct);
            (*pskb)->nfct = NULL;
            CONNTRACK_STAT_INC(invalid);
            return -ret;
    }
           

/設定應答狀态标志位/

if (set_reply)
            set_bit(IPS_SEEN_REPLY_BIT, &ct->status);

    return ret;           

在初始化的時候,我們就提過,連接配接跟蹤子產品将所有支援的協定,都使用struct ip_conntrack_protocol 結構封裝,注冊至全局數組ip_ct_protos,這裡首先調用函數ip_ct_find_proto根據目前資料包的協定值,找到協定注冊對應的子產品。然後調用resolve_normal_ct 函數進一步處理。

繼續閱讀