iptables深入解析:ct篇

ct是netfilter非常重要的基础和架构核心.它为状态防火墙,nat等打下基础. 一直觉的它很神秘,所以就下定决心分析一下.
这里依然不从框架开始说,而是从实际代码着手.
参考内核 kernel3.8.13

先看看它的初始化:
Net/netfilter/nf_conntrack_core.c
int nf_conntrack_init(struct net *net);
入口在nf_conntrack_standalone.c
module_init(nf_conntrack_standalone_init);

它作为网络空间子系统注册进了内核

注册的过程中调用.init 传递给它的net参数是init_net 它是通过net_ns_init初始化到了net_namespace_list链上。

代码不是很多,核心明显是nf_conntrack_init函数

先进入nf_conntrack_init_init_net函数
nf_conntrack_htable_size 赋值和nf_conntrack_max(这个参数可以通过proc来设置.)
它和内存大小有关,大于1G的即默认为16384=16*1024=4*4k;

比如对于4G的内存,那么它的计算:
size=((1024*1024*4k )/(4*4k))/4= 1024*256/4=1024*64=1024*16*4=4*(4*4k)=4*16384
nf_conntrack_max呢?

后续是设置per-cpu变量:有兴趣的可以看看.

初始化通用协议以及建立sysctl,并初始化nf_ct_l3protos为nf_conntrack_l3proto_generic.
nf_conntrack_init_net初始化hash链表和建立cache相关后续讨论.
先了解下基本的初始化工作后,我们从hook点说起ct是如何建立起来的

nf_conntrack_l3proto_ipv4.c

我们会发现它的优先级比较高.除了上面的钩子还有其他的:
还有nf_defrag_ipv4.c

除了hook点,我们需要记住的就是:连接追踪入口 和 连接追踪出口
记录如何生成呢?我们看报文的流程:
1.发送给本机的数据包

流程:PRE_ROUTING—-LOCAL_IN—本地进程

2.需要本机转发的数据包

流程:PRE_ROUTING—FORWARD—POST_ROUTING—外出

3.从本机发出的数据包

流程:LOCAL_OUT—-POST_ROUTING—外出

那么就选择从流程1分析看看ct是如何一步一步建立起来的.
先从入口说起,接收的报文首先经过钩子点NF_INET_PRE_ROUTING

从优先级上先经过ipv4_conntrack_defrag 再经过ipv4_conntrack_in
对于帧接收,查询并交给处理协议我们已经很熟悉不过了,对于ip,当然先进入ip_rcv

Ip_input.c
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
ip_rcv_finish);

ip的处理工作主要在ip_rcv_finish里完成,ip_rcv主要做了些安全检查。
ipv4_conntrack_defrag看看这个函数,参数就是NF_HOOK里传递给它的

用ip_is_fragment判断是否是分片报文,如果有分片则调用nf_ct_ipv4_gather_frags—>ip_defrag
对于ip_defrag的调用的地方很少. 当需要传递给本地更高协议层的时候通过ip_local_deliver来组包.

补充:
NF_STOLEN 模块接管该数据报,告诉Netfilter“忘掉”该数据报。该回调函数将从此开始对数据包的处理,并且Netfilter应当放弃对该数据包做任何的处理。但是,这并不意味着该数据包的资源已经被释放。这个数据包以及它独自的sk_buff数据结构仍然有效,只是回调函数从Netfilter 获取了该数据包的所有权.

首先把skb独立出来,除去owner,然后调用ip_defrag组包,这也是netfilter效率低的原因之一.(重新组报文很耗费内存和时间)
每个分片报文都会创建一个struct ipq *qp来管理

查找是否已经有ipq, 根据ip的id ,saddr,daddr、protocol计算hash值,由于如果属于同一ip报文的分片则这些相同.
从ip4_frags全局的hash链表里查询,如果没有就创建
hlist_add_head(&qp->list, &f->hash[hash]); 这个qp是结构体struct inet_frag_queue
得到ipq后,通过ip_frag_queue把skb加入到队列里.
ip分片会插入到qp->q.fragments里
最后当满足一定条件时,进行IP重组。当收到了第一个和最后一个IP分片,且收到的IP分片的最大长度等于收到的IP分片的总长度时,表明所有的IP分片已收集齐,调用ip_frag_reasm重组包,成功返回0. 关于ip分片与重组参考的资料有很多.
下面看ipv4_conntrack_in
在nf_conntrack_l3proto_ipv4.c中

首先根据协议PF_INET找到链(ipv4)协议号超出范围则使用默认值nf_conntrack_l3proto_generic。
struct nf_conntrack_l3proto __rcu *nf_ct_l3protos[AF_MAX] __read_mostly;
通过接口nf_conntrack_l3proto_register注册了ipv4和ipv6到nf_ct_l3protos
正常的ipv4是:它负责对ip层报文的解析函数API,后续还有l4层相关的.

