天天看點

linux核心netfilter之ip_conntrack子產品的作用--抽象總結

nat規則将資料流的位址資訊進行轉換,轉換了之後需要将轉換後的位址資訊寫入ip_conntrack結構體中,經過nat之後目的位址無非兩個方向,一個是本機(redirect target),一個是其它機器(網關上的nat一般都這樣),于是netfilter需要對這兩個方向的轉換記錄提供支援。

     netfilter的ip_conntrack提供了下列兩個HOOK,ip_conntrack_out_ops用于nat到其它機器的支援,ip_conntrack_local_in_ops提供nat到本機的支援:

static struct nf_hook_ops ip_conntrack_out_ops = {

    .hook        = ip_refrag, //這個名字起得有些怪異,不過也很合理,因為ip_conntrack由于可能需要操作4層以上協定頭或資料載荷,是以必須在PREROUTING這個挂載點上對ip資料包進行defrag操作。

    .owner        = THIS_MODULE,

    .pf        = PF_INET,

    .hooknum    = NF_IP_POST_ROUTING,

    .priority    = NF_IP_PRI_LAST,

};

static struct nf_hook_ops ip_conntrack_local_in_ops = {

    .hook        = ip_confirm,  //這個名字很好,不過也沒有覆寫其全部功能

    .hooknum    = NF_IP_LOCAL_IN,

    .priority    = NF_IP_PRI_LAST-1,

不管是ip_refrag還是ip_confirm,最終都是要調用__ip_conntrack_confirm的,從__ip_conntrack_confirm這個函數的邏輯可以一眼看出ip_conntrack的結構:

int __ip_conntrack_confirm(struct nf_ct_info *nfct)

{

    ...

    hash = hash_conntrack(&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple);

    repl_hash = hash_conntrack(&ct->tuplehash[IP_CT_DIR_REPLY].tuple);

    if (!LIST_FIND(...IP_CT_DIR_ORIGINAL...) //如果原始流和nat後(也可以不做nat,此時兩個tuple是一樣的)的流資訊都沒有加入hash

        && !LIST_FIND(...IP_CT_DIR_REPLY...)) {

        list_prepend(&ip_conntrack_hash[hash],

                 &ct->tuplehash[IP_CT_DIR_ORIGINAL]);

        list_prepend(&ip_conntrack_hash[repl_hash],

                 &ct->tuplehash[IP_CT_DIR_REPLY]);

        ct->timeout.expires += jiffies;

        ...

        set_bit(IPS_CONFIRMED_BIT, &ct->status); //流已經順利地經過了本機

    }

}

最後,通過ip_conntrack為引子來用一種簡單的方式描述一下ip_conntrack在整個netfilter中的作用以及其實作邏輯:

struct ip_conntrack

    struct ip_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX]; //一共2個方向

    struct list_head sibling_list; //related的連接配接

    struct ip_conntrack_expect *master; //和sibling_list的意義相反

    struct {

        struct ip_nat_info info;

        union ip_conntrack_nat_help help;

    } nat;

repl是replace的意思,是這樣的,因為orig是不會被修改的,是以repl雖然字面是reply,實則replace,不管是SNAT還是DNAT,修改的都是資料包往目的地方向的鍊路資訊,對于SNAT來說,資料流進入網關,然後被snat,此時orig的tuple是不會變的,變化的是reply的tuple,因為資料回應回來以後,目的位址就是轉換後的源位址,而源位址就是目前資料流的目的位址,是以repl作為reply和replace來講都是一樣的。

     事實是,tuple隻是為了比對流的,在資料包剛進入時,為了将資料包和一個流相聯系,tuple就有用了,ip_conntrack_tuple中包含足夠的資訊用于比對一個流,包括ip位址和端口号:

struct ip_conntrack_tuple 

    struct ip_conntrack_manip src;  //和下面的dst類似

        u_int32_t ip; //ip位址

        union {

        ...//應用層協定

        } u;

        u_int16_t protonum; //傳輸層協定

    } dst;

}; //該結構包含兩個ip-port對

     下面用一種抽象的方式将ip_contrack以及nat的流程化繁為簡而為一種簡單的形式,從情景角度分析資料包流經nat網關時核心中相關子產品的行為,資料流以(x-y)表示,并且忽略端口資訊,忽略狀态資訊。其實狀态資訊很重要,netfilter的其它子產品可以使用ip_contrack設定的狀态進而做出特殊的決策,同時狀态資訊還可以辨別一個流目前的行為以及目前其需要被給與的行為,忽略之是為了事情更簡單,引出一種分析代碼的方式,凡遺漏之事務容日後視心力與心情加之。

     資料包(a-b)進入R,發生了snat,位址資訊成為了(m-b),雖然發生了nat,(a-b)和(m-b)應該是屬于同一個資料流CK的,ip_conntrack需要作記錄,以便将兩個流綁定在一起,資料從a到b的方向在R處成了由m到b的方向,屬于一個方向,都是源到目的,發生了snat後,資料就可以出去了,既然資料出去R了,我們也就不關心它了,我們關心的是從b發出的回應a資料到達R後如何将之綁定到流CK,資料回來後由于發生過snat流标示顯然是(b-m),于是ip_conntrack需要将(b-m)也綁定到CK,此時我們可以定義出CK了:

