内核版本 2.6.21
连接跟踪(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的连接跟踪项。
至此,已经将连接跟踪模块相关的数据结构都介绍完了,最后一张图来结束本节,这张图将上面介绍的主要数据结构之间的关系,以及它们与上面四个全局变量之间的关系都描述了。