Linux连接跟踪源码分析

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.hLine89,其中包括一个计数器use和一个destroy函数。计数器use对本连接记录的公开引用次数进行计数 */
struct nf_conntrack ct_general;
/*其中的IP_CT_DIR_MAX是一个枚举类型ip_conntrack_dir(位于include/linux/netfilter_ipv4/ip_conntrack_tuple.hLine65)的第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.hLine33)进行位运算来判断连接的状态。其中主要的状态包括:
IPS_EXPECTED(_BIT),表示一个预期的连接
(_BIT),表示一个双向的连接
IPS_ASSURED(_BIT),表示这个连接即使发生超时也不能提早被删除
IPS_CONFIRMED(_BIT),表示这个连接已经被确认(初始包已经发出) */
unsigned long status;
/*其类型timer_list位于include/linux/timer.hLine11,其核心是一个处理函数。这个成员表示当发生连接超时时,将调用此处理函数*/
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.hLine12)共有5个成员:
IP_CT_ESTABLISHED 数据报属于已经完全建立的连接
IP_CT_RELATED 数据报属于一个新的连接,但此连接与一个现有连接相关(预期连接);或者是ICMP错误
IP_CT_NEW 数据报属于一个新的连接
IP_CT_IS_REPLY 数据报属于一个连接的回复
IP_CT_NUMBER 不同IP_CT类型的数量,这里为7NEW仅存于一个方向上 */
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协议,协议类型、源地址、目的地址这三个参数是识别连接所必须的,具体到各个协议,就要提取出各协议的唯一特征数据,如TCPUDP的源端口、目的端口,ICMPIDTYPECODE等值,这些值就是tuple结构要处理的数据。各协议相关数据是以联合(union)形式定义在tuple结构中的,netfilter缺省支持TCPUDPICMP协议,如果还要支持其他IP协议,如GREESPAHSCTP等,需要在联合中添加相应的协议参数值。
ip_conntrack_manipip_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_OUTPRE_ROUTING点可以看作是netfilter的入口,而POST_ROUTINGLOCAL_IN可以看作是出口。也就是说,在每个数据包刚一进入netfilter之后首先都会调用ip_conntrack_defrag做分片处理,紧接着就是对收到的和发出的数据包分别进行ip_conntrack_inip_conntrack_loaclip_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_opsip_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_tcpip_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);
/* 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_listip_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_tupledst子结构中*/
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()来添加/删除协议。

留下评论

邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据