目前核心已經有filter 功能,但是往往實際運用中需要用到一些定制的filter 功能, 是以這個時候僅僅依靠現有的不能完成,于是就出現了conntrack的擴充功能, 最直接的就是tftp helper功能。
先看資料結構:
/*
struct sk_buff {
struct nf_conntrack *nfct;//指向struct nf_conn執行個體
..............
};
*/
struct nf_conn {//每個struct nf_conn執行個體代表一個連接配接。每個skb都有一個指針,指向和它相關聯的連接配接。
/* Usage count in here is 1 for hash table/destruct timer, 1 per skb,
* plus 1 for any connection(s) we are `master' for
*
* Hint, SKB address this struct and refcnt via skb->nfct and
* helpers nf_conntrack_get() and nf_conntrack_put().
* Helper nf_ct_put() equals nf_conntrack_put() by dec refcnt,
* beware nf_ct_get() is different and don't inc refcnt.
*/
struct nf_conntrack ct_general; //對連接配接的引用計數
spinlock_t lock;
u16 cpu;
/* These are my tuples; original and reply */
/* Connection tracking(連結跟蹤)用來跟蹤、記錄每個連結的資訊(目前僅支援IP協定的連接配接跟蹤)。
每個連結由“tuple”來唯一辨別,這裡的“tuple”對不同的協定會有不同的含義,例如對tcp,udp
來說就是五元組: (源IP,源端口,目的IP, 目的端口,協定号),對ICMP協定來說是: (源IP, 目
的IP, id, type, code), 其中id,type與code都是icmp協定的資訊。連結跟蹤是防火牆實作狀态檢
測的基礎,很多功能都需要借助連結跟蹤才能實作,例如NAT、快速轉發、等等。*/
struct nf_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX];//正向和反向的連接配接元組資訊。
/* 這是一個位圖,是一個狀态域。在實際的使用中,它通常與一個枚舉類型ip_conntrack_status(位于include/linux/netfilter_ipv4/ip_conntrack.h,Line33)進行位運算來判斷連接配接的狀态。其中主要的狀态包括:
IPS_EXPECTED(_BIT),表示一個預期的連接配接
IPS_SEEN_REPLY(_BIT),表示一個雙向的連接配接
IPS_ASSURED(_BIT),表示這個連接配接即使發生逾時也不能提早被删除
IPS_CONFIRMED(_BIT),表示這個連接配接已經被确認(初始包已經發出) */
/* 可以設定由enum ip_conntrack_status中描述的狀态 */
/* Have we seen traffic both ways yet? (bitset) */
unsigned long status;//該連接配接的連接配接狀态
/* Timer function; drops refcnt when it goes off. */
struct timer_list timeout; //連接配接垃圾回收定時器 連接配接跟蹤的逾時時間
possible_net_t ct_net;
/* all members below initialized via memset */
u8 __nfct_init_offset[0];
/*結構ip_conntrack_expect位于ip_conntrack.h,這個結構用于将一個預期的連接配接配置設定給現有的連接配接,也就是說本連接配接是這個master的一個預期連接配接*/
/* If we were expected by an expectation, this will be it */
struct nf_conn *master;//如果該連接配接是期望連接配接,指向跟其關聯的主連接配接
#if defined(CONFIG_NF_CONNTRACK_MARK)
u_int32_t mark;
#endif
#ifdef CONFIG_NF_CONNTRACK_SECMARK
u_int32_t secmark;
#endif
/* Extensions */ /*指向擴充結構,該結構中包含一些基于連接配接的功能擴充處理函數 */
struct nf_ct_ext *ext;
/* Storage reserved for other modules, must be the last member */
union nf_conntrack_proto proto; /*存儲特定協定的連接配接跟蹤資訊 也就是不同協定實作連接配接跟蹤的額外參數 */
};
/* Extensions: optional stuff which isn't permanently in struct. */
struct nf_ct_ext {
struct rcu_head rcu;
u16 offset[NF_CT_EXT_NUM];
u16 len;
char data[0];
};

