Linux nf_conntrack連線跟蹤的實現
struct nf_conn {
/* Usage count in here is 1 for hash table/destruct timer, 1 per skb, plus 1 for any connection(s) we are `master' for */
struct nf_conntrack ct_general; /* 連線跟蹤的引用計數 */
spinlock_t lock;
/* Connection tracking(連結跟蹤)用來跟蹤、記錄每個連結的資訊(目前僅支援IP協議的連線跟蹤)。
每個連結由“tuple”來唯一標識,這裡的“tuple”對不同的協議會有不同的含義,例如對tcp,udp
來說就是五元組: (源IP,源埠,目的IP, 目的埠,協議號),對ICMP協議來說是: (源IP, 目
的IP, id, type, code), 其中id,type與code都是icmp協議的資訊。連結跟蹤是防火牆實現狀態檢
測的基礎,很多功能都需要藉助連結跟蹤才能實現,例如NAT、快速轉發、等等。*/
struct nf_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX];
unsigned long status; /* 可以設定由enum ip_conntrack_status中描述的狀態 */
struct nf_conn *master; /* 如果該連線是某個連線的子連線,則master指向它的主連線 */
/* Timer function; drops refcnt when it goes off. */
struct timer_list timeout;
union nf_conntrack_proto proto; /* 用於儲存不同協議的私有資料 */
/* Extensions */
struct nf_ct_ext *ext; /* 用於擴充套件結構 */
};
這個結構非常簡單,其中最主要的就是tuplehash(跟蹤連線雙方向資料)和status(記錄連線狀態),這也連線跟蹤最主要的功能。
在status中可以設定的標誌,由下面的enum ip_conntrack_status描述,它們可以共存。這些標誌設定後就不會再被清除。
enum ip_conntrack_status {
IPS_EXPECTED_BIT = 0, /* 表示該連線是個子連線 */
IPS_SEEN_REPLY_BIT = 1, /* 表示該連線上雙方向上都有資料包了 */
IPS_ASSURED_BIT = 2, /* TCP:在三次握手建立完連線後即設定該標誌。UDP:如果在該連線上的兩個方向都有資料包通過,
則再有資料包在該連線上通過時,就設定該標誌。ICMP:不設定該標誌 */
IPS_CONFIRMED_BIT = 3, /* 表示該連線已被新增到net->ct.hash表中 */
IPS_SRC_NAT_BIT = 4, /*在POSTROUTING處,當替換reply tuple完成時, 設定該標記 */
IPS_DST_NAT_BIT = 5, /* 在PREROUTING處,當替換reply tuple完成時, 設定該標記 */
/* Both together. */
IPS_NAT_MASK = (IPS_DST_NAT | IPS_SRC_NAT),
/* Connection needs TCP sequence adjusted. */
IPS_SEQ_ADJUST_BIT = 6,
IPS_SRC_NAT_DONE_BIT = 7, /* 在POSTROUTING處,已被SNAT處理,並被加入到bysource鏈中,設定該標記 */
IPS_DST_NAT_DONE_BIT = 8, /* 在PREROUTING處,已被DNAT處理,並被加入到bysource鏈中,設定該標記 */
/* Both together */
IPS_NAT_DONE_MASK = (IPS_DST_NAT_DONE | IPS_SRC_NAT_DONE),
IPS_DYING_BIT = 9, /* 表示該連線正在被釋放,核心通過該標誌保證正在被釋放的ct不會被其它地方再次引用。有了這個標誌,當某個連線要被刪除時,即使它還在net->ct.hash中,也不會再次被引用。*/
IPS_FIXED_TIMEOUT_BIT = 10, /* 固定連線超時時間,這將不根據狀態修改連線超時時間。通過函式nf_ct_refresh_acct()修改超時時間時檢查該標誌。 */
IPS_TEMPLATE_BIT = 11, /* 由CT target進行設定(這個target只能用在raw表中,用於為資料包構建指定ct,並打上該標誌),用於表明這個ct是由CT target建立的 */
};
連線跟蹤對該連線上的每個資料包表現為以下幾種狀態之一,由enum ip_conntrack_info表示,被設定在skb->nfctinfo中。
enum ip_conntrack_info {
IP_CT_ESTABLISHED(0), /* 表示這個資料包對應的連線在兩個方向都有資料包通過,並且這是ORIGINAL初始方向資料包(無論是TCP、UDP、ICMP資料包,只要在該連線的兩個方向上已有資料包通過,就會將該連線設定為IP_CT_ESTABLISHED狀態。不會根據協議中的標誌位進行判斷,例如TCP的SYN等)。但它表示不了這是第幾個資料包,也說明不了這個CT是否是子連線。*/
IP_CT_RELATED(1), /* 表示這個資料包對應的連線還沒有REPLY方向資料包,當前資料包是ORIGINAL方向資料包。並且這個連線關聯一個已有的連線,是該已有連線的子連線,(即status標誌中已經設定了IPS_EXPECTED標誌,該標誌在init_conntrack()函式中設定)。但無法判斷是第幾個資料包(不一定是第一個)*/
IP_CT_NEW(2), /* 表示這個資料包對應的連線還沒有REPLY方向資料包,當前資料包是ORIGINAL方向資料包,該連線不是子連線。但無法判斷是第幾個資料包(不一定是第一個)*/
IP_CT_IS_REPLY(3), /* 這個狀態一般不單獨使用,通常以下面兩種方式使用 */
IP_CT_ESTABLISHED + IP_CT_IS_REPLY(3), /* 表示這個資料包對應的連線在兩個方向都有資料包通過,並且這是REPLY應答方向資料包。但它表示不了這是第幾個資料包,也說明不了這個CT是否是子連線。*/
IP_CT_RELATED + IP_CT_IS_REPLY(4), /* 這個狀態僅在nf_conntrack_attach()函式中設定,用於本機返回REJECT,例如返回一個ICMP目的不可達報文, 或返回一個reset報文。它表示不了這是第幾個資料包。*/
IP_CT_NUMBER = IP_CT_IS_REPLY * 2 - 1(5) /* 可表示狀態的總數 */
};
以上就是連線跟蹤裡最重要的資料結構了,用於跟蹤連線、記錄狀態、並對該連線的每個資料包設定一種狀態。
除了上面的主要資料結構外,還有一些輔助資料結構,用於處理不同協議的私有資訊、處理子連線、對conntrack進行擴充套件等。
三層協議(IPv4/IPv6)利用nf_conntrack_proto.c檔案中的nf_conntrack_l3proto_register(struct nf_conntrack_l3proto *proto)和nf_conntrack_l3proto_unregister(struct nf_conntrack_l3proto *proto)在nf_ct_l3protos[]陣列中註冊自己的三層協議處理函式。
四層協議(TCP/UDP)利用nf_conntrack_proto.c檔案中的nf_conntrack_l4proto_register(struct nf_conntrack_l4proto *l4proto)和nf_conntrack_l4proto_unregister(struct nf_conntrack_l4proto *l4proto)在nf_ct_protos[]陣列中註冊自己的四層協議處理函式。
處理一個連線的子連線協議,利用nf_conntrack_helper.c檔案中的nf_conntrack_helper_register(struct nf_conntrack_helper *me)來註冊nf_conntrack_helper結構,和nf_conntrack_expect.c檔案中的nf_ct_expect_related_report(struct nf_conntrack_expect *expect, u32 pid, int report)來註冊nf_conntrack_expect結構。
擴充套件連線跟蹤結構(nf_conn)利用nf_conntrack_extend.c檔案中的nf_ct_extend_register(struct nf_ct_ext_type *type)和nf_ct_extend_unregister(struct nf_ct_ext_type *type)進行擴充套件,並修改連線跟蹤相應程式碼來利用這部分擴充套件功能。
瞭解了上面的資料結構,我們下面來看一下nf_conntrack的執行流程以及如何利用這些資料結構的。首先來看一下nf_conntrack模組載入時的初始化流程。
nf_conntrack的初始化,就是初始化上面提到的那些資料結構,它在核心啟動時呼叫nf_conntrack_standalone_init()函式進行初始化的。初始化完成後,構建出如下圖所示的結構圖,只是不包含下圖中與連線有關的資訊(nf_conn和nf_conntrack_expect結構)。
上圖中有三個HASH桶,ct_hash、expect_hash、helper_hash這三個HASH桶大小在初始化時就已確定,後面不能再更改。其中ct_hash、expect_hash可在載入nf_conntrack.ko模組時通過引數hashsize和expect_hashsize進行設定,而helper_hash不能通過引數修改,它的預設值是page/sizeof(helper_hash)。
下面再來看一個當建立子連線時,各個資料結構之間的關係。
nf_conn和nf_conntrack_expect都有最大個數限制。nf_conn通過全域性變數nf_conntrack_max限制,可通過/proc/sys/net/netfilter/nf_conntrack_max檔案在執行時修改。nf_conntrack_expect通過全域性變數nf_ct_expect_max限制,可通過/proc/sys/net/netfilter/ nf_conntrack_expect_max檔案在執行時修改。nf_conntrack_helper沒有最大數限制,因為這個是通過註冊不同協議的模組新增的,大小取決於動態協議跟蹤模組的多少,一般不會很大。
上面兩幅資料結構圖中,大部分都已介紹過,下面介紹一下netns_ct資料結構,該結構主要用於linux的網路名稱空間,表示nf_conntrack在不同的名稱空間中都有一套獨立的資料資訊(這是另一個話題,這裡就不再深入討論了)。
struct netns_ct {
atomic_t count; /* 當前連線表中連線的個數 */
unsigned int expect_count; /* nf_conntrack_helper建立的期待子連線nf_conntrack_expect項的個數 */
unsigned int htable_size; /* 儲存連線(nf_conn)的HASH桶的大小 */
struct kmem_cache *nf_conntrack_cachep; /* 指向用於分配nf_conn結構而建立的快取記憶體(slab)物件 */
struct hlist_nulls_head *hash; /* 指向儲存連線(nf_conn)的HASH桶 */
struct hlist_head *expect_hash; /* 指向儲存期待子連線nf_conntrack_expect項的HASH桶 */
struct hlist_nulls_head unconfirmed; /* 對於一個連結的第一個包,在init_conntrack()函式中會將該包original方向的tuple結構掛入該鏈,這是因為在此時還不確定該連結會不會被後續的規則過濾掉,如果被過濾掉就沒有必要掛入正式的連結跟蹤表。在ipv4_confirm()函式中,會將unconfirmed鏈中的tuple拆掉,然後再將original方向和reply方向的tuple掛入到正式的連結跟蹤表中,即init_net.ct.hash中,這是因為到達ipv4_confirm()函式時,應經在鉤子NF_IP_POST_ROUTING處了,已經通過了前面的filter表。 通過cat /proc/net/nf_conntrack顯示連線,是不會顯示該鏈中的連線的。但總的連線個數(net->ct.count)包含該鏈中的連線。當登出l3proto、l4proto、helper、nat等資源或在應用層刪除所有連線(conntrack
-F)時,除了釋放confirmed連線(在net->ct.hash中的連線)的資源,還要釋放unconfirmed連線(即在該鏈中的連線)的資源。*/
struct hlist_nulls_head dying; /* 釋放連線時,通告DESTROY事件失敗的ct被放入該鏈中,並設定定時器,等待下次通告。 通過cat /proc/net/nf_conntrack顯示連線,是不會顯示該鏈中的連線的。但總的連線個數(net->ct.count)包含該鏈中的連線。當登出連線跟蹤模組時,同時要清除正再等待被釋放的連線(即該鏈中的連線)*/
struct ip_conntrack_stat __percpu *stat; /* 連線跟蹤過程中的一些狀態統計,每個CPU一項,目的是為了減少鎖 */
int sysctl_events; /* 是否開啟連線事件通告功能 */
unsigned int sysctl_events_retry_timeout; /* 通告失敗後,重試通告的間隔時間,單位是秒 */
int sysctl_acct; /* 是否開啟每個連線資料包統計功能 */
int sysctl_checksum;
unsigned int sysctl_log_invalid; /* Log invalid packets */
#ifdef CONFIG_SYSCTL
struct ctl_table_header *sysctl_header;
struct ctl_table_header *acct_sysctl_header;
struct ctl_table_header *event_sysctl_header;
#endif
int hash_vmalloc; /* 儲存連線(nf_conn)的HASH桶是否是使用vmalloc()進行分配的 */
int expect_vmalloc; /* 儲存期待子連線nf_conntrack_expect項的HASH桶是否是使用vmalloc()進行分配的 */
char *slabname; /* 用於分配nf_conn結構而建立的快取記憶體(slab)物件的名字 */
};
從nf_conntrack的框架來看,它可用於跟蹤任何三層和四協議的連線,但目前在三層協議只實現了IPv4和IPv6的連線跟蹤,下面我們以IPv4為例,介紹一下該協議是如何利用nf_conntrack框架和netfilter實現連線跟蹤的。有關netfilter框架,可參考我的另一個帖子
linux-2.6.35.6核心netfilter框架
首先介紹一下IPv4協議連線跟蹤模組的初始化。
Ipv4連線跟蹤模組註冊了自己的3層協議,和IPv4相關的三個4層協議TCP、UDP、ICMP。註冊後的結構圖如下圖所示:
在netfilter框架中利用nf_register_hook(struct nf_hook_ops *reg)、nf_unregister_hook(struct nf_hook_ops *reg)函式註冊自己的鉤子項,呼叫nf_conntrack_in()函式來建立相應連線。
- 這樣資料包就會經過ipv4註冊的鉤子項,並呼叫nf_conntrack_in()函式建立連線表項,連線表項中的tuple由ipv4註冊的3/4層協議處理函式構建。
- ipv4_conntrack_in() 掛載在NF_IP_PRE_ROUTING點上。該函式主要功能是建立連結,即建立struct nf_conn結構,同時填充struct nf_conn中的一些必要的資訊,例如連結狀態、引用計數、helper結構等。
- ipv4_confirm() 掛載在NF_IP_POST_ROUTING和NF_IP_LOCAL_IN點上。該函式主要功能是確認一個連結。對於一個新連結,在ipv4_conntrack_in()函式中只是建立了struct nf_conn結構,但並沒有將該結構掛載到連結跟蹤的Hash表中,因為此時還不能確定該連結是否會被NF_IP_FORWARD點上的鉤子函式過濾掉,所以將掛載到Hash表的工作放到了ipv4_confirm()函式中。同時,子連結的helper功能也是在該函式中實現的。
- ipv4_conntrack_local() 掛載在NF_IP_LOCAL_OUT點上。該函式功能與ipv4_conntrack_in()函式基本相同,但其用來處理本機主動向外發起的連結。
- nf_conntrack_ipv4_compat_init() --> register_pernet_subsys() --> ip_conntrack_net_init() 建立/proc檔案ip_conntrack和ip_conntrack_expect
nf_conntrack_in()函式流程圖
ipv4_confirm()函式流程圖
death_by_timeout()函式流程圖
上圖中有一點需要說明,由於skb會引用nf_conn,同時會增加它的引用計數,所以當skb被釋放時,也要釋放nf_conn的引用計數,並且在nf_conn引用計數為0時,要釋放全部資源。
當資料包經過nf_conntrack_in()和ipv4_confirm()函式處理流程後,就會建立起3樓第二幅結構圖所示的連線nf_conn。同時這兩個函式已經包含了子連線的處理流程,即流程圖中help和exp的處理。子連線建立後的結構圖如3樓第三幅結構圖,主連結與子連線通過helper和expect關聯起來。
連線跟蹤到此就介紹完了,下面介紹IPv4基於nf_conntrack框架適合實現NAT轉換的。先介紹IPv4-NAT初始化的資源,然後處理流程。
IPv4-NAT連線跟蹤相關部分通過函式nf_nat_init()初始化
-
呼叫nf_ct_extend_register() 註冊一個連線跟蹤的擴充套件功能。

