連線跟蹤(CONNTRACK),顧名思義,就是跟蹤並且記錄連線狀態。Linux為每一個經過網路堆疊的數據包,生成一個新的連線記錄項 (Connection entry)。
基本介紹
- 中文名:連線跟蹤
- 外文名:conntrack
- 含義:跟蹤並且記錄連線狀態
- 目的:生成一個新的連線記錄
連線跟蹤,跟蹤表,3.連線跟蹤的初始化,4.ip_conntrack_in,
連線跟蹤
(CONNTRACK),顧名思義,就是跟蹤並且記錄連線狀態。Linux為每一個經過網路堆疊的數據包,生成一個新的連線記錄項 (Connection entry)。此後,所有屬於此連線的數據包都被唯一地分配給這個連線,並標識連線的狀態。連線跟蹤是防火牆模組的狀態檢測的基礎,同時也是地址轉換中實 現SNAT和DNAT的前提。
那么Netfilter又是如何生成連線記錄項的呢?每一個數據,都有“來源”與“目的”主機,發起連線的主機稱為“來源”,回響“來源”的請求的主機即 為目的,所謂生成記錄項,就是對每一個這樣的連線的產生、傳輸及終止進行跟蹤記錄。由所有記錄項產生的表,即稱為連線跟蹤表。
跟蹤表
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 類型:
[Copy to clipboard][ - ]
CODE:
/* 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) 則用於描述具體的數據包。
[Copy to clipboard][ - ]
CODE:
/* 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;
};
[Copy to clipboard][ - ]
CODE:
/* The manipulable part of the tuple. */
struct ip_conntrack_manip
{
u_int32_t ip;
union ip_conntrack_manip_proto u;
};
[Copy to clipboard][ - ]
CODE:
/* 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中看到它的定義:
[Copy to clipboard][ - ]
CODE:
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 是連線跟蹤的主要模組:
[Copy to clipboard][ - ]
CODE:
static int __init init(void)
{
return init_or_cleanup(1);
}
初始化函式進一步調用init_or_cleanup() 進行模組的初始化,它主要完成hash表的初始化等三個方面的工作:
[Copy to clipboard][ - ]
CODE:
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表相關參數在內一些重要的變數:
[Copy to clipboard][ - ]
CODE:
/*用戶態可以在模組插入的時候,可以使用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不同,註冊了多個鉤子:
[Copy to clipboard][ - ]
CODE:
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,用於處理分片數據 包:
[Copy to clipboard][ - ]
CODE:
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;
}
鉤子的註冊的另一個值得注意的小問題,就是鉤子函式的優先權,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,並初始化它,然後,為這個數據包設定連線狀態。
[Copy to clipboard][ - ]
CODE:
/* 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 函式進一步處理。