这个很重要的结构体struct nf_conntrack_l3proto
找个这个结构体后,调用它节点函数获取l4 协议号和dataoff
然后去找到struct nf_conntrack_l4proto *l4proto这个东西,如果找到即nf_ct_protos[l3proto][l4proto]
异常则为nf_conntrack_l4proto_generic
通过nf_conntrack_l4proto_register注册了tcp、udp、icmp;其他模块还有dccp、gre、sctp、udplite(轻量级用户数据包协议)

根据四层协议error函数check包的正确性。
然后调用resolve_normal_ct .之前我们看到skb->nfct ,一开始肯定为null,它在这个函数里被赋值
首先nf_ct_get_tuple获取struct nf_conntrack_tuple tuple;由它可以判断一个连接即五元组;一个连接由一“去”一“回”两个五元组来唯一确定.
ipv4_pkt_to_tuple 获取srcip、dstip
tuple->src.l3num = l3num;
tuple->src.u3.ip = ap[0];
tuple->dst.u3.ip = ap[1];
tuple->dst.protonum = protonum;
tuple->dst.dir = IP_CT_DIR_ORIGINAL;

然后在解析l4信息:
例如tcp则解析端口:
tuple->src.u.tcp.port = hp->source;
tuple->dst.u.tcp.port = hp->dest;
现在我们有了 srcip、dstip、sportt 、dport,协议号,以及方向信息

