Netfilter之連接跟蹤實現(xiàn)機制初步分析 原作者:Minit 本帖子只作為初步分析,因為時間和自身水平的緣故,其中定存在不妥的地方,希望看到本帖的各位能夠指正。當修正和完善后再提供PDF文檔,謝謝大家的關注。 1. 前言
Netfilter中的連接跟蹤模塊作為地址轉(zhuǎn)換等的基礎,在對Netfilter的實現(xiàn)機制有所了解的基礎上再深入理解連接跟蹤的實現(xiàn)機制,對于充分應用Netfilter框架的功能和擴展其他的模塊有著重大的作用。本文只是簡要的分析連接跟蹤的整體框架,其中的重要數(shù)據(jù)結(jié)構(gòu)和重要函數(shù),并粗略的描繪了數(shù)據(jù)包轉(zhuǎn)發(fā)的連接跟蹤流程。分析的內(nèi)核源碼為2.6.21.2。有關Netfilter的分析請參考:
2. 整體框架
連接跟蹤機制是基于Netfilter架構(gòu)實現(xiàn)的,其在Netfilter的不同鉤子點中注冊了相應的鉤子函數(shù),下面圖2-1描繪了連接跟蹤在Netfilter架構(gòu)中注冊的鉤子函數(shù)。
![]() 3.重要數(shù)據(jù)結(jié)構(gòu)
3.1.連接記錄
在Linux內(nèi)核中,連接記錄由ip_conntrack結(jié)構(gòu)表示,其結(jié)構(gòu)如圖3-1所示。在該結(jié)構(gòu)中,包含一個nf_conntrack類型的結(jié)構(gòu),其記錄了連接記錄被公開應用的計數(shù),也方便其他地方對連接跟蹤的引用。每個連接記錄都對應一個指向連接超時的函數(shù)指針,當較長時間內(nèi)未使用該連接,將調(diào)用該指針所指向的函數(shù)。如果針對某種協(xié)議的連接跟蹤需要擴展模塊的輔助,則在連接記錄中會有一指向ip_conntrack_helper結(jié)構(gòu)體的指針。連接記錄中的結(jié)構(gòu)體ip_conntrack_tuple_hash實際記錄了連接所跟蹤的地址信息(源和目的地址)和協(xié)議的特定信息(端口)。所有連接記錄的ip_conntrack_tuple_hash以散列形式保存在連接跟蹤表中。
3.2.連接跟蹤表
連接跟蹤表是記錄所有連接記錄的散列表,其由全局變量ip_conntrack_hash所指向。連接跟蹤表實際是一個以散列值排列的雙向鏈表數(shù)組,鏈表中的元素即為連接記錄所包含的ip_conntrack_tuple_hash結(jié)構(gòu),表的結(jié)構(gòu)如下圖3-2所示。
3.3.連接跟蹤輔助模塊
在連接跟蹤的實現(xiàn)機制中提供了helper輔助模塊以擴展連接跟蹤功能,一個輔助模塊由一個結(jié)構(gòu)體ip_conntrack_helper保存,該結(jié)構(gòu)如下圖3-3所示,所有注冊的模塊由全局變量helpers所指向的鏈表保存。函數(shù)ip_conntrack_helper_register()和ip_conntrack_helper_unregister()用于在鏈表中添加和刪除ip_conntrack_helper類型的結(jié)構(gòu);顒拥FTP協(xié)議就使用了相應的helper模塊來實現(xiàn)。
3.4.期望連接
在連接跟蹤機制中為了實現(xiàn)對活動協(xié)議的支持,還使用到了結(jié)構(gòu)體ip_conntrack_expect,其用于將預期連接分配給現(xiàn)有連接,有關于活動協(xié)議(如FTP)的分析在此不做分析。ip_conntrack_expect結(jié)構(gòu)如下圖3-4所示。所有的ip_conntrack_expect結(jié)構(gòu)由全局變量ip_conntrack_expect_list指向的全局鏈表保存。
3.5.傳輸協(xié)議
連接跟蹤機制可以支持多種傳輸協(xié)議,不同的協(xié)議所采用的跟蹤方式會有所不同。傳輸協(xié)議用結(jié)構(gòu)ip_conntrack_protocol保存,所有的已注冊的傳輸協(xié)議列表由全局變量ip_ct_protos所指向的一維數(shù)組保存,且按照協(xié)議號的順序依次排列。函數(shù)ip_conntrack_protocol_register()和ip_conntrack_protocol_unregister()用于向協(xié)議列表中添加或刪除一個協(xié)議。
4.重要函數(shù)
4.1.ip_conntrack_defrag()
ip_conntrack_defrag()函數(shù)對分片的包進行重組,其調(diào)用ip_ct_gather_frag()收集已經(jīng)到達的分片包,然后再調(diào)用函數(shù)ip_defrag()實現(xiàn)數(shù)據(jù)分片包的重組。ip_conntrack_defrag()被掛載在鉤子點NF_IP_PRE_ROUTING和NF_IP_LOCAL_OUT,即從外面進來的數(shù)據(jù)包或本地主機生成的數(shù)據(jù)包會首先調(diào)用該函數(shù)。該函數(shù)只操作數(shù)據(jù)包的內(nèi)容,對連接跟蹤記錄沒有影響也沒有操作,如果不需要進行重組操作則直接返回NF_ACCEPT。函數(shù)的定義如下:
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;
}
|
4.2. ip_conntrack_in()
函數(shù)ip_conntrack_in()被掛載在鉤子點NF_IP_PRE_ROUTING,同時該函數(shù)也被掛載在鉤子點NF_IP_LOCAL_OUT的函數(shù)ip_conntrack_local()調(diào)用,連接跟蹤模塊在這兩個鉤子點掛載的函數(shù)對數(shù)據(jù)包的處理區(qū)別僅在于對分片包的重組方式有所不同。
函數(shù)ip_conntrack_in()首先調(diào)用__ip_conntrack_proto_find(),根據(jù)數(shù)據(jù)包的協(xié)議找到其應該使用的傳輸協(xié)議的連接跟蹤模塊,接下來調(diào)用協(xié)議模塊的error()對數(shù)據(jù)包進行正確性檢查,然后調(diào)用函數(shù)resolve_normal_ct()選擇正確的連接跟蹤記錄,如果沒有,則創(chuàng)建一個新紀錄。接著調(diào)用協(xié)議模塊的packet()函數(shù),如果返回失敗,則nf_conntrack_put()將釋放連接記錄。ip_conntrack_in()函數(shù)的源碼如下,函數(shù)resolve_normal_ct()實際操作了數(shù)據(jù)包和連接跟蹤表的內(nèi)容。
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 = 0;
int ret;
/* Previously seen (loopback or untracked)? Ignore. */
if ((*pskb)->nfct) {
CONNTRACK_STAT_INC_ATOMIC(ignore);
return NF_ACCEPT;
}
/* Never happen */
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;
}
......
/* rcu_read_lock()ed by nf_hook_slow */
proto = __ip_conntrack_proto_find((*pskb)->nh.iph->protocol);
/* It may be an special packet, error, unclean...
* inverse of the return code tells to the netfilter
* core what to do with the packet. */
if (proto->error != NULL
&& (ret = proto->error(*pskb, &ctinfo, hooknum)) <= 0) {
CONNTRACK_STAT_INC_ATOMIC(error);
CONNTRACK_STAT_INC_ATOMIC(invalid);
return -ret;
}
if (!(ct = resolve_normal_ct(*pskb, proto,&set_reply,hooknum,&ctinfo))) {
/* Not valid part of a connection */
CONNTRACK_STAT_INC_ATOMIC(invalid);
return NF_ACCEPT;
}
if (IS_ERR(ct)) {
/* Too stressed to deal. */
CONNTRACK_STAT_INC_ATOMIC(drop);
return NF_DROP;
}
IP_NF_ASSERT((*pskb)->nfct);
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_ATOMIC(invalid);
return -ret;
}
if (set_reply && !test_and_set_bit(IPS_SEEN_REPLY_BIT, &ct->status))
ip_conntrack_event_cache(IPCT_STATUS, *pskb);
return ret;
}
|
函數(shù)resolve_normal_ct()搜索與傳遞來的sk_buff結(jié)構(gòu)相匹配的連接記錄,其首先調(diào)用函數(shù)ip_ct_get_tuple(),利用包的協(xié)議模塊的pkt_to_tuple()函數(shù)創(chuàng)建一個ip_conntrack_tuple類型的結(jié)構(gòu),接下來調(diào)用函數(shù)ip_conntrack_find_get()在連接跟蹤表中查找匹配的記錄。如果沒有找到匹配的項,將調(diào)用函數(shù)init_conntrack()創(chuàng)建一個新的連接跟蹤記錄。最后確定數(shù)據(jù)包sk_buff結(jié)構(gòu)的狀態(tài)域的值,對其中的nfct和nfctinfo進行賦值。函數(shù)resolve_normal_ct()的源碼如下所示:
static inline struct nf_conn *
resolve_normal_ct(struct sk_buff *skb,
unsigned int dataoff,
u_int16_t l3num,
u_int8_t protonum,
struct nf_conntrack_l3proto *l3proto,
struct nf_conntrack_l4proto *l4proto,
int *set_reply,
enum ip_conntrack_info *ctinfo)
{
if (!nf_ct_get_tuple(skb, (unsigned int)(skb->nh.raw - skb->data),
dataoff, l3num, protonum, &tuple, l3proto,
l4proto)) {
DEBUGP("resolve_normal_ct: Can't get tuple/n");
return NULL;
}
/* look for tuple match */
h = nf_conntrack_find_get(&tuple, NULL);
if (!h) {
h = init_conntrack(&tuple, l3proto, l4proto, skb, dataoff);
if (!h)
return NULL;
if (IS_ERR(h))
return (void *)h;
}
ct = nf_ct_tuplehash_to_ctrack(h);
......
*ctinfo = IP_CT_NEW;
}
*set_reply = 0;
}
skb->nfct = &ct->ct_general;
skb->nfctinfo = *ctinfo;
return ct;
}
4.3. ip_conntrack_help()
函數(shù)ip_conntrack_help()被掛載在鉤子點NF_IP_LOCAL_IN和NF_IP_POST_ROUTING,其首先根據(jù)傳來的sk_buff結(jié)構(gòu)查找連接跟蹤記錄,如果該包所屬連接有輔助模塊helper,且包符合一定的狀態(tài)要求,則調(diào)用相應輔助模塊的函數(shù)help()處理數(shù)據(jù)包。
static unsigned int ip_conntrack_help(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;
/* This is where we call the helper: as the packet goes out. */
ct = ip_conntrack_get(*pskb, &ctinfo);
if (ct && ct->helper && ctinfo != IP_CT_RELATED + IP_CT_IS_REPLY) {
unsigned int ret;
ret = ct->helper->help(pskb, ct, ctinfo);
if (ret != NF_ACCEPT)
return ret;
}
return NF_ACCEPT;
}
|
4.4. ip_confirm()
函數(shù)ip_confirm()被掛載在鉤子點NF_IP_LOCAL_IN和NF_IP_POST_ROUTING,其對數(shù)據(jù)包再次進行連接跟蹤記錄確認,并將新建的連接跟蹤記錄加到表中?紤]到包可能被過濾掉,之前新建的連接跟蹤記錄實際上并未真正加到連接跟蹤表中,而在最后由函數(shù)ip_confirm()確認后真正添加,實際對傳來的sk_buff進行確認的函數(shù)是__ip_conntrack_confirm()。在該函數(shù)中首先調(diào)用函數(shù)ip_conntrack_get()查找相應的連接跟蹤記錄,如果數(shù)據(jù)包不是IP_CT_DIR_ORIGINAL方向的包,則直接ACCEPT,否則接著調(diào)用hash_conntrack()計算所找到的連接跟蹤記錄的ip_conntrack_tuple類型的hash值,且同時計算兩個方向的值。然后根據(jù)這兩個hash值分別查找連接跟蹤記錄的hash表,如果找到了,則返回NF_DROP,如果未找到,則調(diào)用函數(shù)__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);
}
|
static inline int ip_conntrack_confirm(struct sk_buff **pskb)
{
struct ip_conntrack *ct = (struct ip_conntrack *)(*pskb)->nfct;
int ret = NF_ACCEPT;
if (ct) {
if (!is_confirmed(ct) && !is_dying(ct))
ret = __ip_conntrack_confirm(pskb);
ip_ct_deliver_cached_events(ct);
}
return ret;
}
|
int
__ip_conntrack_confirm(struct sk_buff **pskb)
{
ct = ip_conntrack_get(*pskb, &ctinfo);
if (CTINFO2DIR(ctinfo) != IP_CT_DIR_ORIGINAL)
return NF_ACCEPT;
hash = hash_conntrack(&ct->tuplehash[IP_CT_DIR_ORIGINAL.tuple);
repl_hash = hash_conntrack(&ct->tuplehash[IP_CT_DIR_REPLY.tuple);
/* No external references means noone else could have
confirmed us. */
IP_NF_ASSERT(!is_confirmed(ct));
DEBUGP("Confirming conntrack %p/n", ct);
write_lock_bh(&ip_conntrack_lock);
list_for_each_entry(h, &ip_conntrack_hash[hash, list)
if (ip_ct_tuple_equal(&ct->tuplehash[IP_CT_DIR_ORIGINAL.tuple,
&h->tuple))
goto out;
list_for_each_entry(h, &ip_conntrack_hash[repl_hash, list)
if (ip_ct_tuple_equal(&ct->tuplehash[IP_CT_DIR_REPLY.tuple,
&h->tuple))
goto out;
/* Remove from unconfirmed list */
list_del(&ct->tuplehash[IP_CT_DIR_ORIGINAL.list);
__ip_conntrack_hash_insert(ct, hash, repl_hash);
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_bh(&ip_conntrack_lock);
if (ct->helper)
ip_conntrack_event_cache(IPCT_HELPER, *pskb);
......
out:
CONNTRACK_STAT_INC(insert_failed);
write_unlock_bh(&ip_conntrack_lock);
return NF_DROP;
}
4.5.ip_conntrack_local()
函數(shù)ip_conntrack_local()被掛載在鉤子點NF_IP_LOCAL_OUT,該函數(shù)會調(diào)用ip_conntrack_in(),函數(shù)源碼如下:
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);
}
|
5.數(shù)據(jù)包轉(zhuǎn)發(fā)的連接跟蹤流程
下面以數(shù)據(jù)包轉(zhuǎn)發(fā)為例描述連接跟蹤的流程,其中的函數(shù)及結(jié)構(gòu)體為前幾節(jié)所介紹的一部分,圖中主要想體現(xiàn)數(shù)據(jù)包sk_buff在連接跟蹤流程中的相應改變,連接跟蹤記錄與連接跟蹤表的關系,何時查找和修改連接跟蹤表,輔助模塊以及傳輸協(xié)議如何在連接跟蹤中使用等。所有的函數(shù)說明以及結(jié)構(gòu)體在之前都有描述。發(fā)往本機以及本機發(fā)出的數(shù)據(jù)包的連接跟蹤流程在此不再做分析。
![]()
6.總結(jié)
以上只是簡要分析了Netfilter架構(gòu)中連接跟蹤功能的實現(xiàn)機制,其中很多細節(jié)被忽略,如數(shù)據(jù)包的狀態(tài),連接跟蹤記錄的狀態(tài),具體傳輸協(xié)議的連接跟蹤,主要目的是想要對整個實現(xiàn)框架有所認識。另外,對于較復雜的活動協(xié)議,期望連接與主連接之間的關聯(lián)等并未做分析,希望在以后有時間再做分析。鑒于自身水平有限,且可參考資料較少,因此以上的分析不能保證所描述的內(nèi)容完全正確。
PS:對于其中存在的錯誤問題,希望各位能指正,以便將此分析盡量完善,另外,對于我尚未分析完成的部分,也希望大家能夠提出意見,不甚感謝。
|
|
|