1. 前言
对于多连接协议,netfilter需要对其进行特殊的跟踪和NAT以提供动态的对子连接的支持,详见“防火墙为什么要对多连接协议进行特殊处理”一文。netfilter对这些多连接协议的跟踪和NAT和匹配、目标或IP层协议那样进行模块化的跟踪和NAT处理。
以下内核代码说明都使用Linux-2.4.26版本的内核代码。
2. 协议连接跟踪
2.1 ip_conntrack_helper结构
netfilter中对每个要进行跟踪的多连接协议定义了以下的连接辅助结构,每个多连接协议的连接跟踪处理就是要填写这样一个结构:
include/linux/netfilter_ipv4/ip_conntrack_helper.h
struct ip_conntrack_helper
{
struct list_head list; /* Internal use. */
const char *name; /* name of the module */
unsigned char flags; /* Flags (see above) */
struct module *me; /* pointer to self */
unsigned int max_expected; /* Maximum number of concurrent
* expected connections */
unsigned int timeout; /* timeout for expecteds */
/* Mask of things we will help (compared against server response) */
struct ip_conntrack_tuple tuple;
struct ip_conntrack_tuple mask;
/* Function to call when data passes; return verdict, or -1 to
invalidate. */
int (*help)(const struct iphdr *, size_t len,
struct ip_conntrack *ct,
enum ip_conntrack_info conntrackinfo);
};
结构中包括以下参数:
struct list_head list:将该结构挂接到多连接协议跟踪链表helpers中, helpers链表在ip_conntrack_core.c文件中定义:
static LIST_HEAD(helpers);
注意是static的,只在该文件范围有效;
const char *name:协议名称,字符串常量
unsigned char flags:关于本连接跟踪模块的一些标志;
struct module *me:指向模块本身,统计模块是否被使用
unsigned int max_expected:子连接的数量,这只是表示主连接在每个时刻所拥有的的子连接的数量,而不是主连接的整个生存期内总共生成的子连接的数量,如FTP,不论传多少个文件,建立多少个子连接,每个时刻主连接最多只有一个子连接,一个子连接结束前按协议是不能再派生出第二个子连接的,所以该值为1;
unsigned int timeoout:超时,指在多少时间范围内子连接没有建立的话子连接跟踪失效
struct ip_conntrack_tuple tuple, mask:这两个参数用来描述子连接,判断一个新来的连接是否是主连接期待的子连接,之所以要有mask参数,是因为子连接的某些参数不能确定,如被动模式的FTP传输,只能得到子连接的目的端口而不能确定源端口,所以源端口部分要用mask来进行泛匹配;
结构中包括以下函数:
(*help):连接跟踪基本函数,解析主连接的通信内容,提取出关于子连接的信息,将子连接信息填充到一个struct ip_conntrack_expect结构中,然后将此结构通过调用函数ip_conntrack_expect_related()把子连接的信息添加到系统的期待子连接链表ip_conntrack_expect_list中。返回值是NF_ACCEPT或NF_DROP,或-1表示协议数据非法。该函数在ip_conntrack_in()函数中调用。
2.2 期待的连接的结构
期待的连接结构用来描述所期待的连接,但该连接目前是不存在的,是一个虚构的连接,只是netfilter根据主连接的信息解析出来的可能的子连接的信息,struct ip_conntrack_helper结构中的(*help)成员函数的目的就是建立一个struct ip_conntrack_expect结构:
struct ip_conntrack_expect
{
/* Internal linked list (global expectation list) */
struct list_head list;
/* reference count */
atomic_t use;
/* expectation list for this master */
struct list_head expected_list;
/* The conntrack of the master connection */
struct ip_conntrack *expectant;
/* The conntrack of the sibling connection, set after
* expectation arrived */
struct ip_conntrack *sibling;
/* Tuple saved for conntrack */
struct ip_conntrack_tuple ct_tuple;
/* Timer function; deletes the expectation. */
struct timer_list timeout;
/* Data filled out by the conntrack helpers follow: */
/* We expect this tuple, with the following mask */
struct ip_conntrack_tuple tuple, mask;
/* Function to call after setup and insertion */
int (*expectfn)(struct ip_conntrack *new);
/* At which sequence number did this expectation occur */
u_int32_t seq;
union ip_conntrack_expect_proto proto;
union ip_conntrack_expect_help help;
};
结构中包括以下参数:
struct list_head list:将该结构挂接到期待连接链表
ip_conntrack_expect_list中;
atomic_t use:该期待连接结构的使用次数;
struct list_head expected_list:主连接的期待的子连接的链表;
struct ip_conntrack *expectant:期待连接对应的主连接;
struct ip_conntrack *sibling:期待连接对应的真实的子连接;
struct ip_conntrack_tuple ct_tuple:连接的tuple值
struct timer_list timeout:定时器
struct ip_conntrack_tuple tuple, mask:期待连接相关的tuple和mask;
u_int32_t seq:TCP协议时,主连接中描述子连接的数据起始处对应的序列号值;
union ip_conntrack_expect_proto proto:跟踪各个多连接IP层协议相关的数据;
union ip_conntrack_expect_help help:跟踪各个多连接应用层协议相关的数据;
结构中包括以下函数:
int (*expectfn)(struct ip_conntrack *new):期待连接相关函数,在ip_conntrack_core.c文件的init_conntrack()函数中调用:
...
if (expected && expected->expectfn)
expected->expectfn(conntrack);
...
2.3 多连接协议的连接跟踪过程
2.3.1 过程简述
新连接的数据包进入netfilter系统后先要建立对应连接,检查是否是期待的连接,如果与某期待连接符合,说明该连接是子连接,将连接和主连接建立相关的联系,并对连接设置相关标志,表明是RELATED的连接;否则,说明是主连接,则查找与这个连接对应的协议的连接辅助模块helper,如果找到,执行helper中的(*help)函数检查是否能提取出相关的子连接信息,生成期待的连接信息添加到期待连接链表中。
2.3.2 连接helper查找
在ip_conntrack_core.c文件的init_conntrack()函数中先查找期待连接是否存在,然后调用ip_ct_find_helper()函数来找到与该协议对应的helper函数,注意是用反向的tuple来查找的:
...
// 查找是否期待连接
expected = LIST_FIND(&ip_conntrack_expect_list, expect_cmp,
struct ip_conntrack_expect *, tuple);
...
// 查找连接对应的helper
if (!expected)
conntrack->helper = ip_ct_find_helper(&repl_tuple);
...
在查找helper之前先检查该连接是否存在对应的期待连接,如果是存在期待连接,说明是子连接,子连接就不再去找helper了,这就避免了phrack63的那篇文章中描述的防火墙穿透方法。但这种限制也带来一个问题,比如H.323协议,主连接是H.225协议,会派生出一个H.245的连接,由H.245连接再派生出一个连接进行数据传输,这种情况下H.245的helper就不能通过ip_ct_find_helper()函数来获取了,为解决这个问题,H.323跟踪NAT模块的作者使用了一个很巧妙的方法,就是通过期待连接结构的(*expectfn)函数来解决,在该函数中直接将H.245的helper直接赋值到该连接中。
2.3.3 (*help)函数执行
(*help)函数对主连接的每个合法数据包都会进行检查的,在ip_conntrack_core.c文件的ip_conntrack_in()函数中调用:
...
if (ret != NF_DROP && ct->helper) {
ret = ct->helper->help((*pskb)->nh.iph, (*pskb)->len,
ct, ctinfo);
if (ret == -1) {
// 协议数据格式错误时help函数返回-1,清空该包对应的nfct,则该包在状态检测时将被视为非法包
/* Invalid */
nf_conntrack_put((*pskb)->nfct);
(*pskb)->nfct = NULL;
return NF_ACCEPT;
}
}
...
3. 多连接协议NAT
3.1 ip_nat_helper结构
netfilter中对每个要进行NAT的多连接协议定义了以下结构,每个多连接协议的连接跟踪处理就是要填写这样一个结构:
struct ip_nat_helper
{
struct list_head list; /* Internal use */
const char *name; /* name of the module */
unsigned char flags; /* Flags (see above) */
struct module *me; /* pointer to self */
/* Mask of things we will help: vs. tuple from server */
struct ip_conntrack_tuple tuple;
struct ip_conntrack_tuple mask;
/* Helper function: returns verdict */
unsigned int (*help)(struct ip_conntrack *ct,
struct ip_conntrack_expect *exp,
struct ip_nat_info *info,
enum ip_conntrack_info ctinfo,
unsigned int hooknum,
struct sk_buff **pskb);
/* Returns verdict and sets up NAT for this connection */
unsigned int (*expect)(struct sk_buff **pskb,
unsigned int hooknum,
struct ip_conntrack *ct,
struct ip_nat_info *info);
};
结构中包括以下参数:
struct list_head list:将该结构挂接到多连接协议NAT链表helpers中,helpers链表在ip_nat_core.c文件中定义:
LIST_HEAD(helpers);
注意该定义不是static的,而是全局有效的,连接跟踪的helpers则是static的,只在该文件中起作用,而且屏蔽了ip_nat_core.c中的helpers,所以在此两个文件外的helpers是指nat的helpers;
const char *name:协议名称,字符串常量
unsigned char flags:关于本连接跟踪模块的一些标志;
struct module *me:指向模块本身,统计模块是否被使用
struct ip_conntrack_tuple tuple, mask:这两个参数用来描述进行了NAT后的子连接,同样因为子连接的某些参数不能确定,要用mask来进行泛匹配;
结构中包括以下函数:
(*help):多协议连接NAT操作基本函数,由于作NAT后要修改IP地址以及端口,因此原来的主连接中描述子连接的信息必须进行修改,(*help)函数的功能就要要找到一个空闲的tuple对应新的子连接,修改期待的子连接,然后修改主连接的通信内容,修改关于IP地址和端口部分的描述信息为空闲tuple的信息,由于修改了应用层数据,数据的校验和必须重新计算,而且如果数据长度发生变化,会引起TCP序列号的变化,在连接的协议相关数据中会记录这些变化,对后续的所有数据都要进行相应的调整;该函数在do_bindings()函数中调用;
(*expect):该函数建立子连接对应的NAT相关信息,主连接的NAT相关信息是通过iptables的NAT规则建立的;该函数在ip_nat_statndalone.c的call_expect()函数中调用
在NAT的基本函数ip_nat_fn()中,先调用call_expect(),最后调用的do_bindings(),所以(*expect)函数先调用,(*help)函数后调用。
3.2 多连接协议的NAT过程
2.3.1 过程简述
不论是SNAT还是DNAT,其对应的netfilter的nf_hook_ops节点都要执行ip_nat_fn(),此时与数据包对应的连接已经建立,对于连接的后续包,只是把连接的NAT信息绑定到该包(do_binding()函数);如果是新连接,如果是子连接,则调用协议相关nat_helper的(*expect)函数建立子连接的相关信息;否不论是SNAT还是DNAT都会调用ip_nat_setup_info()函数建立与该连接对应的NAT信息,所以在NAT规则中是不需要加状态是NEW的匹配的,最后将连接的NAT信息绑定到数据包。
在NAT信息的绑定过程中,会检查当前的数据包是否属于期待的要进行NAT修改的包,具体是用exp_for_packet()函数进行检查,该函数中调用相应传输层协议的(*exp_matches_pkt)函数,该函数只在TCP的协议中定义,但UDP没有,没有定义此函数时exp_for_packet()始终返回要求检查;如果要修改,将调用相应nat_helper的(*help)函数来修改数据,修改期待的子连接。
2.3.2 nat helper查找
在ip_nat_core.c文件的ip_nat_setup_info()函数中
...
/* If there's a helper, assign it; based on new tuple. */
if (!conntrack->master)
info->helper = LIST_FIND(&helpers, helper_cmp, struct ip_nat_helper *,
&reply);
...
static inline int
helper_cmp(const struct ip_nat_helper *helper,
const struct ip_conntrack_tuple *tuple)
{
return ip_ct_tuple_mask_cmp(tuple, &helper->tuple, &helper->mask);
}
3.3.3 (*expect)函数执行
在ip_nat_standalone.c文件的ip_nat_fn()函数中调用call_expect()函数,在call_expect()函数中调用(*expect)函数:
static inline int call_expect(struct ip_conntrack *master,
struct sk_buff **pskb,
unsigned int hooknum,
struct ip_conntrack *ct,
struct ip_nat_info *info)
{
return master->nat.info.helper->expect(pskb, hooknum, ct, info);
}
static unsigned int
ip_nat_fn(unsigned int hooknum,
struct sk_buff **pskb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
...
case IP_CT_NEW:
...
if (ct->master
&& master_ct(ct)->nat.info.helper
&& master_ct(ct)->nat.info.helper->expect) {
ret = call_expect(master_ct(ct), pskb,
hooknum, ct, info);
} else {
#ifdef CONFIG_IP_NF_NAT_LOCAL
/* LOCAL_IN hook doesn't have a chain! */
if (hooknum == NF_IP_LOCAL_IN)
ret = alloc_null_binding(ct, info,
hooknum);
else
#endif
ret = ip_nat_rule_find(pskb, hooknum, in, out,
ct, info);
}
...
3.3.3 (*help)函数执行
在ip_nat_core.c文件的do_binding()函数中:
...
// 判断是否是期待的修改点
if (exp_for_packet(exp, pskb)) {
/* FIXME: May be true multiple times in the
* case of UDP!! */
// 因为UDP没有序列号,没办法指示修改点,不象TCP可以用序列号表示,所以所有UDP包
// 都会执行help
DEBUGP("calling nat helper (exp=%p) for packet\n", exp);
// 修改数据参数
ret = helper->help(ct, exp, info, ctinfo,
hooknum, pskb);
if (ret != NF_ACCEPT) {
READ_UNLOCK(&ip_conntrack_lock);
return ret;
}
helper_called = 1;
}
}
/* Helper might want to manip the packet even when there is no
* matching expectation for this packet */
if (!helper_called && helper->flags & IP_NAT_HELPER_F_ALWAYS) {
DEBUGP("calling nat helper for packet without expectation\n");
ret = helper->help(ct, NULL, info, ctinfo,
hooknum, pskb);
if (ret != NF_ACCEPT) {
READ_UNLOCK(&ip_conntrack_lock);
return ret;
}
}
...
有两处地方调用了(*help)函数,第一处(*help)执行了后面的(*help)就不会执行,但即使前面的(*help)没有执行,由于很多helper的flags都设置为0,所以后面那次(*help)基本都不会执行。
4. 结论
netfilter对多连接协议跟踪和NAT处理很好地实现了模块化,只要按固定的格式编写跟踪和NAT程序就能支持新的多连接协议,现在netfilter已经可以支持很多多连接协议,如FTP、TFTP、IRC、TALK、H.323、SIP、MMS、QUAKE3等。