Netfilter之鏈接跟蹤實現機制初步分析php
原文:html
http://blog.chinaunix.net/uid-22227409-id-2656910.htmllinux
什麼是鏈接跟蹤算法
鏈接跟蹤(CONNTRACK),顧名思義,就是跟蹤而且記錄鏈接狀態。Linux爲每個通過網絡堆棧的數據包,生成一個新的鏈接記錄項(Connection entry)。此後,全部屬於此鏈接的數據包都被惟一地分配給這個鏈接,並標識鏈接的狀態。鏈接跟蹤是防火牆模塊的狀態檢測的基礎,同時也是地址轉換中實現SNAT和DNAT的前提。
那麼Netfilter又是如何生成鏈接記錄項的呢?每個數據,都有「來源」與「目的」主機,發起鏈接的主機稱爲「來源」,響應「來源」的請求的主機即爲目的,所謂生成記錄項,就是對每個這樣的鏈接的產生、傳輸及終止進行跟蹤記錄。由全部記錄項產生的表,即稱爲鏈接跟蹤表。數組
Netfilter中的鏈接跟蹤模塊做爲地址轉換等的基礎,在對Netfilter的實現機制有所瞭解的基礎上再深刻理解鏈接跟蹤的實現機制,對於充分應用Netfilter框架的功能和擴展其餘的模塊有重大的做用。網絡
在這裏只是對鏈接跟蹤模塊總體流程的一個粗略描述,主要參考了cu論壇上的兩篇文章:數據結構
http://linux.chinaunix.net/bbs/viewthread.php?tid=1057483架構
http://bbs.chinaunix.net/viewthread.php?tid=815129&extra=&page=1app
總體框架框架
鏈接跟蹤機制是基於Netfilter架構實現的,其在Netfilter的不一樣鉤子點中註冊了相應的鉤子函數,以下圖所示
主要掛載函數以下:
NF_IP_PRE_ROUTING: ip_conntrack_defrag(), ip_conntrack_in();
NF_IP_LOCAL_IN: ip_confirm();
NF_IP_LOCAL_OUT: ip_conntrack_defrag(),ip_conntrack_local();
NF_IP_POST_ROUTING: ip_confirm();
其中ip_conntrack_defrag()用於分片數據包的重組,defrag鉤子函數的優先級高於conntrack,因此重組會在鏈接創建以前執行
ip_conntrack_in()函數根據數據包協議找到其鏈接跟蹤中的對應模塊,若找到,則對sk_buf中的nfct字段進行標記,若沒有,則新建立一個鏈接跟蹤;ip_conntrack_local()實際也是調用了ip_conntrack_in()函數來實現。
ip_confirm()用於將建立新建立的鏈接跟蹤掛載進系統的鏈接跟蹤表中,由於對應某些數據包可能被過濾函數給丟棄了,因此在最後時候LOCAL_IN及POST_ROUTING處纔將新建跟蹤掛在入跟蹤表中。
重要數據結構
Netfilter使用一張鏈接跟蹤表,來描述整個鏈接狀態,這個表在實現上採用了hash算法。
struct list_head *ip_conntrack_hash;
每個hash節點,同時又是一條鏈表的首部,鏈表的每一個節點都是一個struct ip_conntrack_tuple_hash類型;
struct ip_conntrack_tuple_hash
{
struct list_head list;
struct ip_conntrack_tuple tuple;
};
list用於組織鏈表,多元組tuple則用於描述具體的數據包。
對於每一個數據包最基本的要素,就是來源和目的,因此這個數據包就能夠表示爲「源地址/源端口+目的地址/目的端口」(對於沒有端口的協議,如ICMP,可使用其餘辦法替代)。
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成員,用於標識鏈接的方向。
uple 結構僅僅是一個數據包的轉換,並非描述一條完整的鏈接狀態,內核中,描述一個包的鏈接狀態,使用了struct ip_conntrack 結構,能夠在ip_conntrack.h中看到它的定義:
struct ip_conntrack
{
/* 包含了使用計數器和指向刪除鏈接的函數的指針 */
struct nf_conntrack ct_general;
/* 鏈接狀態位,它一般是一個ip_conntrack_status類型的枚舉變量,如IPS_SEEN_REPLY_BIT等*/
unsigned long status;
/* 內核的定時器,用於處理鏈接超時 */
struct timer_list timeout;
#ifdef CONFIG_IP_NF_CT_ACCT
/* Accounting Information (same cache line as other written members) */
struct ip_conntrack_counter counters[IP_CT_DIR_MAX];
#endif
/* If we were expected by an expectation, this will be it */
struct ip_conntrack *master;
/* Current number of expected connections */
unsigned int expecting;
/* Helper, if any. */
struct ip_conntrack_helper *helper;
/* Storage reserved for other modules: */
union ip_conntrack_proto proto;
union ip_conntrack_help help;
#ifdef CONFIG_IP_NF_NAT_NEEDED
struct {
struct ip_nat_info info;
#if defined(CONFIG_IP_NF_TARGET_MASQUERADE) || \
defined(CONFIG_IP_NF_TARGET_MASQUERADE_MODULE)
int masq_index;
#endif
} nat;
#endif /* CONFIG_IP_NF_NAT_NEEDED */
#if defined(CONFIG_IP_NF_CONNTRACK_MARK)
unsigned long mark;
#endif
/* Traversed often, so hopefully in different cacheline to top */
/* These are my tuples; original and reply */
struct ip_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX];
};
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],這樣,一條完整的鏈接已經躍然紙上了。
對於一些特殊的應用則須要ip_conntrack_helper、ip_conntrack_expect提供功能的擴展,這裏只是簡單分析,對於這兩個結構暫不作了解。
重要函數
ip_conntrack_defrag()
ip_conntrack_defrag()函數對分片的包進行重組,其調用ip_ct_gather_frag()收集已經到達的分片包,而後再調用函數ip_defrag()實現數據分片包的重組。ip_conntrack_defrag()被掛載在鉤子點NF_IP_PRE_ROUTING和NF_IP_LOCAL_OUT,即從外面進來的數據包或本地主機生成的數據包會首先調用該函數。該函數只操做數據包的內容,對鏈接跟蹤記錄沒有影響也沒有操做,若是不須要進行重組操做則直接返回NF_ACCEPT。函數的定義以下:
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 *))
{
#if !defined(CONFIG_IP_NF_NAT) && !defined(CONFIG_IP_NF_NAT_MODULE)
/* Previously seen (loopback)? Ignore. Do this before
fragment check. */
if ((*pskb)->nfct)
return NF_ACCEPT;
#endif
/* 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;
}
ip_conntrack_in
函數ip_conntrack_in()被掛載在鉤子點NF_IP_PRE_ROUTING,同時該函數也被掛載在鉤子點NF_IP_LOCAL_OUT的函數ip_conntrack_local()調用,鏈接跟蹤模塊在這兩個鉤子點掛載的函數對數據包的處理區別僅在於對分片包的重組方式有所不一樣。
函數ip_conntrack_in()首先調用__ip_conntrack_proto_find(),根據數據包的協議找到其應該使用的傳輸協議的鏈接跟蹤模塊,接下來調用協議模塊的error()對數據包進行正確性檢查,而後調用函數resolve_normal_ct()選擇正確的鏈接跟蹤記錄,若是沒有,則建立一個新紀錄。接着調用協議模塊的packet()函數,若是返回失敗,則nf_conntrack_put()將釋放鏈接記錄。ip_conntrack_in()函數的源碼以下,函數resolve_normal_ct()實際操做了數據包和鏈接跟蹤表的內容。
/* 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;
}
在函數中首先檢查數據包是否已經被檢查過,或者是否爲分片數據包;
而後根據當前包的協議,查找對應的ip_conntrack_protocol結構,其中包含了鏈接項tuple等一些數據的生成函數,對於不一樣的協議都有其不一樣的數據結構。
對於鏈接跟蹤模塊將全部支持的協議,都使用struct ip_conntrack_protocol 結構封裝,註冊至全局數組ip_ct_protos,這裏首先調用函數ip_ct_find_proto根據當前數據包的協議值,找到協議註冊對應的模塊。而後調用resolve_normal_ct 函數進一步處理。
resolve_normal_ct函數是鏈接跟蹤中最重要的函數之一,它的主要功能就是判斷數據包在鏈接跟蹤表是否存在,若是不存在,則爲數據包分配相應的鏈接跟蹤節點空間並初始化,而後設置鏈接狀態。
/* On success, returns conntrack ptr, sets skb->nfct and ctinfo */
static inline struct ip_conntrack *
resolve_normal_ct(struct sk_buff *skb,
struct ip_conntrack_protocol *proto,
int *set_reply,
unsigned int hooknum,
enum ip_conntrack_info *ctinfo)
{
struct ip_conntrack_tuple tuple;
struct ip_conntrack_tuple_hash *h;
struct ip_conntrack *ct;
IP_NF_ASSERT((skb->nh.iph->frag_off & htons(IP_OFFSET)) == 0);
/*前面提到過,須要將一個數據包轉換成tuple,這個轉換,就是經過ip_ct_get_tuple函數實現的*/
if (!ip_ct_get_tuple(skb->nh.iph, skb, skb->nh.iph->ihl*4,
&tuple,proto))
return NULL;
/*查看數據包對應的tuple在鏈接跟蹤表中是否存在 */
h = ip_conntrack_find_get(&tuple, NULL);
if (!h) {
/*若是不存在,初始化之*/
h = init_conntrack(&tuple, proto, skb);
if (!h)
return NULL;
if (IS_ERR(h))
return (void *)h;
}
/*根據hash表節點,取得數據包對應的鏈接跟蹤結構*/
ct = tuplehash_to_ctrack(h);
/* 判斷鏈接的方向 */
if (DIRECTION(h) == IP_CT_DIR_REPLY) {
*ctinfo = IP_CT_ESTABLISHED + IP_CT_IS_REPLY;
/* Please set reply bit if this packet OK */
*set_reply = 1;
} else {
/* Once we've had two way comms, always ESTABLISHED. */
if (test_bit(IPS_SEEN_REPLY_BIT, &ct->status)) {
DEBUGP("ip_conntrack_in: normal packet for %p\n",
ct);
*ctinfo = IP_CT_ESTABLISHED;
} else if (test_bit(IPS_EXPECTED_BIT, &ct->status)) {
DEBUGP("ip_conntrack_in: related packet for %p\n",
ct);
*ctinfo = IP_CT_RELATED;
} else {
DEBUGP("ip_conntrack_in: new packet for %p\n",
ct);
*ctinfo = IP_CT_NEW;
}
*set_reply = 0;
}
/*設置skb的對應成員,如使用計數器、數據包狀態標記*/
skb->nfct = &ct->ct_general;
skb->nfctinfo = *ctinfo;
return ct;
}
對於新建鏈接,鏈接跟蹤初始化的工做有resolve_normal_ct下的init_conntrack完成,完成struct ip_conntrack數據結構的填充。
在函數的最後完成鏈接狀態的判斷,位於tuple中的dst.dir中,對於初始鏈接,它是IP_CT_DIR_ORIGINAL,對於它的應答包,則爲IP_CT_DIR_REPLY;同時,好比對於TCP協議,它是一個面向鏈接的協議,因此,它的初始或應答包,並不必定就是新建或單純的應答包,而是一個鏈接過程當中的已建鏈接包,因此須要對鏈接狀態作額外的判斷。
ip_confirm
函數ip_confirm()被掛載在鉤子點NF_IP_LOCAL_IN和NF_IP_POST_ROUTING,其對數據包再次進行鏈接跟蹤記錄確認,並將新建的鏈接跟蹤記錄加到表中。考慮到包可能被過濾掉,以前新建的鏈接跟蹤記錄實際上並未真正加到鏈接跟蹤表中,而在最後由函數ip_confirm()確認後真正添加,實際對傳來的sk_buff進行確認的函數是__ip_conntrack_confirm()。在該函數中首先調用函數ip_conntrack_get()查找相應的鏈接跟蹤記錄,若是數據包不是IP_CT_DIR_ORIGINAL方向的包,則直接ACCEPT,不然接着調用hash_conntrack()計算所找到的鏈接跟蹤記錄的ip_conntrack_tuple類型的hash值,且同時計算兩個方向的值。而後根據這兩個hash值分別查找鏈接跟蹤記錄的hash表,若是找到了,則返回NF_DROP,若是未找到,則調用函數__ip_conntrack_hash_insert()將兩個方向的鏈接跟蹤記錄加到hash表中。
static unsigned int ip_confirm(unsigned int hooknum,
struct sk_buff **pskb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
/* We've seen it coming out the other side: confirm it */
return ip_conntrack_confirm(pskb);
}
函數僅是轉向;
/* Confirm a connection: returns NF_DROP if packet must be dropped. */
static inline int ip_conntrack_confirm(struct sk_buff **pskb)
{
if ((*pskb)->nfct
&& !is_confirmed((struct ip_conntrack *)(*pskb)->nfct))
return __ip_conntrack_confirm(pskb);
return NF_ACCEPT;
}
is_comfirmed函數用於判斷數據包是否已經被__ip_conntrack_confirm函數處理過了,它是經過IPS_CONFIRMED_BIT 標誌位來判斷,而這個標誌位固然是在__ip_conntrack_confirm函數中來設置的:
/* Confirm a connection given skb; places it in hash table */
int
__ip_conntrack_confirm(struct sk_buff **pskb)
{
unsigned int hash, repl_hash;
struct ip_conntrack *ct;
enum ip_conntrack_info ctinfo;
/*取得數據包的鏈接狀態*/
ct = ip_conntrack_get(*pskb, &ctinfo);
/* 若是當前包不是一個初始方向的封包,則直接返回. */
if (CTINFO2DIR(ctinfo) != IP_CT_DIR_ORIGINAL)
return NF_ACCEPT;
/*計算初始及應答兩個方向tuple對應的hash值*/
hash = hash_conntrack(&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple);
repl_hash = hash_conntrack(&ct->tuplehash[IP_CT_DIR_REPLY].tuple);
/* We're not in hash table, and we refuse to set up related
connections for unconfirmed conns. But packet copies and
REJECT will give spurious warnings here. */
/* IP_NF_ASSERT(atomic_read(&ct->ct_general.use) == 1); */
/* No external references means noone else could have
confirmed us. */
IP_NF_ASSERT(!is_confirmed(ct));
DEBUGP("Confirming conntrack %p\n", ct);
WRITE_LOCK(&ip_conntrack_lock);
/* 在hash表中查找初始及應答的節點*/
if (!LIST_FIND(&ip_conntrack_hash[hash],
conntrack_tuple_cmp,
struct ip_conntrack_tuple_hash *,
&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple, NULL)
&& !LIST_FIND(&ip_conntrack_hash[repl_hash],
conntrack_tuple_cmp,
struct ip_conntrack_tuple_hash *,
&ct->tuplehash[IP_CT_DIR_REPLY].tuple, NULL)) {
/* Remove from unconfirmed list */
list_del(&ct->tuplehash[IP_CT_DIR_ORIGINAL].list);
/*將當前鏈接(初始和應答的tuple)添加進hash表*/
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]);
/* Timer relative to confirmation time, not original
setting time, otherwise we'd get timer wrap in
weird delay cases. */
ct->timeout.expires += jiffies;
add_timer(&ct->timeout);
atomic_inc(&ct->ct_general.use);
set_bit(IPS_CONFIRMED_BIT, &ct->status);
CONNTRACK_STAT_INC(insert);
WRITE_UNLOCK(&ip_conntrack_lock);
return NF_ACCEPT;
}
CONNTRACK_STAT_INC(insert_failed);
WRITE_UNLOCK(&ip_conntrack_lock);
return NF_DROP;
}
ip_conntrack_local
函數ip_conntrack_local()被掛載在鉤子點NF_IP_LOCAL_OUT,該函數會調用ip_conntrack_in(),函數源碼以下:
static unsigned int ip_conntrack_local(unsigned int hooknum,
struct sk_buff **pskb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
/* root is playing with raw sockets. */
if ((*pskb)->len < sizeof(struct iphdr)
|| (*pskb)->nh.iph->ihl * 4 < sizeof(struct iphdr)) {
if (net_ratelimit())
printk("ipt_hook: happy cracking.\n");
return NF_ACCEPT;
}
return ip_conntrack_in(hooknum, pskb, in, out, okfn);
}
數據包轉發的鏈接跟蹤流程
下面以數據包轉發爲例描述鏈接跟蹤的流程,其中的函數及結構體爲前幾節所介紹的一部分,圖中主要想體現數據包sk_buff在鏈接跟蹤流程中的相應改變,鏈接跟蹤記錄與鏈接跟蹤表的關係,什麼時候查找和修改鏈接跟蹤表,輔助模塊以及傳輸協議如何在鏈接跟蹤中使用等。全部的函數說明以及結構體在以前都有描述。發往本機以及本機發出的數據包的鏈接跟蹤流程在此再也不作分析。
總結
以上只是簡要分析了Netfilter架構中鏈接跟蹤功能的實現機制,其中不少細節被忽略,主要目的是學習,瞭解整個實現框架。
後記
對於Linux 2.4版本的內核中,不支持基於ipv6協議的鏈接跟蹤,在2.6之後開始支持,目前我須要學習版本爲2.6.30,只看了鏈接跟蹤這一塊,感受變化比較大,不少數據結構及函數名稱發生了改變,不過總體思想沒變。
比照總結版原本說,對於每個包的truple結構的內容發生了改變,雖然一樣只是保護src及dst,可是對於其中的變量發生了改變,同時兼容於ipv4地址以及ipv6地址。
而對於整個鏈接跟蹤的創建流程來講,對於ipv4及ipv6分別註冊了相應的鉤子函數,如conntrack_in,defrag等。在這些函數的過程當中採用了抽象的方法,首先完成基於本身特殊協議的一些變量的設置,以後從ipv4及ipv6的過程當中抽象出共同的函數,如nf_conntrack_in等,經過這樣的方式來完成鏈接跟蹤機制的實現。
因爲剛開始接觸,不少細節還不瞭解,還沒能分析明天,打算明天仿照上面畫一個鏈接跟蹤的流程圖出來。。。。