然后查询追踪全局表是否已经有了这个流,hash_conntrack_raw计算hash值
__nf_conntrack_find_get:
hlist_nulls_for_each_entry_rcu(h, n, &net->ct.hash[bucket],
如果找到则返回,否则返回null,不过它返回的是类型:

对于第一个包肯定为null, 然后init_conntrack创建它.
先反转tuple得到repl_tuple
__nf_conntrack_alloc 申请struct nf_conn *ct;
从cache里申请
ct = kmem_cache_alloc(net->ct.nf_conntrack_cachep, gfp);
然后初始化
ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple = *orig;
ct->tuplehash[IP_CT_DIR_ORIGINAL].hnnode.pprev = NULL;/* save hash for reusing when confirming */
*(unsigned long *)(&ct->tuplehash[IP_CT_DIR_REPLY].hnnode.pprev) = hash;
ct->tuplehash[IP_CT_DIR_REPLY].tuple = *repl;

并设置ct定时器 death_by_timeout
l4proto->new(ct, skb, dataoff, timeouts) 设置l4 ct参数。
关于nf_ct_acct_ext_add这里先不讨论.

然后找到协议注册的expect :struct nf_conntrack_expect
exp = nf_ct_find_expectation(net, zone, tuple);
它查找的是net->ct.expect_hash[h] ,即当前ct所期望关联的tuple

我们看看内核注册了哪些expect
通过nf_ct_expect_alloc申请,这个貌似和上层应用关联用的。
对于应用的关联,不是很清楚,简单看看tftp的nf_conntrack_tftp_init

它有两个关键的地方:
1.tftp[i][j].help = tftp_help;
2.nf_conntrack_helper_register(&tftp[i][j]);

这个tftp是struct nf_conntrack_helper结构体。关于helper这里说明一下:
Netfilter的连接跟踪为我们提供了一个非常有用的功能模块:helper。该模块可以使我们以很小的代价来完成对连接跟踪功能的扩展。这种应用场景需求一般是,当一个数据包即将离开Netfilter框架之前,我们可以对数据包再做一些最后的处理.
同时还有个补充tftp[i][j].expect_policy = &tftp_exp_policy; 它也是相关的
它把helper加入hlist:全局nf_ct_helper_hash[h]

我们__nf_ct_try_assign_helper这个被init_conntrack调用,也就是新建ct的时候
一开始net->ct.expect_hash应该为null
但是expect_hash和nf_ct_helper_hash 又是如何关联起来的呢?
nf_ct_expect_insert会操作expect_hash并插入,它最后封装在nf_ct_expect_related
刚才说到tftp expect对吧,tftp_help里刚好调用了它
在tftp_help里它申请一个exp = nf_ct_expect_alloc(ct); 然后初始化nf_ct_expect_init。
最后调用nf_ct_expect_related把这个exp和具体的ct关联到expect_hash里。
它属于被动的,还得从tftp说起,虽然它依helper方式把注册进了help_hash。
但是它又是如何运作起来的呢?毕竟这个时候只是静态的注册而已,即需要触发tftp_help函数.
要触发它,就需要找到注册的helper,就需要计算hash。刚好在__nf_ct_try_assign_helper中有
__nf_conntrack_helper_find查找注册的helper和当前ct的关联.
我们看看查找的时候用的tuple:

这个参数我们知道就是当前tuple的反转五元组. 而查找的时候计算hash值只用到了五元组的协议号、端口 (还有一个是ipv4 or ipv6)
(跟我们之前查找ct的时候计算的hash需要的参数少了很多.) 很明显helper注册的时候也用了这样的hash算法.
回头看看tftp_helper注册的时候:
点击(此处)折叠或打开

这两个值是事先给定好的. 其实发现没有,虽然很容易关联,但是也面临着冲突的问题.所以需要补全ip和端口信息
既然找到了那么如何处理呢?

help是什么呢?struct nf_conn_help *help;

nf_ct_helper_ext_add扩展ct的ext空间. 然后把找到的helper指针赋给help->helper:

那么以后我们就可以通过help = nfct_help(ct);这样的接口找到我们关联的helper了.
关于查找exp补充说明一下:
expected函数有什么作用?
当一个新的包到达init_conntrack时,就会根据包中的源地址、目的地址等信息填充一个struct nf_conn实例,通常定义为ct的变量。接下来检查当前的连接是否是另外一条已经存在连接的期望连接:

如果exp不为空,就表示当前的连接是另外一条已经存在连接的期望连接.接下来,就是expectfn的工作了:根据master的连接跟踪信息更新新建立的ct连接跟踪信息,并放到连接跟踪表中,详见nf_nat_follow_master函数(因为expectfn通常指向的nf_nat_follow_master).
回到主线函数:
最后把ct加入一个未认证的hlist:

并返回return &ct->tuplehash[IP_CT_DIR_ORIGINAL].
我们看如果直接找到了ct那么下面的工作很简单:设置一些状态值然后赋值skb
skb->nfct = &ct->ct_general;
skb->nfctinfo = *ctinfo; // 对于第一次报文 值为:IP_CT_NEW
return ct;
skb建立ct关联后,然后更新ct的状态,调用l4协议的packet函数:

以上只是简单流程的分析
通过上面的分析我们知道当我们用到ct的时候,把skb->nfct强制类型转换就可以了
虽然nfct是struct nf_conntrack *nfct;

但是我们看到struct nf_conn的结构体

我们是不是明白了为什么skb->nfct那么使用。新的内核也提供了接口:

连接追踪用结构体 struct nf_conn表示 ,而状态信息用enum ip_conntrack_info 表示
1. IP_CT_ESTABLISHED
Packet是一个已建连接的一部分,在其初始方向。
2. IP_CT_RELATED
Packet属于一个已建连接的相关连接,在其初始方向。
3. IP_CT_NEW
Packet试图建立新的连接
4. IP_CT_ESTABLISHED+IP_CT_IS_REPLY
Packet是一个已建连接的一部分,在其响应方向。
5. IP_CT_RELATED+IP_CT_IS_REPLY
Packet属于一个已建连接的相关连接,在其响应方向
刚才我们分析了第一个过来的包,属于新建连接,即IP_CT_NEW。
对于每个进来的包都先获取struct nf_conntrack_tuple信息 和查询或者创建struct nf_conntrack_tuple_hash
接着我们需要看的是ip_conntrack_help()和ip_confirm();优先级上先是helper 然后是confirm.对于新版内核接口名字有所改变:ipv4_help/ipv4_confirm

这个函数很简单,直接找到之前关联的helper然后调用help函数.对于tftp这个helper,它的help即:tftp_help
我们看看help做了什么工作.
首先获取协议头,然后根据协议的特性来填充expt的信息.完善起来.首先是expt->tuple的填充,它除了srcport,其他就是当前ct的tuple的反转tuple。
还有把当前ct赋expt->master=ct.当然关于这个expt->tuple的dport即源端口肯能会根据具体协议重新获取,比如ftp协议被动模式下PASV命令 响应码是227 它里面包含了ip和端口信息。然后把expt插入到之前我们提到过的expect_hash里. 我们回头看看,假如我们查找到了exp那么意味着什么呢?首先它是新建连接,但是它的目的ip和端口,也就是expt的目的ip和端口即所期望的.而建立起这个expt的ct的源ip和源端口和expt的目的ip和目的端口一样.那么意味着建立expt的ct能更快的和当前报文建立联系.也就是经常说的ct过程中一“去”一“回”快速联系起来,当然关于helper针对不同的协议还需要我们自行写解析函数去获取想要的信息.
或许是时候该看看最后一层的处理函数了.ipv4_confirm
直接看nf_conntrack_confirm

函数并不复杂,利用源方向的hash和反方向的hash,查找ct全局表,为什么呢 ,因为在这个报处理的过程中,可能会收到反方向的报文而建立ct.所以如果两个hash任意一个找到表里已有,则返回NF_DROP. 紧接着从unconfirm的hlist删除.设置ct->status |= IPS_CONFIRMED; 添加ct定时器.最后把来和回的tuple_hash都添加到ct全局表中.

到这里,整个流程已经结束了,看起来有点枯燥,后续会补上框架图.仅仅是从代码层去一窥其神秘,在代码里我们见到不少nat相关的东西,一开始我们就说了ct是nat的基础.

1 收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部