iptables深入解析:ct篇

發表於2015-08-24

ct是netfilter非常重要的基礎和架構核心.它為狀態防火牆,nat等打下基礎. 一直覺的它很神祕,所以就下定決心分析一下.
這裡依然不從框架開始說,而是從實際程式碼著手.
參考核心 kernel3.8.13

先看看它的初始化:
Net/netfilter/nf_conntrack_core.c
int nf_conntrack_init(struct net *net);
入口在nf_conntrack_standalone.c
module_init(nf_conntrack_standalone_init);

它作為網路空間子系統註冊進了核心

註冊的過程中呼叫.init 傳遞給它的net引數是init_net 它是通過net_ns_init初始化到了net_namespace_list鏈上。

程式碼不是很多,核心明顯是nf_conntrack_init函式

先進入nf_conntrack_init_init_net函式
nf_conntrack_htable_size 賦值和nf_conntrack_max(這個引數可以通過proc來設定.)
它和記憶體大小有關,大於1G的即預設為16384=16*1024=4*4k;

比如對於4G的記憶體,那麼它的計算:
size=((1024*1024*4k )/(4*4k))/4= 1024*256/4=1024*64=1024*16*4=4*(4*4k)=4*16384
nf_conntrack_max呢?

後續是設定per-cpu變數:有興趣的可以看看.

初始化通用協議以及建立sysctl,並初始化nf_ct_l3protos為nf_conntrack_l3proto_generic.
nf_conntrack_init_net初始化hash連結串列和建立cache相關後續討論.
先了解下基本的初始化工作後,我們從hook點說起ct是如何建立起來的

nf_conntrack_l3proto_ipv4.c

我們會發現它的優先順序比較高.除了上面的鉤子還有其他的:
還有nf_defrag_ipv4.c

除了hook點,我們需要記住的就是:連線追蹤入口 和 連線追蹤出口
記錄如何生成呢?我們看報文的流程:
1.傳送給本機的資料包

流程:PRE_ROUTING—-LOCAL_IN—本地程式

2.需要本機轉發的資料包

流程:PRE_ROUTING—FORWARD—POST_ROUTING—外出

3.從本機發出的資料包

流程:LOCAL_OUT—-POST_ROUTING—外出

那麼就選擇從流程1分析看看ct是如何一步一步建立起來的.
先從入口說起,接收的報文首先經過鉤子點NF_INET_PRE_ROUTING

從優先順序上先經過ipv4_conntrack_defrag 再經過ipv4_conntrack_in
對於幀接收,查詢並交給處理協議我們已經很熟悉不過了,對於ip,當然先進入ip_rcv

Ip_input.c
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
ip_rcv_finish);

ip的處理工作主要在ip_rcv_finish裡完成,ip_rcv主要做了些安全檢查。
ipv4_conntrack_defrag看看這個函式,引數就是NF_HOOK裡傳遞給它的

用ip_is_fragment判斷是否是分片報文,如果有分片則呼叫nf_ct_ipv4_gather_frags—>ip_defrag
對於ip_defrag的呼叫的地方很少. 當需要傳遞給本地更高協議層的時候通過ip_local_deliver來組包.

補充:
NF_STOLEN 模組接管該資料包,告訴Netfilter“忘掉”該資料包。該回撥函式將從此開始對資料包的處理,並且Netfilter應當放棄對該資料包做任何的處理。但是,這並不意味著該資料包的資源已經被釋放。這個資料包以及它獨自的sk_buff資料結構仍然有效,只是回撥函式從Netfilter 獲取了該資料包的所有權.

首先把skb獨立出來,除去owner,然後呼叫ip_defrag組包,這也是netfilter效率低的原因之一.(重新組報文很耗費記憶體和時間)
每個分片報文都會建立一個struct ipq *qp來管理

查詢是否已經有ipq, 根據ip的id ,saddr,daddr、protocol計算hash值,由於如果屬於同一ip報文的分片則這些相同.
從ip4_frags全域性的hash連結串列裡查詢,如果沒有就建立
hlist_add_head(&qp->list, &f->hash[hash]); 這個qp是結構體struct inet_frag_queue
得到ipq後,通過ip_frag_queue把skb加入到佇列裡.
ip分片會插入到qp->q.fragments裡
最後當滿足一定條件時,進行IP重組。當收到了第一個和最後一個IP分片,且收到的IP分片的最大長度等於收到的IP分片的總長度時,表明所有的IP分片已收集齊,呼叫ip_frag_reasm重組包,成功返回0. 關於ip分片與重組參考的資料有很多.
下面看ipv4_conntrack_in
在nf_conntrack_l3proto_ipv4.c中

首先根據協議PF_INET找到鏈(ipv4)協議號超出範圍則使用預設值nf_conntrack_l3proto_generic。
struct nf_conntrack_l3proto __rcu *nf_ct_l3protos[AF_MAX] __read_mostly;
通過介面nf_conntrack_l3proto_register註冊了ipv4和ipv6到nf_ct_l3protos
正常的ipv4是:它負責對ip層報文的解析函式API,後續還有l4層相關的.

