请输入您要查询的百科知识:

 

词条 conntrack
释义

连接跟踪连接跟踪(CONNTRACK),顾名思义,就是跟踪并且记录连接状态。Linux为每一个经过网络堆栈的数据包,生成一个新的连接记录项 (Connection entry)。

连接跟踪

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 类型:

[Copy to clipboard][ - ]

CODE:

/* 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的转换。结构定义如下:

[Copy to clipboard][ - ]

CODE:

/* 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;

};

[Copy to clipboard][ - ]

CODE:

/* The manipulable part of the tuple. */

struct ip_conntrack_manip

{

u_int32_t ip;

union ip_conntrack_manip_proto u;

};

[Copy to clipboard][ - ]

CODE:

/* 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中看到它的定义:

[Copy to clipboard][ - ]

CODE:

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 是连接跟踪的主要模块:

[Copy to clipboard][ - ]

CODE:

static int __init init(void)

{

return init_or_cleanup(1);

}

初始化函数进一步调用init_or_cleanup() 进行模块的初始化,它主要完成hash表的初始化等三个方面的工作:

[Copy to clipboard][ - ]

CODE:

static int init_or_cleanup(int init)

{

/*初始化连接跟踪的一些变量、数据结构,如初始化连接跟踪表的大小,Hash表的大小等*/

ret = ip_conntrack_init();

if (ret < 0)

goto cleanup_nothing;

/*创建proc 文件系统的对应节点*/

#ifdef CONFIG_PROC_FS

……

#endif

/*为连接跟踪注册Hook */

ret = nf_register_hook(&ip_conntrack_defrag_ops);

if (ret < 0) {

printk("ip_conntrack: can't register pre-routing defrag hook.\");

goto cleanup_proc_stat;

}

……

}

3.2 ip_conntrack_init

ip_conntrack_init 函数用于初始化连接跟踪的包括hash表相关参数在内一些重要的变量:

[Copy to clipboard][ - ]

CODE:

/*用户态可以在模块插入的时候,可以使用hashsize参数,指明hash 表的大小*/

static int hashsize;

module_param(hashsize, int, 0400);

int __init ip_conntrack_init(void)