- 呼叫register_pernet_subsys() --> nf_nat_net_init() 建立net->ipv4.nat_bysource的HASH表,大小等於net->ct.htable_size。
-
初始化nf_nat_protos[]陣列,為TCP、UDP、ICMP協議指定專用處理結構,其它協議都指向預設處理結構。

- 為nf_conntrack_untracked連線設定IPS_NAT_DONE_MASK標誌。
- 將NAT模組的全域性變數l3proto指向IPV4協議的nf_conntrack_l3proto結構。
- 設定全域性指標nf_nat_seq_adjust_hook指向nf_nat_seq_adjust()函式。
- 設定全域性指標nfnetlink_parse_nat_setup_hook指向nfnetlink_parse_nat_setup()函式。
-
設定全域性指標nf_ct_nat_offset指向nf_nat_get_offset()函式。
IPv4-NAT功能的iptables部分通過函式nf_nat_standalone_init()初始化
- 呼叫nf_nat_rule_init() --> nf_nat_rule_net_init()在iptables中註冊一個NAT表(通過ipt_register_table()函式,參考另一個帖子iptables)
-
呼叫 nf_nat_rule_init() 註冊SNAT target和DNAT target(通過xt_register_target()函式)

-
呼叫nf_register_hooks() 掛載NAT的HOOK函式,橙色部分為NAT掛載的HOOK函式(參考另一個帖子netfilter)

