/*
需要对conntrack进行功能扩展的协议,会初始化一个struct nf_conntrack_helper 实例,把该实例注册到Netfilter中管理的全局哈希表中。
查找helper使用的hash 算法
static unsigned int helper_hash(const struct nf_conntrack_tuple *tuple)
{
return (((tuple->src.l3num << 8) | tuple->dst.protonum) ^
(\_\_force \_\_u16)tuple->src.u.all) % nf\_ct\_helper\_hsize;
}
查找时使用的L3协议类型,L4协议类型及L4协议的源端口三元组信息来查找的helper。因为识别不同的L5协议一般都是通过这三元组来识别的。
注册helper实例
Netfilter中定义了一个哈希表来管理和存储不同协议注册的helper实例。
static struct hlist_head *nf_ct_helper_hash __read_mostly;
注意nf_conn_help 和 nf_conntrack_helper的区别
*/
int nf_conntrack_helper_register(struct nf_conntrack_helper *me)
{
struct nf_conntrack_tuple_mask mask = { .src.u.all = htons(0xFFFF) };
unsigned int h = helper_hash(&me->tuple);
struct nf_conntrack_helper *cur;
int ret = 0;
BUG\_ON(me->expect\_policy == NULL);
BUG\_ON(me->expect\_class\_max >= NF\_CT\_MAX\_EXPECT\_CLASSES);
BUG\_ON(strlen(me->name) > NF\_CT\_HELPER\_NAME\_LEN - 1);
mutex\_lock(&nf\_ct\_helper\_mutex);
hlist\_for\_each\_entry(cur, &nf\_ct\_helper\_hash\[h\], hnode) {
if (nf\_ct\_tuple\_src\_mask\_cmp(&cur->tuple, &me->tuple, &mask)) {
ret = -EEXIST;
goto out;
}
}
hlist\_add\_head\_rcu(&me->hnode, &nf\_ct\_helper\_hash\[h\]);
nf\_ct\_helper\_count++;
out:
mutex_unlock(&nf_ct_helper_mutex);
return ret;
}
conntrack关联相关的helper处理时是在:
init_conntrack --{
if (!exp) {// 如果不存在 从新赋值 ct->ext->…->help->helper = helper
__nf_ct_try_assign_helper(ct, tmpl, GFP_ATOMIC);
NF_CT_STAT_INC(net, new);
}
}
看下tftp的例子:
struct nf_conntrack_helper {
struct hlist_node hnode; /* Internal use. */
//helper的名字,iptables扩展match helper会使用
char name[NF_CT_HELPER_NAME_LEN]; /* name of the module */
struct module *me; /* pointer to self */
//协议的期望连接的策略参数,包括最大期望连接数及最大连接超时时间*/
const struct nf_conntrack_expect_policy *expect_policy;
/\* length of internal data, ie. sizeof(struct nf\_ct\_\*\_master) \*/
size\_t data\_len;
//元组信息,表示不同协议的helper,使用该元组的一些字段来在Netfilter中查找helper
/* Tuple of things we will help (compared against server response) */
struct nf_conntrack_tuple tuple;
/\* Function to call when data passes; return verdict, or -1 to
invalidate. \*///不同协议自己实现自己的helper处理函数
int (\*help)(struct sk\_buff \*skb,
unsigned int protoff,
struct nf\_conn \*ct,
enum ip\_conntrack\_info conntrackinfo);
void (\*destroy)(struct nf\_conn \*ct);
int (\*from\_nlattr)(struct nlattr \*attr, struct nf\_conn \*ct);
int (\*to\_nlattr)(struct sk\_buff \*skb, const struct nf\_conn \*ct);
unsigned int expect\_class\_max;
unsigned int flags;
unsigned int queue\_num; /\* For user-space helpers. \*/
};
static const struct nf_conntrack_expect_policy tftp_exp_policy = {
.max_expected = 1,
.timeout = 5 * 60,
};
static void nf_conntrack_tftp_fini(void)
{
int i, j;
for (i = 0; i < ports\_c; i++) {
for (j = 0; j < 2; j++)
nf\_conntrack\_helper\_unregister(&tftp\[i\]\[j\]);
}
}
/*
list add struct nf_conntrack_helper add nf_ct_helper_hash
*/
/*
tftp[i][0].tuple.src.l3num = AF_INET;
tuple.dst.protonum = IPPROTO_UDP;
uple.src.u.udp.port = htons(ports[i]);
help = tftp_help;
*/
static int __init nf_conntrack_tftp_init(void)
{
int i, j, ret;
if (ports\_c == 0)
ports\[ports\_c++\] = TFTP\_PORT;
for (i = 0; i < ports\_c; i++) {
memset(&tftp\[i\], 0, sizeof(tftp\[i\]));
tftp\[i\]\[0\].tuple.src.l3num = AF\_INET;
tftp\[i\]\[1\].tuple.src.l3num = AF\_INET6;
for (j = 0; j < 2; j++) {
tftp\[i\]\[j\].tuple.dst.protonum = IPPROTO\_UDP;
tftp\[i\]\[j\].tuple.src.u.udp.port = htons(ports\[i\]);
tftp\[i\]\[j\].expect\_policy = &tftp\_exp\_policy;
tftp\[i\]\[j\].me = THIS\_MODULE;
tftp\[i\]\[j\].help = tftp\_help;
if (ports\[i\] == TFTP\_PORT)
sprintf(tftp\[i\]\[j\].name, "tftp");
else
sprintf(tftp\[i\]\[j\].name, "tftp-%u", i);
ret = nf\_conntrack\_helper\_register(&tftp\[i\]\[j\]);
if (ret) {
pr\_err("failed to register helper for pf: %u port: %u\\n",
tftp\[i\]\[j\].tuple.src.l3num, ports\[i\]);
ports\_c = i;
nf\_conntrack\_tftp\_fini();
return ret;
}
}
}
return 0;
}
/*/在init函数中对nf_conntrack_helper结构体的tuple进行了部分初始化,由于tftp数据包的协议和port是固定的,
同时注册了help函数为tftp_help(),函数定义如下,----------,参数skb是当前处理的数据包,ct是相应的conntrack,处理tftp read/write request的时候会进入这个函数。
*/
/*
函数主要做了两件事情,首先初始化了一个ip_conntrack_expect结构,并将conntrack中应答方向的tuple结构附值给exp-〉tuple,然后调用:ip_conntrack_expect_related(exp, ct)
将这个expect结构与当前的连接状态关联起来,并把它注册到一个专门组织expect结构的全局链表ip_conntrack_expect_list里。
expect结构有什么用呢?当有返回的数据包时,首先仍然是搜索hash表,如果找不到可以匹配的连接,还会在全局链表里搜索匹配的expect结构,然后找到相关的连接状态。
为什么要这样做呢?假设有主机A向主机B发送消息,如下
A(10.10.10.1:1001) ——> B(10.10.10.2:69)
从A的1001端口发往B的69端口,连接跟踪模块跟踪并记录了次条连接,保存在一个ip_conntrack结构里(用tuple来识别),但是我们知道,一个连接有两个方向,我们怎么确定两个方向相反的数据包是属于同一个连接的呢?最简单的判断方法就是将地址和端口号倒过来,就是说如果有下面的数据包:
B(10.10.10.2:69)——> A(10.10.10.1:1001)
虽然源/目的端口/地址全都不一样,不能匹配初始数据包的tuple,但是与对应的repl_tuple完全匹配,所以显然应该是同一个连接,所以见到这样的数据包时就可以直接确定其所属的连接了,当然不需要什么expect
然而不是所有协议都这么简单, 对于tftp协议,相应的数据包可能是
B(10.10.10.2:1002)——> A(10.10.10.1:1001)
并不完全颠倒,就是说不能直接匹配初始数据包的tuple的反防向的repl_tuple,在hash表里找不到对应的节点,但我们仍然认为它和前面第一条消息有密切的联系,甚至我们可以明确,将所有下面形式的数据包都归属于这一连接的相关连接
B(10.10.10.2:XXX)——> A(10.10.10.1:1001)
怎么实现这一想法呢,只好再多创建一个expect了,它的tuple结构和repl_tuple完全相同,只是在mask中将源端口位置0,就是说不比较这一项,只要其他项匹配就OK。
(注意一下,ip_conntrack_expect结构里有个mask,ip_conntrack_helper里也有个mask,即使是同一个连接,它们的值也是不一样的。)
以上就是helper和expect的作用了,但是具体的实现方法还跟协议有关,像ftp的连接跟踪就相当复杂。
从help函数返回后,连接跟踪的第一阶段就结束了。
*/
static int tftp_help(struct sk_buff *skb,
unsigned int protoff,
struct nf_conn *ct,
enum ip_conntrack_info ctinfo)
{
const struct tftphdr *tfh;
struct tftphdr _tftph;
struct nf_conntrack_expect *exp;
struct nf_conntrack_tuple *tuple;
unsigned int ret = NF_ACCEPT;
typeof(nf_nat_tftp_hook) nf_nat_tftp;
tfh = skb\_header\_pointer(skb, protoff + sizeof(struct udphdr),
sizeof(\_tftph), &\_tftph);
if (tfh == NULL)
return NF\_ACCEPT;
switch (ntohs(tfh->opcode)) {
case TFTP\_OPCODE\_READ:
case TFTP\_OPCODE\_WRITE:
/\*
在nf_ct_expect_cachep上分配一个expect连接,同时赋两个值:
exp->master = ct,
exp->use = 1。
*/
/* RRQ and WRQ works the same way
ORGINAL :A(10.10.10.1:1001) ——> B(10.10.10.2:69)
REPLY B(10.10.10.2:69)——> A(10.10.10.1:1001)
生成的expect为:
ORGINAL tuple: A(10.10.10.1:0) ——> B(10.10.10.2:69)
exp->master = ct。注意,expect只有一个tuple,即只有一个方向,
这里只看到ORIGNAL方向的tuple,只是因为tuple的dir没赋值,默认为0。
*/
nf_ct_dump_tuple(&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple);
nf_ct_dump_tuple(&ct->tuplehash[IP_CT_DIR_REPLY].tuple);
exp = nf\_ct\_expect\_alloc(ct);//exp->master = ct;
if (exp == NULL) {
nf\_ct\_helper\_log(skb, ct, "cannot alloc expectation");
return NF\_DROP;
}
/\* 根据ct初始化expect \*/
tuple = &ct->tuplehash\[IP\_CT\_DIR\_REPLY\].tuple;
/\*初始化expect exp->class=NF\_CT\_EXPECT\_CLASS\_DEFAULT\*/
nf\_ct\_expect\_init(exp, NF\_CT\_EXPECT\_CLASS\_DEFAULT,
nf\_ct\_l3num(ct),
&tuple->src.u3, &tuple->dst.u3,
IPPROTO\_UDP, NULL, &tuple->dst.u.udp.port);
pr\_debug("expect: ");
nf\_ct\_dump\_tuple(&exp->tuple);
nf\_nat\_tftp = rcu\_dereference(nf\_nat\_tftp\_hook);
/\* 数据包需要走NAT时,if成立,局域网传输则else成立。 \*/
if (nf\_nat\_tftp && ct->status & IPS\_NAT\_MASK)
//*如果ct做了NAT,就调用nf_nat_tftp指向的函数,这里它指向nf_nat_tftp.c中的help()函数。*/
ret = nf_nat_tftp(skb, ctinfo, exp);//如果ct做了NAT,就调用nf_nat_tftp指向的函数,这里它指向nf_nat_tftp.c中的help()函数
else if (nf_ct_expect_related(exp) != 0) {//调用nf_ct_expect_insert 插入 nf_ct_expect_hash 同时ct.expect_count++
nf_ct_helper_log(skb, ct, "cannot add expectation");
ret = NF_DROP;
}
nf_ct_expect_put(exp);
break;
case TFTP_OPCODE_DATA:
case TFTP_OPCODE_ACK:
pr_debug("Data/ACK opcode\n");
break;
case TFTP_OPCODE_ERROR:
pr_debug("Error opcode\n");
break;
default:
pr_debug("Unknown opcode\n");
}
return ret;
}
/*
首先仍然是获取协议信息
proto = ip_ct_find_proto((*pskb)->nh.iph->protocol);
然后调用resolve_normal_ct()
ct = resolve_normal_ct(*pskb, proto,&set_reply,hooknum,&ctinfo)
在resolve_normal_ct函数内部,
get_tuple(skb->nh.iph, skb, skb->nh.iph->ihl*4, &tuple, proto)
然后查找hash表
h = ip_conntrack_find_get(&tuple, NULL)
这时候,还是举刚才的例子,如果之前曾有这样的数据包通过:
A(10.10.10.1:1001) ——> B(10.10.10.2:69)
那么相应的连接状态应该已经建立了,这时候如果有完全相反的数据包
B(10.10.10.2:69) ——> A(10.10.10.1:1001)
那么在搜索hash表的时候就能顺利找到对应的连接,找到以后就简单了
skb->nfct = &h->ctrack->infos[*ctinfo]
将连接状态附值给nfct位,接下来就没什么特别的事情要做了,设置几个标志位,检查一下数据包等等
我们主要关心的还是这样的情况,如果后续的数据包是这样的:
B(10.10.10.2:1002) ——> A(10.10.10.1:1001)
那么程序执行到ip_conntrack_find_get处时,会发现在hash表里找不到与当前数据包相匹配的连接,于是还是调用init_conntrack()创建连接
接下来的部分都和初始连接一样,计算hash值,初始化一个ip_conntrack等等,直到:
expected = LIST_FIND(&ip_conntrack_expect_list, expect_cmp,
struct ip_conntrack_expect *, tuple)
之前注册的expect被找到了,找到以后,进行下面的操作
if (expected) {
DEBUGP("conntrack: expectation arrives ct=%p exp=%p\n",
conntrack, expected);
/* 设置状态位为IPS_EXPECTED_BIT,当前的连接是一个预期的连接 */
__set_bit(IPS_EXPECTED_BIT, &conntrack->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将主连接和子连接关联起来 */
conntrack->master = expected;
expected->sibling = conntrack;
/* 将此连接从预期连接全局链表里删除(只是从全局链表里删除,但连接本身还在),因为此连接即将被正式添加到全局的连接表里,
所以下次如果再有B(10.10.10.2:1002) ——> A(10.10.10.1:1001)这样的数据包,就能直接从hash表里找到了,不必再借用expect。
因此主连接中的预期连接数expecting也自动减1
LIST_DELETE(&ip_conntrack_expect_list, expected);
expected->expectant->expecting--;
nf_conntrack_get(&master_ct(conntrack)->infos[0]);
}
接下来,如果expect里的expectfn函数有定义的话就执行它(一般是没有的)
if (expected && expected->expectfn)
expected->expectfn(conntrack);
后面的过程略去了,和之前差不多
*/
1/ 根据数据包的ct初始化一个expect连接;tftp请求(读或写)只能从client到server
static int nf_ct_expect_insert(struct nf_conntrack_expect *exp)
{
/\* 获得exp->master的help \*/
struct nf\_conn\_help \*master\_help = nfct\_help(exp->master);
struct nf\_conntrack\_helper \*helper;
struct net \*net = nf\_ct\_exp\_net(exp);
unsigned int h = nf\_ct\_expect\_dst\_hash(net, &exp->tuple);
/\* two references : one for hash insert, one for the timer \*/
atomic\_add(2, &exp->use);
/* 插入到help->expectations链表 */
hlist_add_head(&exp->lnode, &master_help->expectations);////如果有多个相关联的期望连接,链接起来
master_help->expecting[exp->class]++;
/* 插入到全局的expect_hash表 */
hlist_add_head_rcu(&exp->hnode, &nf_ct_expect_hash[h]);
net->ct.expect_count++;
/* 设置并启动定时器 */
setup_timer(&exp->timeout, nf_ct_expectation_timed_out,
(unsigned long)exp);
helper = rcu_dereference_protected(master_help->helper,
lockdep_is_held(&nf_conntrack_expect_lock));
if (helper) {
exp->timeout.expires = jiffies +
helper->expect_policy[exp->class].timeout * HZ;
}
add_timer(&exp->timeout);
NF\_CT\_STAT\_INC(net, expect\_create);
return 0;
}
当tftp请求包进入nf_conntrack_in的时候,由于没有ct条目,所以调用init_conntrack()尝试新建一个条目,在这个函数中,根据skb新建两个方向的tuple,:
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);
}
/\*在全局的期望连接链表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() */
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);
在全局的期望连接链表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()的;
tftp_help回调函数中会执行nf_nat_tftp_hook---》也就是 nat的help函数
黄色部分内容是处理tftp请求包时初始化的,蓝色的数组是全局hash表。tftp数据包就根据上面的内容进行NAT转换;在nf_ct_find_expectation()查找期望连接时,
如果找到了,就把该期望连接从struct nf_conn_help结构的链表以及全局的期望连接链表expect_hash中删除。在expect_hash中的连接通过设置的超时时间来超时;
初始化help没有 命中tftp_help---》触发tftp_help时 会设置生成 nf_conntrack_expect 并插入全局连表, 第二次时 能找到exp 所以执行exp ---expertfn
来自:https://blog.csdn.net/jasonchen_gbd/article/details/44877343
static unsigned int help(struct sk_buff *skb,
enum ip_conntrack_info ctinfo,
struct nf_conntrack_expect *exp)
{
const struct nf_conn *ct = exp->master;
exp->saved\_proto.udp.port
= ct->tuplehash\[IP\_CT\_DIR\_ORIGINAL\].tuple.src.u.udp.port;
exp->dir = IP\_CT\_DIR\_REPLY;
/\*
还注意到在help()函数中将exp-> expectfn赋值为nf_nat_follow_master(),这个函数的作用在后面会提到。
上面的内容是在客户端发送tftp请求后触发的动作,主要的效果就是生成了一个期望连接并可以被使用了。
下面以请求读数据来看一下传输数据时的数据包变化。
*/
exp->expectfn = nf_nat_follow_master;
if (nf_ct_expect_related(exp) != 0) {
nf_ct_helper_log(skb, exp->master, "cannot add expectation");
return NF_DROP;
}
return NF_ACCEPT;
}
static void __exit nf_nat_tftp_fini(void)
{
RCU_INIT_POINTER(nf_nat_tftp_hook, NULL);
synchronize_rcu();
}
static int __init nf_nat_tftp_init(void)
{
BUG_ON(nf_nat_tftp_hook != NULL);
RCU_INIT_POINTER(nf_nat_tftp_hook, help);
return 0;
}
手机扫一扫
移动阅读更方便
你可能感兴趣的文章