這個很重要的結構體struct nf_conntrack_l3proto
找個這個結構體後,呼叫它節點函式獲取l4 協議號和dataoff
然後去找到struct nf_conntrack_l4proto *l4proto這個東西,如果找到即nf_ct_protos[l3proto][l4proto]
異常則為nf_conntrack_l4proto_generic
通過nf_conntrack_l4proto_register註冊了tcp、udp、icmp;其他模組還有dccp、gre、sctp、udplite(輕量級使用者資料包協議)

根據四層協議error函式check包的正確性。
然後呼叫resolve_normal_ct .之前我們看到skb->nfct ,一開始肯定為null,它在這個函式裡被賦值
首先nf_ct_get_tuple獲取struct nf_conntrack_tuple tuple;由它可以判斷一個連線即五元組;一個連線由一“去”一“回”兩個五元組來唯一確定.
ipv4_pkt_to_tuple 獲取srcip、dstip
tuple->src.l3num = l3num;
tuple->src.u3.ip = ap[0];
tuple->dst.u3.ip = ap[1];
tuple->dst.protonum = protonum;
tuple->dst.dir = IP_CT_DIR_ORIGINAL;

然後在解析l4資訊:
例如tcp則解析埠:
tuple->src.u.tcp.port = hp->source;
tuple->dst.u.tcp.port = hp->dest;
現在我們有了 srcip、dstip、sportt 、dport,協議號,以及方向資訊