針對上圖中的nf_nat_setup_info()函式進一步描述
下面對NAT轉換演算法中重要部分做一些文字說明
- 每個ct在第一個包就會做好snat與dnat, nat的資訊全放在reply tuple中,orig tuple不會被改變。一旦第一個包建立好nat資訊後,後續再也不會修改tuple內容了。
- orig tuple中的地址資訊與reply tuple中的地址資訊就是原始資料包的資訊。例如對A->B資料包同時做snat與dnat,PREROUTING處B被dnat到D,POSTROUTING處A被snat到C。則ct的內容是: A->B | D->C, A->B說明了orig方向上資料包剛到達牆時的地址內容,D->C說明reply方向上資料包剛到達牆時的地址內容。
- 在程式碼中有很多!dir操作,原理是: 當為了反向的資料包做事情的時候就取反向tuple的資料,這樣才能保證NAT後的tuple資訊被正確使用。
- bysource鏈中連結了所有CT(做過NAT和未做過NAT),通過ct->nat->bysource,HASH值的計算使用的是CT的orig tuple。其作用是,當為一個新連線做SNAT,需要得到地址對映時,首先對該鏈進行查詢,查詢此源IP、協議和埠號是否已經做過了對映。如果做過的話,就需要在SNAT轉換時,對映為相同的源IP和埠號。為什麼要這麼做呢?因為對於UDP來說,有些協議可能會用相同埠和同一主機不同的埠(或不同的主機)進行通訊。此時,由於目的地不同,原來已有的對映不可使用,需要一個新的連線。但為了保證通訊的的正確性,此時,就要對映為相同的源IP和埠號。其實就是為NAT的打洞服務的。所以bysource就是以源IP、協議和埠號為hash值的一個表,這樣在做snat時保證相同的ip+port影射到相同的ip+port。
- IP_NAT_RANGE_PROTO_RANDOM指的是做nat時,當計算埠時,如果沒有此random標誌,則會先使用原始得tuple中的埠試一下看是否可用,如果可用就使用該原始埠作為nat後的埠, 即儘量保證轉換後的埠與轉換前的埠保持一致。如果不可用,再根據nat的埠演算法計算出一個埠。 如果有此標記,則直接根據埠演算法計算出埠。
- 第一個包之後,ct的兩個方向的tuple內容就固定了,所有的nat操作都必須在第一個包就完成。所以會有daddr = &ct->tuplehash[!dir].tuple.dst.u3;這樣的操作。
- IPS_SRC_NAT與IPS_DST_NAT,如果被設定,表示經過了NAT,並且ct中的tuple被做過SNAT或DNAT。
-
資料包永遠都是在PREROUTING鏈做目的地址和目的埠轉換,在POSTROUTING鏈做原地址和原埠轉換。是否要做NAT轉換則要根資料包方向(dir)和NAT標誌(IPS_SRC_NAT或IPS_DST_NAT)來判斷。
在PREROUTING鏈上--->資料包是original方向、並且連線上設定IPS_DST_NAT標誌,或資料包是reply方向、並且連線上設定IPS_SRC_NAT標誌,則做DNAT轉換。
在POSTROUTING鏈上--->資料包是original方向、並且連線上設定IPS_SRC_NAT標誌,或資料包是reply方向、並且連線上設定IPS_DST_NAT標誌,則做SNAT轉換。 - IPS_DST_NAT_DONE_BIT與IPS_SRC_NAT_DONE_BIT,表示該ct進入過NAT模組,已經進行了源或者目的NAT判斷,但並不表示ct中的tuple被修改過。
- 源目的nat都是在第一個包就判斷完成的,假設先添加了snat策略,第一個包通過,這時又添加了dnat策略, 第二個包到來時是不會匹配dnat策略的 。
- 對於一個ct,nf_nat_setup_info函式最多隻能進入2次,第一次DNAT,第二次SNAT。在nf_nat_follow_master函式中,第一次SNAT,第二次DNAT。
下面針對有無子連線的NAT做一下對比
無子連線的NAT
- 一個ct用於跟蹤一個連線的雙方向資料,ct->orig_tuple用於跟蹤初始方向資料,ct->reply_tuple用於跟蹤應答方向資料。當根據初始方向資料構建ct->orig_tuple時,同時要構建出ct->reply_tuple,用於識別同一連線上應答方向資料。
- 如果初始方向的資料在通過防火牆後被做了NAT轉換,為識別出NAT資料的應答資料包,則對ct->reply_tuple也要做NAT轉換。同時ct上做好相應NAT標記。
- 因此,上面的資訊在初始方向第一個資料包通過後,就要求全部建立好,並且不再改變。
-
一個連線上不同方向的資料,都有相對應的tuple(orig_tuple和reply_tuple),所以該連線後續資料都將被識別出來。如果ct上有NAT標記,則根據要去往方向(即另一個方向)的tuple對資料做NAT轉換。所以會有ct->tuplehash[!dir].tuple這樣的操作。
有子連線的NAT
- 子連線是由主連線構建的expect項識別出來的。
- help用於構建expect項,它期待哪個方向的連線,則用那個方向的tuple和資料包中資料通道資訊構建expect項。例如期待和當前資料包相反方向的連線,則用相反方向的tuple中的資訊(ct->tuplehash[!dir].tuple)。呼叫help時,NAT轉換都已完成(tuple中都包含有正確的識別各自方向的資訊),所以這時所使用的資訊都是正確和所期望的資訊。
- 如果子連線還可能有子連線,則構建expect項時,初始化一個helper結構,並賦值給expect->helper指標。
- 如果該連線已被做了NAT轉換,則對資料包中資料通道資訊也要做NAT轉換。這個工作是在help中完成的。同時,為保證子連線與主連線做相同的NAT,要為expect->expectfn()指標初始化一個函式,用於對子連線做NAT轉換。
-
在新建一個連線時,如果匹配上某個expect項,則說明該連線是子連線。這時要根據expect項中資訊對子連線做下面兩件事:
如果expect->helper指標不為空,則為該子連線關聯expect指定的help項。
如果expect->expectfn()指標不為空,這呼叫指標指向的函式,對子連線做NAT轉換。這樣後面NAT模組就會根據子連線中資訊對子連線資料做NAT轉換。
