Netfilter 连接跟踪与状态检测的实现
时间:2006-08-21
来源:互联网
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 是连接跟踪的主要模块:
- static int __init init(void)
- {
- return init_or_cleanup(1);
- }
初始化函数进一步调用init_or_cleanup() 进行模块的初始化,它主要完成hash表的初始化等三个方面的工作:
- 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.\n");
- goto cleanup_proc_stat;
- }
- ……
- }
3.2 ip_conntrack_init
ip_conntrack_init 函数用于初始化连接跟踪的包括hash表相关参数在内一些重要的变量:
- /*用户态可以在模块插入的时候,可以使用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\n", 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\n");
- 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.\n");
- 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\n");
- 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\n");
- 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\n");
- 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不同,注册了多个钩子:
- ret = nf_register_hook(&ip_conntrack_defrag_ops);
- if (ret < 0) {
- printk("ip_conntrack: can't register pre-routing defrag hook.\n");
- 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.\n");
- goto cleanup_defragops;
- }
- ……
整个Hook注册好后,如下图所示:
上图中,粗黑体标识函数就是连接跟踪注册的钩子函数,除此之外,用于处理分片包和处理复杂协议的钩子函数在上图中没有标识出来。处理分片包的钩子用于重组分片,用于保证数据在进入连接跟踪模块不会是一个分片数据包。例如,在数据包进入NF_IP_PRE_ROUTING Hook点,主要的连接跟踪函数是ip_conntrack_in,然而,在它之前,还注册了ip_conntrack_defrag,用于处理分片数据包:
- 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,并初始化它,然后,为这个数据包设置连接状态。
- /* 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)\n",
- (*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 函数进一步处理。
[ 本帖最后由 独孤九贱 于 2006-8-22 08:48 编辑 ]
作者: 独孤九贱 发布时间: 2006-08-21
resolve_normal_ct 函数是连接跟踪中最重要的函数之一,它的主要功能就是判断数据包在连接跟踪表是否存在,如果不存在,则为数据包分配相应的连接跟踪节点空间并初始化,然后设置连接状态:
- /* On success, returns conntrack ptr, sets skb->nfct and ctinfo */
- static inline struct ip_conntrack *
- resolve_normal_ct(struct sk_buff *skb,
- struct ip_conntrack_protocol *proto,
- int *set_reply,
- unsigned int hooknum,
- enum ip_conntrack_info *ctinfo)
- {
- struct ip_conntrack_tuple tuple;
- struct ip_conntrack_tuple_hash *h;
- struct ip_conntrack *ct;
-
- IP_NF_ASSERT((skb->nh.iph->frag_off & htons(IP_OFFSET)) == 0);
-
- /*前面提到过,需要将一个数据包转换成tuple,这个转换,就是通过ip_ct_get_tuple函数实现的*/
- if (!ip_ct_get_tuple(skb->nh.iph, skb, skb->nh.iph->ihl*4,
- &tuple,proto))
- return NULL;
-
- /*查看数据包对应的tuple在连接跟踪表中是否存在 */
- h = ip_conntrack_find_get(&tuple, NULL);
- if (!h) {
- /*如果不存在,初始化之*/
- h = init_conntrack(&tuple, proto, skb);
- if (!h)
- return NULL;
- if (IS_ERR(h))
- return (void *)h;
- }
- /*根据hash表节点,取得数据包对应的连接跟踪结构*/
- ct = tuplehash_to_ctrack(h);
-
- /* 判断连接的方向 */
- if (DIRECTION(h) == IP_CT_DIR_REPLY) {
- *ctinfo = IP_CT_ESTABLISHED + IP_CT_IS_REPLY;
- /* Please set reply bit if this packet OK */
- *set_reply = 1;
- } else {
- /* Once we've had two way comms, always ESTABLISHED. */
- if (test_bit(IPS_SEEN_REPLY_BIT, &ct->status)) {
- DEBUGP("ip_conntrack_in: normal packet for %p\n",
- ct);
- *ctinfo = IP_CT_ESTABLISHED;
- } else if (test_bit(IPS_EXPECTED_BIT, &ct->status)) {
- DEBUGP("ip_conntrack_in: related packet for %p\n",
- ct);
- *ctinfo = IP_CT_RELATED;
- } else {
- DEBUGP("ip_conntrack_in: new packet for %p\n",
- ct);
- *ctinfo = IP_CT_NEW;
- }
- *set_reply = 0;
- }
- /*设置skb的对应成员,如使用计数器、数据包状态标记*/
- skb->nfct = &ct->ct_general;
- skb->nfctinfo = *ctinfo;
- return ct;
- }
这个函数包含了连接跟踪中许多重要的步骤
n 调用ip_ct_get_tuple函数,把数据包转换为tuple;
n ip_conntrack_find_get函数,根据tuple查找连接跟踪表;
n init_conntrack函数,初始化一条连接;
n 判断连接方向,设置连接状态;
5.1 数据包的转换
ip_ct_get_tuple 实现数据包至tuple的转换,这个转换,主要是根据数据包的套接字对来进行转换的:
- int ip_ct_get_tuple(const struct iphdr *iph,
- const struct sk_buff *skb,
- unsigned int dataoff,
- struct ip_conntrack_tuple *tuple,
- const struct ip_conntrack_protocol *protocol)
- {
- /* Never happen */
- if (iph->frag_off & htons(IP_OFFSET)) {
- printk("ip_conntrack_core: Frag of proto %u.\n",
- iph->protocol);
- return 0;
- }
- /*设置来源、目的地址*/
- tuple->src.ip = iph->saddr;
- tuple->dst.ip = iph->daddr;
- tuple->dst.protonum = iph->protocol;
- tuple->dst.dir = IP_CT_DIR_ORIGINAL;
-
- return protocol->pkt_to_tuple(skb, dataoff, tuple);
- }
回忆一下我们前面分析协议的初始化中协议初始化的部份,pkt_to_tuple 函数指针,以每种协议的不同而不同,以TCP协议为例:
- static int tcp_pkt_to_tuple(const struct sk_buff *skb,
- unsigned int dataoff,
- struct ip_conntrack_tuple *tuple)
- {
- struct tcphdr _hdr, *hp;
-
- /* 获取TCP报头*/
- hp = skb_header_pointer(skb, dataoff, 8, &_hdr);
- if (hp == NULL)
- return 0;
- /*根据报头的端口信息,设置tuple对应成员*/
- tuple->src.u.tcp.port = hp->source;
- tuple->dst.u.tcp.port = hp->dest;
-
- return 1;
- }
TCP协议中,根据来源和目的端口设置,其它协议类似,读者可以对比分析。
5.2 Hash 表的搜索
要对Hash表进行遍历,首要需要找到hash表的入口,然后来遍历该入口指向的链表。每个链表的节点是struct ip_conntrack_tuple_hash,它封装了tuple,所谓封装,就是把待查找的tuple与节点中已存的tuple相比较,我们来看这一过程的实现。
计算hash值,是调用hash_conntrack函数,根据数据包对应的tuple实现的:
- unsigned int hash = hash_conntrack(tuple);
-
- 这样,tuple对应的hash表入口即为ip_conntrack_hash[hash],也就是链表的首节点,然后调用ip_conntrack_find_get函数进行查找:
- struct ip_conntrack_tuple_hash *
- ip_conntrack_find_get(const struct ip_conntrack_tuple *tuple,
- const struct ip_conntrack *ignored_conntrack)
- {
- struct ip_conntrack_tuple_hash *h;
-
- READ_LOCK(&ip_conntrack_lock);
- /*搜索链表*/
- h = __ip_conntrack_find(tuple, ignored_conntrack);
- if (h) /*查找到了,使用计数器累加*/
- atomic_inc(&tuplehash_to_ctrack(h)->ct_general.use);
- READ_UNLOCK(&ip_conntrack_lock);
-
- return h;
- }
链表是内核中一个标准的双向链表,可以调用宏list_for_each_entry 进遍历链表:
- static struct ip_conntrack_tuple_hash *
- __ip_conntrack_find(const struct ip_conntrack_tuple *tuple,
- const struct ip_conntrack *ignored_conntrack)
- {
- struct ip_conntrack_tuple_hash *h;
- unsigned int hash = hash_conntrack(tuple);
-
- MUST_BE_READ_LOCKED(&ip_conntrack_lock);
- list_for_each_entry(h, &ip_conntrack_hash[hash], list) {
- if (conntrack_tuple_cmp(h, tuple, ignored_conntrack)) {
- CONNTRACK_STAT_INC(found);
- return h;
- }
- CONNTRACK_STAT_INC(searched);
- }
-
- return NULL;
- }
list_for_each_entry在以&ip_conntrack_hash[hash]为起始地址的链表中,逐个搜索其成员,比较这个节点中的tuple是否与待查找的tuple是否一致,这个比较过程,是通过conntrack_tuple_cmp 函数实现的:
- conntrack_tuple_cmp(const struct ip_conntrack_tuple_hash *i,
- const struct ip_conntrack_tuple *tuple,
- const struct ip_conntrack *ignored_conntrack)
- {
- MUST_BE_READ_LOCKED(&ip_conntrack_lock);
- return tuplehash_to_ctrack(i) != ignored_conntrack
- && ip_ct_tuple_equal(tuple, &i->tuple);
- }
tuplehash_to_ctrack 函数主要是取连接跟踪ip_conntrack中的连接方向,判断它是否等于ignored_conntrack,对与这里的比较而言,ignored_conntrack传递过来的为NULL。
主要的比较函数是ip_ct_tuple_equal函数,函数分为“来源”和“目的”进行比较:
- static inline int ip_ct_tuple_src_equal(const struct ip_conntrack_tuple *t1,
- const struct ip_conntrack_tuple *t2)
- {
- return t1->src.ip == t2->src.ip
- && t1->src.u.all == t2->src.u.all;
- }
-
- static inline int ip_ct_tuple_dst_equal(const struct ip_conntrack_tuple *t1,
- const struct ip_conntrack_tuple *t2)
- {
- return t1->dst.ip == t2->dst.ip
- && t1->dst.u.all == t2->dst.u.all
- && t1->dst.protonum == t2->dst.protonum;
- }
-
- static inline int ip_ct_tuple_equal(const struct ip_conntrack_tuple *t1,
- const struct ip_conntrack_tuple *t2)
- {
- return ip_ct_tuple_src_equal(t1, t2) && ip_ct_tuple_dst_equal(t1, t2);
- }
这里的比较,除了IP地址之外,并没有直接比较“端口”,这是因为像ICMP协议这样的并没有“端口”协议,struct ip_conntrack_tuple 结构中,与协议相关的,如端口等,都定义成union类型,这样,就可以直接使用u.all,而不用再去管TCP,UDP还是ICMP了。
5.3 连接初始化
内核使用ip_conntrack结构来描述一个数据包的连接状态,init_conntrack函数就是在连接状态表中不存在当前数据包时,初始化一个ip_conntrack结构,此结构被Netfilter用来描述一条连接,前面分析hash表时,已经分析了它的tuplehash成员:
- struct ip_conntrack
- {
- /* 包含了使用计数器和指向删除连接的函数的指针 */
- struct nf_conntrack ct_general;
-
- /* 连接状态位,它通常是一个ip_conntrack_status类型的枚举变量,如IPS_SEEN_REPLY_BIT等*/
- unsigned long status;
-
- /* 内核的定时器,用于处理连接超时 */
- struct timer_list timeout;
-
- #ifdef CONFIG_IP_NF_CT_ACCT
- /* Accounting Information (same cache line as other written members) */
- struct ip_conntrack_counter counters[IP_CT_DIR_MAX];
- #endif
- /* If we were expected by an expectation, this will be it */
- struct ip_conntrack *master;
-
- /* Current number of expected connections */
- unsigned int expecting;
-
- /* Helper, if any. */
- struct ip_conntrack_helper *helper;
-
- /* Storage reserved for other modules: */
- union ip_conntrack_proto proto;
-
- union ip_conntrack_help help;
-
- #ifdef CONFIG_IP_NF_NAT_NEEDED
- struct {
- struct ip_nat_info info;
- #if defined(CONFIG_IP_NF_TARGET_MASQUERADE) || \
- defined(CONFIG_IP_NF_TARGET_MASQUERADE_MODULE)
- int masq_index;
- #endif
- } nat;
- #endif /* CONFIG_IP_NF_NAT_NEEDED */
-
- #if defined(CONFIG_IP_NF_CONNTRACK_MARK)
- unsigned long mark;
- #endif
-
- struct ip_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX];
- };
-
-
- static struct ip_conntrack_tuple_hash *
- init_conntrack(const struct ip_conntrack_tuple *tuple,
- struct ip_conntrack_protocol *protocol,
- struct sk_buff *skb)
- {
- struct ip_conntrack *conntrack;
- struct ip_conntrack_tuple repl_tuple;
- size_t hash;
- struct ip_conntrack_expect *exp;
-
- /*如果计算hash值的随机数种子没有被初始化,则初始化之*/
- if (!ip_conntrack_hash_rnd_initted) {
- get_random_bytes(&ip_conntrack_hash_rnd, 4);
- ip_conntrack_hash_rnd_initted = 1;
- }
-
- /*计算hash值*/
- hash = hash_conntrack(tuple);
-
- /*判断连接跟踪表是否已满*/
- if (ip_conntrack_max
- && atomic_read(&ip_conntrack_count) >= ip_conntrack_max) {
- /* Try dropping from this hash chain. */
- if (!early_drop(&ip_conntrack_hash[hash])) {
- if (net_ratelimit())
- printk(KERN_WARNING
- "ip_conntrack: table full, dropping"
- " packet.\n");
- return ERR_PTR(-ENOMEM);
- }
- }
-
- /*根据当前的tuple取反,计算该数据包的“应答”的tuple*/
- if (!ip_ct_invert_tuple(&repl_tuple, tuple, protocol)) {
- DEBUGP("Can't invert tuple.\n");
- return NULL;
- }
- /*为数据包对应的连接分配空间*/
- conntrack = kmem_cache_alloc(ip_conntrack_cachep, GFP_ATOMIC);
- if (!conntrack) {
- DEBUGP("Can't allocate conntrack.\n");
- return ERR_PTR(-ENOMEM);
- }
- /*初始化该结构*/
- memset(conntrack, 0, sizeof(*conntrack));
- /*使用计数器累加*/
- atomic_set(&conntrack->ct_general.use, 1);
- /*设置destroy函数指针*/
- conntrack->ct_general.destroy = destroy_conntrack;
- /*设置正反两个方向的tuple*/
- conntrack->tuplehash[IP_CT_DIR_ORIGINAL].tuple = *tuple;
- conntrack->tuplehash[IP_CT_DIR_REPLY].tuple = repl_tuple;
- if (!protocol->new(conntrack, skb)) {
- kmem_cache_free(ip_conntrack_cachep, conntrack);
- return NULL;
- }
- /* 初始化时间计数器,并设置超时初始函数 */
- init_timer(&conntrack->timeout);
- conntrack->timeout.data = (unsigned long)conntrack;
- conntrack->timeout.function = death_by_timeout;
-
- WRITE_LOCK(&ip_conntrack_lock);
- exp = find_expectation(tuple);
-
- if (exp) {
- DEBUGP("conntrack: expectation arrives ct=%p exp=%p\n",
- conntrack, exp);
- /* Welcome, Mr. Bond. We've been expecting you... */
- __set_bit(IPS_EXPECTED_BIT, &conntrack->status);
- conntrack->master = exp->master;
- #if CONFIG_IP_NF_CONNTRACK_MARK
- conntrack->mark = exp->master->mark;
- #endif
- nf_conntrack_get(&conntrack->master->ct_general);
- CONNTRACK_STAT_INC(expect_new);
- } else {
- conntrack->helper = ip_ct_find_helper(&repl_tuple);
-
- CONNTRACK_STAT_INC(new);
- }
-
- /* 这里,并没有直接就把该连接加入hash表,而是先加入到unconfirmed链表中. */
- list_add(&conntrack->tuplehash[IP_CT_DIR_ORIGINAL].list, &unconfirmed);
-
- atomic_inc(&ip_conntrack_count);
- WRITE_UNLOCK(&ip_conntrack_lock);
-
- if (exp) {
- if (exp->expectfn)
- exp->expectfn(conntrack, exp);
- destroy_expect(exp);
- }
-
- /*返回的是初始方向的hash节点*/
- return &conntrack->tuplehash[IP_CT_DIR_ORIGINAL];
- }
在前文中提到过,一条完整的连接,采用struct ip_conntrack 结构描述,初始化函数的主要功能,就是分配一个这样的空间,然后初始化它的一些成员。
在这个函数中,有三个重要的地方需要注意,一个是根据当前tuple,计算出应答方向的tuple,它是调用ip_ct_invert_tuple 函数实现的:
- int
- ip_ct_invert_tuple(struct ip_conntrack_tuple *inverse,
- const struct ip_conntrack_tuple *orig,
- const struct ip_conntrack_protocol *protocol)
- {
- inverse->src.ip = orig->dst.ip;
- inverse->dst.ip = orig->src.ip;
- inverse->dst.protonum = orig->dst.protonum;
- inverse->dst.dir = !orig->dst.dir;
-
- return protocol->invert_tuple(inverse, orig);
- }
这个函数事实上,与前面讲的tuple的转换是一样的,只是来了个乾坤大挪移,把来源和目的,以及方向对调了。
另一个重点的是函数对特殊协议的支持,我们这里暂时跳过了这部份。
第三个地方是调用协议的new函数:
if (!protocol->new(conntrack, skb)) {
kmem_cache_free(ip_conntrack_cachep, conntrack);
return NULL;
}
new 函数指定在每个封包第一次创建连接时被调用,它根据协议的不同,所处理的过程不同,以ICMP协议为例:
- /* Called when a new connection for this protocol found. */
- static int icmp_new(struct ip_conntrack *conntrack,
- const struct sk_buff *skb)
- {
- static u_int8_t valid_new[]
- = { [ICMP_ECHO] = 1,
- [ICMP_TIMESTAMP] = 1,
- [ICMP_INFO_REQUEST] = 1,
- [ICMP_ADDRESS] = 1 };
-
- if (conntrack->tuplehash[0].tuple.dst.u.icmp.type >= sizeof(valid_new)
- || !valid_new[conntrack->tuplehash[0].tuple.dst.u.icmp.type]) {
- /* Can't create a new ICMP `conn' with this. */
- DEBUGP("icmp: can't create new conn with type %u\n",
- conntrack->tuplehash[0].tuple.dst.u.icmp.type);
- DUMP_TUPLE(&conntrack->tuplehash[0].tuple);
- return 0;
- }
- atomic_set(&conntrack->proto.icmp.count, 0);
- return 1;
- }
对于ICMP协议而言,仅有ICMP 请求回显、时间戳请求、信息请求(已经很少用了)、地址掩码请求这四个“请求”,可能是一个“新建”的连接,所以,ICMP协议的new函数判断是否是一个全法的ICMP新建连接,如果是非法的,则返回0,否则,初始化协议使用计数器,返回1。
5.4 连接状态的判断
resolve_normal_ct 函数的最后一个重要的工作是对连接状态的判断,tuple中包含一个“方向”成员dst.dir,对于一个初始连接,它是IP_CT_DIR_ORIGINAL:
tuple->dst.dir = IP_CT_DIR_ORIGINAL;
而它的应答包的tuple,则为IP_CT_DIR_REPLY:
inverse->dst.dir = !orig->dst.dir;
IP_CT_DIR_ORIGINAL 和IP_CT_DIR_REPLY都是枚举变量:
- enum ip_conntrack_dir
- {
- IP_CT_DIR_ORIGINAL,
- IP_CT_DIR_REPLY,
- IP_CT_DIR_MAX
- };
宏DIRECTION 就根据tuple中对应成员的值,判断数据包的方向,
/* If we're the first tuple, it's the original dir. */
#define DIRECTION(h) ((enum ip_conntrack_dir)(h)->tuple.dst.dir)
但是,还有一些特殊地方,比如TCP协议,它是一个面向连接的协议,所以,它的“初始”或“应答”包,并不一定就是“新建”或单纯的“应答”包,而是在一个连接过程中的“已建连接包”,另一个,如FTP等 复杂协议,它们还存在一些“关联”的连接,当然这两部份目前还没有涉及到,但并不影响我们分析如下这段代码:
- /* 如果是一个应答包 ,设置状态为已建+应答*/
- if (DIRECTION(h) == IP_CT_DIR_REPLY) {
- *ctinfo = IP_CT_ESTABLISHED + IP_CT_IS_REPLY;
- /* 设置应答标志变量 */
- *set_reply = 1;
- } else {
- /* 新建连接方过来的数据包,对面向连接的协议而言,可能是一个已建连接,判断其标志位*/
- if (test_bit(IPS_SEEN_REPLY_BIT, &ct->status)) {
- DEBUGP("ip_conntrack_in: normal packet for %p\n",
- ct);
- *ctinfo = IP_CT_ESTABLISHED;
- } else if (test_bit(IPS_EXPECTED_BIT, &ct->status)) {
- DEBUGP("ip_conntrack_in: related packet for %p\n",
- ct);
- *ctinfo = IP_CT_RELATED; //关联连接
- } else {
- DEBUGP("ip_conntrack_in: new packet for %p\n",
- ct);
- *ctinfo = IP_CT_NEW; //否则,则为一个新建连接
- }
- *set_reply = 0;
- }
-
- /*设置数据包skb与连接状态的关联*/
- skb->nfct = &ct->ct_general;
- /*每个sk_buff都将与ip_conntrack的一个状态关联,所以从sk_buff可以得到相应ip_conntrack的状态,即数据包的状态*/
- skb->nfctinfo = *ctinfo;
- return ct;
以上的代表所表示的发送或应答的状态如下图所示:
6. ip_confirm
以上的工作事实上都很简单,基本思路是:
一个包来了,转换其tuple,看其在连接跟踪表中没有,有的话,更新其状态,以其做一些与协议相关的工作,如果没有,则分配一个新的连接表项,并与skb_buff关连,但是问题是,这个表项,还没有被加入连接表当中来。其实这样做的理由很简单,因为这个时候,这个包是否有机会活命还是个未知数,例如被其它模块给Drop了……所以,要等到一切安全了,再来将这个表项插入至连接跟踪表。
这个“一切安全”当然是Netfilter所有的模块处理完了,最完全了。
当数据包要离开Netfilter时,它会穿过NF_IP_POST_ROUTING Hook点,状态跟踪模块在这里注册了ip_refrag函数(前面谈到过它的优先级是很低的)。这个Hook函数的工作,也可以猜测到了:“判断表项是否已经在连接跟踪表中了,如果没有,就将其插入表中”!
- static unsigned int ip_refrag(unsigned int hooknum,
- struct sk_buff **pskb,
- const struct net_device *in,
- const struct net_device *out,
- int (*okfn)(struct sk_buff *))
- {
- struct rtable *rt = (struct rtable *)(*pskb)->dst;
-
- /* ip_confirm函数用于处理将tuple加入hash表等重要的后续处理 */
- if (ip_confirm(hooknum, pskb, in, out, okfn) != NF_ACCEPT)
- return NF_DROP;
-
- /* 在连接跟踪开始之前,对分片包进行了重组,这里判断数据包是否需要分片,如果要分片,就调用ip_fragment分片函数将数据包分片发送出去,因为数据包已经被发送走了,所以,在它之后的任何Hook函数已经没有意思了 */
- if ((*pskb)->len > dst_mtu(&rt->u.dst) &&
- !skb_shinfo(*pskb)->tso_size) {
- /* No hook can be after us, so this should be OK. */
- ip_fragment(*pskb, okfn);
- return NF_STOLEN;
- }
- return NF_ACCEPT;
- }
ip_confirm 函数是状态跟踪的另一个重要的函数:
- static unsigned int ip_confirm(unsigned int hooknum,
- struct sk_buff **pskb,
- const struct net_device *in,
- const struct net_device *out,
- int (*okfn)(struct sk_buff *))
- {
- return ip_conntrack_confirm(pskb);
- }
函数仅是转向,将控制权转交给ip_conntrack_confirm函数:
- static inline int ip_conntrack_confirm(struct sk_buff **pskb)
- {
- if ((*pskb)->nfct
- && !is_confirmed((struct ip_conntrack *)(*pskb)->nfct))
- return __ip_conntrack_confirm(pskb);
- return NF_ACCEPT;
- }
is_comfirmed函数用于判断数据包是否已经被__ip_conntrack_confirm函数处理过了,它是通过IPS_CONFIRMED_BIT 标志位来判断,而这个标志位当然是在__ip_conntrack_confirm函数中来设置的:
[code
int
__ip_conntrack_confirm(struct sk_buff **pskb)
{
unsigned int hash, repl_hash;
struct ip_conntrack *ct;
enum ip_conntrack_info ctinfo;
/*取得数据包的连接状态*/
ct = ip_conntrack_get(*pskb, &ctinfo);
/* 如果当前包不是一个初始方向的封包,则直接返回. */
if (CTINFO2DIR(ctinfo) != IP_CT_DIR_ORIGINAL)
return NF_ACCEPT;
/*计算初始及应答两个方向tuple对应的hash值*/
hash = hash_conntrack(&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple);
repl_hash = hash_conntrack(&ct->tuplehash[IP_CT_DIR_REPLY].tuple);
/* IP_NF_ASSERT(atomic_read(&ct->ct_general.use) == 1); */
/* No external references means noone else could have
confirmed us. */
IP_NF_ASSERT(!is_confirmed(ct));
DEBUGP("Confirming conntrack %p\n", ct);
WRITE_LOCK(&ip_conntrack_lock);
/* 在hash表中查找初始及应答的节点*/
if (!LIST_FIND(&ip_conntrack_hash[hash],
conntrack_tuple_cmp,
struct ip_conntrack_tuple_hash *,
&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple, NULL)
&& !LIST_FIND(&ip_conntrack_hash[repl_hash],
conntrack_tuple_cmp,
struct ip_conntrack_tuple_hash *,
&ct->tuplehash[IP_CT_DIR_REPLY].tuple, NULL)) {
/* Remove from unconfirmed list */
list_del(&ct->tuplehash[IP_CT_DIR_ORIGINAL].list);
/*主要的工作就在于此了:将当前连接表项(初始和应答的tuple)添加进hash表*/
list_prepend(&ip_conntrack_hash[hash],
&ct->tuplehash[IP_CT_DIR_ORIGINAL]);
list_prepend(&ip_conntrack_hash[repl_hash],
&ct->tuplehash[IP_CT_DIR_REPLY]);
/* Timer relative to confirmation time, not original
setting time, otherwise we'd get timer wrap in
weird delay cases. */
ct->timeout.expires += jiffies;
add_timer(&ct->timeout);
atomic_inc(&ct->ct_general.use);
set_bit(IPS_CONFIRMED_BIT, &ct->status);
CONNTRACK_STAT_INC(insert);
WRITE_UNLOCK(&ip_conntrack_lock);
return NF_ACCEPT;
}
CONNTRACK_STAT_INC(insert_failed);
WRITE_UNLOCK(&ip_conntrack_lock);
return NF_DROP;
}[/code]
这样,一条新建连接就被加入到表项当中了。如果其有后续连接,如应答,进入连接跟踪表,又转换其tuple,然后查到此表项,循环中……
[ 本帖最后由 独孤九贱 于 2007-12-7 11:11 编辑 ]
作者: 独孤九贱 发布时间: 2006-08-21
作者: platinum 发布时间: 2006-08-21
哈,九贱兄要将 netfilter 和 iptables 玩到通通透啊
还没有呢,ALG的代码还没有整理出来,这个状态检测,只是普通的状态检测的实现,如ICMP,UDP,麻烦点的TCP的处理,ALG,如FTP协议的处理,都只是拿下了原理,框架,写了笔记,有空的时候,就把代码分析继续贴上来,呵呵。
欢迎大家回贴讨论!
作者: 独孤九贱 发布时间: 2006-08-22
作者: liyanux 发布时间: 2006-08-22
作者: chenyajun5 发布时间: 2006-08-24
ALG是啥?
ALG:application level gateway
一种在安全设备里面分析和修改应用层协议内容的技术
用于创建动态连接和修改协议内容。
这里仅指的连接跟踪中对动态协议,如FTP的支持;
作者: 独孤九贱 发布时间: 2006-08-24
作者: 急不通 发布时间: 2006-08-25
顺便问个问题: 连接跟踪表的条目容量,可以在加载ip_conntrack时通过hashsize来指定,但是如果把这个模块编译进内核,是否就无法制定hashsize了?如果要加大连接跟踪表的容量,直接修改 ip_conntrack_max有用吗? ...
只有直接改源码了……
作者: 独孤九贱 发布时间: 2006-08-26
作者: lincoln_834100 发布时间: 2006-09-06
作者: nevermind1997 发布时间: 2006-11-15
九贱兄乃神人也

作者: alibase 发布时间: 2006-11-30
1,连接跟踪表是一张链表,链表的每个节点是一个连接?
2,“表的大小由ip_conntrack_htable_size 全局变量决定”是否意味着有ip_conntrack_htable_size 个节点
3,“整个连接跟踪表大小使用全局变量ip_conntrack_max描述”中ip_conntrack_max和ip_conntrack_htable_size 有什么不同?
4,“整个连接跟踪表大小使用全局变量ip_conntrack_max描述,与hash表的关系是ip_conntrack_max = 8 * ip_conntrack_htable_size”,这里的hash表是什么东东?是和一个连接节点相关吗?还是和整个链表相关?也就hash表究竟代表什么?
不好意思,看我都说糊涂了
期待中...........
作者: alibase 发布时间: 2006-12-08
在受益中,有些地方不明白,希望楼主指教
1,连接跟踪表是一张链表,链表的每个节点是一个连接?
2,“表的大小由ip_conntrack_htable_size 全局变量决定”是否意味着有ip_conntrack_htable_size 个节点
3 ...
你确实说得很糊涂……
连接跟踪表是一张hash链表,仅此而已……
A1->node1->node2->node3……
A2->node1->node2->node3……
……
AN->node1->……->nodeN
每一个node是一个数据包对应的多元组,请求和应答两个多元组构成一个完整连接,另外,还要描述的是ALG的关联连接……
作者: 独孤九贱 发布时间: 2006-12-08
作者: alibase 发布时间: 2006-12-11
作者: hedandi 发布时间: 2006-12-30
作者: scuwb 发布时间: 2007-11-22
作者: mogo 发布时间: 2007-12-05
作者: kwest 发布时间: 2007-12-06
看不到图,期待楼主现身。
看不到图,是因为我不知道如何把图上传并插入到文章中
作者: 独孤九贱 发布时间: 2007-12-07
比如,有 ip_conntrack_ftp,也有 xt_conntrack_ftp,代码几乎相同,这样设计用意何在?
作者: platinum 发布时间: 2007-12-07
作者: ShadowStar 发布时间: 2007-12-07
应该是为了向下兼容,对之前的程序提供接口吧。
那 xt_table 的用途呢?以前没有,现在为什么要增加这个呢?“向下兼容”的好像有点说不通……
作者: platinum 发布时间: 2007-12-07
还要好事者使IPv6,拿个x来统一
作者: sisi8408 发布时间: 2007-12-09
那 xt_table 的用途呢?以前没有,现在为什么要增加这个呢?“向下兼容”的好像有点说不通……
The netfilter core team has released iptables-1.4.0rc1. This is the first release candidate of the new iptables branch 1.4. This release candidate adds support for the generic xtables infrastructure that strongly improves IPv6 support. Also several accumulated bugfixed are included. Test it!
貌似为了增强对ipv6协议的支持,引入了通用的xtables架构……
作者: 独孤九贱 发布时间: 2007-12-10

作者: pywj777 发布时间: 2008-01-18
作者: linux_ha 发布时间: 2008-01-21
诚恳的问一句 :楼主是做什么的?工作内容主要是什么?
这很重要吗?我的工作很不起眼的,一个IT棒棒……

作者: 独孤九贱 发布时间: 2008-01-22
作者: z-cf 发布时间: 2008-01-24
作者: Godbach 发布时间: 2008-07-22
热门阅读
-
office 2019专业增强版最新2021版激活秘钥/序列号/激活码推荐 附激活工具
阅读:74
-
如何安装mysql8.0
阅读:31
-
Word快速设置标题样式步骤详解
阅读:28
-
20+道必知必会的Vue面试题(附答案解析)
阅读:37
-
HTML如何制作表单
阅读:22
-
百词斩可以改天数吗?当然可以,4个步骤轻松修改天数!
阅读:31
-
ET文件格式和XLS格式文件之间如何转化?
阅读:24
-
react和vue的区别及优缺点是什么
阅读:121
-
支付宝人脸识别如何关闭?
阅读:21
-
腾讯微云怎么修改照片或视频备份路径?
阅读:28