然後查詢追蹤全域性表是否已經有了這個流,hash_conntrack_raw計算hash值
__nf_conntrack_find_get:
hlist_nulls_for_each_entry_rcu(h, n, &net->ct.hash[bucket],
如果找到則返回,否則返回null,不過它返回的是型別:

對於第一個包肯定為null, 然後init_conntrack建立它.
先反轉tuple得到repl_tuple
__nf_conntrack_alloc 申請struct nf_conn *ct;
從cache裡申請
ct = kmem_cache_alloc(net->ct.nf_conntrack_cachep, gfp);
然後初始化
ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple = *orig;
ct->tuplehash[IP_CT_DIR_ORIGINAL].hnnode.pprev = NULL;/* save hash for reusing when confirming */
*(unsigned long *)(&ct->tuplehash[IP_CT_DIR_REPLY].hnnode.pprev) = hash;
ct->tuplehash[IP_CT_DIR_REPLY].tuple = *repl;

並設定ct定時器 death_by_timeout
l4proto->new(ct, skb, dataoff, timeouts) 設定l4 ct引數。
關於nf_ct_acct_ext_add這裡先不討論.

然後找到協議註冊的expect :struct nf_conntrack_expect
exp = nf_ct_find_expectation(net, zone, tuple);
它查詢的是net->ct.expect_hash[h] ,即當前ct所期望關聯的tuple

我們看看核心註冊了哪些expect
通過nf_ct_expect_alloc申請,這個貌似和上層應用關聯用的。
對於應用的關聯,不是很清楚,簡單看看tftp的nf_conntrack_tftp_init

它有兩個關鍵的地方:
1.tftp[i][j].help = tftp_help;
2.nf_conntrack_helper_register(&tftp[i][j]);

這個tftp是struct nf_conntrack_helper結構體。關於helper這裡說明一下:
Netfilter的連線跟蹤為我們提供了一個非常有用的功能模組:helper。該模組可以使我們以很小的代價來完成對連線跟蹤功能的擴充套件。這種應用場景需求一般是,當一個資料包即將離開Netfilter框架之前,我們可以對資料包再做一些最後的處理.
同時還有個補充tftp[i][j].expect_policy = &tftp_exp_policy; 它也是相關的
它把helper加入hlist:全域性nf_ct_helper_hash[h]

我們__nf_ct_try_assign_helper這個被init_conntrack呼叫,也就是新建ct的時候
一開始net->ct.expect_hash應該為null
但是expect_hash和nf_ct_helper_hash 又是如何關聯起來的呢?
nf_ct_expect_insert會操作expect_hash並插入,它最後封裝在nf_ct_expect_related
剛才說到tftp expect對吧,tftp_help裡剛好呼叫了它
在tftp_help裡它申請一個exp = nf_ct_expect_alloc(ct); 然後初始化nf_ct_expect_init。
最後呼叫nf_ct_expect_related把這個exp和具體的ct關聯到expect_hash裡。
它屬於被動的,還得從tftp說起,雖然它依helper方式把註冊進了help_hash。
但是它又是如何運作起來的呢?畢竟這個時候只是靜態的註冊而已,即需要觸發tftp_help函式.
要觸發它,就需要找到註冊的helper,就需要計算hash。剛好在__nf_ct_try_assign_helper中有
__nf_conntrack_helper_find查詢註冊的helper和當前ct的關聯.
我們看看查詢的時候用的tuple:

這個引數我們知道就是當前tuple的反轉五元組. 而查詢的時候計算hash值只用到了五元組的協議號、埠 (還有一個是ipv4 or ipv6)
(跟我們之前查詢ct的時候計算的hash需要的引數少了很多.) 很明顯helper註冊的時候也用了這樣的hash演算法.
回頭看看tftp_helper註冊的時候:
點選(此處)摺疊或開啟

這兩個值是事先給定好的. 其實發現沒有,雖然很容易關聯,但是也面臨著衝突的問題.所以需要補全ip和埠資訊
既然找到了那麼如何處理呢?

help是什麼呢?struct nf_conn_help *help;

nf_ct_helper_ext_add擴充套件ct的ext空間. 然後把找到的helper指標賦給help->helper:

那麼以後我們就可以通過help = nfct_help(ct);這樣的介面找到我們關聯的helper了.
關於查詢exp補充說明一下:
expected函式有什麼作用?
當一個新的包到達init_conntrack時,就會根據包中的源地址、目的地址等資訊填充一個struct nf_conn例項,通常定義為ct的變數。接下來檢查當前的連線是否是另外一條已經存在連線的期望連線:

如果exp不為空,就表示當前的連線是另外一條已經存在連線的期望連線.接下來,就是expectfn的工作了:根據master的連線跟蹤資訊更新新建立的ct連線跟蹤資訊,並放到連線跟蹤表中,詳見nf_nat_follow_master函式(因為expectfn通常指向的nf_nat_follow_master).
回到主線函式:
最後把ct加入一個未認證的hlist:

並返回return &ct->tuplehash[IP_CT_DIR_ORIGINAL].
我們看如果直接找到了ct那麼下面的工作很簡單:設定一些狀態值然後賦值skb
skb->nfct = &ct->ct_general;
skb->nfctinfo = *ctinfo; // 對於第一次報文 值為:IP_CT_NEW
return ct;
skb建立ct關聯後,然後更新ct的狀態,呼叫l4協議的packet函式:

以上只是簡單流程的分析
通過上面的分析我們知道當我們用到ct的時候,把skb->nfct強制型別轉換就可以了
雖然nfct是struct nf_conntrack *nfct;

但是我們看到struct nf_conn的結構體

我們是不是明白了為什麼skb->nfct那麼使用。新的核心也提供了介面:

連線追蹤用結構體 struct nf_conn表示 ,而狀態資訊用enum ip_conntrack_info 表示
1. IP_CT_ESTABLISHED
Packet是一個已建連線的一部分,在其初始方向。
2. IP_CT_RELATED
Packet屬於一個已建連線的相關連線,在其初始方向。
3. IP_CT_NEW
Packet試圖建立新的連線
4. IP_CT_ESTABLISHED+IP_CT_IS_REPLY
Packet是一個已建連線的一部分,在其響應方向。
5. IP_CT_RELATED+IP_CT_IS_REPLY
Packet屬於一個已建連線的相關連線,在其響應方向
剛才我們分析了第一個過來的包,屬於新建連線,即IP_CT_NEW。
對於每個進來的包都先獲取struct nf_conntrack_tuple資訊 和查詢或者建立struct nf_conntrack_tuple_hash
接著我們需要看的是ip_conntrack_help()和ip_confirm();優先順序上先是helper 然後是confirm.對於新版核心介面名字有所改變:ipv4_help/ipv4_confirm

這個函式很簡單,直接找到之前關聯的helper然後呼叫help函式.對於tftp這個helper,它的help即:tftp_help
我們看看help做了什麼工作.
首先獲取協議頭,然後根據協議的特性來填充expt的資訊.完善起來.首先是expt->tuple的填充,它除了srcport,其他就是當前ct的tuple的反轉tuple。
還有把當前ct賦expt->master=ct.當然關於這個expt->tuple的dport即源埠肯能會根據具體協議重新獲取,比如ftp協議被動模式下PASV命令 響應碼是227 它裡面包含了ip和埠資訊。然後把expt插入到之前我們提到過的expect_hash裡. 我們回頭看看,假如我們查詢到了exp那麼意味著什麼呢?首先它是新建連線,但是它的目的ip和埠,也就是expt的目的ip和埠即所期望的.而建立起這個expt的ct的源ip和源埠和expt的目的ip和目的埠一樣.那麼意味著建立expt的ct能更快的和當前報文建立聯絡.也就是經常說的ct過程中一“去”一“回”快速聯絡起來,當然關於helper針對不同的協議還需要我們自行寫解析函式去獲取想要的資訊.
或許是時候該看看最後一層的處理函式了.ipv4_confirm
直接看nf_conntrack_confirm

函式並不複雜,利用源方向的hash和反方向的hash,查詢ct全域性表,為什麼呢 ,因為在這個報處理的過程中,可能會收到反方向的報文而建立ct.所以如果兩個hash任意一個找到表裡已有,則返回NF_DROP. 緊接著從unconfirm的hlist刪除.設定ct->status |= IPS_CONFIRMED; 新增ct定時器.最後把來和回的tuple_hash都新增到ct全域性表中.

到這裡,整個流程已經結束了,看起來有點枯燥,後續會補上框架圖.僅僅是從程式碼層去一窺其神祕,在程式碼裡我們見到不少nat相關的東西,一開始我們就說了ct是nat的基礎.

相關文章