{

unsigned int i;

int ret;

/* 如果模块指明了hash表的大小,则使用指定值,否则,根据内存的大小,来计算一个默认值. ,hash表的大小,是使用全局变量ip_conntrack_htable_size 来描述*/

if (hashsize) {

ip_conntrack_htable_size = hashsize;

} else {

ip_conntrack_htable_size

= (((num_physpages << PAGE_SHIFT) / 16384)

/ sizeof(struct list_head));

if (num_physpages > (1024 * 1024 * 1024 / PAGE_SIZE))

ip_conntrack_htable_size = 8192;

if (ip_conntrack_htable_size < 16)

ip_conntrack_htable_size = 16;

}

/*根据hash表的大小,计算最大的连接跟踪表数*/

ip_conntrack_max = 8 * ip_conntrack_htable_size;

printk("ip_conntrack version %s (%u buckets, %d max)"

" - %Zd bytes per conntrack\", IP_CONNTRACK_VERSION,

ip_conntrack_htable_size, ip_conntrack_max,

sizeof(struct ip_conntrack));

/*注册socket选项*/

ret = nf_register_sockopt(&so_getorigdst);

if (ret != 0) {

printk(KERN_ERR "Unable to register netfilter socket option\");

return ret;

}

/* 初始化内存分配标识变量 */

ip_conntrack_vmalloc = 0;

/*为hash表分配连续内存页*/

ip_conntrack_hash

=(void*)__get_free_pages(GFP_KERNEL,

get_order(sizeof(struct list_head)

*ip_conntrack_htable_size));

/*分配失败,尝试调用vmalloc重新分配*/

if (!ip_conntrack_hash) {

ip_conntrack_vmalloc = 1;

printk(KERN_WARNING "ip_conntrack: falling back to vmalloc.\");

ip_conntrack_hash = vmalloc(sizeof(struct list_head)

* ip_conntrack_htable_size);

}

/*仍然分配失败*/

if (!ip_conntrack_hash) {

printk(KERN_ERR "Unable to create ip_conntrack_hash\");

goto err_unreg_sockopt;

}

ip_conntrack_cachep = kmem_cache_create("ip_conntrack",

sizeof(struct ip_conntrack), 0,

0, NULL, NULL);

if (!ip_conntrack_cachep) {

printk(KERN_ERR "Unable to create ip_conntrack slab cache\");

goto err_free_hash;

}

ip_conntrack_expect_cachep = kmem_cache_create("ip_conntrack_expect",

sizeof(struct ip_conntrack_expect),

0, 0, NULL, NULL);

if (!ip_conntrack_expect_cachep) {

printk(KERN_ERR "Unable to create ip_expect slab cache\");

goto err_free_conntrack_slab;

}

/* Don't NEED lock here, but good form anyway. */

WRITE_LOCK(&ip_conntrack_lock);

/* 注册协议。对不同协议,连接跟踪记录的参数不同,所以不同的协议定义了不同的 ip_conntrack_protocol结构来处理与协议相关的内容。这些结构被注册到一个全局的链表中,在使用时根据协议去查找,并调用相应的处理函数来完成相应的动作。*/

for (i = 0; i < MAX_IP_CT_PROTO; i++)

ip_ct_protos[i] = &ip_conntrack_generic_protocol;

ip_ct_protos[IPPROTO_TCP] = &ip_conntrack_protocol_tcp;

ip_ct_protos[IPPROTO_UDP] = &ip_conntrack_protocol_udp;

ip_ct_protos[IPPROTO_ICMP] = &ip_conntrack_protocol_icmp;

WRITE_UNLOCK(&ip_conntrack_lock);

/*初始化hash表*/

for (i = 0; i < ip_conntrack_htable_size; i++)

INIT_LIST_HEAD(&ip_conntrack_hash[i]);

/* For use by ipt_REJECT */

ip_ct_attach = ip_conntrack_attach;

/* Set up fake conntrack:

- to never be deleted, not in any hashes */

atomic_set(&ip_conntrack_untracked.ct_general.use, 1);

/* - and look it like as a confirmed connection */

set_bit(IPS_CONFIRMED_BIT, &ip_conntrack_untracked.status);

return ret;

err_free_conntrack_slab:

kmem_cache_destroy(ip_conntrack_cachep);

err_free_hash:

free_conntrack_hash();

err_unreg_sockopt:

nf_unregister_sockopt(&so_getorigdst);

return -ENOMEM;

}

在这个函数中,有两个重点的地方值得注意,一个是hash表的相关变量的初始化、内存空间的分析等等,另一个是协议的注册。

连接跟踪由于针对每种协议的处理,都有些细微不同的地方,举个例子,我们前面讲到数据包至tuple的转换,TCP的转换与ICMP的转换肯定不同的,因 为ICMP连端口的概念也没有,所以,对于每种协议的一些特殊处理的函数,需要进行封装,struct ip_conntrack_protocol 结构就实现了这一封装,在初始化工作中,针对最常见的TCP、UDP和ICMP协议,定义了ip_conntrack_protocol_tcp、 ip_conntrack_protocol_udp和ip_conntrack_protocol_icmp三个该类型的全局变量,初始化函数中,将它 们封装至ip_ct_protos 数组,这些,在后面的数据包处理后,就可以根据包中的协议值,使用ip_ct_protos[协议值],找到注册的协议节点,就可以方便地调用协议对应的 处理函数了,我们在后面将看到这一调用过程。

3.2 钩子函数的注册

init_or_cleanup 函数在创建/proc文件系统完成后,会调用nf_register_hook 函数注册钩子,进行连接跟踪,按优先级和Hook不同,注册了多个钩子:

[Copy to clipboard][ - ]

CODE:

ret = nf_register_hook(&ip_conntrack_defrag_ops);

if (ret < 0) {

printk("ip_conntrack: can't register pre-routing defrag hook.\");

goto cleanup_proc_stat;

}

ret = nf_register_hook(&ip_conntrack_defrag_local_out_ops);

if (ret < 0) {

printk("ip_conntrack: can't register local_out defrag hook.\");

goto cleanup_defragops;

}

……

整个Hook注册好后,如下图所示:

上图中,粗黑体标识函数就是连接跟踪注册的钩子函数,除此之外,用于处理分片包和处理复杂协议的钩子函数在上图中没有标识出来。处理分片包的钩子用于重组 分片,用于保证数据在进入连接跟踪模块不会是一个分片数据包。例如,在数据包进入NF_IP_PRE_ROUTING Hook点,主要的连接跟踪函数是ip_conntrack_in,然而,在它之前,还注册了ip_conntrack_defrag,用于处理分片数据 包:

[Copy to clipboard][ - ]

CODE:

static unsigned int ip_conntrack_defrag(unsigned int hooknum,

struct sk_buff **pskb,

const struct net_device *in,

const struct net_device *out,

int (*okfn)(struct sk_buff *))

{

/* Gather fragments. */

if ((*pskb)->nh.iph->frag_off & htons(IP_MF|IP_OFFSET)) {

*pskb = ip_ct_gather_frags(*pskb,

hooknum == NF_IP_PRE_ROUTING ?

IP_DEFRAG_CONNTRACK_IN :

IP_DEFRAG_CONNTRACK_OUT);

if (!*pskb)

return NF_STOLEN;

}

return NF_ACCEPT;

}

对 于我们本章的分析而言,主要是以“Linux做为一个网关主机,转发过往数据”为主线,更多关注的是在NF_IP_PRE_ROUTING和 NF_IP_POSTROUTING两个Hook点上注册的两个钩子函数ip_conntrack_in和ip_refrag(这个函数主要执行的是 ip_confirm函数)。

钩子的注册的另一个值得注意的小问题,就是钩子函数的优先级,NF_IP_PRE_ROUTING上的优先级是NF_IP_PRI_CONNTRACK ,意味着它的优先级是很高的,这也意味着每个输入数据包首先被传输到连接跟踪模块,才会进入其它优先级较低的模块。同样 地,NF_IP_POSTROUTING上的优先级为NF_IP_PRI_CONNTRACK_CONFIRM,优先级是很低的,也就是说,等到其它优先 级高的模块处理完成后,才会做最后的处理,然后将数据包送出去。

4.ip_conntrack_in

数据包进入Netfilter后,会调用ip_conntrack_in函数,以进入连接跟踪模块,ip_conntrack_in 主要完成的工作就是判断数据包是否已在连接跟踪表中,如果不在,则为数据包分配ip_conntrack,并初始化它,然后,为这个数据包设置连接状态。

[Copy to clipboard][ - ]

CODE:

/* Netfilter hook itself. */

unsigned int ip_conntrack_in(unsigned int hooknum,

struct sk_buff **pskb,

const struct net_device *in,

const struct net_device *out,

int (*okfn)(struct sk_buff *))

{

struct ip_conntrack *ct;

enum ip_conntrack_info ctinfo;

struct ip_conntrack_protocol *proto;

int set_reply;

int ret;

/* 判断当前数据包是否已被检查过了 */

if ((*pskb)->nfct) {

CONNTRACK_STAT_INC(ignore);

return NF_ACCEPT;

}

/* 分片包当会在前一个Hook中被处理,事实上,并不会触发该条件 */

if ((*pskb)->nh.iph->frag_off & htons(IP_OFFSET)) {

if (net_ratelimit()) {

printk(KERN_ERR "ip_conntrack_in: Frag of proto %u (hook=%u)\",

(*pskb)->nh.iph->protocol, hooknum);

}

return NF_DROP;

}

/* 将当前数据包设置为未修改 */

(*pskb)->nfcache |= NFC_UNKNOWN;

/*根据当前数据包的协议,查找与之相应的struct ip_conntrack_protocol结构*/

proto = ip_ct_find_proto((*pskb)->nh.iph->protocol);

/* 没有找到对应的协议. */

if (proto->error != NULL

&& (ret = proto->error(*pskb, &ctinfo, hooknum)) <= 0) {

CONNTRACK_STAT_INC(error);

CONNTRACK_STAT_INC(invalid);

return -ret;

}

/*在全局的连接表中,查找与当前包相匹配的连接结构,返回的是struct ip_conntrack *类型指针,它用于描述一个数据包的连接状态*/

if (!(ct = resolve_normal_ct(*pskb, proto,&set_reply,hooknum,&ctinfo))) {

/* Not valid part of a connection */

CONNTRACK_STAT_INC(invalid);

return NF_ACCEPT;

}

if (IS_ERR(ct)) {

/* Too stressed to deal. */

CONNTRACK_STAT_INC(drop);

return NF_DROP;

}

IP_NF_ASSERT((*pskb)->nfct);

/*Packet函数指针,为数据包返回一个判断,如果数据包不是连接中有效的部分,返回-1,否则返回NF_ACCEPT。*/

ret = proto->packet(ct, *pskb, ctinfo);

if (ret < 0) {

/* Invalid: inverse of the return code tells

* the netfilter core what to do*/

nf_conntrack_put((*pskb)->nfct);

(*pskb)->nfct = NULL;

CONNTRACK_STAT_INC(invalid);

return -ret;

}

/*设置应答状态标志位*/

if (set_reply)

set_bit(IPS_SEEN_REPLY_BIT, &ct->status);

return ret;

}

在初始化的时候,我们就提过,连接跟踪模块将所有支持的 协议,都使用struct ip_conntrack_protocol 结构封装,注册至全局数组ip_ct_protos,这里首先调用函数ip_ct_find_proto根据当前数据包的协议值,找到协议注册对应的模 块。然后调用resolve_normal_ct 函数进一步处理。

随便看

 

百科全书收录4421916条中文百科知识,基本涵盖了大多数领域的百科知识,是一部内容开放、自由的电子版百科全书。

 

Copyright © 2004-2023 Cnenc.net All Rights Reserved
更新时间:2025/2/27 13:12:20