之所以叫“浅析”,主要是分析其流程,很多细节的地方没有一一注解出来,之所以以tftp为范本来剖析,主要是因为它简单,呵呵,这篇贴子,作为旧贴的一个补充,好为对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,于是,它就被识别出来是一个“关联”连接了!!


更多相关文章

  1. 实力解剖一枚挖矿脚本,风骚操作亮瞎双眼
  2. 一文读懂 TS 中 Object, object, {} 类型之间的区别
  3. 重学 Kotlin —— object,史上最 “快” 单例 ?
  4. 如何正确使用 Python 的 Lambda 函数
  5. Python 加速运行技巧
  6. 强!8 个 Python 优化提速的小技巧!
  7. 阅读代码:Spark 与 Flink 中的 RPC 实现
  8. oracle学习笔记
  9. 作用域 常用函数 三元运算符及if条件

随机推荐

  1. 在提交注册表单时使用jQuery显示错误
  2. PHP 导出 万级别数据 时间测试
  3. PHP面试题集PHP面试题集
  4. 添加到数据库后,保持在同一页面而不刷新它
  5. 使用jQuery或Javascript重定向到具有值的
  6. 在Web Page中包含PHP代码
  7. 求助,Xdebug的配置,缓存问题。
  8. PHP实现接口方法时出现致命错误
  9. 计算字符串的MD5哈希值
  10. PHP中类和对象的相关函数