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);
1 2 3 4 |
static int __init nf_conntrack_standalone_init(void) { return register_pernet_subsys(&nf_conntrack_net_ops); } |
它作為網路空間子系統註冊進了核心
1 2 3 4 |
static struct pernet_operations nf_conntrack_net_ops = { .init = nf_conntrack_net_init, .exit = nf_conntrack_net_exit, }; |
註冊的過程中呼叫.init 傳遞給它的net引數是init_net 它是通過net_ns_init初始化到了net_namespace_list鏈上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
static int nf_conntrack_net_init(struct net *net) { int ret; ret = nf_conntrack_init(net); if (ret < 0) goto out_init; ret = nf_conntrack_standalone_init_proc(net); if (ret < 0) goto out_proc; net->ct.sysctl_checksum = 1; net->ct.sysctl_log_invalid = 0; ret = nf_conntrack_standalone_init_sysctl(net); if (ret < 0) goto out_sysctl; return 0; out_sysctl: nf_conntrack_standalone_fini_proc(net); out_proc: nf_conntrack_cleanup(net); out_init: return ret; } |
程式碼不是很多,核心明顯是nf_conntrack_init函式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
int nf_conntrack_init(struct net *net) { int ret; if (net_eq(net, &init_net)) { ret = nf_conntrack_init_init_net(); if (ret < 0) goto out_init_net; } ret = nf_conntrack_proto_init(net); if (ret < 0) goto out_proto; ret = nf_conntrack_init_net(net); if (ret < 0) goto out_net; if (net_eq(net, &init_net)) { /* For use by REJECT target */ RCU_INIT_POINTER(ip_ct_attach, nf_conntrack_attach); RCU_INIT_POINTER(nf_ct_destroy, destroy_conntrack); /* Howto get NAT offsets */ RCU_INIT_POINTER(nf_ct_nat_offset, NULL); } return 0; out_net: nf_conntrack_proto_fini(net); out_proto: if (net_eq(net, &init_net)) nf_conntrack_cleanup_init_net(); out_init_net: return ret; } |
先進入nf_conntrack_init_init_net函式
nf_conntrack_htable_size 賦值和nf_conntrack_max(這個引數可以通過proc來設定.)
它和記憶體大小有關,大於1G的即預設為16384=16*1024=4*4k;
1 2 3 |
nf_conntrack_htable_size = (((totalram_pages << PAGE_SHIFT) / 16384) // 16384 =16k =16*1024=4*4k / sizeof(struct hlist_head)); |
比如對於4G的記憶體,那麼它的計算:
size=((1024*1024*4k )/(4*4k))/4= 1024*256/4=1024*64=1024*16*4=4*(4*4k)=4*16384
nf_conntrack_max呢?
1 2 3 4 5 6 7 |
/* Use a max. factor of four by default to get the same max as * with the old struct list_heads. When a table size is given * we use the old value of 8 to avoid reducing the max. * entries. */ max_factor = 4; } nf_conntrack_max = max_factor * nf_conntrack_htable_size; |
後續是設定per-cpu變數:有興趣的可以看看.
1 |
per_cpu(nf_conntrack_untracked, cpu) |
1 2 |
DEFINE_PER_CPU(struct nf_conn, nf_conntrack_untracked); EXPORT_PER_CPU_SYMBOL(nf_conntrack_untracked); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
struct nf_conntrack_l4proto nf_conntrack_l4proto_generic __read_mostly = { .l3proto = PF_UNSPEC, .l4proto = 255, .name = "unknown", .pkt_to_tuple = generic_pkt_to_tuple, .invert_tuple = generic_invert_tuple, .print_tuple = generic_print_tuple, .packet = generic_packet, .get_timeouts = generic_get_timeouts, .new = generic_new, #if IS_ENABLED(CONFIG_NF_CT_NETLINK_TIMEOUT) .ctnl_timeout = { .nlattr_to_obj = generic_timeout_nlattr_to_obj, .obj_to_nlattr = generic_timeout_obj_to_nlattr, .nlattr_max = CTA_TIMEOUT_GENERIC_MAX, .obj_size = sizeof(unsigned int), .nla_policy = generic_timeout_nla_policy, }, #endif /* CONFIG_NF_CT_NETLINK_TIMEOUT */ .init_net = generic_init_net, .get_net_proto = generic_get_net_proto, }; |
初始化通用協議以及建立sysctl,並初始化nf_ct_l3protos為nf_conntrack_l3proto_generic.
nf_conntrack_init_net初始化hash連結串列和建立cache相關後續討論.
先了解下基本的初始化工作後,我們從hook點說起ct是如何建立起來的
nf_conntrack_l3proto_ipv4.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
/* Connection tracking may drop packets, but never alters them, so make it the first hook. */ static struct nf_hook_ops ipv4_conntrack_ops[] __read_mostly = { { .hook = ipv4_conntrack_in, .owner = THIS_MODULE, .pf = NFPROTO_IPV4, .hooknum = NF_INET_PRE_ROUTING, .priority = NF_IP_PRI_CONNTRACK, }, { .hook = ipv4_conntrack_local, .owner = THIS_MODULE, .pf = NFPROTO_IPV4, .hooknum = NF_INET_LOCAL_OUT, .priority = NF_IP_PRI_CONNTRACK, }, { .hook = ipv4_helper, .owner = THIS_MODULE, .pf = NFPROTO_IPV4, .hooknum = NF_INET_POST_ROUTING, .priority = NF_IP_PRI_CONNTRACK_HELPER, }, { .hook = ipv4_confirm, .owner = THIS_MODULE, .pf = NFPROTO_IPV4, .hooknum = NF_INET_POST_ROUTING, .priority = NF_IP_PRI_CONNTRACK_CONFIRM, }, { .hook = ipv4_helper, .owner = THIS_MODULE, .pf = NFPROTO_IPV4, .hooknum = NF_INET_LOCAL_IN, .priority = NF_IP_PRI_CONNTRACK_HELPER, }, { .hook = ipv4_confirm, .owner = THIS_MODULE, .pf = NFPROTO_IPV4, .hooknum = NF_INET_LOCAL_IN, .priority = NF_IP_PRI_CONNTRACK_CONFIRM, }, } |
我們會發現它的優先順序比較高.除了上面的鉤子還有其他的:
還有nf_defrag_ipv4.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
static struct nf_hook_ops ipv4_defrag_ops[] = { { .hook = ipv4_conntrack_defrag, .owner = THIS_MODULE, .pf = NFPROTO_IPV4, .hooknum = NF_INET_PRE_ROUTING, .priority = NF_IP_PRI_CONNTRACK_DEFRAG, }, { .hook = ipv4_conntrack_defrag, .owner = THIS_MODULE, .pf = NFPROTO_IPV4, .hooknum = NF_INET_LOCAL_OUT, .priority = NF_IP_PRI_CONNTRACK_DEFRAG, }, }; |
除了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裡傳遞給它的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
static unsigned int ipv4_conntrack_defrag(unsigned int hooknum, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)) { struct sock *sk = skb->sk; struct inet_sock *inet = inet_sk(skb->sk); if (sk && (sk->sk_family == PF_INET) && inet->nodefrag) return NF_ACCEPT; #if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE) #if !defined(CONFIG_NF_NAT) && !defined(CONFIG_NF_NAT_MODULE) /* Previously seen (loopback)? Ignore. Do this before fragment check. */ if (skb->nfct && !nf_ct_is_template((struct nf_conn *)skb->nfct)) return NF_ACCEPT; #endif #endif /* Gather fragments. */ if (ip_is_fragment(ip_hdr(skb))) { enum ip_defrag_users user = nf_ct_defrag_user(hooknum, skb); // IP_DEFRAG_CONNTRACK_IN if (nf_ct_ipv4_gather_frags(skb, user)) return NF_STOLEN; } return NF_ACCEPT; } |
用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來管理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/* Describe an entry in the "incomplete datagrams" queue. */ struct ipq { struct inet_frag_queue q; u32 user; __be32 saddr; __be32 daddr; __be16 id; u8 protocol; u8 ecn; /* RFC3168 support */ int iif; unsigned int rid; struct inet_peer *peer; }; |
查詢是否已經有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中
1 2 3 4 5 6 7 8 |
static unsigned int ipv4_conntrack_in(unsigned int hooknum, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)) { return nf_conntrack_in(dev_net(in), PF_INET, hooknum, skb); } |
首先根據協議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層相關的.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
struct nf_conntrack_l3proto nf_conntrack_l3proto_ipv4 __read_mostly = { .l3proto = PF_INET, .name = "ipv4", .pkt_to_tuple = ipv4_pkt_to_tuple, .invert_tuple = ipv4_invert_tuple, .print_tuple = ipv4_print_tuple, .get_l4proto = ipv4_get_l4proto, #if defined(CONFIG_NF_CT_NETLINK) || defined(CONFIG_NF_CT_NETLINK_MODULE) .tuple_to_nlattr = ipv4_tuple_to_nlattr, .nlattr_tuple_size = ipv4_nlattr_tuple_size, .nlattr_to_tuple = ipv4_nlattr_to_tuple, .nla_policy = ipv4_nla_policy, #endif #if defined(CONFIG_SYSCTL) && defined(CONFIG_NF_CONNTRACK_PROC_COMPAT) .ctl_table_path = "net/ipv4/netfilter", #endif .init_net = ipv4_init_net, .me = THIS_MODULE, } |
這個很重要的結構體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,不過它返回的是型別:
1 2 3 4 5 |
/* Connections have two entries in the hash table: one for each way */ struct nf_conntrack_tuple_hash { struct hlist_nulls_node hnnode; struct nf_conntrack_tuple tuple; }; |
對於第一個包肯定為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:
1 |
__nf_ct_helper_find(&ct->tuplehash[IP_CT_DIR_REPLY].tuple) |
這個引數我們知道就是當前tuple的反轉五元組. 而查詢的時候計算hash值只用到了五元組的協議號、埠 (還有一個是ipv4 or ipv6)
(跟我們之前查詢ct的時候計算的hash需要的引數少了很多.) 很明顯helper註冊的時候也用了這樣的hash演算法.
回頭看看tftp_helper註冊的時候:
點選(此處)摺疊或開啟
1 2 |
tftp[i][j].tuple.dst.protonum = IPPROTO_UDP; tftp[i][j].tuple.src.u.udp.port = htons(ports[i]); |
這兩個值是事先給定好的. 其實發現沒有,雖然很容易關聯,但是也面臨著衝突的問題.所以需要補全ip和埠資訊
既然找到了那麼如何處理呢?
1 2 3 4 5 6 7 |
if (help == NULL) { help = nf_ct_helper_ext_add(ct, helper, flags); if (help == NULL) { ret = -ENOMEM; goto out; } } |
help是什麼呢?struct nf_conn_help *help;
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/* nf_conn feature for connections that have a helper */ struct nf_conn_help { /* Helper. if any */ struct nf_conntrack_helper __rcu *helper; struct hlist_head expectations; /* Current number of expected connections */ u8 expecting[NF_CT_MAX_EXPECT_CLASSES]; /* private helper information. */ char data[]; }; |
nf_ct_helper_ext_add擴充套件ct的ext空間. 然後把找到的helper指標賦給help->helper:
1 |
rcu_assign_pointer(help->helper, helper); |
那麼以後我們就可以通過help = nfct_help(ct);這樣的介面找到我們關聯的helper了.
關於查詢exp補充說明一下:
expected函式有什麼作用?
當一個新的包到達init_conntrack時,就會根據包中的源地址、目的地址等資訊填充一個struct nf_conn例項,通常定義為ct的變數。接下來檢查當前的連線是否是另外一條已經存在連線的期望連線:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
spin_lock_bh(&nf_conntrack_lock); exp = nf_ct_find_expectation(net, zone, tuple); if (exp) { pr_debug("conntrack: expectation arrives ct=%p exp=%p\n", ct, exp); /* Welcome, Mr. Bond. We've been expecting you... */ __set_bit(IPS_EXPECTED_BIT, &ct->status); ct->master = exp->master; if (exp->helper) { help = nf_ct_helper_ext_add(ct, exp->helper, GFP_ATOMIC); if (help) rcu_assign_pointer(help->helper, exp->helper); } #ifdef CONFIG_NF_CONNTRACK_MARK ct->mark = exp->master->mark; #endif #ifdef CONFIG_NF_CONNTRACK_SECMARK ct->secmark = exp->master->secmark; #endif nf_conntrack_get(&ct->master->ct_general); NF_CT_STAT_INC(net, expect_new); } |
如果exp不為空,就表示當前的連線是另外一條已經存在連線的期望連線.接下來,就是expectfn的工作了:根據master的連線跟蹤資訊更新新建立的ct連線跟蹤資訊,並放到連線跟蹤表中,詳見nf_nat_follow_master函式(因為expectfn通常指向的nf_nat_follow_master).
回到主線函式:
最後把ct加入一個未認證的hlist:
1 2 3 |
/* Overload tuple linked list to put us in unconfirmed list. */ hlist_nulls_add_head_rcu(&ct->tuplehash[IP_CT_DIR_ORIGINAL].hnnode, &net->ct.unconfirmed); |
並返回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函式:
1 |
ret = l4proto->packet(ct, skb, dataoff, ctinfo, pf, hooknum, timeouts); |
以上只是簡單流程的分析
通過上面的分析我們知道當我們用到ct的時候,把skb->nfct強制型別轉換就可以了
雖然nfct是struct nf_conntrack *nfct;
1 2 3 4 5 |
#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE) struct nf_conntrack { atomic_t use; }; #endif |
但是我們看到struct nf_conn的結構體
1 2 3 4 5 6 |
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; ... } |
我們是不是明白了為什麼skb->nfct那麼使用。新的核心也提供了介面:
1 2 3 4 5 6 7 |
/* Return conntrack_info and tuple hash for given skb. */ static inline struct nf_conn * nf_ct_get(const struct sk_buff *skb, enum ip_conntrack_info *ctinfo) { *ctinfo = skb->nfctinfo; return (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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
static unsigned int ipv4_helper(unsigned int hooknum, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)) { struct nf_conn *ct; enum ip_conntrack_info ctinfo; const struct nf_conn_help *help; const struct nf_conntrack_helper *helper; unsigned int ret; /* This is where we call the helper: as the packet goes out. */ ct = nf_ct_get(skb, &ctinfo); if (!ct || ctinfo == IP_CT_RELATED_REPLY) return NF_ACCEPT; help = nfct_help(ct); if (!help) return NF_ACCEPT; /* rcu_read_lock()ed by nf_hook_slow */ helper = rcu_dereference(help->helper); if (!helper) return NF_ACCEPT; ret = helper->help(skb, skb_network_offset(skb) + ip_hdrlen(skb), ct, ctinfo); if (ret != NF_ACCEPT && (ret & NF_VERDICT_MASK) != NF_QUEUE) { nf_log_packet(NFPROTO_IPV4, hooknum, skb, in, out, NULL, "nf_ct_%s: dropping packet", helper->name); } return ret; } |
這個函式很簡單,直接找到之前關聯的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
1 2 3 |
/* Confirm a connection given skb; places it in hash table */ int __nf_conntrack_confirm(struct sk_buff *skb) |
函式並不複雜,利用源方向的hash和反方向的hash,查詢ct全域性表,為什麼呢 ,因為在這個報處理的過程中,可能會收到反方向的報文而建立ct.所以如果兩個hash任意一個找到表裡已有,則返回NF_DROP. 緊接著從unconfirm的hlist刪除.設定ct->status |= IPS_CONFIRMED; 新增ct定時器.最後把來和回的tuple_hash都新增到ct全域性表中.
1 2 3 4 5 6 7 8 9 10 11 |
static void __nf_conntrack_hash_insert(struct nf_conn *ct, unsigned int hash, unsigned int repl_hash) { struct net *net = nf_ct_net(ct); hlist_nulls_add_head_rcu(&ct->tuplehash[IP_CT_DIR_ORIGINAL].hnnode, &net->ct.hash[hash]); hlist_nulls_add_head_rcu(&ct->tuplehash[IP_CT_DIR_REPLY].hnnode, &net->ct.hash[repl_hash]); } |
到這裡,整個流程已經結束了,看起來有點枯燥,後續會補上框架圖.僅僅是從程式碼層去一窺其神祕,在程式碼裡我們見到不少nat相關的東西,一開始我們就說了ct是nat的基礎.