2015年(60)
2014年(99)
2013年(7)
分类: 网络与安全
2014-08-15 15:20:39
原文地址:Linux连接跟踪源码分析 作者:jasenwan88
IP Connection tracking
连接跟踪用来跟踪和记录连接状态,是netfilter的一部份,也是通过在hook点上注册相应的结构来工作的。
无论是发送,接收,还是转发的数据包,都要经过两个conntrack模块。
第一个conntrack点的优先级是最高的,所有数据包进入netfilter后都会首先被它处理,其作用是创建ip_conntrack结构。而最后一个conntrack的优先级最低,总是在数据包离开netfilter之前做最后的处理,它的作用是将该数据包的连接跟踪结构添加到系统的连接状态表中
1. ip_conntarck结构 ip_conntrack.h
内核中用一个ip_conntrack结构来描述一个连接的状态
struct ip_conntrack
{
/* nf_conntrack结构定义于include/linux/skbuff.h,Line89,其中包括一个计数器use和一个destroy函数。计数器use对本连接记录的公开引用次数进行计数 */
struct nf_conntrack ct_general;
/*其中的IP_CT_DIR_MAX是一个枚举类型ip_conntrack_dir(位于include/linux/netfilter_ipv4/ip_conntrack_tuple.h,Line65)的第3个成员,从这个结构实例在源码中的使用看来,实际上这是定义了两个tuple多元组的hash表项tuplehash[IP_CT_DIR_ORIGINAL/0]和tuplehash[IP_CT_DIR_REPLY/1],利用两个不同方向的tuple定位一个连接,同时也可以方便地对ORIGINAL以及REPLY两个方向进行追溯*/
struct ip_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX];
/* 这是一个位图,是一个状态域。在实际的使用中,它通常与一个枚举类型ip_conntrack_status(位于include/linux/netfilter_ipv4/ip_conntrack.h,Line33)进行位运算来判断连接的状态。其中主要的状态包括:
IPS_EXPECTED(_BIT),表示一个预期的连接
(_BIT),表示一个双向的连接
IPS_ASSURED(_BIT),表示这个连接即使发生超时也不能提早被删除
IPS_CONFIRMED(_BIT),表示这个连接已经被确认(初始包已经发出) */
unsigned long status;
/*其类型timer_list位于include/linux/timer.h,Line11,其核心是一个处理函数。这个成员表示当发生连接超时时,将调用此处理函数*/
struct timer_list timeout;
/*所谓“预期的连接”的链表,其中存放的是我们所期望的其它相关连接*/
struct list_head sibling_list;
/*目前的预期连接数量*/
unsigned int expecting;
/*结构ip_conntrack_expect位于ip_conntrack.h,这个结构用于将一个预期的连接分配给现有的连接,也就是说本连接是这个master的一个预期连接*/
struct ip_conntrack_expect *master;
/* helper模块。这个结构定义于ip_conntrack_helper.h,这个模块提供了一个可以用于扩展Conntrack功能的接口。经过连接跟踪HOOK的每个数据报都将被发给每个已经注册的helper模块(注册以及卸载函数分别为ip_conntrack_helper_register()以及ip_conntrack_helper_unregister(),分别位于ip_conntrack_core.c)。这样我们就可以进行一些动态的连接管理了*/
struct ip_conntrack_helper *helper;
/*一系列的nf_ct_info类型(定义于include/linux/skbuff.h ,Line92,实际上就是nf_conntrack结构)的结构,每个结构对应于某种状态的连接。这一系列的结构会被sk_buff结构的nfct指针所引用,描述了所有与此连接有关系的数据报。其状态由枚举类型ip_conntrack_info定义(位于include/linux/netfilter_ipv4/ip_conntrack.h,Line12)共有5个成员:
IP_CT_ESTABLISHED: 数据报属于已经完全建立的连接
IP_CT_RELATED: 数据报属于一个新的连接,但此连接与一个现有连接相关(预期连接);或者是ICMP错误
IP_CT_NEW: 数据报属于一个新的连接
IP_CT_IS_REPLY: 数据报属于一个连接的回复
IP_CT_NUMBER: 不同IP_CT类型的数量,这里为7,NEW仅存于一个方向上 */
struct nf_ct_info infos[IP_CT_NUMBER];
/* 为其他模块保留的部分 */
union ip_conntrack_proto proto;
union ip_conntrack_help help;
#ifdef CONFIG_IP_NF_NAT_NEEDED
struct {
struct ip_nat_info info;
union ip_conntrack_nat_help help;
#if defined(CONFIG_IP_NF_TARGET_MASQUERADE) || \
defined(CONFIG_IP_NF_TARGET_MASQUERADE_MODULE)
int masq_index;
#endif
#if defined(CONFIG_IP_NF_RTSP) || defined(CONFIG_IP_NF_RTSP_MODULE)
struct ip_nat_rtsp_info rtsp_info;
#endif
} nat;
#endif /* CONFIG_IP_NF_NAT_NEEDED */
#if defined(CONFIG_IP_NF_CONNTRACK_MARK)
unsigned long mark;
#endif
};
struct ip_conntrack_tuple_hash结构描述链表中的节点,这个数组包含“初始”和“应答”两个成员(tuplehash[IP_CT_DIR_ORIGINAL]和tuplehash[IP_CT_DIR_REPLY]),所以,当一个数据包进入连接跟踪模块后,先根据这个数据包的套接字对转换成一个“初始的”tuple,赋值给tuplehash[IP_CT_DIR_ORIGINAL],然后对这个数据包“取反”,计算出“应答”的tuple,赋值给tuplehash[IP_CT_DIR_REPLY],这样,一条完整的连接已经跃然纸上了。
enum ip_conntrack_dir
{
IP_CT_DIR_ORIGINAL,
IP_CT_DIR_REPLY,
IP_CT_DIR_MAX
};
2. 连接跟踪表
Netfilter用“来源地址/来源端口+目的地址/目的端口”,即一个“tuple”,来唯一标识一个连接。用一张连接跟踪表来描述所有的连接状态,该表用了hash算法。
hash表用一个全局指针来描述(ip_conntrack_core.c)
struct list_head *ip_conntrack_hash;
表的大小,即hash节点的个数由ip_conntrack_htable_size全局变量决定,默认是根据内存计算出来的。而每个hash节点又是一条链表的首部,所以,连接跟踪表就是一个由ip_conntrack_htable_size 条链表构成的一个hash表,整个连接跟踪表大小使用全局变量ip_conntrack_max描述,与hash表的关系是ip_conntrack_max = 8 * ip_conntrack_htable_size。
链表的每个节点,都是一个ip_conntrack_tuple_hash结构:
struct ip_conntrack_tuple_hash
{
/* 用来组织链表 */
struct list_head list;
/* 用来描述一个tuple */
struct ip_conntrack_tuple tuple;
/* this == &ctrack->tuplehash[DIRECTION(this)]. */
struct ip_conntrack *ctrack;
};
实际描述一个tuple的是ip_conntrack_tuple结构 ip_conntrack_tuple.h
struct ip_conntrack_tuple
{
/* 源 */
struct ip_conntrack_manip src;
/* These are the parts of the tuple which are fixed. */
struct {
/* 目的地址 */
u_int32_t ip;
union {
/* Add other protocols here. */
u_int64_t all;
struct {
u_int16_t port;
} tcp;
struct {
u_int16_t port;
} udp;
struct {
u_int8_t type, code;
} icmp;
struct {
u_int16_t protocol;
u_int8_t version;
u_int32_t key;
} gre;
struct {
u_int16_t spi;
} esp;
} u;
/* 协议类型 */
u_int16_t protonum;
} dst;
};
对于所有IP协议,协议类型、源地址、目的地址这三个参数是识别连接所必须的,具体到各个协议,就要提取出各协议的唯一特征数据,如TCP、UDP的源端口、目的端口,ICMP的ID、TYPE、CODE等值,这些值就是tuple结构要处理的数据。各协议相关数据是以联合(union)形式定义在tuple结构中的,netfilter缺省支持TCP、UDP和ICMP协议,如果还要支持其他IP协议,如GRE、ESP、AH、SCTP等,需要在联合中添加相应的协议参数值。
ip_conntrack_manip和ip_conntrack_manip_proto ip_conntrack_tuple.h
struct ip_conntrack_manip
{
u_int32_t ip;
union ip_conntrack_manip_proto u;
};
union ip_conntrack_manip_proto
{
/* Add other protocols here. */
u_int32_t all;
struct {
u_int16_t port;
} tcp;
struct {
u_int16_t port;
} udp;
struct {
u_int16_t id;
} icmp;
struct {
u_int32_t key;
} gre;
struct {
u_int16_t spi;
} esp;
};
Netfilter将每一个数据包转换成tuple,再根据tuple计算出hash值,这样,就可以使用ip_conntrack_hash[hash_id]找到hash表中链表的入口,并组织链表;找到hash表中链表入口后,如果链表中不存在此“tuple”,则是一个新连接,就把tuple插入到链表的合适位置;两个节点tuple[ORIGINAL]和tuple[REPLY]虽然是分开的,在两个链表当中,但是如前所述,它们同时又被封装在ip_conntrack结构的tuplehash数组中
3. 连接跟踪初始化
初始化函数init()调用init_or_cleanup(1)函数 ip_conntrack_standalone.c
static int __init init(void)
{
return init_or_cleanup(1);
}
3.1 init_or_cleanup()函数
int init_or_cleanup函数,(ip_conntrack_standalone.c)参数为1则执行init,为0则执行clean,它主要做三件工作:
1. 调用ip_conntrack_init()初始化连接跟踪表的相关变量,见3.2
2. 初始化proc文件系统节点
3. 为连接跟踪注册hook
static int init_or_cleanup(int init)
{
struct proc_dir_entry *proc;
int ret = 0;
if (!init) goto cleanup;
/* 初始化连接跟踪的一些变量和数据结构,如连接跟踪表的大小,Hash表的大小等 */
ret = ip_conntrack_init();
if (ret < 0)
goto cleanup_nothing;
/* 初始化proc文件系统 */
proc = proc_net_create("ip_conntrack", 0440, list_conntracks);
proc = proc_net_create("ip_clear_dnsconntrack",0644,clear_dns_conntracks);
if (!proc) goto cleanup_init;
proc->owner = THIS_MODULE;
/* 为连接跟踪注册hook,一共六个,所在的hook点、注册的hook函数和优先级分别如下(和最开始的图是一致的):
NF_IP_PRE_ROUTING:
ip_conntrack_defrag NF_IP_PRI_CONNTRACK_DEFRAG
ip_conntrack_in NF_IP_PRI_CONNTRACK
NF_IP_LOCAL_OUT:
ip_conntrack_defrag NF_IP_PRI_CONNTRACK_DEFRAG
ip_conntrack_local NF_IP_PRI_CONNTRACK
NF_IP_POST_ROUTING:
ip_refrag NF_IP_PRI_LAST
NF_IP_LOCAL_IN:
ip_confirm NF_IP_PRI_LAST-1
优先级的顺序为:
NF_IP_PRI_FIRST (最高)
NF_IP_PRI_CONNTRACK_DEFRAG
NF_IP_PRI_CONNTRACK
NF_IP_PRI_MANGLE
NF_IP_PRI_NAT_DST
NF_IP_PRI_FILTER
NF_IP_PRI_NAT_SRC
NF_IP_PRI_LAST (最低)
我们知道,LOCAL_OUT和PRE_ROUTING点可以看作是netfilter的入口,而POST_ROUTING和LOCAL_IN可以看作是出口。也就是说,在每个数据包刚一进入netfilter之后首先都会调用ip_conntrack_defrag做分片处理,紧接着就是对收到的和发出的数据包分别进行ip_conntrack_in和ip_conntrack_loacl(ip_conntrack_loacl里还是调用了ip_conntrack_in)。而在数据包即将离开netfilter之前,会对进入主机的数据包进行ip_confirm处理,对发出的数据包进行ip_refrag处理(ip_refrag里也会调用ip_confirm)。 这就是整个连接跟踪模块在netfilter中的分布情况。
另外,我们分析的是2.6.8的内核,在linux2.6.12中,这个地方还增加了两个hook,分别是ip_conntrack_helper_out_ops和ip_conntrack_helper_in_ops。将helper模块相关的处理提前了。 */
ret = nf_register_hook(&ip_conntrack_defrag_ops);
if (ret < 0) {
printk("ip_conntrack: can't register pre-routing defrag hook.\n");
goto cleanup_proc;
}
ret = nf_register_hook(&ip_conntrack_defrag_local_out_ops);
if (ret < 0) {
printk("ip_conntrack: can't register local_out defrag hook.\n");
goto cleanup_defragops;
}
ret = nf_register_hook(&ip_conntrack_in_ops);
if (ret < 0) {
printk("ip_conntrack: can't register pre-routing hook.\n");
goto cleanup_defraglocalops;
}
ret = nf_register_hook(&ip_conntrack_local_out_ops);
if (ret < 0) {
printk("ip_conntrack: can't register local out hook.\n");
goto cleanup_inops;
}
ret = nf_register_hook(&ip_conntrack_out_ops);
if (ret < 0) {
printk("ip_conntrack: can't register post-routing hook.\n");
goto cleanup_inandlocalops;
}
ret = nf_register_hook(&ip_conntrack_local_in_ops);
if (ret < 0) {
printk("ip_conntrack: can't register local in hook.\n");
goto cleanup_inoutandlocalops;
}
#ifdef CONFIG_SYSCTL
ip_ct_sysctl_header = register_sysctl_table(ip_ct_net_table, 0);
if (ip_ct_sysctl_header == NULL) {
printk("ip_conntrack: can't register to sysctl.\n");
goto cleanup;
}
#endif
return ret;
cleanup:
#ifdef CONFIG_SYSCTL
unregister_sysctl_table(ip_ct_sysctl_header);
#endif
nf_unregister_hook(&ip_conntrack_local_in_ops);
cleanup_inoutandlocalops:
nf_unregister_hook(&ip_conntrack_out_ops);
cleanup_inandlocalops:
nf_unregister_hook(&ip_conntrack_local_out_ops);
cleanup_inops:
nf_unregister_hook(&ip_conntrack_in_ops);
cleanup_defraglocalops:
nf_unregister_hook(&ip_conntrack_defrag_local_out_ops);
cleanup_defragops:
nf_unregister_hook(&ip_conntrack_defrag_ops);
cleanup_proc:
proc_net_remove("ip_conntrack");
proc_net_remove("ip_clear_dnsconntrack");
cleanup_init:
ip_conntrack_cleanup();
cleanup_nothing:
return ret;
}
3.2 ip_conntrack_init() 函数
ip_conntrack_init 函数(ip_conntrack_core.c)用于初始化连接跟踪的包括hash表相关参数在内一些重要的变量:
int __init ip_conntrack_init(void)
{
unsigned int i;
int ret;
/* Idea from tcp.c: use 1/16384 of memory. On i386: 32MB
* machine has 256 buckets. >= 1GB machines have 8192 buckets. */
/* 如果指定hash表的大小则用制定值,否则根据内存计算 */
if (hashsize) {
ip_conntrack_htable_size = hashsize;
} else {
ip_conntrack_htable_size
= (((num_physpages << PAGE_SHIFT) / 16384)
/ sizeof(struct list_head));
if (num_physpages > (1024 * 1024 * 1024 / PAGE_SIZE))
ip_conntrack_htable_size = 8192;
if (ip_conntrack_htable_size < 16)
ip_conntrack_htable_size = 16;
}
ip_conntrack_max = 8 * ip_conntrack_htable_size;
#ifdef CONFIG_MIPS_BRCM
ip_conntrack_max=0;
#endif
printk("ip_conntrack version %s (%u buckets, %d max)"
" - %Zd bytes per conntrack\n", IP_CONNTRACK_VERSION,
ip_conntrack_htable_size, ip_conntrack_max,
sizeof(struct ip_conntrack));
/*注册socket选项*/
ret = nf_register_sockopt(&so_getorigdst);
if (ret != 0) {
printk(KERN_ERR "Unable to register netfilter socket option\n");
return ret;
}
/* 为hash表分配连续内存页 */
ip_conntrack_hash = vmalloc(sizeof(struct list_head)
* ip_conntrack_htable_size);
if (!ip_conntrack_hash) {
printk(KERN_ERR "Unable to create ip_conntrack_hash\n");
goto err_unreg_sockopt;
}
/* 分配高速缓存 */
ip_conntrack_cachep = kmem_cache_create("ip_conntrack",
sizeof(struct ip_conntrack), 0,
SLAB_HWCACHE_ALIGN,
NULL, NULL);
if (!ip_conntrack_cachep) {
printk(KERN_ERR "Unable to create ip_conntrack slab cache\n");
goto err_free_hash;
}
/* Don't NEED lock here, but good form anyway. */
WRITE_LOCK(&ip_conntrack_lock);
/* netfilter中对每个要进行跟踪的IP协议定义了一个ip_conntrack_protocol结构,每个IP协议的连接跟踪处理就是要填写这样一个结构,这里的ip_conntrack_protocol_tcp,ip_conntrack_protocol_udp等都是这个结构,用list_append将这些需要跟踪的协议组织成链表 */
list_append(&protocol_list, &ip_conntrack_protocol_tcp);
list_append(&protocol_list, &ip_conntrack_protocol_udp);
list_append(&protocol_list, &ip_conntrack_protocol_icmp);
list_append(&protocol_list, &ip_conntrack_protocol_esp);
WRITE_UNLOCK(&ip_conntrack_lock);
/* 初始化hash表 */
for (i = 0; i < ip_conntrack_htable_size; i++)
INIT_LIST_HEAD(&ip_conntrack_hash[i]);
/* For use by ipt_REJECT */
ip_ct_attach = ip_conntrack_attach;
/* Set up fake conntrack:
- to never be deleted, not in any hashes */
atomic_set(&ip_conntrack_untracked.ct_general.use, 1);
/* - and look it like as a confirmed connection */
set_bit(IPS_CONFIRMED_BIT, &ip_conntrack_untracked.status);
/* - and prepare the ctinfo field for REJECT & NAT. */
ip_conntrack_untracked.infos[IP_CT_NEW].master =
ip_conntrack_untracked.infos[IP_CT_RELATED].master =
ip_conntrack_untracked.infos[IP_CT_RELATED + IP_CT_IS_REPLY].master =
&ip_conntrack_untracked.ct_general;
return ret;
err_free_hash:
vfree(ip_conntrack_hash);
err_unreg_sockopt:
nf_unregister_sockopt(&so_getorigdst);
return -ENOMEM;
}
4. 协议的扩展,ip_conntrack_protocol结构
各种协议使用一个全局的协议列表存放,即protocol_list(ip_conntrack_core.h),使用结构ip_conntrack_protocol来表示(ip_conntrack_protocol.h)
struct ip_conntrack_protocol
{
/* Next pointer. */
struct list_head list;
/* Protocol number. */
u_int8_t proto;
/* Protocol name */
const char *name;
/* 其指向函数的作用是将协议加入到ip_conntrack_tuple的dst子结构中 */
int (*pkt_to_tuple)(const struct sk_buff *skb,
unsigned int dataoff,
struct ip_conntrack_tuple *tuple);
/* 其指向函数的作用是将源和目的多元组中协议部分的值进行互换,包括IP地址、端口等 */
int (*invert_tuple)(struct ip_conntrack_tuple *inverse,
const struct ip_conntrack_tuple *orig);
/* 打印多元组中的协议信息 */
unsigned int (*print_tuple)(char *buffer,
const struct ip_conntrack_tuple *);
/* 打印整个连接记录 */
unsigned int (*print_conntrack)(char *buffer,
const struct ip_conntrack *);
/* 判断数据包是否合法,并调整相应连接的信息,也就是实现各协议的状态检测,对于UDP等本身是无连接的协议的判断比较简单,netfilter建立一个虚拟连接,每个新发包都是合法包,只等待回应包到后连接都结束;但对于TCP之类的有状态协议必须检查数据是否符合协议的状态转换过程,这是靠一个状态转换数组实现的。返回数据报的verdict值 */
int (*packet)(struct ip_conntrack *conntrack,
const struct sk_buff *skb,
enum ip_conntrack_info ctinfo);
/* 当此协议的一个新连接发生时,调用其指向的这个函数,调用返回true时再继续调用packet()函数 */
int (*new)(struct ip_conntrack *conntrack, const struct sk_buff *skb);
/* 删除一个连接状态 */
void (*destroy)(struct ip_conntrack *conntrack);
/* 判断是否有数据报匹配预期的连接 */
int (*exp_matches_pkt)(struct ip_conntrack_expect *exp,
const struct sk_buff *skb);
/ * 指向模块本身,统计模块是否被使用 */
struct module *me;
};
要编写自己的IP协议跟踪模块,先要分析这些协议头中哪些信息可以用来唯一识别连接,作NAT时要修改哪些信息,把这些信息添加到ip_conntrack_tuple结构的联合中;然后填写该协议的ip_conntrack_protocol结构,实现结构中的内部函数;最后在ip_conntrack_init()函数中用list_append()将此结构挂接到协议跟踪链表中,也可以在各自的模块初始化时用ip_conntrack_protocol_register()和ip_conntrack_protocol_unregister()来添加/删除协议。
5. 两个主要的连接跟踪函数: ip_conntrack_in()和ip_confirm()
5.1.1 ip_conntrack_in()函数ip_conntrack_core.c
接收倒的数据包进入Netfilter后,首先进行分片处理,然后就会调用ip_conntrack_in函数, ip_conntrack_in 主要完成的工作就是判断数据包是否已在连接跟踪表中,如果不在,则为数据包分配ip_conntrack,并初始化它,然后,为这个数据包设置连接状态。
unsigned int ip_conntrack_in(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;
struct ip_conntrack_protocol *proto;
int set_reply;
int ret;
/* 分片包会在前一个Hook中被处理,事实上,并不会触发该条件 */
if ((*pskb)->nh.iph->frag_off & htons(IP_OFFSET)) {
return NF_ACCEPT;
if (net_ratelimit()) {
printk(KERN_ERR "ip_conntrack_in: Frag of proto %u (hook=%u)\n",
(*pskb)->nh.iph->protocol, hooknum);
}
return NF_DROP;
}
/* 将当前数据包设置为未修改 */
(*pskb)->nfcache |= NFC_UNKNOWN;
/* 判断当前数据包是否已被检查过了 */
if ((*pskb)->nfct)
return NF_ACCEPT;
/* 根据当前数据包的协议,查找与之相应的struct ip_conntrack_protocol结构 */
proto = ip_ct_find_proto((*pskb)->nh.iph->protocol);
/* 如果是icmp错误报文 */
if ((*pskb)->nh.iph->protocol == IPPROTO_ICMP
&& icmp_error_track(*pskb, &ctinfo, hooknum))
return NF_ACCEPT;
/* 在全局的连接表中,查找与当前包相匹配的连接结构,返回的是struct ip_conntrack *类型指针,它用于描述一个数据包的连接状态 */
if (!(ct = resolve_normal_ct(*pskb, proto,&set_reply,hooknum,&ctinfo)))
return NF_ACCEPT;
if (IS_ERR(ct))
return NF_DROP;
IP_NF_ASSERT((*pskb)->nfct);
/* 如果注册了相应的协议的ip_conntrack_protocol结构,则在这里调用其中的packet函数做一些检查 */
ret = proto->packet(ct, *pskb, ctinfo);
if (ret == -1) {
/* Invalid */
nf_conntrack_put((*pskb)->nfct);
(*pskb)->nfct = NULL;
return NF_ACCEPT;
}
/* 如果注册了相应协议的ip_conntrack_helper结构,则在这里调用其help函数 */
if (ret != NF_DROP && ct->helper) {
ret = ct->helper->help(*pskb, ct, ctinfo);
if (ret == -1) {
/* Invalid */
nf_conntrack_put((*pskb)->nfct);
(*pskb)->nfct = NULL;
return NF_ACCEPT;
}
}
if (set_reply)
set_bit(IPS_SEEN_REPLY_BIT, &ct->status);
return ret;
}
连接跟踪模块将所有支持的协议,都使用struct ip_conntrack_protocol 结构封装,注册至全局链表中,这里首先调用函数ip_ct_find_proto根据当前数据包的协议值,找到协议注册对应的模块。然后调用resolve_normal_ct 函数进一步处理。
5.1.2 resolve_normal_ct()函数 ip_conntrack_core.c
函数判断数据包在连接跟踪表是否存在,如果不存在,则为数据包分配相应的连接跟踪节点空间并初始化,然后设置连接状态。
static inline struct ip_conntrack *
resolve_normal_ct(struct sk_buff *skb,
struct ip_conntrack_protocol *proto,
int *set_reply,
unsigned int hooknum,
enum ip_conntrack_info *ctinfo)
{
struct ip_conntrack_tuple tuple;
struct ip_conntrack_tuple_hash *h;
IP_NF_ASSERT((skb->nh.iph->frag_off & htons(IP_OFFSET)) == 0);
/* 将数据包转换成tuple */
if (!get_tuple(skb->nh.iph, skb, skb->nh.iph->ihl*4, &tuple, proto))
return NULL;
/* 查找对应的tuple在连接跟踪表中是否存在 */
h = ip_conntrack_find_get(&tuple, NULL);
/* 如果不存在,初始化该连接 */
if (!h) {
h = init_conntrack(&tuple, proto, skb);
if (!h)
return NULL;
if (IS_ERR(h))
return (void *)h;
}
/* 判断连接方向 */
if (DIRECTION(h) == IP_CT_DIR_REPLY) {
*ctinfo = IP_CT_ESTABLISHED + IP_CT_IS_REPLY;
/* Please set reply bit if this packet OK */
*set_reply = 1;
} else {
/* Once we've had two way comms, always ESTABLISHED. */
if (test_bit(IPS_SEEN_REPLY_BIT, &h->ctrack->status)) {
DEBUGP("ip_conntrack_in: normal packet for %p\n",
h->ctrack);
*ctinfo = IP_CT_ESTABLISHED;
} else if (test_bit(IPS_EXPECTED_BIT, &h->ctrack->status)) {
DEBUGP("ip_conntrack_in: related packet for %p\n",
h->ctrack);
*ctinfo = IP_CT_RELATED;
} else {
DEBUGP("ip_conntrack_in: new packet for %p\n",
h->ctrack);
*ctinfo = IP_CT_NEW;
}
*set_reply = 0;
}
/* 设置skb的对应成员,如使用计数器、数据包状态标记 */
skb->nfct = &h->ctrack->infos[*ctinfo];
return h->ctrack;
}
5.1.3 获取tuple结构 ip_conntrack_core.c
get_tuple()函数将数据包转换成tuple结构
int get_tuple(const struct iphdr *iph,
const struct sk_buff *skb,
unsigned int dataoff,
struct ip_conntrack_tuple *tuple,
const struct ip_conntrack_protocol *protocol)
{
/* Never happen */
if (iph->frag_off & htons(IP_OFFSET)) {
printk("ip_conntrack_core: Frag of proto %u.\n",
iph->protocol);
return 0;
}
/* 设置来源、目的地址和协议号 */
tuple->src.ip = iph->saddr;
tuple->dst.ip = iph->daddr;
tuple->dst.protonum = iph->protocol;
tuple->src.u.all = tuple->dst.u.all = 0;
/* 这里根据协议的不同调用各自的函数 */
return protocol->pkt_to_tuple(skb, dataoff, tuple);
}
以tcp协议为例,ip_conntrack_proto_tcp.c :
static int tcp_pkt_to_tuple(const struct sk_buff *skb,
unsigned int dataoff,
struct ip_conntrack_tuple *tuple)
{
struct tcphdr hdr;
/* Actually only need first 8 bytes. */
if (skb_copy_bits(skb, dataoff, &hdr, 8) != 0)
return 0;
/* 根据报头的端口信息,设置tuple对应成员 */
tuple->src.u.tcp.port = hdr.source;
tuple->dst.u.tcp.port = hdr.dest;
return 1;
}
5.1.4 搜索hash表
要对Hash表进行遍历,首要需要找到hash表的入口,然后来遍历该入口指向的链表。每个链表的节点是struct ip_conntrack_tuple_hash,它封装了tuple,所谓封装,就是把待查找的tuple与节点中已存的tuple相比较。
struct ip_conntrack_tuple_hash *
ip_conntrack_find_get(const struct ip_conntrack_tuple *tuple,
const struct ip_conntrack *ignored_conntrack)
{
struct ip_conntrack_tuple_hash *h;
READ_LOCK(&ip_conntrack_lock);
/* 查找整个hash表,返回一个节点 */
h = __ip_conntrack_find(tuple, ignored_conntrack);
/* 找到则累加计数器 */
if (h)
atomic_inc(&h->ctrack->ct_general.use);
READ_UNLOCK(&ip_conntrack_lock);
return h;
}
计算hash值,是调用hash_conntrack函数,根据数据包对应的tuple实现的:
static struct ip_conntrack_tuple_hash *
__ip_conntrack_find(const struct ip_conntrack_tuple *tuple,
const struct ip_conntrack *ignored_conntrack)
{
struct ip_conntrack_tuple_hash *h;
/* 计算tuple的hash值,这样,tuple对应的hash表入口即为ip_conntrack_hash[hash],也就是链表的首节点 */
unsigned int hash = hash_conntrack(tuple);
MUST_BE_READ_LOCKED(&ip_conntrack_lock);
/* 找到链表入口后遍历链表,返回一个节点。比较函数是conntrack_tuple_cmp() */
h = LIST_FIND(&ip_conntrack_hash[hash],
conntrack_tuple_cmp,
struct ip_conntrack_tuple_hash *,
tuple, ignored_conntrack);
return h;
}
5.1.5 init_conntrack()函数 ip_conntrack_core.c
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 *conntrack;
struct ip_conntrack_tuple repl_tuple;
size_t hash;
struct ip_conntrack_expect *expected;
int i;
static unsigned int drop_next;
/* 如果计算hash值的随机数种子没有被初始化,则初始化之 */
if (!ip_conntrack_hash_rnd_initted) {
get_random_bytes(&ip_conntrack_hash_rnd, 4);
ip_conntrack_hash_rnd_initted = 1;
}
/* 计算hash值 */
hash = hash_conntrack(tuple);
/* 判断连接跟踪表是否已满 */
if (ip_conntrack_max &&
atomic_read(&ip_conntrack_count) >= ip_conntrack_max) {
/* Try dropping from random chain, or else from the
chain about to put into (in case they're trying to
bomb one hash chain). */
unsigned int next = (drop_next++)%ip_conntrack_htable_size;
if (!early_drop(&ip_conntrack_hash[next])
&& !early_drop(&ip_conntrack_hash[hash])) {
#if defined(CONFIG_MIPS_BRCM)
/* Sorry, we have to kick one out regardless. */
while (!regardless_drop(&ip_conntrack_hash[next]))
next = (drop_next++)%ip_conntrack_htable_size;
#else
if (net_ratelimit())
printk(KERN_WARNING
"ip_conntrack: table full, dropping"
" packet.\n");
return ERR_PTR(-ENOMEM);
#endif
}
}
/* 根据当前的tuple取反,计算该数据包的“应答”的tuple */
if (!invert_tuple(&repl_tuple, tuple, protocol)) {
DEBUGP("Can't invert tuple.\n");
return NULL;
}
/* 为数据包对应的连接分配空间 */
conntrack = kmem_cache_alloc(ip_conntrack_cachep, GFP_ATOMIC);
if (!conntrack) {
DEBUGP("Can't allocate conntrack.\n");
return ERR_PTR(-ENOMEM);
}
/* 初始化该结构,使用计数器累加,设置destroy函数指针,设置两个方向的tuple */
memset(conntrack, 0, sizeof(*conntrack));
atomic_set(&conntrack->ct_general.use, 1);
conntrack->ct_general.destroy = destroy_conntrack;
conntrack->tuplehash[IP_CT_DIR_ORIGINAL].tuple = *tuple;
conntrack->tuplehash[IP_CT_DIR_ORIGINAL].ctrack = conntrack;
conntrack->tuplehash[IP_CT_DIR_REPLY].tuple = repl_tuple;
conntrack->tuplehash[IP_CT_DIR_REPLY].ctrack = conntrack;
for (i=0; i < IP_CT_NUMBER; i++)
conntrack->infos[i].master = &conntrack->ct_general;
/* 创建一个该协议对应的ip_conntrack_protocol结构 */
if (!protocol->new(conntrack, skb)) {
kmem_cache_free(ip_conntrack_cachep, conntrack);
return NULL;
}
/* 初始化时间计数器,并设置超时初始函数 */
init_timer(&conntrack->timeout);
conntrack->timeout.data = (unsigned long)conntrack;
conntrack->timeout.function = death_by_timeout;
/* 初始化预期连接链表 */
INIT_LIST_HEAD(&conntrack->sibling_list);
WRITE_LOCK(&ip_conntrack_lock);
/* Need finding and deleting of expected ONLY if we win race */
READ_LOCK(&ip_conntrack_expect_tuple_lock);
/* 在预期连接表中查找,有没有匹配的预期连接 */
expected = LIST_FIND(&ip_conntrack_expect_list, expect_cmp,
struct ip_conntrack_expect *, tuple);
READ_UNLOCK(&ip_conntrack_expect_tuple_lock);
/* 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 (expected && !is_confirmed(expected->expectant))
expected = NULL;
/* 如果没有找到期望的连接,则搜索相关的helper结构 */
if (!expected)
conntrack->helper = ip_ct_find_helper(&repl_tuple);
/* If the expectation is dying, then this is a loser. */
if (expected
&& expected->expectant->helper->timeout
&& ! del_timer(&expected->timeout))
expected = NULL;
if (expected) {
DEBUGP("conntrack: expectation arrives ct=%p exp=%p\n",
conntrack, expected);
/* Welcome, Mr. Bond. We've been expecting you... */
__set_bit(IPS_EXPECTED_BIT, &conntrack->status);
conntrack->master = expected;
expected->sibling = conntrack;
#if CONFIG_IP_NF_CONNTRACK_MARK
conntrack->mark = expected->expectant->mark;
#endif
LIST_DELETE(&ip_conntrack_expect_list, expected);
expected->expectant->expecting--;
nf_conntrack_get(&master_ct(conntrack)->infos[0]);
}
atomic_inc(&ip_conntrack_count);
WRITE_UNLOCK(&ip_conntrack_lock);
if (expected && expected->expectfn)
expected->expectfn(conntrack);
/* 返回初始方向的hash节点 */
return &conntrack->tuplehash[IP_CT_DIR_ORIGINAL];
}
5.2.1 ip_ confirm函数 ip_conntrack_standalone.c
ip_confirm直接返回ip_conntrack_confirm(),而ip_conntrack_confirm()里又调用了__ip_conntrack_confirm()函数
static unsigned int ip_confirm(unsigned int hooknum,
struct sk_buff **pskb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
/* We've seen it coming out the other side: confirm it */
return ip_conntrack_confirm(*pskb);
}
static inline int ip_conntrack_confirm(struct sk_buff *skb)
{
if (skb->nfct
&& !is_confirmed((struct ip_conntrack *)skb->nfct->master))
return __ip_conntrack_confirm(skb->nfct);
return NF_ACCEPT;
}
在数据包穿过filter到达post_routing或local_in节点时,__ip_conntrack_confirm函数将该连接正式加入连接状态表中
int
__ip_conntrack_confirm(struct nf_ct_info *nfct)
{
unsigned int hash, repl_hash;
struct ip_conntrack *ct;
enum ip_conntrack_info ctinfo;
ct = __ip_conntrack_get(nfct, &ctinfo);
/* ipt_REJECT uses ip_conntrack_attach to attach related
ICMP/TCP RST packets in other direction. Actual packet
which created connection will be IP_CT_NEW or for an
expected connection, IP_CT_RELATED. */
if (CTINFO2DIR(ctinfo) != IP_CT_DIR_ORIGINAL)
return NF_ACCEPT;
hash = hash_conntrack(&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple);
repl_hash = hash_conntrack(&ct->tuplehash[IP_CT_DIR_REPLY].tuple);
/* We're not in hash table, and we refuse to set up related
connections for unconfirmed conns. But packet copies and
REJECT will give spurious warnings here. */
/* IP_NF_ASSERT(atomic_read(&ct->ct_general.use) == 1); */
/* No external references means noone else could have
confirmed us. */
IP_NF_ASSERT(!is_confirmed(ct));
DEBUGP("Confirming conntrack %p\n", ct);
WRITE_LOCK(&ip_conntrack_lock);
/* See if there's one in the list already, including reverse:
NAT could have grabbed it without realizing, since we're
not in the hash. If there is, we lost race. */
if (!LIST_FIND(&ip_conntrack_hash[hash],
conntrack_tuple_cmp,
struct ip_conntrack_tuple_hash *,
&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple, NULL)
&& !LIST_FIND(&ip_conntrack_hash[repl_hash],
conntrack_tuple_cmp,
struct ip_conntrack_tuple_hash *,
&ct->tuplehash[IP_CT_DIR_REPLY].tuple, NULL)) {
list_prepend(&ip_conntrack_hash[hash],
&ct->tuplehash[IP_CT_DIR_ORIGINAL]);
list_prepend(&ip_conntrack_hash[repl_hash],
&ct->tuplehash[IP_CT_DIR_REPLY]);
/* Timer relative to confirmation time, not original
setting time, otherwise we'd get timer wrap in
weird delay cases. */
ct->timeout.expires += jiffies;
add_timer(&ct->timeout);
atomic_inc(&ct->ct_general.use);
set_bit(IPS_CONFIRMED_BIT, &ct->status);
WRITE_UNLOCK(&ip_conntrack_lock);
return NF_ACCEPT;
}
WRITE_UNLOCK(&ip_conntrack_lock);
return NF_DROP;
}
6. 具体协议的连接跟踪源码分析
前面是conntrack的总体框架,下面以tftp为例分析其具体实现
相关的文件: ip_conntrack_tftp.c ip_conntrack_tftp.h
6.1 helper和expect结构
6.1.1 ip_conntrack_helper结构
每个使用动态地址和端口的协议一般都定义了数据结构ip_conntrack_helper,并把它放到一个全局的链表中。这类协议一般都会有控制连接和数据连接两个不同的连接,如FTP协议等。
struct ip_conntrack_helper
{
struct list_head list; /* 链表头 */
const char *name; /* 模块的名称 */
unsigned char flags;
struct module *me; /* 指向模块本身 */
unsigned int max_expected; /* 期望的连接数的最大值 */
unsigned int timeout; /* 超时位 */
struct ip_conntrack_tuple tuple;
struct ip_conntrack_tuple mask;
int (*help)(struct sk_buff *skb,
struct ip_conntrack *ct,
enum ip_conntrack_info conntrackinfo);
};
6.1.2 tftp的init()初始化
将协议对应的ip_conntrack_helper结构数组初始化,并链接到全局链表里
static int __init init(void)
{
int i, ret;
char *tmpname;
if (!ports[0])
ports[0]=TFTP_PORT;
for (i = 0 ; (i < MAX_PORTS) && ports[i] ; i++) {
/* Create helper structure */
memset(&tftp[i], 0, sizeof(struct ip_conntrack_helper));
tftp[i].tuple.dst.protonum = IPPROTO_UDP; /* 协议号17 */
tftp[i].tuple.src.u.udp.port = htons(ports[i]); /* 源端口500 */
tftp[i].mask.dst.protonum = 0xFFFF;
tftp[i].mask.src.u.udp.port = 0xFFFF;
tftp[i].max_expected = 1;
tftp[i].timeout = 0;
tftp[i].flags = IP_CT_HELPER_F_REUSE_EXPECT;
tftp[i].me = THIS_MODULE;
tftp[i].help = tftp_help; /* tftp_help函数,最重要的部分 */
tmpname = &tftp_names[i][0];
if (ports[i] == TFTP_PORT)
sprintf(tmpname, "tftp");
else
sprintf(tmpname, "tftp-%d", i);
tftp[i].name = tmpname;
DEBUGP("port #%d: %d\n", i, ports[i]);
/* 将tftp的helper结构注册到一个全局链表&helpter里 */
ret=ip_conntrack_helper_register(&tftp[i]);
if (ret) {
printk("ERROR registering helper for port %d\n",
ports[i]);
fini();
return(ret);
}
ports_c++;
}
return(0);
}
6.1.3 ip_conntrack_expect结构
结构定义在ip_conntrack.h,ip_conntrack里的master变量就是这个结构,它的作用后面再说
struct ip_conntrack_expect
{
/* 链表头,在被某连接引用之前,所有expect结构都由此链表维护 */
struct list_head list;
/* 引用计数 */
atomic_t use;
/* 主连接的预期的子连接的链表 */
struct list_head expected_list;
/* 期待者,即预期连接对应的主连接,换句话说就是将此连接当作是其预期连接的连接... */
struct ip_conntrack *expectant;
/* 预期连接对应的真实的子连接 */
struct ip_conntrack *sibling;
/* 连接的tuple值 */
struct ip_conntrack_tuple ct_tuple;
/* 定时器 */
struct timer_list timeout;
/* 预期连接的tuple和mask,搜索预期连接时要用到的 */
struct ip_conntrack_tuple tuple, mask;
/* 预期连接函数,一般是NULL,有特殊需要时才定义 */
int (*expectfn)(struct ip_conntrack *new);
/* TCP协议时,主连接中描述子连接的数据起始处对应的序列号值 */
u_int32_t seq;
/* 跟踪各个多连接IP层协议相关的数据 */
union ip_conntrack_expect_proto proto;
/* 跟踪各个多连接应用层协议相关的数据 */
union ip_conntrack_expect_help help;
};
6.2数据包在连接跟踪模块中的行进过程
6.2.1 ip_conntrack_in阶段
数据包进入netfilter后,首先会调用ip_conntrack_in()函数,确认数据包未被检查过后,首先确定数据包使用的协议类型,如果是ICMP错误报文的话要做特别处理:
proto = ip_ct_find_proto((*pskb)->nh.iph->protocol);
proto是一个ip_conntrack_protocol指针,该函数根据sk_buff的protocol字段在全局的protocol_list变量里搜索匹配的ip_conntrack_protocol结构,bcm源码里注册了esp,gre,icmp,tcp,udp等协议
这里返回的是udp协议的ip_conntrack_protocol结构ip_conntrack_protocol_udp
接下来就要在连接跟踪表内查找属于该数据包的连接状态,如果能找到,说明该数据包是之前某连接的后续报文,那么只需要做一些标记处理就可以放行了,如果找不到,说明是一个新的连接,那么就创建一个新的连接状态,然后仍然放行(连接跟踪只是跟踪记录连接状态,不修改也不过滤)。将找到的,或者新建的连接状态返回给参数ct,ct就是一个ip_conntrack指针。
ct = resolve_normal_ct(*pskb, proto,&set_reply,hooknum,&ctinfo)
前面说了连接跟踪表是一个采用hash算法的链表数组,搜索hash表药先获取该连接的tuple结构,一般包括协议、来源/目的地址、来源/目的端口等,因不同协议而异(参考前面对tuple结构的描述)。resolve_normal_ct调用get_tuple函数获取报文的tuple结构,返回到第四个参数&tuple里:
get_tuple(skb->nh.iph, skb, skb->nh.iph->ihl*4, &tuple, proto)
tftp使用udp协议,固定端口是69。
然后就调用函数:
h = ip_conntrack_find_get(&tuple, NULL)
在连接跟踪表里查找连接,h是一个ip_conntrack_tuple_hash指针,是链表的一个节点
查找的时候先计算hash值
hash = hash_conntrack(tuple)
根据hash值找到链表的入口,然后遍历链表比较tuple
LIST_FIND(&ip_conntrack_hash[hash],
conntrack_tuple_cmp,
struct ip_conntrack_tuple_hash *,
tuple, ignored_conntrack);
查不到就新建一个连接,查到了就跳过这一步,这里假设是第一个数据包,那么肯定查不到
h = init_conntrack(&tuple, proto, skb)
新建连接时首先也要计算hash值(又算一遍),为了将来决定将它放入哪条链用
hash = hash_conntrack(tuple)
接下来计算相反方向的tuple,即repl_tuple
invert_tuple(&repl_tuple, tuple, protocol)
只是简单的将来源/目的颠倒而已
一个完整的连接状态是包括两个方向的tuple的,可以看到在ip_conntrack结构中
struct ip_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX]
定义了一个conntrack_tuple_hash数组,它包括tuplehash[IP_CT_DIR_ORIGINAL]和 tuplehash[IP_CT_DIR_REPLY]两个元素
然后就要为新的连接创建一个ip_conntrack结构了
atomic_set(&conntrack->ct_general.use, 1);
conntrack->ct_general.destroy = destroy_conntrack;
conntrack->tuplehash[IP_CT_DIR_ORIGINAL].tuple = *tuple;
conntrack->tuplehash[IP_CT_DIR_ORIGINAL].ctrack = conntrack;
conntrack->tuplehash[IP_CT_DIR_REPLY].tuple = repl_tuple;
conntrack->tuplehash[IP_CT_DIR_REPLY].ctrack = conntrack;
引用计数加1,初始化了一个destory函数,用来销毁该结构的。然后是两个方向的tuple
ctrack是tuple里的一个指针,用来指向它所匿属于的ip_conntrack(两个互相指,可能是为了传递参数的时候比较方便,不过这个ctrack在新的内核中被去掉了)
将所有状态的infos变量都初始化,infos是一个nf_ct_info结构的数组,实际上是一个nf_conntrack结构数组,包含一个引用计数和一个销毁函数,见前面关于infos的定义
for (i=0; i < IP_CT_NUMBER; i++)
conntrack->infos[i].master = &conntrack->ct_general;
ct_general是ip_conntrack的第一个变量,结合下面的语句:
skb->nfct = &h->ctrack->infos[*ctinfo]
我们就可以通过:
struct ip_conntrack *ct = (struct ip_conntrack *)nfct->master
来从sk_buff里的nfct变量获取其所属连接的ip_conntrack结构
因为是一个新的连接,所以要调用对应的协议的ip_conntrack_protocol结构里的new函数,这个函数的作用因协议不同而异,而udp协议的new函数实际上不做任何事情,直接返回1
protocol->new(conntrack, skb)
初始化一些超时相关的变量
init_timer(&conntrack->timeout);
conntrack->timeout.data = (unsigned long)conntrack;
conntrack->timeout.function = death_by_timeout;
根据tuple在预期连接全局链表里搜索预期的连接,比较函数是expect_cmp
expected = LIST_FIND(&ip_conntrack_expect_list, expect_cmp,
struct ip_conntrack_expect *, tuple)
到底什么是预期的连接,现在还不是很清楚,但ip_conntrack_expect_list全局变量还没出现过,应该是空的,所以什么也查不到
查不到预期的连接,就查找有没有对应的helper结构,跟据tuple对应的repl_tuple,在全局链表helpers里搜索,此时协议是udp,目的端口是69
conntrack->helper = ip_ct_find_helper(&repl_tuple)
可想而知,在conntrack_tftp模块初始化时注册的tftp的helper结构被找到了,还是来看一下最终的比较函数
第一个参数是当前数据包的repl_tuple,后两个参数是tftp初始化时ip_conntrack_helper里的tuple和mask
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));
}
mask用来区分哪些内容需要比较,哪些不需要,在ip_conntrack_tftp.c的初始化函数里,我们看到
tftp[i].mask.dst.protonum = 0xFFFF;
tftp[i].mask.src.u.udp.port = 0xFFFF;
所以比较函数里起作用的只有划线的两行,即协议和端口号,这里是UDP 69
最终将tftp的helper结构返回给了conntrack->helper
从init_conntrack函数返回,接下来确定数据包属于连接跟踪的哪个的状态,即ctinfo的值(是个枚举类型,这里应该是IP_CT_NEW,因为是第一个数据包),然后将对应的infos变量附给sk_buff *skb中的nfct,这样从skb里也可以得到本数据包的连接状态信息了
在前面见到过“ if ((*pskb)->nfct) return NF_ACCEPT;”之类的语句,意思就是每个包与一个状态结构关联,如果关联已存在,就不需要重复检查
skb->nfct = &h->ctrack->infos[*ctinfo]
从resolve_normal_ct函数里返回后调用ip_conntrack_protocol里的packet函数,对udp协议来说只是刷新一下超时位
ip_ct_refresh(conntrack, ip_ct_udp_timeout);
接下来要调用help函数了,之前把tftp注册的helper结构附给了conntrack-〉helper
ret = ct->helper->help(*pskb, ct, ctinfo)
最关键的地方到了,看看tftp_help函数做了些什么
static int tftp_help(struct sk_buff *skb,
struct ip_conntrack *ct,
enum ip_conntrack_info ctinfo)
{
struct tftphdr tftph;
struct ip_conntrack_expect *exp;
if (skb_copy_bits(skb, skb->nh.iph->ihl * 4 + sizeof(struct udphdr),
&tftph, sizeof(tftph)) != 0)
return NF_ACCEPT;
switch (ntohs(tftph.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);
/* 初始化了一个ip_conntrack_expect结构 */
exp = ip_conntrack_expect_alloc();
if (exp == NULL)
return NF_ACCEPT;
/* 将expect的tuple设置为连接的应答方向的tuple,然后将源/目的地址,目的端口以及协议的mask位都置了1,只有源端口置0,也就是tftp里的69端口对应的mask */
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 = 0xffff;
exp->expectfn = NULL;
DEBUGP("expect: ");
DUMP_TUPLE(&exp->tuple);
DUMP_TUPLE(&exp->mask);
/* 将创建的expect结构与当前的连接关联起来 */
ip_conntrack_expect_related(exp, ct);
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;
}
函数主要做了两件事情,首先初始化了一个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函数返回后,连接跟踪的第一阶段就结束了。
6.2.2 ip_confirm阶段
在3.1 init_or_cleanup()里面可以看到,数据包再次进入ip_conntrack模块是在NF_IP_LOCAL_IN点上,由ip_confirm函数来处理。
ip_confirm直接返回ip_conntrack_confirm(*pskb)
ip_conntrack_confirm检查一下数据包是否已经被检查过了,然后调用
__ip_conntrack_confirm(skb->nfct)
int __ip_conntrack_confirm(struct nf_ct_info *nfct)
{
unsigned int hash, repl_hash;
struct ip_conntrack *ct;
enum ip_conntrack_info ctinfo;
/* 获取该数据包的连接状态,就是通过前面提过的
struct ip_conntrack *ct = (struct ip_conntrack *)nfct->master
来实现的 */
ct = __ip_conntrack_get(nfct, &ctinfo);
/* 如果当前包不是一个初始方向的数据包,则直接返回,因为该连接状态在之前已经被添加到连接状态表里了,所以不需要再做什么。 */
if (CTINFO2DIR(ctinfo) != IP_CT_DIR_ORIGINAL)
return NF_ACCEPT;
/* 计算两个方向的hash值 */
hash = hash_conntrack(&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple);
repl_hash = hash_conntrack(&ct->tuplehash[IP_CT_DIR_REPLY].tuple);
IP_NF_ASSERT(!is_confirmed(ct));
DEBUGP("Confirming conntrack %p\n", ct);
WRITE_LOCK(&ip_conntrack_lock);
/* 根据计算出来的hash值,在hash表中找到合适的节点,将当前连接插入,并设置相应的超时和状态位。 */
if (!LIST_FIND(&ip_conntrack_hash[hash],
conntrack_tuple_cmp,
struct ip_conntrack_tuple_hash *,
&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple, NULL)
&& !LIST_FIND(&ip_conntrack_hash[repl_hash],
conntrack_tuple_cmp,
struct ip_conntrack_tuple_hash *,
&ct->tuplehash[IP_CT_DIR_REPLY].tuple, NULL)) {
list_prepend(&ip_conntrack_hash[hash],
&ct->tuplehash[IP_CT_DIR_ORIGINAL]);
list_prepend(&ip_conntrack_hash[repl_hash],
&ct->tuplehash[IP_CT_DIR_REPLY]);
ct->timeout.expires += jiffies;
add_timer(&ct->timeout);
atomic_inc(&ct->ct_general.use);
set_bit(IPS_CONFIRMED_BIT, &ct->status);
WRITE_UNLOCK(&ip_conntrack_lock);
return NF_ACCEPT;
}
WRITE_UNLOCK(&ip_conntrack_lock);
return NF_DROP;
}
/* 到此,一个连就正式的被添加到系统的连接状态表里面了。 */
6.2.3 连接跟踪对后续报文的处理
首先仍然是获取协议信息
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);
后面的过程略去了,和之前差不多