- 论坛徽章:
- 0
|
之所以叫“浅析”,主要是分析其流程,很多细节的地方没有一一注解出来,之所以以tftp为范本来剖析,主要是因为它简单,呵呵,这篇贴子,作为旧贴
http://bbs.chinaunix.net/viewthread.php?tid=815129&extra=page%3D1%26filter%3Ddigest
的一个补充,好为对Netfliter的状态跟踪分析的结束……也希望,下一步“Netfliter的地址转换的实现”能早点写出来……
注:这些贴子,包括iptables,Netfilter的包过滤,Netfliter的状态检测,都只是笔记性质的贴子,供有共同兴趣的朋友一起讨论,其中有不少错误的地方,希望大家指正,(并不是谦虚,我自己也在不断地改正和完善)!另,照旧,源码版本是2.6.12
1、模块的注册
源码在ip_conntrack_tftp.c中:
init函数中定义了
- static struct ip_conntrack_helper tftp[MAX_PORTS];
复制代码 并初始化它,并注册它:
- memset(&tftp[i], 0, sizeof(struct ip_conntrack_helper));
- ……
- ret=ip_conntrack_helper_register(&tftp[i]);
复制代码
tftp是一个数组,最大允许MAX_PORTS个,并且变量ports_c决定其个数,因为它做为注册时for循环的终值,目前,只注册了一个tftp。
tftp是一个ip_conntrack_helper类型,我在后文中,会把它叫做“helper”模块,也就是说,初始化函数中,调用ip_conntrack_helper_register函数注册了一个tftp的helper模块。
在tftp的成员的赋初始化值的时候,我们可以对照理解struct ip_conntrack_helper结构的许多重要的成员:
- tftp[i].tuple.dst.protonum = IPPROTO_UDP; //协议
- tftp[i].tuple.src.u.udp.port = htons(ports[i]); //目标端口,即69,这样,UDP:69成为认识tftp的唯一标志
- tftp[i].mask.dst.protonum = 0xFF; //目标地址掩码,以及下面一个源端口掩码,以做比较之用
- tftp[i].mask.src.u.udp.port = 0xFFFF;
- tftp[i].max_expected = 1; //最大expect,这是什么东东?后面会详解
- tftp[i].timeout = 5 * 60; /* 5 minutes */ //超时时间
- tftp[i].me = THIS_MODULE;
- tftp[i].help = tftp_help; //这个函数指针是最重要的东东了,后面再来分析它的具体作用
复制代码
ip_conntrack_helper_register函数实质上是把该模块添加进以全局变量helpers为首的链表中去:
- int ip_conntrack_helper_register(struct ip_conntrack_helper *me)
- {
- BUG_ON(me->timeout == 0);
- WRITE_LOCK(&ip_conntrack_lock);
- list_prepend(&helpers, me);
- WRITE_UNLOCK(&ip_conntrack_lock);
- return 0;
- }
复制代码
OK,tftp的helper模块被注册了,它什么时候被调用?以及它有什么用呢??
回忆在连接跟踪的初时化时,注册的两个钩子:
/*连接跟踪初始化时,注册helper Hook*/
static struct nf_hook_ops ip_conntrack_helper_out_ops = {
.hook = ip_conntrack_help,
.owner = THIS_MODULE,
.pf = PF_INET,
.hooknum = NF_IP_POST_ROUTING,
.priority = NF_IP_PRI_CONNTRACK_HELPER, /*此优先级比同Hook上的ip_confirm的高*/
};
static struct nf_hook_ops ip_conntrack_helper_in_ops = {
.hook = ip_conntrack_help,
.owner = THIS_MODULE,
.pf = PF_INET,
.hooknum = NF_IP_LOCAL_IN,
.priority = NF_IP_PRI_CONNTRACK_HELPER,
};
对于中转包过滤来讲,我们关心第一个钩子,它注册在NF_IP_POST_ROUTING Hook上,并且,比我们讲过的ip_confirm优先级要高。
这样,也就是数据包经过这个Hook点时,ip_conntrack_help 函数将被调用。
2.我的例子
结合一个实际的tftp传输来分析代码,先来看这个例子(该例取自《TCP/IP详解卷一》p161)
- 1. 192.168.0.1:1106 -> 192.168.1.1:69 udp 19 PRQ "test1.c"
- 2. 192.168.1.1:1077 -> 192.168.0.1:1106 udp 516
- 3. 192.168.0.1:1106 -> 192.168.1.1:1077 udp 4
- 4. 192.168.1.1:1077 -> 192.168.0.1:1106 udp 454
- 5. 192.168.0.1:1106 -> 192.168.1.1:1077 udp 4
复制代码
第1行,是192.168.0.1发出了一个“读请求”,文件名是test1.c;
第2行是,192.168.1.1 回应了读请求,将文件的数据,共516字节发送给请求者,注意,这里的来源端口不是69,而变成了1077;
第3行是一个回应包
第4,5行类似;
对于第1行,即新请求一个连接,回忆我前一次的描述,连接跟踪模块会执行以下函数:
在NF_IP_PRE_ROUTING Hook处调用钩子函数ip_conntrack_in,接着进入resolve_normal_ct函数,由于这是个新连接,所以,找不
到与之对应的tuple,于是进入了init_conntrack,初始化一个连接。
- static struct ip_conntrack_tuple_hash *
- init_conntrack(const struct ip_conntrack_tuple *tuple,
- struct ip_conntrack_protocol *protocol,
- struct sk_buff *skb)
- {
- struct ip_conntrack_expect *exp;
-
- ……
- exp = find_expectation(tuple);
- if (exp) {
- DEBUGP("conntrack: expectation arrives ct=%p exp=%p\n",
- conntrack, exp);
- /* Welcome, Mr. Bond. We've been expecting you... */
- __set_bit(IPS_EXPECTED_BIT, &conntrack->status);
- conntrack->master = exp->master;
- #if CONFIG_IP_NF_CONNTRACK_MARK
- conntrack->mark = exp->master->mark;
- #endif
- nf_conntrack_get(&conntrack->master->ct_general);
- CONNTRACK_STAT_INC(expect_new);
- } else {
- conntrack->helper = ip_ct_find_helper(&repl_tuple);
- CONNTRACK_STAT_INC(new);
- }
- ……
- }
复制代码
exp是一个struct ip_conntrack_expect类型,find_expectation看样子应该是根据该数据包对应的tuple,查找一个struct ip_conntrack_expect类型的节点,expect是什么东东?暂时不管它,因为我们目前还没有提到它,所以,find_expectation什么也查不到,那么接下来那个if...else...则会进入else判断:
- else
- {
- conntrack->helper = ip_ct_find_helper(&repl_tuple);
- CONNTRACK_STAT_INC(new);
- }
复制代码
ip_ct_find_helper函数根据当前数据包对应的repl_tuple,在helpers链表中查找是否有相应的helper模块:
PS:当前数据包的tuple是:
192.168.0.1:1106 192.168.1.1:69 udp
则repl_tuple为:
192.168.1.1:69 192.168.0.1:1106 udp
- static struct ip_conntrack_helper *ip_ct_find_helper(const struct ip_conntrack_tuple *tuple)
- {
- return LIST_FIND(&helpers, helper_cmp,
- struct ip_conntrack_helper *,
- tuple);
- }
复制代码
比较函数是helper_cmp:
- static inline int helper_cmp(const struct ip_conntrack_helper *i,
- const struct ip_conntrack_tuple *rtuple)
- {
- return ip_ct_tuple_mask_cmp(rtuple, &i->tuple, &i->mask);
- }
复制代码
实际转向给了ip_ct_tuple_mask_cmp函数:
- static inline int ip_ct_tuple_mask_cmp(const struct ip_conntrack_tuple *t,
- const struct ip_conntrack_tuple *tuple,
- const struct ip_conntrack_tuple *mask)
- {
- return !(((t->src.ip ^ tuple->src.ip) & mask->src.ip)
- || ((t->dst.ip ^ tuple->dst.ip) & mask->dst.ip)
- || ((t->src.u.all ^ tuple->src.u.all) & mask->src.u.all)
- || ((t->dst.u.all ^ tuple->dst.u.all) & mask->dst.u.all)
- || ((t->dst.protonum ^ tuple->dst.protonum)
- & mask->dst.protonum));
- }
复制代码
对照一下tftp模块初始化时的helper的各成员值和当前数据包repl_tuple(192.168.1.1:69 192.168.0.1:1106 udp),可以发现,最终tftp注册的helper模块将被正确地查找出来!!
这样,当前tftp的连接conntrack的helper指针就指向了tftp模块。这一点非常重要。
- conntrack->helper = ip_ct_find_helper(&repl_tuple);
复制代码
这个数据包继续前进,当它进入NF_IP_POST_ROUTING Hook点时,会进入ip_conntrack_help函数:
/*根据数据包,查找对应的连接,如果此连接有关链的helper模块,则调用help函数*/
- 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) {
- unsigned int ret;
- ret = ct->helper->help(pskb, ct, ctinfo);
- if (ret != NF_ACCEPT)
- return ret;
- }
- return NF_ACCEPT;
- }
复制代码
这个函数只有一件事,就是发现了tftp的这个连接(192.168.0.1:1106 192.168.1.1:69 udp),有相应的helper模块,于是,调用helper模块的help函数,于是,我们再回来看ip_conntrack_tftp.c中,这个help函数的实现:
- static int tftp_help(struct sk_buff **pskb,
- struct ip_conntrack *ct,
- enum ip_conntrack_info ctinfo)
- {
- struct tftphdr _tftph, *tfh;
- struct ip_conntrack_expect *exp;
- unsigned int ret = NF_ACCEPT;
- tfh = skb_header_pointer(*pskb,
- (*pskb)->nh.iph->ihl*4+sizeof(struct udphdr),
- sizeof(_tftph), &_tftph);
- if (tfh == NULL)
- return NF_ACCEPT;
- switch (ntohs(tfh->opcode)) {
- /* RRQ and WRQ works the same way */
- case TFTP_OPCODE_READ:
- case TFTP_OPCODE_WRITE:
- DEBUGP("");
- DUMP_TUPLE(&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple);
- DUMP_TUPLE(&ct->tuplehash[IP_CT_DIR_REPLY].tuple);
- exp = ip_conntrack_expect_alloc();
- if (exp == NULL)
- return NF_DROP;
- exp->tuple = ct->tuplehash[IP_CT_DIR_REPLY].tuple;
- exp->mask.src.ip = 0xffffffff;
- exp->mask.dst.ip = 0xffffffff;
- exp->mask.dst.u.udp.port = 0xffff;
- exp->mask.dst.protonum = 0xff;
- exp->expectfn = NULL;
- exp->master = ct;
- DEBUGP("expect: ");
- DUMP_TUPLE(&exp->tuple);
- DUMP_TUPLE(&exp->mask);
- if (ip_nat_tftp_hook)
- ret = ip_nat_tftp_hook(pskb, ctinfo, exp);
- else if (ip_conntrack_expect_related(exp) != 0) {
- ip_conntrack_expect_free(exp);
- ret = NF_DROP;
- }
- break;
- case TFTP_OPCODE_DATA:
- case TFTP_OPCODE_ACK:
- DEBUGP("Data/ACK opcode\n");
- break;
- case TFTP_OPCODE_ERROR:
- DEBUGP("Error opcode\n");
- break;
- default:
- DEBUGP("Unknown opcode\n");
- }
- return NF_ACCEPT;
- }
复制代码
这个函数很简单,它只关注tftp操作码的读和写,发现,如果是这两个操作码的话,就先分配一个struct ip_conntrack_expect结构:
- exp = ip_conntrack_expect_alloc();
复制代码
然后,初始化它:
- exp->tuple = ct->tuplehash[IP_CT_DIR_REPLY].tuple;
- exp->mask.src.ip = 0xffffffff;
- exp->mask.dst.ip = 0xffffffff;
- exp->mask.dst.u.udp.port = 0xffff;
- exp->mask.dst.protonum = 0xff;
- exp->expectfn = NULL;
- exp->master = ct;
复制代码
最后,将它注册:
- ip_conntrack_expect_related(exp) != 0
复制代码
是到了解释expect的时候了:
对于tftp来讲,它的请求连接是:
192.168.0.1:1106 -> 192.168.1.1:69 udp
我们希望它同其它普通协议一样,应答包是:
192.168.1.1:69 -> 192.168.0.1:1106 udp
而不是:
192.168.1.1:1077 -> 192.168.0.1:1106 udp
所以,这个expect就用来存储,该连接所“期望”的应答包,仅此而已,这也是给它的成员tuple初始化时,初始化的是当前连接的应答的tuple的原因:
- exp->tuple = ct->tuplehash[IP_CT_DIR_REPLY].tuple;
复制代码
后面的那些mask,用于比较用。master指针让expect指向了当前连接。
至于注册,它与注册helper一样,是一个插入链表的过程:
- int ip_conntrack_expect_related(struct ip_conntrack_expect *expect)
- {
- struct ip_conntrack_expect *i;
- int ret;
- DEBUGP("ip_conntrack_expect_related %p\n", related_to);
- DEBUGP("tuple: "); DUMP_TUPLE(&expect->tuple);
- DEBUGP("mask: "); DUMP_TUPLE(&expect->mask);
- WRITE_LOCK(&ip_conntrack_lock);
- list_for_each_entry(i, &ip_conntrack_expect_list, list) {
- if (expect_matches(i, expect)) {
- /* Refresh timer: if it's dying, ignore.. */
- if (refresh_timer(i)) {
- ret = 0;
- /* We don't need the one they've given us. */
- ip_conntrack_expect_free(expect);
- goto out;
- }
- } else if (expect_clash(i, expect)) {
- ret = -EBUSY;
- goto out;
- }
- }
- /* Will be over limit? */
- if (expect->master->helper->max_expected &&
- expect->master->expecting >= expect->master->helper->max_expected)
- evict_oldest_expect(expect->master);
- ip_conntrack_expect_insert(expect);
- ret = 0;
- out:
- WRITE_UNLOCK(&ip_conntrack_lock);
- return ret;
- }
复制代码
首先看是否已经有相应节点,如没有,则插入之,不同的是,这次的链表首部是ip_conntrack_expect_list。
OK,数据包
192.168.0.1:1106 -> 192.168.1.1:69 udp
接下来就进入ip_confirm,然后离开本机。
当回来的数据传输的包进入Netfliter:
- 192.168.1.1:1077 -> 192.168.0.1:1106 udp
复制代码
因为端口已经变成了1077,而不是69,所以它不会同第一条连接的repl_tuple匹配(废话,当然不匹配了,否则还用搞这么复杂),所以,当然没有属于它的连接,数据包也会进入init_conntrack,初始化一个连接:
- static struct ip_conntrack_tuple_hash *
- init_conntrack(const struct ip_conntrack_tuple *tuple,
- struct ip_conntrack_protocol *protocol,
- struct sk_buff *skb)
- {
- struct ip_conntrack_expect *exp;
-
- ……
- exp = find_expectation(tuple);
- if (exp) {
- DEBUGP("conntrack: expectation arrives ct=%p exp=%p\n",
- conntrack, exp);
- /* Welcome, Mr. Bond. We've been expecting you... */
- __set_bit(IPS_EXPECTED_BIT, &conntrack->status);
- conntrack->master = exp->master;
- #if CONFIG_IP_NF_CONNTRACK_MARK
- conntrack->mark = exp->master->mark;
- #endif
- nf_conntrack_get(&conntrack->master->ct_general);
- CONNTRACK_STAT_INC(expect_new);
- } else {
- conntrack->helper = ip_ct_find_helper(&repl_tuple);
- CONNTRACK_STAT_INC(new);
- }
- ……
- }
复制代码
这一次,find_expectation函数根据当前数据包的tuple,查找有没有对应的expect,很幸运,我们刚才注册的expect被查到了:
- static struct ip_conntrack_expect *
- find_expectation(const struct ip_conntrack_tuple *tuple)
- {
- struct ip_conntrack_expect *i;
- list_for_each_entry(i, &ip_conntrack_expect_list, list) {
- /* If master is not in hash table yet (ie. packet hasn't left
- this machine yet), how can other end know about expected?
- Hence these are not the droids you are looking for (if
- master ct never got confirmed, we'd hold a reference to it
- and weird things would happen to future packets). */
- if (ip_ct_tuple_mask_cmp(tuple, &i->tuple, &i->mask)
- && is_confirmed(i->master)
- && del_timer(&i->timeout)) {
- unlink_expect(i);
- return i;
- }
- }
- return NULL;
- }
复制代码
比较函数仍然是ip_ct_tuple_mask_cmp,再来看一遍它的代码:
- static inline int ip_ct_tuple_mask_cmp(const struct ip_conntrack_tuple *t,
- const struct ip_conntrack_tuple *tuple,
- const struct ip_conntrack_tuple *mask)
- {
- return !(((t->src.ip ^ tuple->src.ip) & mask->src.ip)
- || ((t->dst.ip ^ tuple->dst.ip) & mask->dst.ip)
- || ((t->src.u.all ^ tuple->src.u.all) & mask->src.u.all)
- || ((t->dst.u.all ^ tuple->dst.u.all) & mask->dst.u.all)
- || ((t->dst.protonum ^ tuple->dst.protonum)
- & mask->dst.protonum));
- }
复制代码
回忆初始化tftp的expect时,作为比较用的mask的源端口并没有被赋值:
- exp->mask.src.ip = 0xffffffff;
- exp->mask.dst.ip = 0xffffffff;
- exp->mask.dst.u.udp.port = 0xffff;
- exp->mask.dst.protonum = 0xff;
- exp->expectfn = NULL;
复制代码
所以,对于这条应答的包来讲,尽管它的来源端口是1077,而不是我们希望的69,但
((t->src.u.all ^ tuple->src.u.all) & mask->src.u.all)仍然为0,所以,它仍然被查找出来了。
这样,Netfilter发现该连接有对应的expect,哈哈,终于找到你了,于是:
- __set_bit(IPS_EXPECTED_BIT, &conntrack->status);
复制代码
设置该连接为“关连”标志位(等回到resolve_normal_ct函数中,再将此连接设置为IP_CT_RELATED)
,这样,关连的连接就被识别出来了。并且,该连接的master指针,指向了第一条连接:
conntrack->master = exp->master;
主要的流程就这么简单!!
小结一下:
首先,特殊的协议注册一个helper,helper模块根据协议的某些特性,如(udp & dport==69),“帮助”我们发现一条连接是“特殊协议”,于是调用help函数,初始化一个“期望”连接expect,事实上,这个expect主要的作用仅仅是比较。当回来的应答包穿过Netfilter时,它被发现有一个expect,于是,它就被识别出来是一个“关联”连接了!! |
|