enum nf_ct_ext_id {
NF_CT_EXT_HELPER,
#if defined(CONFIG_NF_NAT) || defined(CONFIG_NF_NAT_MODULE)
NF_CT_EXT_NAT,
#endif
NF_CT_EXT_SEQADJ,
NF_CT_EXT_ACCT,
#ifdef CONFIG_NF_CONNTRACK_EVENTS
NF_CT_EXT_ECACHE,
#endif
#ifdef CONFIG_NF_CONNTRACK_ZONES
NF_CT_EXT_ZONE,
#endif
#ifdef CONFIG_NF_CONNTRACK_TIMESTAMP
NF_CT_EXT_TSTAMP,
#endif
#ifdef CONFIG_NF_CONNTRACK_TIMEOUT
NF_CT_EXT_TIMEOUT,
#endif
#ifdef CONFIG_NF_CONNTRACK_LABELS
NF_CT_EXT_LABELS,
#endif
#if IS_ENABLED(CONFIG_NETFILTER_SYNPROXY)
NF_CT_EXT_SYNPROXY,
#endif
NF_CT_EXT_NUM,
};
View Code
連接配接跟蹤資訊塊中的ext字段的類型為struct nf_ct_ext,它指向的記憶體區域包含了一個用于管理擴充資訊的頭部以及目前添加的所有擴充。
offset[i]表示的ID為i的擴充距離data指針的偏移,len表示ext所指整塊記憶體的長度;每個擴充有兩部分組成:為了對齊可能有的paddng以及擴充本身。
擴充具體是什麼類型是由注冊該擴充的子產品決定的,每個需要使用擴充的子產品都會有一個ID,
系統中所有的擴充類型儲存在全局數組nf_ct_ext_types
struct nf_ct_ext_type {
/* Destroys relationships (can be NULL). */
void (*destroy)(struct nf_conn *ct);
/* Called when realloacted (can be NULL).
Contents has already been moved. */
void (*move)(void *new, void *old);
//每種擴充有一個唯一的類型辨別,定義見上方
enum nf_ct_ext_id id;
unsigned int flags;
/* Length and min alignment. */
u8 len;//實際擴充結構的長度
u8 align;///實際擴充結構需要幾位元組對齊
/* initial size of nf_ct_ext. */
//給連接配接添加第一個擴充功能時,這時struct nf_ct_ext還沒建立,
// 需要申請struct nf_ct_ext 和擴充功能私有資料一起的記憶體大小。
u8 alloc_size;//實際一個擴充結構需要配置設定的記憶體大小,由三部分組成:struct nf_ct_ext + 對齊填充 + len
};
擴充類型的注冊
擴充都是可選的,開啟時對應的子產品會向連接配接跟蹤子系統注冊各自的擴充:以helper為例
/* This MUST be called in process context. */
int nf_ct_extend_register(struct nf_ct_ext_type *type)
{
int ret = 0;
mutex_lock(&nf_ct_ext_type_mutex);
//每種類型的擴充隻能注冊一種
if (nf_ct_ext_types[type->id]) {
ret = -EBUSY;
goto out;
}
/* This ensures that nf_ct_ext_create() can allocate enough area
before updating alloc_size */
//計算實際需要為擴充結構配置設定的記憶體的大小
type->alloc_size = ALIGN(sizeof(struct nf_ct_ext), type->align)
+ type->len;
//将要注冊的擴充類型注冊到全局數組中
rcu_assign_pointer(nf_ct_ext_types[type->id], type);
update_alloc_size(type);
out:
mutex_unlock(&nf_ct_ext_type_mutex);
return ret;
來看下是怎麼和conntrack建立連接配接的:先看資料結構
static struct nf_ct_ext_type helper_extend __read_mostly = {
.len = sizeof(struct nf_conn_help),
.align = __alignof__(struct nf_conn_help),
.id = NF_CT_EXT_HELPER,
};
/* nf_conn feature for connections that have a helper */
struct nf_conn_help {
/* Helper. if any */
struct nf_conntrack_helper __rcu *helper;//指向相應的Netfiler中注冊的helper執行個體
struct hlist_head expectations; //如果有多個相關聯的期望連接配接,連結起來
/* Current number of expected connections */
u8 expecting[NF_CT_MAX_EXPECT_CLASSES];
/* private helper information. */
char data[];
};
建立新連接配接函數init_conntrack()中,如果新的連接配接不是一個期望連接配接,那麼會查找該連接配接是否有helper子產品關注,如果有,那麼會調用nf_ct_helper_ext_add()為新的連接配接跟蹤資訊塊設定ext字段,相關代碼如下:
/* Allocate a new conntrack: we return -ENOMEM if classification
failed due to stress. Otherwise it really is unclassifiable. */
static struct nf_conntrack_tuple_hash *
init_conntrack(struct net *net, struct nf_conn *tmpl,
const struct nf_conntrack_tuple *tuple,
struct nf_conntrack_l3proto *l3proto,
struct nf_conntrack_l4proto *l4proto,
struct sk_buff *skb,
unsigned int dataoff, u32 hash)
{
struct nf_conn *ct;
struct nf_conn_help *help;
struct nf_conntrack_tuple repl_tuple;
struct nf_conntrack_ecache *ecache;
struct nf_conntrack_expect *exp = NULL;
const struct nf_conntrack_zone *zone;
struct nf_conn_timeout *timeout_ext;
struct nf_conntrack_zone tmp;
unsigned int *timeouts;
/* 根據tuple制作一個repl_tuple。主要是調用L3和L4的invert_tuple方法 */
if (!nf_ct_invert_tuple(&repl_tuple, tuple, l3proto, l4proto)) {
pr_debug("Can't invert tuple.\n");
return NULL;
}
/* 在cache中申請一個nf_conn結構,把tuple和repl_tuple指派給ct的tuplehash[]數組,
并初始化ct.timeout定時器函數為death_by_timeout(),但不啟動定時器。 *
*/
zone = nf_ct_zone_tmpl(tmpl, skb, &tmp);
ct = __nf_conntrack_alloc(net, zone, tuple, &repl_tuple, GFP_ATOMIC,
hash);
if (IS_ERR(ct))
return (struct nf_conntrack_tuple_hash *)ct;
if (tmpl && nfct_synproxy(tmpl)) {
nfct_seqadj_ext_add(ct);
nfct_synproxy_ext_add(ct);
}
timeout_ext = tmpl ? nf_ct_timeout_find(tmpl) : NULL;
if (timeout_ext) {
timeouts = nf_ct_timeout_data(timeout_ext);
if (unlikely(!timeouts))
timeouts = l4proto->get_timeouts(net);
} else {
timeouts = l4proto->get_timeouts(net);
}
/* 對tcp來說,下面函數就是将L4層字段如window, ack等字段
賦給ct->proto.tcp.seen[0],由于建立立的連接配接才調這裡,是以
不用給reply方向的ct->proto.tcp.seen[1]指派 */
if (!l4proto->new(ct, skb, dataoff, timeouts)) {
nf_conntrack_free(ct);
pr_debug("can't track with proto module\n");
return NULL;
}
if (timeout_ext)
nf_ct_timeout_ext_add(ct, rcu_dereference(timeout_ext->timeout),
GFP_ATOMIC);
/* 為acct和ecache兩個ext配置設定空間。不過之後一般不會被初始化,是以用不到 */
nf_ct_acct_ext_add(ct, GFP_ATOMIC);
nf_ct_tstamp_ext_add(ct, GFP_ATOMIC);
nf_ct_labels_ext_add(ct);
ecache = tmpl ? nf_ct_ecache_find(tmpl) : NULL;
nf_ct_ecache_ext_add(ct, ecache ? ecache->ctmask : 0,
ecache ? ecache->expmask : 0,
GFP_ATOMIC);
local_bh_disable();
/*
會在全局的期望連接配接連結清單expect_hash中查找是否有比對建立tuple的期望連接配接。第一次過來的資料包肯定是沒有的,
于是走else分支,__nf_ct_try_assign_helper()函數去nf_ct_helper_hash哈希表中比對目前tuple,
由于我們在本節開頭提到nf_conntrack_tftp_init()已經把tftp的helper extension添加進去了,
是以可以比對成功,于是把找到的helper指派給nfct_help(ct)->helper,而這個helper的help方法就是tftp_help()。
當tftp請求包走到ipv4_confirm的時候,會去執行這個help方法,即tftp_help(),也就是建立一個期望連接配接
當後續tftp傳輸資料時,在nf_conntrack_in裡面,建立tuple後,在expect_hash表中查可以比對到建立tuple的期望連接配接(因為隻根據源端口來比對),
是以上面代碼的if成立,是以ct->master被指派為exp->master,并且,還會執行exp->expectfn()函數,這個函數上面提到是指向nf_nat_follow_master()的,
該函數根據ct的master來給ct做NAT,ct在經過這個函數處理前後的tuple分别為:
*/
/* 在helper 函數中 回生成expect 并加入全局連結清單 同時 expect_count++*/
if (net->ct.expect_count) {
/* 如果在期望連接配接連結清單中 */
spin_lock(&nf_conntrack_expect_lock);
exp = nf_ct_find_expectation(net, zone, tuple);
/* 如果在期望連接配接連結清單中 */
if (exp) {
pr_debug("expectation arrives ct=%p exp=%p\n",
ct, exp);
/* Welcome, Mr. Bond. We've been expecting you... */
__set_bit(IPS_EXPECTED_BIT, &ct->status);
/* conntrack的master位指向搜尋到的expected,而expected的sibling位指向conntrack……..解釋一下,這時候有兩個conntrack,
一個是一開始的初始連接配接(比如69端口的那個)也就是主連接配接conntrack1,
一個是現在正在處理的連接配接(1002)子連接配接conntrack2,兩者和expect的關系是:
1. expect的sibling指向conntrack2,而expectant指向conntrack1,
2. 一個主連接配接conntrack1可以有若幹個expect(int expecting表示目前數量),這些
expect也用一個連結清單組織,conntrack1中的struct list_head sibling_list就是該
連結清單的頭。
3. 一個子連接配接隻有一個主連接配接,conntrack2的struct ip_conntrack_expect *master
指向expect
通過一個中間結構expect将主連接配接和子連接配接關聯起來 */
/* exp->master safe, refcnt bumped in nf_ct_find_expectation */
ct->master = exp->master;
if (exp->helper) {/* helper的ext以及help連結清單配置設定空間 */
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_CT_STAT_INC(net, expect_new);
}
spin_unlock(&nf_conntrack_expect_lock);
}
if (!exp) {// 如果不存在 從新指派 ct->ext->...->help->helper = helper
__nf_ct_try_assign_helper(ct, tmpl, GFP_ATOMIC);
NF_CT_STAT_INC(net, new);
}
/* Now it is inserted into the unconfirmed list, bump refcount */
nf_conntrack_get(&ct->ct_general);
/* 将這個tuple添加到unconfirmed連結清單中,因為資料包還沒有出去,
是以不知道是否會被丢棄,是以暫時先不添加到conntrack hash中 */
nf_ct_add_to_unconfirmed_list(ct);
local_bh_enable();
if (exp) {
if (exp->expectfn)
exp->expectfn(ct, exp);
nf_ct_expect_put(exp);
}
return &ct->tuplehash[IP_CT_DIR_ORIGINAL];
}
// 連接配接建立時給conntrack添加helper擴充功能
struct nf_conn_help *
nf_ct_helper_ext_add(struct nf_conn *ct,
struct nf_conntrack_helper *helper, gfp_t gfp)
{
struct nf_conn_help *help;
help = nf_ct_ext_add_length(ct, NF_CT_EXT_HELPER,
helper->data_len, gfp);
if (help)
INIT_HLIST_HEAD(&help->expectations);
else
pr_debug("failed to add helper extension area");
return help;
}

void *__nf_ct_ext_add_length(struct nf_conn *ct, enum nf_ct_ext_id id,
size_t var_alloc_len, gfp_t gfp)
{
struct nf_ct_ext *old, *new;
int i, newlen, newoff;
struct nf_ct_ext_type *t;
/* Conntrack must not be confirmed to avoid races on reallocation. */
NF_CT_ASSERT(!nf_ct_is_confirmed(ct));
//之前該連接配接跟蹤資訊塊上還沒有任何擴充,那麼按照注冊時的對齊以及
//擴充大小配置設定記憶體,對于剛配置設定的連接配接跟蹤資訊塊,
old = ct->ext;
if (!old)/*如果該conntrack的擴充功能的記憶體還沒用申請,就申請記憶體并添加該擴充功能*/
return nf_ct_ext_create(&ct->ext, id, var_alloc_len, gfp);
if (__nf_ct_ext_exist(old, id))//不能在同一個連接配接上重複添加相同類型的擴充
return NULL;
rcu_read_lock();//從全局數組中找到擴充類型,該擴充類型之前已經注冊de
t = rcu_dereference(nf_ct_ext_types[id]);
BUG_ON(t == NULL);
newoff = ALIGN(old->len, t->align);//新擴充添加到ext->data數組的末尾,計算偏移量和新的總大小
newlen = newoff + t->len + var_alloc_len;
rcu_read_unlock();
//需要重新配置設定
new = __krealloc(old, newlen, gfp);
if (!new)
return NULL;
if (new != old) {
for (i = 0; i < NF_CT_EXT_NUM; i++) {
if (!__nf_ct_ext_exist(old, i))
continue;
rcu_read_lock();
t = rcu_dereference(nf_ct_ext_types[i]);
if (t && t->move)//将之前ext的内容拷貝到新記憶體區域的開始
t->move((void *)new + new->offset[i],
(void *)old + old->offset[i]);
rcu_read_unlock();
}//将原來的記憶體區域釋放并ext指向新的記憶體
kfree_rcu(old, rcu);
ct->ext = new;
}
new->offset[id] = newoff;
new->len = newlen;
memset((void *)new + newoff, 0, newlen - newoff);
return (void *)new + newoff;
}
static void* nf_ct_ext_create(struct nf_ct_ext **ext, enum nf_ct_ext_id id, gfp_t gfp)
{
unsigned int off, len;
struct nf_ct_ext_type *t;
rcu_read_lock();
t = rcu_dereference(nf_ct_ext_types[id]);
BUG_ON(t == NULL);
off = ALIGN(sizeof(struct nf_ct_ext), t->align);
len = off + t->len;
rcu_read_unlock();
*ext = kzalloc(t->alloc_size, gfp);
if (!*ext)
return NULL;
(*ext)->offset[id] = off;
(*ext)->len = len;
return (void *)(*ext) + off;
}
目前連接配接跟蹤資訊塊隻關聯了一個擴充,配置設定都很簡單