struct Contrack {

    Two-Direct[2];

}CK = {

    Two-Direct[0] = (a-b);

    Two-Direct[1] = (b-m);

#define IP_CT_DIR_ORIGINAL 0

#define IP_CT_DIR_REPLY 1

和上面的ip_conntrack對比一下看看少了什麼?既然上面的例子是在R處發生了snat,那麼nat的指導資訊顯然也應該在CK中,加上後就圓滿了。nat資訊實際上是一個數組,并且是兩個方向上的,比如下列規則:

-A POSTROUTING -d 172.16.0.0/255.255.0.0 -o eth0 -j MASQUERADE

實際上有兩條nat規則,一個是到達172.16.0.0網絡的資料源位址轉化為eth0的位址,另一條是從172.16.0.0回應的資料包的位址還要轉化回來,否則資料就有去無回了,是以針對一個源位址(-s)或者目的位址(-d),一共需要有(兩個方向*一條流最大挂載點)條nat規則,看一下一個流最大可以有幾個挂載點,如果資料隻是過路,那麼最大也就兩個,既作snat,又做dnat,可是如果去本機出入的流,那麼很有可能會有三個挂載點,是以nat規則一共需要3對也就是6條,用數組表示就是nat_info[6],于是CK成了:

CK = {

    NAT-Info[] = {,,,,,};

最終NAT-Info中的元素是什麼呢?肯定是位址資訊了,最簡單的方式就是每個元素就是一個ip-port對,然後通過數組下标來索引nat的類型,比如定義:

#define SRC_NAT 0

#define OPPOSITE_SRC 1

#define DST_NAT 2

#define OPPOSITE_DST 3

...

有了上述定義後來初始化nat資訊數組:

NAT-Info[] = {{src_ip_to,port1},{src_ip_from,port2},

          {dst_ip_to,port3},{dst_ip_from,port4},

           ...}; //對應于上述CK的定義,這裡的src_ip_to就是m,而src_ip_from就是a

src_ip_to是需要轉換成的新源ip,src_ip_from是回應資料需要轉換回的原始源ip,dst字首的ip位址的含義類似,這張表初始化完了之後,對應資料流再有資料包來的時候就可以直接通過查這張表來進行位址轉換了。由于來回兩個方向的流都被映射進了CK結構體,是以不管哪個方向過來資料,(a-b)也好,(b-m)也好,都會對應到同一個CK,這在linux中是通過hash實作的,既然找到了CK,從CK中取出NAT-Info就可以得到如何轉化位址的資訊,很顯然,不可能每次資料包到來時都要查nat表,而是在一個流的第一個包到達時就确定了NAT-Info,也就是一個流(CK)建立的時候,建立一個CK之後,查找nat表,如果有規則命中,那麼根據nat表的規則來建立NAT-Info資訊,同時還要更新Two-Direct[1]為新的轉換後的流(注意是反向的),該流的第一個資料包流出機器或者流往使用者層的時候将上述流資訊記錄到hash中,兩個方向的都要記錄,如果再有包來臨,不管哪個方向的,請求包還是回應包,通過位址資訊查詢hash都可以找到CK,然後到了nat的時候,直接從CK将NAT-Info取出即可,取出後判斷目前是哪個HOOK,根據目前的HHOK來使用NAT-Info中的資訊實行位址轉換。整個過程中,CK的作用就是追蹤連接配接,它最大的共享就是一個流的第一個包來的時候建立CK,之後nat會使用這個初始CK查nat表,之後再來資料包CK以及其中的資訊比如nat資訊就可以直接取出來使用了,是conntrack子產品取出,後續子產品使用。

     不要被核心代碼中複雜的細節所蒙蔽,其實每一段代碼每一個機制的思想都是很簡單的,正如ip_conntrack-nat的思想以及資料結構的設計和我上述的說明一樣,如果能通過閱讀代碼将資料結構抽象成最簡單的形式并且剖析出思想,那麼閱讀代碼才算是有了收獲,否則總有一天會迷失于茫茫字元海中而不可自拔,最終隻見樹木不見森林,搞得自己也不想再鑽研了。了解了大緻的流程之後,再次閱讀代碼的時候就要詳細些了,以下是幾個比較重要的函數:

ip_nat_setup_info:初始化ip_nat_info資訊;

find_best_ips_proto_fast:初始化nat後的新的tuple;

ip_conntrack_alter_reply:配置由于nat而改變的反向tuple;

do_bindings:nat子產品實施nat轉換。

 本文轉自 dog250 51CTO部落格,原文連結:http://blog.51cto.com/dog250/1271815

繼續閱讀