Netfilter 连接跟踪与状态检测的实现
作者:九贱
www.skynet.org.cn
原创,欢迎转载,转载,请注明出处
内核版本:2.6.12
本文只是一部份,详细分析了连接跟踪的基本实现,对于ALG部份,还没有写,在整理笔记,欢迎大家提意见,批评指正。
1.什么是连接跟踪
连接跟踪(CONNTRACK),顾名思义,就是跟踪并且记录连接状态。Linux为每一个经过网络堆栈的数据包,生成一个新的连接记录项(Connection entry)。此后,所有属于此连接的数据包都被地分配给这个连接,并标识连接的状态。连接跟踪是防火墙模块的状态检测的基础,同时也是地址转换中实现SNAT和DNAT的前提。
那么Netfilter又是如何生成连接记录项的呢?每一个数据,都有“来源”与“目的”主机,发起连接的主机称为“来源”,响应“来源”的请求的主机即为目的,所谓生成记录项,就是对每一个这样的连接的产生、传输及终止进行跟踪记录。由所有记录项产生的表,即称为连接跟踪表。
2.连接跟踪表
Netfilter使用一张连接跟踪表,来描述整个连接状态,这个表在实现算法上采用了hash算法。我们先来看看这个hash 表的实现。
整个hash表用全局指针ip_conntrack_hash 指针来描述,它定义在ip_conntrack_core.c中:
struct list_head *ip_conntrack_hash;
这个hash表的大小是有限制的,表的大小由ip_conntrack_htable_size 全局变量决定,这个值,用户态可以在模块插入时传递,默认是根据内存大小计算出来的。
每一个hash节点,同时又是一条链表的首部,所以,连接跟踪表就由ip_conntrack_htable_size 条链表构成,整个连接跟踪表大小使用全局变量ip_conntrack_max描述,与hash表的关系是ip_conntrack_max = 8 * ip_conntrack_htable_size。
链表的每个节点,都是一个struct ip_conntrack_tuple_hash 类型:
/* Connections have two entries in the hash table: one for each way */
struct ip_conntrack_tuple_hash
{
struct list_head list;
struct ip_conntrack_tuple tuple;
};
这个结构有两个成员,list 成员用于组织链表。多元组(tuple) 则用于描述具体的数据包。
每个数据包基本的要素,就是“来源”和“目的”,从Socket套接字角度来讲,连接两端用“地址+端口”的形式来标识一个连接(对于没有端口的协议,如ICMP,可以使用其它办法替代),所以,这个数据包就可以表示为“来源地址/来源端口+目的地址/目的端口”,Netfilter用结构struct ip_conntrack_tuple 结构来封装这个“来源”和“目的”,封装好的struct ip_conntrack_tuple结构节点在内核中就称为“tuple”。终实现“封装”,就是根据来源/目的地址、端口这些要素,来进行一个具体网络封包到tuple的转换。结构定义如下:
/* The protocol-specific manipulable parts of the tuple: always in
network order! */
union ip_conntrack_manip_proto
{
/* Add other protocols here. */
u_int16_t all;
struct {
u_int16_t port;
} tcp;
struct {
u_int16_t port;
} udp;
struct {
u_int16_t id;
} icmp;
struct {
u_int16_t port;
} sctp;
};
/* The manipulable part of the tuple. */
struct ip_conntrack_manip
{
u_int32_t ip;
union ip_conntrack_manip_proto u;
};
/* This contains the information to distinguish a connection. */
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_int16_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 port;
} sctp;
} u;
/* The protocol. */
u_int8_t protonum;
/* The direction (for tuplehash) */
u_int8_t dir;
} dst;
};
struct ip_conntrack_tuple 中仅包含了src、dst两个成员,这两个成员基本一致:包含ip以及各个协议的端口,值得注意的是,dst成员中有一个dir成员,dir是direction 的缩写,标识一个连接的方向,后面我们会看到它的用法。
tuple 结构仅仅是一个数据包的转换,并不是描述一条完整的连接状态,内核中,描述一个包的连接状态,使用了struct ip_conntrack 结构,可以在ip_conntrack.h中看到它的定义:
struct ip_conntrack
{
……
/* These are my tuples; original and reply */
struct ip_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX];
};
这里仅仅是分析hash表的实现,所以,我们仅需注意struct ip_conntrack结构的后一个成员tuplehash,它是一个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],这样,一条完整的连接已经跃然纸上了。
后一要注意的问题,就是对于每一条连接,寻找链表在hash表的入口,也就是如计算hash值。我们关心的是一条连接,连接是由“请求”和“应答”的数据包组成,数据包会被转化成tuple,所以,hash值就是根据tuple,通过一定的hash算法实现,这样,整个hash表如下图所示:
如图,小结一下:
n 整个hash表用ip_conntrack_hash 指针数组来描述,它包含了ip_conntrack_htable_size个元素,用户态可以在模块插入时传递,默认是根据内存大小计算出来的;
n 整个连接跟踪表的大小使用全局变量ip_conntrack_max描述,与hash表的关系是ip_conntrack_max = 8 * ip_conntrack_htable_size;
n hash链表的每一个节点是一个struct ip_conntrack_tuple_hash结构,它有两个成员,一个是list,一个是tuple;
n Netfilter将每一个数据包转换成tuple,再根据tuple计算出hash值,这样,就可以使用ip_conntrack_hash[hash_id]找到hash表中链表的入口,并组织链表;
n 找到hash表中链表入口后,如果链表中不存在此“tuple”,则是一个新连接,就把tuple插入到链表的合适位置;
n 图中两个节点tuple[ORIGINAL]和tuple[REPLY],虽然是分开的,在两个链表当中,但是如前所述,它们同时又被封装在ip_conntrack结构的tuplehash数组中,这在图中,并没有标注出来;
n 链表的组织采用的是双向链表,上图中没有完整表示出来;
当然,具体的实现要稍微麻烦一点,主要体现在一些复杂的应用层协议上来,例如主动模式下的FTP协议,服务器在连接建立后,会主动打开高端口与客户端进行通讯,这样,由于端口变换了,我们前面说的连接表的实现就会遇到麻烦。Netfilter为这些协议提供了一个巧秒的解决办法,我们在本章中,先分析连接跟踪的基本实现,然后再来分析Netfilter对这些特殊的协议的支持的实现。
3.连接跟踪的初始化
3.1 初始化函数
ip_conntrack_standalone.c 是连接跟踪的主要模块: