原帖地址:http://bbs.chinaunix.net/forum.php?mod=viewthread&tid=4082396&fromuid=29353251
最近在整理以前的笔记,在Godbach的鼓励下,准备写一个有关nf_conntrack的总结。与之前写的iptables算是姐妹篇,因为iptables和nf_conntrack应该算是netfilter中比较重要的两个模块了。同时它们之间也互相搭配应用。还是老样子,多上图,少上字。
我认为理解一个程序的实现主要从两点来理解,一个是业务的处理流程,另一个就是数据的组织结构。根据这两点,我们先来介绍几个重要的数据结构:
连接跟踪,顾名思义,就是识别一个连接上双方向的数据包,同时记录状态。下面看一下它的数据结构:
struct nf_conn {
/* Usage count in here is 1 for hash table/destruct timer, 1 per skb, plus 1 for any connection(s) we are `master' for */
struct nf_conntrack ct_general; /* 连接跟踪的引用计数 */
spinlock_t lock;
/* Connection tracking(链接跟踪)用来跟踪、记录每个链接的信息(目前仅支持IP协议的连接跟踪)。
每个链接由“tuple”来唯一标识,这里的“tuple”对不同的协议会有不同的含义,例如对tcp,udp
来说就是五元组: (源IP,源端口,目的IP, 目的端口,协议号),对ICMP协议来说是: (源IP, 目
的IP, id, type, code), 其中id,type与code都是icmp协议的信息。链接跟踪是防火墙实现状态检
测的基础,很多功能都需要借助链接跟踪才能实现,例如NAT、快速转发、等等。*/
struct nf_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX];
unsigned long status; /* 可以设置由enum ip_conntrack_status中描述的状态 */
struct nf_conn *master; /* 如果该连接是某个连接的子连接,则master指向它的主连接 */
/* Timer function; drops refcnt when it goes off. */
struct timer_list timeout;
union nf_conntrack_proto proto; /* 用于保存不同协议的私有数据 */
/* Extensions */
struct nf_ct_ext *ext; /* 用于扩展结构 */
};
enum ip_conntrack_status {
IPS_EXPECTED_BIT = 0, /* 表示该连接是个子连接 */
IPS_SEEN_REPLY_BIT = 1, /* 表示该连接上双方向上都有数据包了 */
IPS_ASSURED_BIT = 2, /* TCP:在三次握手建立完连接后即设定该标志。UDP:如果在该连接上的两个方向都有数据包通过,
则再有数据包在该连接上通过时,就设定该标志。ICMP:不设置该标志 */
IPS_CONFIRMED_BIT = 3, /* 表示该连接已被添加到net->ct.hash表中 */
IPS_SRC_NAT_BIT = 4, /*在POSTROUTING处,当替换reply tuple完成时, 设置该标记 */
IPS_DST_NAT_BIT = 5, /* 在PREROUTING处,当替换reply tuple完成时, 设置该标记 */
/* Both together. */
IPS_NAT_MASK = (IPS_DST_NAT | IPS_SRC_NAT),
/* Connection needs TCP sequence adjusted. */
IPS_SEQ_ADJUST_BIT = 6,
IPS_SRC_NAT_DONE_BIT = 7, /* 在POSTROUTING处,已被SNAT处理,并被加入到bysource链中,设置该标记 */
IPS_DST_NAT_DONE_BIT = 8, /* 在PREROUTING处,已被DNAT处理,并被加入到bysource链中,设置该标记 */
/* Both together */
IPS_NAT_DONE_MASK = (IPS_DST_NAT_DONE | IPS_SRC_NAT_DONE),
IPS_DYING_BIT = 9, /* 表示该连接正在被释放,内核通过该标志保证正在被释放的ct不会被其它地方再次引用。有了这个标志,当某个连接要被删
除时,即使它还在net->ct.hash中,也不会再次被引用。*/
IPS_FIXED_TIMEOUT_BIT = 10, /* 固定连接超时时间,这将不根据状态修改连接超时时间。通过函数nf_ct_refresh_acct()修改超时时间时检查该标志。 */
IPS_TEMPLATE_BIT = 11, /* 由CT target进行设置(这个target只能用在raw表中,用于为数据包构建指定ct,并打上该标志),用于表明这个ct是由CT target创建的 */
};
enum ip_conntrack_info {
IP_CT_ESTABLISHED(0), /* 表示这个数据包对应的连接在两个方向都有数据包通过,并且这是ORIGINAL初始方向数据包(无论是TCP、UDP、ICMP数据包,
只要在该连接的两个方向上已有数据包通过,就会将该连接设置为IP_CT_ESTABLISHED状态。不会根据协议中的标志位进行判断,
例如TCP的SYN等)。但它表示不了这是第几个数据包,也说明不了这个CT是否是子连接。*/
IP_CT_RELATED(1), /* 表示这个数据包对应的连接还没有REPLY方向数据包,当前数据包是ORIGINAL方向数据包。并且这个连接关联一个已有的连接,
是该已有连接的子连接,(即status标志中已经设置了IPS_EXPECTED标志,该标志在init_conntrack()函数中设置)。但无法
判断是第几个数据包(不一定是第一个)*/
IP_CT_NEW(2), /* 表示这个数据包对应的连接还没有REPLY方向数据包,当前数据包是ORIGINAL方向数据包,该连接不是子连接。但无法判断是
第几个数据包(不一定是第一个)*/
IP_CT_IS_REPLY(3), /* 这个状态一般不单独使用,通常以下面两种方式使用 */
IP_CT_ESTABLISHED + IP_CT_IS_REPLY(3), /* 表示这个数据包对应的连接在两个方向都有数据包通过,并且这是REPLY应答方向数据包。但它表示不了这是
第几个数据包,也说明不了这个CT是否是子连接。*/
IP_CT_RELATED + IP_CT_IS_REPLY(4), /* 这个状态仅在nf_conntrack_attach()函数中设置,用于本机返回REJECT,例如返回一个ICMP目的不可达报文,
或返回一个reset报文。它表示不了这是第几个数据包。*/
IP_CT_NUMBER = IP_CT_IS_REPLY * 2 - 1(5) /* 可表示状态的总数 */
};
除了上面的主要数据结构外,还有一些辅助数据结构,用于处理不同协议的私有信息、处理子连接、对conntrack进行扩展等。
三层协议(IPv4/IPv6)利用nf_conntrack_proto.c文件中的nf_conntrack_l3proto_register(struct nf_conntrack_l3proto *proto)和nf_conntrack_l3proto_unregister(struct nf_conntrack_l3proto *proto)在nf_ct_l3protos[]数组中注册自己的三层协议处理函数。
四层协议(TCP/UDP)利用nf_conntrack_proto.c文件中的nf_conntrack_l4proto_register(struct nf_conntrack_l4proto *l4proto)和nf_conntrack_l4proto_unregister(struct nf_conntrack_l4proto *l4proto)在nf_ct_protos[]数组中注册自己的四层协议处理函数。
处理一个连接的子连接协议,利用nf_conntrack_helper.c文件中的nf_conntrack_helper_register(struct nf_conntrack_helper *me)来注册nf_conntrack_helper结构,和nf_conntrack_expect.c文件中的nf_ct_expect_related_report(struct nf_conntrack_expect *expect, u32 pid, int report)来注册nf_conntrack_expect结构。
扩展连接跟踪结构(nf_conn)利用nf_conntrack_extend.c文件中的nf_ct_extend_register(struct nf_ct_ext_type *type)和nf_ct_extend_unregister(struct nf_ct_ext_type *type)进行扩展,并修改连接跟踪相应代码来利用这部分扩展功能。
了解了上面的数据结构,我们下面来看一下nf_conntrack的执行流程以及如何利用这些数据结构的。首先来看一下nf_conntrack模块加载时的初始化流程。
nf_conntrack的初始化,就是初始化上面提到的那些数据结构,它在内核启动时调用nf_conntrack_standalone_init()函数进行初始化的。初始化完成后,构建出如下图所示的结构图,只是不包含下图中与连接有关的信息(nf_conn和nf_conntrack_expect结构)。
上图中有三个HASH桶,ct_hash、expect_hash、helper_hash这三个HASH桶大小在初始化时就已确定,后面不能再更改。其中ct_hash、expect_hash可在加载nf_conntrack.ko模块时通过参数hashsize和expect_hashsize进行设定,而helper_hash不能通过参数修改,它的默认值是page/sizeof(helper_hash)。
下面再来看一个当创建子连接时,各个数据结构之间的关系。
nf_conn和nf_conntrack_expect都有最大个数限制。nf_conn通过全局变量nf_conntrack_max限制,可通过/proc/sys/net/netfilter/nf_conntrack_max文件在运行时修改。nf_conntrack_expect通过全局变量nf_ct_expect_max限制,可通过/proc/sys/net/netfilter/ nf_conntrack_expect_max文件在运行时修改。nf_conntrack_helper没有最大数限制,因为这个是通过注册不同协议的模块添加的,大小取决于动态协议跟踪模块的多少,一般不会很大。
上面两幅数据结构图中,大部分都已介绍过,下面介绍一下netns_ct数据结构,该结构主要用于linux的网络命名空间,表示nf_conntrack在不同的命名空间中都有一套独立的数据信息(这是另一个话题,这里就不再深入讨论了)。
struct netns_ct {
atomic_t count; /* 当前连接表中连接的个数 */
unsigned int expect_count; /* nf_conntrack_helper创建的期待子连接nf_conntrack_expect项的个数 */
unsigned int htable_size; /* 存储连接(nf_conn)的HASH桶的大小 */
struct kmem_cache *nf_conntrack_cachep; /* 指向用于分配nf_conn结构而建立的高速缓存(slab)对象 */
struct hlist_nulls_head *hash; /* 指向存储连接(nf_conn)的HASH桶 */
struct hlist_head *expect_hash; /* 指向存储期待子连接nf_conntrack_expect项的HASH桶 */
struct hlist_nulls_head unconfirmed; /* 对于一个链接的第一个包,在init_conntrack()函数中会将该包original方向的tuple结构挂入该链,这
是因为在此时还不确定该链接会不会被后续的规则过滤掉,如果被过滤掉就没有必要挂入正式的链接
跟踪表。在ipv4_confirm()函数中,会将unconfirmed链中的tuple拆掉,然后再将original方向和reply
方向的tuple挂入到正式的链接跟踪表中,即init_net.ct.hash中,这是因为到达ipv4_confirm()函数时,
应经在钩子NF_IP_POST_ROUTING处了,已经通过了前面的filter表。
通过cat /proc/net/nf_conntrack显示连接,是不会显示该链中的连接的。但总的连接个数(net->ct.count)
包含该链中的连接。
当注销l3proto、l4proto、helper、nat等资源或在应用层删除所有连接(conntrack -F)时,除了释放
confirmed连接(在net->ct.hash中的连接)的资源,还要释放unconfirmed连接(即在该链中的连接)
的资源。*/
struct hlist_nulls_head dying; /* 释放连接时,通告DESTROY事件失败的ct被放入该链中,并设置定时器,等待下次通告。
通过cat /proc/net/nf_conntrack显示连接,是不会显示该链中的连接的。但总的连接个数(net->ct.count)
包含该链中的连接。
当注销连接跟踪模块时,同时要清除正再等待被释放的连接(即该链中的连接)*/
struct ip_conntrack_stat __percpu *stat; /* 连接跟踪过程中的一些状态统计,每个CPU一项,目的是为了减少锁 */
int sysctl_events; /* 是否开启连接事件通告功能 */
unsigned int sysctl_events_retry_timeout; /* 通告失败后,重试通告的间隔时间,单位是秒 */
int sysctl_acct; /* 是否开启每个连接数据包统计功能 */
int sysctl_checksum;
unsigned int sysctl_log_invalid; /* Log invalid packets */
#ifdef CONFIG_SYSCTL
struct ctl_table_header *sysctl_header;
struct ctl_table_header *acct_sysctl_header;
struct ctl_table_header *event_sysctl_header;
#endif
int hash_vmalloc; /* 存储连接(nf_conn)的HASH桶是否是使用vmalloc()进行分配的 */
int expect_vmalloc; /* 存储期待子连接nf_conntrack_expect项的HASH桶是否是使用vmalloc()进行分配的 */
char *slabname; /* 用于分配nf_conn结构而建立的高速缓存(slab)对象的名字 */
};
从nf_conntrack的框架来看,它可用于跟踪任何三层和四协议的连接,但目前在三层协议只实现了IPv4和IPv6的连接跟踪,下面我们以IPv4为例,介绍一下该协议是如何利用nf_conntrack框架和netfilter实现连接跟踪的。有关netfilter框架,可参考我的另一个帖子
linux-2.6.35.6内核netfilter框架
首先介绍一下IPv4协议连接跟踪模块的初始化。
Ipv4连接跟踪模块注册了自己的3层协议,和IPv4相关的三个4层协议TCP、UDP、ICMP。注册后的结构图如下图所示:
在netfilter框架中利用nf_register_hook(struct nf_hook_ops *reg)、nf_unregister_hook(struct nf_hook_ops *reg)函数注册自己的钩子项,调用nf_conntrack_in()函数来建立相应连接。
如上面所示,IPv4连接跟踪模块已初始化完成,下面我们来看一下它创建连接的流程图。上图中连接的建立主要由三个函数来完成,即ipv4_conntrack_in(),ipv4_confirm()与ipv4_conntrack_local()。其中ipv4_conntrack_in()与ipv4_conntrack_local()都是通过调用函数nf_conntrack_in()来实现的,所以下面我们主要关注nf_conntrack_in()与ipv4_confirm()这两个函数。nf_conntrack_in()函数主要完成创建链接、添加链接的扩展结构(例如helper, acct结构)、设置链接状态等。ipv4_confirm()函数主要负责确认链接(即将链接挂入到正式的链接表中)、执行helper函数、启动链接超时定时器等。另外还有一个定时器函数death_by_timeout(), 该函数负责链接到期时删除该链接。
nf_conntrack_in()函数流程图
ipv4_confirm()函数流程图
death_by_timeout()函数流程图
上图中有一点需要说明,由于skb会引用nf_conn,同时会增加它的引用计数,所以当skb被释放时,也要释放nf_conn的引用计数,并且在nf_conn引用计数为0时,要释放全部资源。
当数据包经过nf_conntrack_in()和ipv4_confirm()函数处理流程后,就会建立起3楼第二幅结构图所示的连接nf_conn。同时这两个函数已经包含了子连接的处理流程,即流程图中help和exp的处理。子连接建立后的结构图如3楼第三幅结构图,主链接与子连接通过helper和expect关联起来。
连接跟踪到此就介绍完了,下面介绍IPv4基于nf_conntrack框架适合实现NAT转换的。先介绍IPv4-NAT初始化的资源,然后处理流程。
IPv4-NAT连接跟踪相关部分通过函数nf_nat_init()初始化