分类: LINUX
2016-02-21 19:22:22
原文地址:Netfilter源码分析--5、连接跟踪 作者:jazy333
连接跟踪用来跟踪和记录连接状态,是netfilter的一部分,它是NAT等重要功能的基础,也是通过在挂载点注册来完成相应工作的。本章对连接跟踪的分析仍然以IPV4为例。
连接跟踪用一个tuple来标识唯一的连接,用“来源地址/来源端口+目标地址/目标端口“来表示一个tuple,同时将一个tuple分为两个方向:ORIGINAL和REPLY。ORIGINAL指的是连接发起端为”源地址/源端口“为tuple的“来源地址/来源端口”,REPLY指的是连接的”目标地址/目标端口“端的回应报文,这时”目标地址/目标端口“为tuple的源地址/源端口。
连接跟踪对报文的处理主要分为两个过程,第一个过程对于新接受到的报文,通过函数nf_conntrack_in来完成,连接跟踪根据sk_buff的信息提取出tuple的信息,判断此连接是否已记录,如果没有记录创建相应的连接;第二个过程是确认阶段,通过函数nf_conntrack_confirm来完成,这个阶段将已经确认的连接放到一个表中。下面以ipv4为例用两个流程图来表示这两个函数的处理过程。
连接跟踪根据TCP/IP协议的模型,将一个连接所要使用到的协议栈的协议分为三层协议,四层协议,和应用层协议,三层协议用数据结构nf_conntrack_l3proto来表示,四层协议用数据结构nf_conntrack_l4proto来表示,不同的应用层协议实现了不同的结构这里不做赘叙。每个协议结构体内都有对应的操作函数,在下节做详细的说明。
连接跟踪的相关信息建立以后,通过扩展网络协议栈的核心数据结构sk_buff来将相关信息存储到sk_buff中,在sk_buff中扩展了两个成员变量:struct nf_conntrack *nfct和nfctinfo:3来分别表示连接及相关信息。
与前面提到的表的创建类似,连接跟踪也是创建自己的hook操作,通过hook操作来完成对连接的记录,连接状态的改变等等。IPV4在四个挂载点注册了hook操作,主要通过两个函数来完成:ipv4_conntrack_in, ipv4_confirm,这两个函数以上面提到的两个函数为基础,实际上这个函数是与具体协议相关的,而上面两个函数是从所有的协议中提炼出来的共同的处理流程。
当数据包接过挂载点时,钩子函数会调用对应的链,但调用到连接跟踪处理函数时,连接就被记录下来,下面是具体的流程:
图5.2 ipv4_confirm
nf_conn是连接跟踪的核心数据结构,表示一个连接,整个连接跟踪的操作过程都围绕它展开,这个结构的定义及相关的解释如下:
struct nf_conn {
/*这个结构在include/linux/skbuff.h中定义,其中包括一个计数器,计数器表示本记录本公开引用的次数*/
struct nf_conntrack ct_general;
/*这个结构用于标识唯一的连接,它是一个有两个成员的数组,即连接的连个方向ORIGINAL和REPLY*/
struct nf_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX];
/*连接的各种状态,各种状态的取值下面详细介绍*/
unsigned long status;
/* 如果这个连接是期望连接,那它一定有一个主连接,这个成员表示主连接 */
struct nf_conn *master;
/* 定时器函数 */
struct timer_list timeout;
/* 协议的特有的信息,一般指第4层协议所特有的信息 */
union nf_conntrack_proto proto;
/* 扩展,是指这个成员是可扩展的,它可以存放应用层协议的helper信息,可以存放NAT的一些信息,也可存放统计信息,连接跟踪主要用它来存放helper信息和统计信息*/
struct nf_ct_ext *ext;
};
nf_conn成员的status的取值及含义如下:
/* 期望连接*/
IPS_EXPECTED
/* 连接的两个方向上都发现了报文. */
IPS_SEEN_REPLY
/* 此种连接永远不能过早的过期. */
IPS_ASSURED
/*确认的连接,最初报文已经处理 */
IPS_CONFIRMED
/* 此种连接需要在orignal方向上进行SRC NAT */
IPS_SRC_NAT
/* 此种连接需要在original方向进行DST NAT */
IPS_DST_NAT
/*上述两个都需要 */
IPS_NAT_MASK
/* 连接需要TCP 报文顺序调整. */
IPS_SEQ_ADJUST
/* NAT 初始化位. */
IPS_SRC_NAT_DONE
IPS_DST_NAT_DONE
/* 上述两个都需要*/
IPS_NAT_DONE_MASK = (IPS_DST_NAT_DONE | IPS_SRC_NAT_DONE),
/*死掉的连接*/
IPS_DYING
/* timeout的连接*/
IPS_FIXED_TIMEOUT
nf_conntrack_tuple是用来标识每个连接的数据结构,其核心就是用源和目的的IP和端口来标识这个唯一的连接,不同的传输层协议可能还需要其他的标识,因此在nf_conntrack_tuple的定义中用一个联合来表示不同协议。
struct nf_conntrack_tuple
{
struct nf_conntrack_man src;
/* These are the parts of the tuple which are fixed. */
struct {
union nf_inet_addr u3;
union {
/* Add other protocols here. */
__be16 all;
struct {
__be16 port;
} tcp;
struct {
__be16 port;
} udp;
struct {
u_int8_t type, code;
} icmp;
struct {
__be16 port;
} dccp;
struct {
__be16 port;
} sctp;
struct {
__be16 key;
} gre;
} u;
/* 协议号 */
u_int8_t protonum;
/* tuple的方向*/
u_int8_t dir;
} dst;
};
连接跟踪中并不是直接的存储一个tuple而是将其做hash,存储它的hash值。
连接跟踪将TCP/IP协议的三,四层协议抽象为两个数据结构,不同的结构有不同函数把协议的相关的信息存储到nf_conn结构当中去,其中三层协议的抽象为:
struct nf_conntrack_l3proto
{
/* L3 协议号,如PF_INET */
u_int16_t l3proto;
/* 协议名字 */
const char *name;
/*填充函数的第三个参数,成功返回true
*/
bool (*pkt_to_tuple)(const struct sk_buff *skb, unsigned int nhoff,
struct nf_conntrack_tuple *tuple);
/*
* 反转tuple的协议部分,有些报文不能被反转,返回0 */
bool (*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 *);
/*
* 跟踪前调用
* *dataoff: skb中协议头(tcp或udp)的偏移
* *protonum: protocol number
*/
int (*get_l4proto)(const struct sk_buff *skb, unsigned int nhoff,
unsigned int *dataoff, u_int8_t *protonum);
/*将tuple转换为netlink的属性结构*/
int (*tuple_to_nlattr)(struct sk_buff *skb, const struct nf_conntrack_tuple *t);
/*
* 计算 tuple nlattr的尺寸
*/
int (*nlattr_tuple_size)(void);
int (*nlattr_to_tuple)(struct nlattr *tb[],struct nf_conntrack_tuple *t);
const struct nla_policy *nla_policy;
size_t nla_size;
/* 是否在模型中定义*/
struct module *me;
};
四层协议的抽象为:
struct nf_conntrack_l4proto
{
/* L3 协议名字. */
u_int16_t l3proto;
/* L4 协议编号. */
u_int8_t l4proto;
/* 填充第三个参数 dataoff 是网络协议的偏移 */
bool (*pkt_to_tuple)(const struct sk_buff *skb, unsigned int dataoff,
struct nf_conntrack_tuple *tuple);
/* 同L3协议*/
bool (*invert_tuple)(struct nf_conntrack_tuple *inverse,
const struct nf_conntrack_tuple *orig);
/* 设置第一个参数的proto成员相关信息*/
int (*packet)(struct nf_conn *ct,
const struct sk_buff *skb,
unsigned int dataoff,
enum ip_conntrack_info ctinfo,
u_int8_t pf,
unsigned int hooknum);
/*设置第一个参数的proto成员相关信息*/
bool (*new)(struct nf_conn *ct, const struct sk_buff *skb,
unsigned int dataoff);
/* Called when a conntrack entry is destroyed */
void (*destroy)(struct nf_conn *ct);
int (*error)(struct net *net, struct sk_buff *skb, unsigned int dataoff,
enum ip_conntrack_info *ctinfo,
u_int8_t pf, unsigned int hooknum);
/*以下大部分与L3协议的函数含义相同*/
/* Print out the per-protocol part of the tuple. Return like seq_* */
int (*print_tuple)(struct seq_file *s,
const struct nf_conntrack_tuple *);
/* Print out the private part of the conntrack. */
int (*print_conntrack)(struct seq_file *s, const struct nf_conn *);
int (*to_nlattr)(struct sk_buff *skb, struct nlattr *nla,
const struct nf_conn *ct);
/* Calculate protoinfo nlattr size */
int (*nlattr_size)(void);
/* convert nfnetlink attributes to protoinfo */
int (*from_nlattr)(struct nlattr *tb[], struct nf_conn *ct);
int (*tuple_to_nlattr)(struct sk_buff *skb,const struct nf_conntrack_tuple *t);
/* Calculate tuple nlattr size */
int (*nlattr_tuple_size)(void);
int (*nlattr_to_tuple)(struct nlattr *tb[],struct nf_conntrack_tuple *t);
const struct nla_policy *nla_policy;
size_t nla_size;
/* 协议名字 */
const char *name;
/* Module (if any) which this is connected to. */
struct module *me;
};
连接跟踪同时也提供了对协议的注册函数,对于不同类型的协议,设置不同的成员函数,同时将对应的数据结构注册到全局的数据结构当中去,连接跟踪在进行hook操作时,会根据sk_buff提供的协议信息找到对应的结构。
连接跟踪为每个应用成的协议通过一个helper结构,同过这个结构的成员函数应用层的协议可以做一些与自己协议相关的工作,定义如下:
struct nf_conntrack_helper
{
struct hlist_node hnode; /* Internal use. */
const char *name; /* 模型名字 */
struct module *me; /* pointer to self */
const struct nf_conntrack_expect_policy *expect_policy;
/* 哪个tuple需要我们的帮助*/
struct nf_conntrack_tuple tuple;
/*真正的执行函数 */
int (*help)(struct sk_buff *skb,unsigned int protoff,struct nf_conn *ct,
enum ip_conntrack_info conntrackinfo);
void (*destroy)(struct nf_conn *ct);
int (*to_nlattr)(struct sk_buff *skb, const struct nf_conn *ct);
unsigned int expect_class_max;
};
连接跟踪的nf_conn结构通过ext成员存储helper信息,其定义如下:
struct nf_ct_ext {
struct rcu_head rcu;
u8 offset[NF_CT_EXT_NUM];//offset数组
u8 len;//整个结构长度
char data[0];//可变数据区
};
nf_ct_ext可以存储三种类别的扩展,分别为:
NF_CT_EXT_HELPER,// nf_conntrack_helper
NF_CT_EXT_NAT,//NAT相关信息
NF_CT_EXT_ACCT,//统计信息
期望连接是指对于那些活动协议而言,例如对FTP协议,FTP协议的连接分为控制连接和数据连接,对于这种类型的协议,连接跟踪认为这两次连接应该归结到一个跟踪里,这样,当FTP的控制连接建立起来后,它的数据连接就是控制连接的期望连接,而数据连接就是控制连接的master。期望连接可用如下的数据结构表示:
struct nf_conntrack_expect
{
/* 期望连接成员列表 */
struct hlist_node lnode;
/* 用于net名字空间的hash链表*/
struct hlist_node hnode;
/* 期望的tuple及其相关的mask */
struct nf_conntrack_tuple tuple;
struct nf_conntrack_tuple_mask mask;
/* 相关函数,很少使用*/
void (*expectfn)(struct nf_conn *new, struct nf_conntrack_expect *this);
/* 新连接的helper函数 */
struct nf_conntrack_helper *helper;
/* 此连接的主连接 */
struct nf_conn *master;
/* 定时器函数. */
struct timer_list timeout;
/* 引用计数*/
atomic_t use;
/* 标识*/
unsigned int flags;
/* Expectation class */
unsigned int class;
struct rcu_head rcu;
};
本节将以应用层协议FTP为例,对连接跟踪整个过程做详细的介绍,同时假定网络层的通信协议为IPV4:
我们假设ftp client端的ip为:193.168.18.133,ftp服务器端的ip地址为193.168.18.135
Client端通过命令 与服务器建立连接,这时服务器的连接跟踪建立如下的跟踪:
ORIGINAL:193.168.18.133.1059->193.168.18.135.21
REPLY: 193.168.18.135.21->193.168.18.133.1059
一次简单的连接跟踪建立起来。当用户在client端输入dir命令后,client向server端发送一个PORT的ftp命令,服务器端的连接跟踪机制接收到这个连接后,调用ftp协议相关的help函数,这个help函数将初始化一个期望连接,并与主连接相关联,期望连接的相关tuple为:
193.168.133.XXXX->193.168.18.135.20
XXXX表示端口任意,可以用一个mask完成,当客户端发起一个数据连接时:
193.168.133.1059->1933.168.18.135.20
Server端发现此连接为期望连接,不会在重新初始化一个新的跟踪。