内核版本 2.6.21
一、ip层netfilter的连接跟踪模块的概念及相关的数据结构分析
连接跟踪(CONNTRACK)就是跟踪并且记录连接状态。包括 TCP 、UDP、ICMP 等协议类型的连接。其主要是判断该数据包是什么状态。根据数据包的源ip地址、目的ip地址、源端口、目的端口、协议号来确定一条连接。
因为连接跟踪支持TCP、UDP、ICMP等协议,而不同的协议,其处理上会有一些不同,因此增加了协议相关的连接跟踪结构,即nf_conntrack_protocol、nf_conntrack_l3proto,其中nf_conntrack_protocol是四层协议的连接跟踪相关的结构,这个结构定义了四层协议相关的接口函数,使用这些函数我们能从一个skb中,抽取到在协议层上唯一识别一个数据流的信息。而nf_conntrack_l3proto为连接跟踪中三层协议相关的结构,这个结构定义了三层协议相关的接口函数,使用这些函数我们能从一个skb中,抽取到在协议层上唯一识别一个数据流的信息。结合三层与四层协议的这些函数,我们就能唯一确定一条流了,这两个协议层相关的结构是实现数据流从个性到共性的归纳依据。
那么连接跟踪模块在netfilter中与哪些hook有关呢,即一个数据包从进入一个接口到从一个接口发出需要经历哪些过程呢?
1、连接跟踪模块的hook回调函数分别注册在NF_IP_PRE_ROUTING、
NF_IP_LOCAL_OUT、NF_IP_POST_ROUTING、NF_IP_LOCAL_IN,从netfilter的角 度来说,NF_IP_PRE_ROUTING、NF_IP_LOCAL_OUT为netfilter的入口,而 NF_IP_POST_ROUTING、NF_IP_LOCAL_IN为netfilter的出口hook点。
那连接跟踪模块就是在netfilter的入口处,最先处理进入的数据包,根据数据包三层与四层协议的特点抽象出一个连接跟踪项;而在数据包的出口处对一个连接跟踪项进行确认。
2、如果仅考虑连接跟踪模块的hook函数,则对于不同的数据包,其经过natfilter的路 径大概有三个,下面就按这三个路径进行分析,看不同路径下调用的连接跟踪
hook函数是否相同。
A)发往本机的数据包
ipv4_conntrack_defrag->ipv4_conntrack_in->ipv4_conntrack_help->ipv4_confirm
B)需要本机转发的数据包
ipv4_conntrack_defrag->ipv4_conntrack_in->ipv4_conntrack_help->ipv4_confirm
C)本机发出的数据包
ipv4_conntrack_local->ipv4_conntrack_help->ipv4_confirm
上面的函数中,ipv4_conntrack_defrag用于对分段数据进行重组的,ipv4_conntrack_in对数据流创建相应的连接跟踪项,而ipv4_conntrack_help函数实现期望连接的创建、协议相关ALG功能的实现等功能,ipv4_confirm则是将新的连接跟踪项添加到连接跟踪表中。
在开始分析连接跟踪前,还有两个问题需要描述一下:
1. 有些应用层协议的c/s模式的设计中,需要两条通道,以ftp为例,需要建立一条server port 21的命令通道,还需要建立一条server port 20 /(端口号大于1024)的数据通道用于数据传输。这时候如果还按照平常的流程分别为这两条数据通道建立两条连接跟踪项,而且每条连接跟踪项的状态都是从NEW_STATE开始进行转变,反而有些问题,因为先建立的命令通道相关的连接跟踪项已经从NEW_STATE转换为IP_CT_ESTABLISHED了,这时对于新建立的数据通道,其连接跟踪状态也应该可以直接设置成 IP_CT_ESTABLISHED了,而不必从NEW_STATE开始经过一系列的转换了。
2. 有些应用层协议会存放三层的ip地址,当我们对从lan侧出去的数据包的三层ip地址进行了SNAT映射后,数据包的三层源地址就转变成wan侧地址了,但是应用层协议中携带的源ip地址仍然是lan侧内网的地址,如果不把应用层携带了源ip地址也进行SNAT映射,则在应用层协议之间建立的数据通道的数据可能就无法进入到lan侧。
基于以上问题,连接跟踪模块提出了期望连接与helper函数两个概念。
其中期望连接就是解决第一个问题,当创建一个连接跟踪项时,会根据reply方向的tuple结构查找helpers链表,并与该连接跟踪项进行绑定,接着在调用函数ipv4_conntrack_help时就调用该连接跟踪项的helper函数,注册的helper函数如果有期望连接的概念,就会创建一个期望连接,并添加到期望连接表中。
而helper函数的另一个功能就是用来解决第二个问题的,但是要想完整的解决第二个问题,还需要一个nat转换函数,用于对应用层中携带的ip地址进行NAT转换操作。
老规矩,还是先来分析相应的数据结构,结合代码与上面的分析,我们梳理出如下主要的数据结构:nf_conntrack_l3proto、nf_conntrack_protocol、nf_conn、nf_conntrack_tuple、nf_conntrack_expect
下面我们就分析下这几个数据结构。
这个结构即是连接跟踪项的抽象,这个结构中包括数据连接项的两个方向的tuple值,tuple变量可以唯一确定一个数据包属于哪一条数据连接,而且也用来查找已注册的helper函数、NAT转换也以tuple变量中的ip地址、端口号等值实现功能。
根据数据连接抽象出来的数据结构struct nf_conn
{
/*连接跟踪的引用计数及指向销毁一个连接跟踪项的函数指针*/
struct nf_conntrack ct_general;
/*一个数据连接对应的tuple结构变量,包括数据包的原始发送方向与数据包的
应答方向的tuple,也就是唯一确定一条数据连接的五元组信息
NAT操作就是通过这个功能起作用
*/
struct nf_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX];
/*连接跟踪项的状态,包括连接跟踪项的状态以及NAT转换是否设置的状态*/
unsigned long status;
/*连接跟踪的超时时间*/
struct timer_list timeout;
#ifdef CONFIG_NF_CT_ACCT
/* Accounting Information (same cache line as other written members) */
struct ip_conntrack_counter counters[IP_CT_DIR_MAX];
#endif
/*当一个连接跟踪模块是一个期望连接跟踪项时,则该成员
指向该连接跟踪项的主连接跟踪项。这个牵扯到期望连接
*/
struct nf_conn *master;
/*如果该连接有期望连接,则该值统计期望连接的个数*/
unsigned int expecting;
/*数据连接跟踪项的id*/
unsigned int id;
/*数据连接的helper结构,通过helper结构的help函数,能够实现ALG等功能(sip 协议就需要通过help函数
实现ALG功能)。*/
struct nf_conntrack_helper *helper;
u_int32_t features;
/*该数据连接跟踪项所关联的四层协议相关数据结构*/
union nf_conntrack_proto proto;
/*用于防火墙的mark,通过iptables的mark模块,能够实现对数据流打mark的功 能*/
#if defined(CONFIG_NF_CONNTRACK_MARK)
u_int32_t mark;
#endif
/*ftp协议相关的数据结构*/
union nf_conntrack_help *help;
/*该数据连接跟踪项所关联的三层协议相关数据结构*/
union {
struct nf_conntrack_ipv4 *ipv4;
} l3proto;
/*可变长度数组*/
void *data[0];
};
这个函数几乎将所有连接跟踪相关的数据结构都包含了,首先是数据结构nf_conntrack,该数据结构主要是用于对连接跟踪项的引用计数以及销毁连接跟踪项的函数指针。
struct nf_conntrack {
atomic_t use;/*引用计数*/
void (*destroy)(struct nf_conntrack *);/*销毁函数*/
};
这个数据结构虽然没有显式的出现在上面的数据结构中,其被包裹在数据结构nf_conntrack_tuple_hash中了,我们先分析nf_conntrack_tuple,然后再顺带分析nf_conntrack_tuple_hash
该数据结构为将数据流抽象成连接跟踪项的归纳依据,根据该结构成员值,我们能唯一确定一个数据流,下面我们分析一下这个数据结构。
该结构成员包括源ip地址、目的ip地址、源端口号/源序列、目的端口号/目的序列号、协议号等信息。以及最重要的,该tuple结构在所属的连接中是属于origin还是reply方向
struct nf_conntrack_tuple
{
/*源方向上的三层、四层协议相关的识别信息*/
struct nf_conntrack_man src;
/* 下面的信息时目的方向的三层、四层协议相关的识别信息*/
struct {
union {
/*当三层协议不是ipv4、ipv6时,使用该成员存储*/
u_int32_t all[NF_CT_TUPLE_L3SIZE];
/*当三层协议是ipv4时,使用该成员存储ipv4地址*/
u_int32_t ip;
/*当三层协议是ipv6成员时,使用该成员存储ipv6地址*/
u_int32_t ip6[4];
} u3;
union {
/* 当四层协议不是tcp、udp、icmp、stcp时,则使用该成员存储四层协议识别信息*/
u_int16_t all;
struct {
/*对于tcp来说,则通过端口号进行识别*/
u_int16_t port;
} tcp;
struct {
/*对于udp来说,则通过端口号进行识别*/
u_int16_t port;
} udp;
struct {
/*对于 icmp 来说,则需要根据类型与代码号进行判断*/
u_int8_t type, code;
} icmp;
struct {
u_int16_t port; //对于sctp,也是根据端口号进行识别
} sctp;
} u;
/* 四层协议号 */
u_int8_t protonum;
/* The direction (for tuplehash) */
u_int8_t dir;
} dst;
};
以上仔细分析了目的方向上的三层、四层协议的信息,源方向上的三层、四层协议识别信息的结构体nf_conntrack_man ,对我们来说还很神秘,下面分析之。
该结构体主要用来存储一个三层ip地址、四层端口号、三层协议号等信息。
struct nf_conntrack_man
{
/*三层协议相关的识别信息*/
union nf_conntrack_man_l3proto u3;
/*四层协议相关的识别信息*/
union nf_conntrack_man_proto u;
/* 三层协议号 */
u_int16_t l3num;
};
1.2.1.1 nf_conntrack_man_l3proto
这是一个联合体,定义如下:
该联合体主要用来存储ipv4地址或者v6地址或者其他三层协议的识别信息。
union nf_conntrack_man_l3proto {
/*当三层协议非ipv4、ipv6时,使用该成员值存储三层识别信息*/
u_int32_t all[NF_CT_TUPLE_L3SIZE];
/*当三层协议是ipv4时,使用该成员存储ipv4地址*/
u_int32_t ip;
/*当三层协议是ipv6时,使用该成员存储ipv6地址*/
u_int32_t ip6[4];
};
1.2.1.2 nf_conntrack_man_proto
该联合体主要用来存储四层协议的识别信息,包括端口号
或者序列号等
union nf_conntrack_man_proto
{
/* 其他四层协议对应的识别信息,当不是tcp、udp、icmp、sctp时,使用该成员存储四层协议的识别信息*/
u_int16_t all;
/*当是tcp协议时,使用该成员存储四层协议识别信息,即端口号*/
struct {
u_int16_t port;
} tcp;
/*当是udp协议时,使用该成员存储四层协议识别信息,即端口号*/
struct {
u_int16_t port;
} udp;
/*当是icmp协议时,使用该成员存储四层协议识别信息,即类型与代码值*/
struct {
u_int16_t id;
} icmp;
/*当是sctp协议时,使用该成员存储四层协议识别信息,即端口号*/
struct {
u_int16_t port;
} sctp;
};
当我们一层一层剥开 nf_conntrack_man时,发现其与nf_conntrack_tuple中的三层、四层目的协议识别成员的定义几乎是一致的,哎。。。。
nf_conntrack_tuple结构介绍完了,那我们就顺带把nf_conntrack_tuple_hash介绍一下,定义如下:
与nf_conntrack_tuple相比,就多了一个链表成员,这个链接成员主要是起到将nf_conntrack_tuple_hash链接到数组nf_conntrack_hash[]的某一个链表中。主要是在ipv4_confirm中,对于一个刚创建的连接跟踪项,将其origin、reply方向的tuple_hash插入到数组nf_conntrack_hash[]的某一个链表中,当再有该数据流到来时,直接根据tuple值就可以很快的找到nf_conn。
struct nf_conntrack_tuple_hash
{
/*链表结构*/
struct list_head list;
struct nf_conntrack_tuple tuple;
};
接下来分析一下这个数据结构,这个数据结构也是很重要的,通过这个结构的help指针,能够实现期望连接的建立以及ALG的功能,下面来看下这个函数的定义。
struct nf_conntrack_helper
{
/*链表结构,实现将所有的nf_conntrack_helper变量链接在一起*/
struct list_head list;
/* helper变量的名称*/
const char *name;
/*标识该变量是否属于一个模块*/
struct module *me;
/*允许的最大期望连接*/
unsigned int max_expected;
/*超时定时器*/
unsigned int timeout;
/* 该helper结构属于哪几条数据流,通过tuple与mask结构,能够判断出一个连接跟踪项是否可以拥有该helper变量。*/
struct nf_conntrack_tuple tuple;
struct nf_conntrack_tuple mask;
/* help函数指针,实现创建期望连接与ALG等功能的函数 */
int (*help)(struct sk_buff **pskb,
unsigned int protoff,
struct nf_conn *ct,
enum ip_conntrack_info conntrackinfo);
int (*to_nfattr)(struct sk_buff *skb, const struct nf_conn *ct);
};
相比于连接跟踪项是根据数据包创建的,而helper变量则是在系统初始化或者模块初始化时定义并注册的,通过该结构的tuple、mask变量,能够抽象出一类应用层协议所具有的特性,比如ftp协议,我们可以只定义server 的端口值为21,所有符合四层server的端口值为21的连接跟踪项均可以将其nf_conn->helper指针指向该helper变量的首地址。
helper变量的注册函数为nf_conntrack_helper_register,通过该函数将一个已初始化的helper变量插入到helpers链表中,在创建新的数据连接项时,通过遍历helpers链表,并比较tuple、mask,即可以找到符合要求的helper变量。
该结构为四层协议相关的数据结构,定义如下:
union nf_conntrack_proto {
/* insert conntrack proto private data here */
struct ip_ct_sctp sctp;
struct ip_ct_tcp tcp;
struct ip_ct_icmp icmp;
struct nf_ct_icmpv6 icmpv6;
};
由于四层协议相关的连接跟踪代码,我还没有理解,此处先不分析这个结构。
1.5 nf_conntrack_ipv4
/该数据连接跟踪项所关联的三层协议相关数据结构
union {
struct nf_conntrack_ipv4 *ipv4;
} l3proto;
struct nf_conntrack_ipv4 {
#ifdef CONFIG_IP_NF_NAT_NEEDED
struct nf_conntrack_ipv4_nat *nat;
#endif
};
这个数据结构的用法我还不清楚,希望后面分析代码时能补充。
该结构为三层协议相关的连接跟踪相关的数据结构,通过该函数,我们可以从一个数据包中抽象出tuple结构中三层协议相关的成员变量等。
struct nf_conntrack_l3proto
{
/* 链表指针,指向下一个nf_conntrack_l3proto结构变量*/
struct list_head list;
/*三层协议号*/
u_int16_t l3proto;
/*协议名称*/
const char *name;
/*从一个数据包中抽象出三层协议相关的五元组值*/
int (*pkt_to_tuple)(const struct sk_buff *skb, unsigned int nhoff,
struct nf_conntrack_tuple *tuple);
对于一个给定的tuple结构,对其三层相关元组进行取反操作并赋值给
新的tuple变量inverse
*/
int (*invert_tuple)(struct nf_conntrack_tuple *inverse,
const struct nf_conntrack_tuple *orig);
/*打印一个tuple变量中三层相关的信息*/
int (*print_tuple)(struct seq_file *s,
const struct nf_conntrack_tuple *);
/*打印一个数据连接变量中三层相关的信息*/
int (*print_conntrack)(struct seq_file *s, const struct nf_conn *);
/*对数据包进行三层相关的处理,并修改数据连接相关的值*/
int (*packet)(struct nf_conn *conntrack,
const struct sk_buff *skb,
enum ip_conntrack_info ctinfo);
/*当创建一个新的数据连接时,调用该函数进行初始化*/
int (*new)(struct nf_conn *conntrack, const struct sk_buff *skb);
/*删除一个数据连接变量时被调用*/
void (*destroy)(struct nf_conn *conntrack);
/*
* Called before tracking.
* *dataoff: offset of protocol header (TCP, UDP,...) in *pskb
* *protonum: protocol number
*/
int (*prepare)(struct sk_buff **pskb, unsigned int hooknum,
unsigned int *dataoff, u_int8_t *protonum);
u_int32_t (*get_features)(const struct nf_conntrack_tuple *tuple);
int (*tuple_to_nfattr)(struct sk_buff *skb,
const struct nf_conntrack_tuple *t);
int (*nfattr_to_tuple)(struct nfattr *tb[],
struct nf_conntrack_tuple *t);
/* Module (if any) which this is connected to. */
struct module *me;
};
该结构为四层协议相关的连接跟踪相关的数据结构,通过该函数,我们可以从一个数据包中抽象出tuple结构中四层协议相关的成员变量等。
struct nf_conntrack_protocol
{
/*用于将已注册的四层协议的nf_conntrack连接在一起*/
struct list_head list;
u_int16_t l3proto;/*三层协议号*/
u_int8_t proto;/*四层协议号*/
const char *name;/*协议名称*/
/*根据数据包的四层协议特点,抽象出数据包在四层上的五元组信息
包括port num 或者type 与code值等*/
int (*pkt_to_tuple)(const struct sk_buff *skb,
unsigned int dataoff,
struct nf_conntrack_tuple *tuple);
/*根据原始的五元组信息,计算出一个反转五元组信息*/
int (*invert_tuple)(struct nf_conntrack_tuple *inverse,
const struct nf_conntrack_tuple *orig);
/*打印一个五元组信息对应的nf_conntrack_tuple变量*/
int (*print_tuple)(struct seq_file *s,
const struct nf_conntrack_tuple *);
/*打印nf_conn的相关信息,不同的四层协议选择打印的内容可能不同*/
int (*print_conntrack)(struct seq_file *s, const struct nf_conn *);
/*对数据连接结构中协议层相关的参数进行配置。*/
int (*packet)(struct nf_conn *conntrack,
const struct sk_buff *skb,
unsigned int dataoff,
enum ip_conntrack_info ctinfo,
int pf,
unsigned int hooknum);
/*当刚创建一个该协议的数据连接时,需要调用该函数
对数据连接中该协议层相关的值进行初始化*/
int (*new)(struct nf_conn *conntrack, const struct sk_buff *skb,
unsigned int dataoff);
/*当删除一个该协议相关的nf_conn时,需要调用该函数*/
void (*destroy)(struct nf_conn *conntrack);
/*错误处理函数*/
int (*error)(struct sk_buff *skb, unsigned int dataoff,
enum ip_conntrack_info *ctinfo,
int pf, unsigned int hooknum);
/* nfnetink相关的变量,这部分代码我还没有阅读到,先搁置*/
int (*to_nfattr)(struct sk_buff *skb, struct nfattr *nfa,
const struct nf_conn *ct);
/* convert nfnetlink attributes to protoinfo */
int (*from_nfattr)(struct nfattr *tb[], struct nf_conn *ct);
int (*tuple_to_nfattr)(struct sk_buff *skb,
const struct nf_conntrack_tuple *t);
int (*nfattr_to_tuple)(struct nfattr *tb[],
struct nf_conntrack_tuple *t);
/* Module (if any) which this is connected to. */
struct module *me;
};
通过1.5、1.6的这两个协议相关的数据结构,我们就可以从一个数据流中,归纳出一个头数据连接跟踪项,且该连接跟踪项能够唯一确定一条流。
该数据结构是对一个期望连接的抽象
struct nf_conntrack_expect
{
/*将该变量连接到一个链表中 */
struct list_head list;
/* 确定一条期望连接所对应的tuple与mask,与helper结构体中的tuple、mask的作用类似 */
struct nf_conntrack_tuple tuple, mask;
/*相应的函数指针 */
void (*expectfn)(struct nf_conn *new,
struct nf_conntrack_expect *this);
/* 指向主数据连接跟踪项 */
struct nf_conn *master;
/* 超时计数器*/
struct timer_list timeout;
/* 引用计数*/
atomic_t use;
/* Unique ID */
unsigned int id;
/* Flags */
unsigned int flags;
#ifdef CONFIG_NF_NAT_NEEDED
/* This is the original per-proto part, used to map the
* expected connection the way the recipient expects. */
union nf_conntrack_manip_proto saved_proto;
/* Direction relative to the master connection. */
enum ip_conntrack_dir dir;
#endif
};
以上就是连接跟踪所需要的主要数据结构,另外还有几个全局变量,需要说明一下。
struct list_head nf_conntrack_expect_list;
struct list_head *nf_conntrack_hash;
static LIST_HEAD(helpers);
static LIST_HEAD(unconfirmed);
这四个变量都是list_head类型的变量,其中链表 nf_conntrack_expect_list用于将所有已注册的nf_conntrack_expect变量连接在一起
链表helpers用于将所有已注册的nf_conntrack_helper连接在一起
链表unconfirmed用于将所有刚创建的,且没有confirm的连接跟踪项的origin tuple连接在一起
数组nf_conntrack_hash中的每一个链表,都用来连接以创建成功的,且已经confirm的连接跟踪项。
至此,已经将连接跟踪模块相关的数据结构都介绍完了,最后一张图来结束本节,这张图将上面介绍的主要数据结构之间的关系,以及它们与上面四个全局变量之间的关系都描述了。
二、 ip层netfilter的连接跟踪模块初始化
在上一节中分析了连接跟踪模块相关的数据结构,本节就开始分析连接跟踪模块相关的初始化,下一节理解连接跟踪模块的hook机制。
在分析连接跟踪模块代码之前,先说明几点:
1.连接跟踪模块的helper结构能够实现期望连接的建立以及相关协议的ALG功能。
2.连接跟踪为NAT或者状态防火墙的实现提供了依据
连接跟踪模块的初始化过程分别在三个地方进行,一个用于注册连接跟踪模块相关的hook回调函数;一个用于创建连接跟踪项与期望连接项相关的slab缓存;一个用于注册连接跟踪中协议相关的变量。
主要是 nf_conntrack_init进行初始化操作,该函数定义在nf_conntrack_core.c中。
主要完成以下功能:
a.设置nf_conntrack_htable_size、nf_conntrack_max的值
b.为nf_conntrack_hash申请内存并初始化
c.为连接跟踪项与期望连接跟踪项创建slab缓存。
在nf_conntrack_standalone.c中定义的函数,主要是调用上1.1中介绍的函数进行初始化,然后在/proc/net文件系统中创建相应的文件以及在/proc/sys/net中创建连接跟踪模块相关的文件
/*
nf_conntrack_standalone的初始化与销毁函数
对于初始化:
1.调用nf_conntrack_init进行连接模块相关的初始化,主要是
设置连接跟踪数的最大值、为连接跟踪项或者期望连接跟踪
创建slab缓存等操作。
2.在proc/net目录下创建连接跟踪相关的文件,主要nf_conntrack、nf_conntrack_expect
查看如下:
3.在/proc/sys文件系统中创建连接跟踪相关的内容。
*/
Ipv4协议中,注册的连接跟踪模块相关的回调函数有如下几个:
/*
在连接跟踪PRE_ROUTING链上注册的hook回调函数,主要
是分段数据包进行重组。优先级高于ipv4_conntrack_in
*/
然后在nf_conntrack_l3proto_ipv4.c的init_or_cleanup里,通过nf_register_hook将以上的nf_hook_ops添加到nf_hooks[][]数组的相应的链表中。
定义ipv4协议相关的nf_conntrack_l3proto变量,其中比较重要的是ipv4_pkt_to_tuple、
ipv4_invert_tuple、ipv4_tuple_to_nfattr、 ipv4_nfattr_to_tuple。下面一一分析之。
功能:根据ip头部获取源ip地址与目的ip地址,并写入tuple变量中。
功能:根据原始的tuple的源ip、目的ip值,设置reply的tuple值,新的tuple值的源、目的ip值与原始的tuple的源ip、目的ip值是反过来的。
/*
1. 计算数据包的三层数据部分相对于skb->data的偏移量。
2.获取四层协议的协议号
*/
功能:tuple结构中的三层源ip、目的ip地址按照nfnetlink规定的形式进行填充
功能:将nfnetlink消息传递过来的变量,转换成tuple结构中的三层源ip、目的ip地址,
然后在nf_conntrack_l3proto_ipv4.c的init_or_cleanup里,通过nf_conntrack_l3proto_register将nf_conntrack_l3proto_ipv4的地址存放在nf_ct_l3protos[PF_INET]。
定义tcp协议相关的nf_conntrack_protocol 变量,其中比较重要的是tcp_pkt_to_tuple、
tcp_invert_tuple、nf_ct_port_tuple_to_nfattr、 nf_ct_port_nfattr_to_tuple。这几个函数实现的功能与nf_conntrack_l3proto_ipv4中相应的函数相似,只不过ipv4中针对的是ip地址,此处针对的是端口号而已。
而函数tcp_packet主要是针对tcp协议的状态变化而定义的,主要用于状态防火墙的。
然后在nf_conntrack_l3proto_ipv4.c的init_or_cleanup里,通过nf_conntrack_protocol_register将nf_conntrack_protocol_tcp4的地址存放在nf_ct_protos[PF_INET][TCP]。
至此,分析完了连接跟踪模块的初始化代码部分。
三、 ip层netfilter的连接跟踪模块代码分析
上一节分析了连接跟踪模块相关的初始化代码,本节分析连接中hook函数。在分析之前,我们再次回顾一下数据包在连接跟踪模块中的走向。
发往本地的数据:
对于连接跟踪来说,只在hook点NF_IP_PRE_ROUTING、NF_IP_LOCAL_IN两个hook点进入到连接跟踪模块,按优先级顺序,依次调用
ipv4_conntrack_defrag->ipv4_conntrack_in->ipv4_conntrack_help->ipv4_confirm,其中ipv4_conntrack_defrag主要是对分段数据包进行重组操作;接着调用ipv4_conntrack_in为数据包的nfct指针进行赋值以及更新连接跟踪项的状态等;然后调用ipv4_conntrack_help执行数据包对应的连接跟踪项的helper指针,实现创建期望连接或者ALG等功能;最后调用ipv4_confirm将一个未确认的连接跟踪项进行确认操作,并加入确认链表中。
本地转发的数据:
对于连接跟踪来说,只在hook点NF_IP_PRE_ROUTING、NF_IP_POST_ROUTING两个hook点进入到连接跟踪模块,按优先级顺序,依次调用
ipv4_conntrack_defrag->ipv4_conntrack_in->ipv4_conntrack_help->ipv4_confirm,我们发现调用的函数与发往本机的数据包调用的函数及顺序是一样的。
本机发出的数据:
对于连接跟踪来说,只在hook点NF_IP_LOCAL、NF_IP_POST_ROUTING两个hook点进入到连接跟踪模块,按优先级顺序,依次调用
ipv4_conntrack_defrag->ipv4_conntrack_local->ipv4_conntrack_help->ipv4_confirm,我们发现调用的函数与发往本机的数据包调用的函数及顺序相比,只有ipv4_conntrack_local与ipv4_conntrack_in不一样,相比于ipv4_conntrack_in,ipv4_conntrack_local增加了对数据包大小的判断。
下面我们就对这些函数进行分析。
功能:对分段数据包实现数据重组操作。
主要是调用函数 nf_ct_ipv4_gather_frags实现数据包重组。
1.1 nf_ct_ipv4_gather_frags
而nf_ct_ipv4_gather_frags主要是通过函数ip_defrag实现分段重组的。
这个数据主要实现以下功能:
a)当是一个新的数据包时,则为该数据包创建对应的连接跟踪项,创建成果后进入c
b)当已经为传递过来的数据包创建了连接跟踪项,则进入c
c)更新连接跟踪项的状态
d)为数据包的nfct指针赋值
该函数只是简单的调用nf_conntrack_in,看来实现上述功能的函数即是nf_conntrack_in。
该函数主要被PREROUTING、与LOCAL_OUT两个hook点时,才会被调用
功能: 实现对数据的连接跟踪,更新连接跟踪项的连接状态
目前就是根据五元组识别一条数据流
这个函数可以说是连接跟踪模块的重中之重,
首先它将数据包的类型按如下
几个类型处理:
1.当这是一个新的数据流的第一个数据包时,则会根据该数据包的五元组信息
创建一个新的连接跟踪项,并初始化该连接跟踪项的tuple_hash、helper的值,最后
将该连接跟踪项的原始方向的tuple_hash添加到unconfirmed链表中,且该连接不是期望 连接
2.当这是一个新的数据流的第一个数据包时,则会根据该数据包的五元组信息
创建一个新的连接跟踪项,并初始化该连接跟踪项的tuple_hash、helper的值,最后
将该连接跟踪项的原始方向的tuple_hash添加到unconfirmed链表中,且该连接是期望连 接
3. 当该数据包是原始方向的非第一个数据包,但当该数据包进入连接跟踪模块时,连接跟踪模块还没有收到reply方向的数据包
4.当该数据包是原始方向的非第一个数据包, 且到改数据包进入连接跟踪模块时,连接跟踪 模块已经接收到reply方向的数据包
5.当该数据包是reply方向的数据包。
主要也就对数据包进行上面五种分类
当是第一类数据包时,则创建一个新的连接跟踪项,并作相应的初始化工作,且
将数据包的nfctinfo设置为IP_CT_NEW;
当是第二类数据包时,则创建一个新的连接跟踪项,并作相应的初始化工作,且
将数据包的nfctinfo设置为IP_CT_RELATED;
当时第三类数据包时,将数据包的nfctinfo设置为IP_CT_NEW;
当是第四种数据包时,将数据包的nfctinfo设置为IP_CT_ESTABLISHED;
当时第五种状态时,将数据包的的nfctinfo设置为IP_CT_ESTABLISHED+IP_CT_IS_REPLY;
然后再调用四层协议相关的packet函数,根据数据包的四层内容值,更新四层协议相关的
状态值(比如tcp协议,就会在其nf_conntrack_protocol->packet设置四层相关的状态改变)。
当数据包是reply方向,且置位连接跟踪项的status的IPS_SEEN_REPLY_BIT后,发现IPS_SEEN_REPLY_BIT位原来的值为0,则会调用nf_conntrack_event_cache设置事件通知为IPCT_STATUS,在ipv4_confirm函数里,会调用函数nf_ct_deliver_cached_events
通过通知链的回调函数,将需要更新的status通过netlink机制发送给nfnetlink模块,由其再执行更新状态操作。
最后,为skb的nfct指针赋值,即将skb的nfct指向该数据包对应的连接跟踪项,当数据包再进入NAT模式时,即可以根据这个指针获取到数据包对应的连接跟踪项,从而实现NAT相关的操作。
该函数的流程图如下,其中__nf_ct_l3proto_find就是发现该数据包对应的三层协议数据结构,对于ipv4就是我们上一节所说的nf_conntrack_l3proto_ipv4。
而l3proto->prepare就是nf_conntrack_l3proto_ipv4的 ipv4_prepare函数;
__nf_ct_proto_find就是找到该数据包对应的四层协议对应的数据结构,对于tcp就是变量nf_conntrack_protocol_tcp4;
resolve_normal_ct函数实现连接跟踪项的创建以及连接跟踪项状态改变等操作;
proto->packet函数用于更新该连接跟踪项对应的四层协议的状态,对于tcp则是函数 tcp_packet。
由此,我们知道resolve_normal_ct是连接跟踪模块中协议无关处理函数,且对连接跟踪模块来说非常的重要,我们分析一下。
该函数的功能是
a)若连接跟踪项不存在时,则根据已知条件创建一个nf_conn,并放入unconfirm表中,执行c)
b)若连接跟踪项已存在,则执行c)
c)更新连接跟踪项的状态
d)设置传入数据包的nfct指针
其中nf_ct_get_tuple主要是根据数据包的三层、四层协议获取相应的五元组值;
nf_conntrack_find_get主要是根据传入的tuple变量,在hash数组nf_conntrack_hash中相应的hash链表中查找是否有满足条件的nf_conntrack_tuple_hash,对于一个已确认的连接跟踪项,是能根据其tuple值在hash数组nf_conntrack_hash中相应的hash链表中查找到满足条件的nf_conntrack_tuple_hash变量的,若没有查找到,则说明还没有创建该tuple变量对应的连接跟踪项。
当连接跟踪项没有创建时,则会调用函数init_conntrack创建连接跟踪项;
接着调用nf_ct_tuplehash_to_ctrack获取相应的连接跟踪项;
然后根据数据包对应于连接跟踪项的方向,设置连接跟踪项的状态;
最后设置skb->nfctinfo、skb->nfct。
下面一一分析上面使用到的函数。
该函数的作用是主要是根据传递的nf_conntrack_tuple类型的变量,在nf_conntrack_hash[]中相关的链表中,查找是否有满足要求的nf_conntrack_tuple_hash类型的变量。
nf_conntrack_tuple_hash查找函数
该函数主要是__nf_conntrack_find的封装函数,相比于__nf_conntrack_find,增加了锁机制。
那我们就分析下__nf_conntrack_find。
nf_conntrack_tuple_hash查找函数
1、根据nf_conntrack_tuple计算hash值
2、根据hash值,从数组nf_conntrack_hash中取出相应的hash链表
3、遍历hash链表,查找是否存在nf_conntrack_tuple变量相关的nf_conntrack_tuple_hash
若存在则返回相应的值;若不存在,则返回NULL
如果找到相应的tuple_hash,也就是找到相应的数据连接跟踪项。因为我们可以根据
nf_ct_tuplehash_to_ctrack函数,找到tuple_hash所属的数据连接跟踪项,其实就是通过contain_of实现的。
2.1.3 init_conntrack
这一次分析init_conntrack函数,这个函数包含的信息量也是蛮大的。这个函数主要实现以下功能:
A)调用函数__nf_conntrack_alloc创建一个数据连接跟踪项
B)根据刚创建的连接跟踪项的原始方向的nf_conntrack_tuple变量,查找期望连接链表,看能否找到符合条件的期望连接跟踪。
若查找到,则更新新创建连接跟踪项的master指针,以及调用exp->expectfn;
若没有查找到:
则遍历helpers链表,找到符合条件的nf_conntrack_helper变量后,则更新连接跟踪项的helper指针。
C)将新创建的连接跟踪项插入到unconfirmed链表中。
对于连接跟踪项,是使用定时器超时机制来实现异步垃圾回收的。所以也要对连接跟踪项的垃圾回收机制进行分析下。
那我们先分析下这个函数,具体的代码分析如下:
功能:根据nf_conntrack_tuple结构变量、nf_conntrack_l3proto、nf_conntrack_protocol结构变量创建一个新的nf_conntrack_tuple_hash变量
2.1.2.1__nf_conntrack_alloc
接下来分析这个函数,其功能如下:
1.调用kmem_cache_alloc,创建一个新的连接跟踪项
2.设置连接跟踪项的origin、reply方向的tuple值
3.初始化连接跟踪项的定时器,设置超时处理函数为death_by_timeout
4.设置连接跟踪项的销毁函数为destroy_conntrack
5.全局连接跟踪项统计值nf_conntrack_count加1
2.1.2.2 __nf_ct_helper_find
功能:根据传入的nf_conntrack_tuple类型的参数,查找符合条件的helper
static struct nf_conntrack_helper *
__nf_ct_helper_find(const struct nf_conntrack_tuple *tuple)
{
return LIST_FIND(&helpers, helper_cmp,
struct nf_conntrack_helper *,
tuple);
}
2.1.2.3 连接跟踪项的异步垃圾处理机制
在ipv4_confirm中,对新的连接跟踪项进行确认时,会启动连接跟踪项的超时定时器,以后再收到数据包时,会在nf_conntrack_in中调用四层协议的packet函数里调用函数nf_ct_refresh_acct更新定时器的超时处理时间,这样的话,只要一个连接跟踪项对应的数据流一直有数据发收发,则连接跟踪项就不会超时,若在一定时间内没有再收到数据包,则会超时,从而调用超时处理函数,用于释放该数据连接跟踪项占用的内存。
所以我们就跟着超时处理函数,看下是如何释放连接跟踪项的内存的。
2.1.2.3.1death_by_timeout
连接跟踪项的超时处理函数
1. 调用clean_from_lists将该连接跟踪项的两个方向的tuple变量从ip_conntrack_hash[x]链表中删除
2. 调用nf_ct_put,自减nf_conn的引用计数,若自减后的引用计数为0,则调用
nf_conn->ct_general->destroy销毁nf_conn变量
可以看到是在nf_ct_put中触发的对连接跟踪项所占内存的释放
2.1.2.3.2nf_ct_put&nf_conntrack_put
/*
nf_conn的引用计数减一
*/
/*
1.nfct的引用计数减一
2.若减一后的引用计数值为0,则调用nfct->destroy销毁nf_conn
*/
我们在函数__nf_conntrack_alloc知道nfct->destroy对应的函数为destroy_conntrack。
继续分析
2.1.2.3.3 destroy_conntrack
/*
功能:销毁一个连接跟踪项
1.发送destory消息给nfnetlink模块,让nfnetlink模块执行该连接跟踪相关联的内容
2.调用连接跟踪项中三、四层协议相关的销毁函数
3.因为未确认的连接跟踪项是放在unconntrack链表中的,因此对于未确认的连接跟踪
项,还需要将该连接跟踪项origin方向的tuple从unconntrack链表上删除
4.若该连接为期望连接,调用nf_ct_put,减去对主连接的引用
5.调用nf_conntrack_free释放连接跟踪项占用的内存。
*/
至此,即为异步清理的整个过程。
我们在__nf_conntrack_alloc里有看到一个通过清理机制,那也分析下。
2.1.2.4 连接跟踪项的同步垃圾处理机制
同步垃圾处理是通过函数early_drop实现的。
/*
1.调用LIST_FIND_B遍历链表chain中的每一个tuple_hash,查找符合
删除条件的tuple_hahs
2.若找到符合条件的tuple_hash,则获取该tuple_hash所属的连接跟踪项
3.调用death_by_timeout,强制删除该连接跟踪项
*/
接着就是由nf_ct_put接管同步垃圾回收了,剩下的流程就会异步垃圾处理机制是一样的了。
至此,函数ipv4_conntrack_in的分析告一段落。
接下来我们分析ipv4_conntrack_help。
这个函数还是比较简单的,首先根据传入的数据包;然后找到该数据包对应的数据连接跟踪项;接着若该数据连接跟踪项的helper指针不为空,则调用该helper结构对应的help函数,实现创建期望连接或者相关协议的ALG功能(即对应用层协议中的数据部分如果有ip地址,且该数据连接跟踪项对应的数据流有开启NAT机制时,则将应用层协议中的数据部分的ip地址也进行NAT操作)。
/*
1.从skb->nfct中获取该数据包对应的连接跟踪项
2.如果该连接管跟踪项的helper指针不为0,则调用
ct->helper->help执行该连接跟踪项对应的helper函数,用来创建
期望连接或者对数据包的应用层携带的ip地址信息进行nat操作
*/
进入到这个函数里的数据包,说明其连接跟踪项均已创建,此时需要做的就是对于还没有进行确认的连接跟踪项,实现确认操作,即是将该连接跟踪项对应的原始方向与应答方向的nf_conntrack_tuple_hash变量插入到nf_conntrack_hash[]数组对应的hash链表中。
/*
1.确认一个数据包对应的连接跟踪项
*/
其实就是调用函数nf_conntrack_confirm,下面分析一下:
1.首先判断该数据连接跟踪项是否已经被确认,即是判断该连接跟踪项的status中的IPS_CONFIRMED_BIT位是否为1
2.若连接跟踪项尚未有确认,则调用函数__nf_conntrack_confirm进行确认
3.调用nf_ct_deliver_cached_events触发连接跟踪项对应的消息通知回调,判断是否需要向nfnetlink模块发送相应的消息。
该函数主要确认一个连接跟踪项,确认操作只会发生在连接跟踪项的状态不为reply时。
1.调用CTINFO2DIR,判断是否是原始方向发送的数据包(仅对原始方向的数据包对应的
连接跟踪项进行确认操作。)
在
2. 若1中判断通过后,分别计算original、reply方向上的tuple变量对应的hash值,分别
为hash、repl_hash
3. 若连接跟踪项的original、reply方向上的tuple_hash变量均没有对应的nf_conntrack_hash[]链表中
则首先将original方向上的tuple_hash从unconntrack链表中删除,然后将连接跟踪项的original、
reply方向上的tuple_hash变量添加到相对应的nf_conntrack_hash[]链表中
置位连接跟踪项的status中的IPS_CONFIRMED_BIT,并启动超时定时器,用于实现对连接
跟踪项所占内存的超时回收功能。
*/
至此将连接跟踪模块相应的hook函数都分析完了。
四、总结
对于linux代码,如果说只是为了理解连接跟踪模块的工作原理,总感觉是少点东西的,还需要分析其编码思想。
下面就分析一下连接跟踪模块的一些编程理解
1. 使用链表的方式将期望连接跟踪项、连接跟踪项连接在一起,且对于连接跟踪项来说,对于未被确认的连接跟踪项,专门放置在一个链表中而已被确认的连接跟踪项放置在确认连接跟踪表中
2.需要维护连接跟踪项的状态,即需要设计好连接跟踪项的状态机机制
3.因为连接跟踪项是需要占用内存的,那肯定要有一个垃圾回收机制,既要有异步垃圾回收,也要有同步垃圾回收,还要考虑好什么时候更新异步垃圾回收对应的超时定时器等
功能。
在内核模块中链表被大量的应用,作为阅读代码的基础,首先需要理解链表原理以及内核中的实现机制。而在网络模块中,一般都要涉及到状态机的设计以及垃圾回收机制等(邻居子系统中也使用的状态机以及垃圾回收机制,而CAM表同样设计垃圾回收机制,所以当我们设计网络相关模块时,一定要考虑垃圾回收机制等)。