连接跟踪.docx
《连接跟踪.docx》由会员分享,可在线阅读,更多相关《连接跟踪.docx(33页珍藏版)》请在冰豆网上搜索。
连接跟踪
连接跟踪
目录
1引言1
1.1编写目的1
1.2编写背景1
1.3预期读者和阅读建议1
1.4文档约定1
1.5参考资料1
2详细设计1
2.1Netfilter介绍1
2.2连接跟踪4
2.3期望连接13
2.4连接跟踪初始化18
2.5连接跟踪状态20
3问题总结26
1
引言
1.1编写目的
编写该文档的目的是为了更好地理解防火墙,连接跟踪也是网关中安全业务的基础。
为了能更好地理解连接跟踪,本详细设计报告描述了Netfilter框架简介、会话的处理流程、数据结构的组织、关键函数。
1.2编写背景
无
1.3预期读者和阅读建议
本文档的预期读者包括网关安全模块开发人员。
开发人员应重点理解函数实现和数据结构之间的关系。
1.4文档约定
无
1.5参考资料
2详细设计
2.1Netfilter介绍
Netfilter是2.4.x内核引入的,提供了对2.0.x内核中的ipfw以及2.2.x内核中的ipchains的兼容和改进,工作在内核空间中,通常结合ip_table内核模块一起使用以构造linux下的防火墙。
Linux内核中,netfilter是一个包过滤框架,默认地,它在这个框架上实现了包过滤、状态检测、网络地址转换和包标记等多种功能,因为它设计的开放性,任何有内核开发经验的开发人员,也可以很容易地利用它提供接口,在内核的数据链路层、网络层,实现自己的功能模块。
2.1.1Hook点
Netfilter的整体思想为:
嵌入到内核IP协议栈的一系列调用入口,通过挂钩,将钩子函数设置在报文处理的路径上,以达到控制数据包流通的作用,参照图2-1。
这些路径可以分为三类:
流入的、流经的和流出的,具体为PRE_ROUTING,LOCAL_IN,FORWARD,LOCAL_OUT,POST_ROUTING,这些点,简单称之为Hook点。
这5个Hook的含义如下:
PRE-ROUTING:
刚刚进入网络层的数据包通过此点(刚刚进行完版本号,校验
和等检测),路由之前
LOCAL_IN:
在选路确定之后,且数据包的目的是本地主机
FORWARDING:
目的地是其它主机地数据包
LOCAL_OUT:
来自本机进程的数据包在其离开本地主机的过程中
POST_ROUTING:
在数据包离开本地主机之前
网络包在网关中的流经及处理流程,如图2-1所示。
在这些点上注册了一些函数,这些函数可以叫做hook函数,当数据包流经hook点的时候,Netfilter会调用在当前hook点上注册的这些hook函数并根据hook函数的返回值来决定该数据包该如何处理。
有五种处理方式:
NF_ACCEPT继续正常传输数据报。
这个返回值告诉Netfilter:
到目前为止,该数据包还是被接受的并且该数据包应当被递交到网络协议栈的下一个阶段。
NF_DROP丢弃该数据报,不再传输。
NF_STOLEN模块接管该数据报,告诉Netfilter“忘掉”该数据报。
该回调函数将从此开始对数据包的处理,并且Netfilter应当放弃对该数据包做任何的处理。
但是,这并不意味着该数据包的资源已经被释放。
这个数据包以及它独自的sk_buff数据结构仍然有效,只是回调函数从Netfilter获取了该数据包的所有权。
NF_QUEUE对该数据报进行排队(通常用于将数据报给用户空间的进程进行处理)
NF_REPEAT再次调用该回调函数,应当谨慎使用这个值,以免造成死循环。
NF_STOP不再继续走该HOOK点上的其他钩子函数,继续走Netfilter的流程
图2-1Hook处理
Netfilter定义了一个二维数组structlist_headnf_hooks[NPROTO][NF_MAX_HOOKS]来保存各个协议的各个挂载点,参见图2-2。
每一个nf_hooks[协议号][hook点]元素都是一个链表头,所以链表中可以存在多个元素,每个元素都是一个包含有处理函数的结构体。
图2-2nf_hook数组
由于包总是从PRE_ROUTING/LOCAL_OUT两个钩子进入Netfilter,本文中把这两个钩子叫做入口。
包总是从LOCAL_IN/POST_ROUTING两个钩子点走完Netfilter,把这两个钩子叫做出口。
图2-3Netfilter结构体关系示意图
2.1.2Netfilter原理
Netfilter使用宏NF_HOOK在协议栈内部切入到Netfilter框架中,定义如下:
#defineNF_HOOK(pf,hook,skb,indev,outdev,okfn)\
NF_HOOK_THRESH(pf,hook,skb,indev,outdev,okfn,INT_MIN)
关于宏NF_HOOK各个参数的解释说明:
1)pf:
协议族名,Netfilter架构同样可以用于IP层之外,因此这个变量还可以有诸如PF_INET6,PF_DECnet等名字。
2)hook:
HOOK点的名字,对于IP层,就是取上面的五个值;
3)skb:
不解释;
4)indev:
数据包进来的设备,以structnet_device结构表示;
5)outdev:
数据包出去的设备,以structnet_device结构表示;
(后面可以看到,以上五个参数将传递给nf_register_hook中注册的处理函数。
)
6)okfn:
是个函数指针,当所有的该HOOK点的所有登记函数调用完后,转而走此流程。
而NF_HOOK_THRESH又是一个宏:
#defineNF_HOOK_THRESH(pf,hook,skb,indev,outdev,okfn,thresh)\
({int__ret;\
if((__ret=nf_hook_thresh(pf,hook,&(skb),indev,outdev,okfn,thresh,1))==1)\
__ret=(okfn)(skb);\
__ret;})
NF_HOOK_THRESH宏内部调用了nf_hook_thresh函数。
nf_hook_thresh通过传入的参数pf协议号和hook点,找到链表头指针nf_hooks[协议号][hook点],如图2-4所示,然后遍历调用在链表上注册过的函数hookfn,并根据hookfn的调用返回值的情况来进行相应处理。
图2-4hook函数链表
由于多个函数都会被调用,并且被调用的函数还能决定该数据包的去留,所以存在一个优先级thresh(宏定义中),thresh来规定,哪个函数先被调用,哪个函数后被调用。
优先级数值越小表示优先级越高,如果优先级一样,则按照注册的时候,挂载的先后顺序依次处理。
见下面部分代码:
list_for_each_continue_rcu(*i,head){/*遍历*/
structnf_hook_ops*elem=(structnf_hook_ops*)*i;/*获取元素*/
if(hook_thresh>elem->priority)/*优先级比hook_thresh高(值小)的执行*/
continue;
verdict=elem->hook(hook,skb,indev,outdev,okfn);/*调用hook函数*/
……
}
上述的元素是用到了下面这个结构体:
structnf_hook_ops{
structlist_headlist;
nf_hookfn*hook;/*回调函数*/
structmodule*owner;/*归属模块*/
u_int8_tpf;/*协议号*/
unsignedinthooknum;/*Hook点*/
intpriority;/*调用的优先级*/
};
注册一个钩子回调函数通过调用注册函数nf_register_hook来实现,nf_register_hook内部实现上插入到链表的时候,根据优先级priority进行排序插入,递归调用的时候,就直接使高优先级的先被调用,低优先级后被调用。
2.1.3Netfilter切入点
1.net/ipv4/ip_input.c里的ip_rcv函数。
该函数主要用来处理网络层的IP报文的入口函数,它到Netfilter框架的切入点为:
NF_HOOK(PF_INET,NF_IP_PRE_ROUTING,skb,dev,NULL,ip_rcv_finish)
如果协议栈当前收到了一个IP报文(PF_INET),那么就把这个报文传到Netfilter的NF_IP_PRE_ROUTING过滤点,去检查在过滤点(nf_hooks[2][0])是否已经有人注册了相关的用于处理数据包的钩子函数。
如果有,则挨个去遍历链表nf_hooks[2][0]去寻找匹配的match和相应的target,根据返回到Netfilter框架中的值来进一步决定该如何处理该数据包(由钩子模块处理还是交由ip_rcv_finish函数继续处理)。
2.net/ipv4/ip_forward.c中的ip_forward函数,它的切入点为:
NF_HOOK(PF_INET,NF_IP_FORWARD,skb,skb->dev,rt->u.dst.dev,ip_forward_finish);
在经过路由抉择后,所有需要本机转发的报文都会交由ip_forward函数进行处理。
这里,该函数由NF_IP_FOWARD过滤点切入到Netfilter框架,在nf_hooks[2][2]过滤点执行匹配查找。
最后根据返回值来确定ip_forward_finish函数的执行情况。
3.net/ipv4/ip_output.c中的ip_output函数,它切入Netfilter框架的形式为:
NF_HOOK_COND(PF_INET,NF_IP_POST_ROUTING,skb,NULL,dev,ip_finish_output,!
(IPCB(skb)->flags&IPSKB_REROUTED));
这里我们看到切入点从无条件宏NF_HOOK改成了有条件宏NF_HOOK_COND,调用该宏的条件是:
如果协议栈当前所处理的数据包skb中没有重新路由的标记,数据包才会进入Netfilter框架。
否则直接调用ip_finish_output函数走协议栈去处理。
同样,如果需要陷入Netfilter框架则数据包会在nf_hooks[2][4]过滤点去进行匹配查找。
4.在net/ipv4/ip_input.c中的ip_local_deliver函数。
该函数处理所有目的地址是本机的数据包,其切入函数为:
NF_HOOK(PF_INET,NF_IP_LOCAL_IN,skb,skb->dev,NULL,ip_local_deliver_finish);
发给本机的数据包,首先全部会去nf_hooks[2][1]过滤点上检测是否有相关数据包的回调处理函数,如果有则执行匹配和动作,最后根据返回值执行ip_local_deliver_finish函数。
5.net/ipv4/ip_output.c中的ip_push_pending_frames函数。
该函数是将IP分片重组成完整的IP报文,然后发送出去。
进入Netfilter框架的切入点为:
NF_HOOK(PF_INET,NF_IP_LOCAL_OUT,skb,NULL,skb->dst->dev,dst_output);
对于所有从本机发出去的报文都会首先去Netfilter的nf_hooks[2][3]过滤点去过滤。
2.2连接跟踪
连接跟踪(CONNTRACK),顾名思义,就是跟踪并且记录连接状态。
Linux为每一个经过网络协议栈的数据包,根据5元组信息(源IP、目的IP、源端口、目的端口和协议号)来生成一个新的连接记录项(Connectionentry)。
此后,所有属于此连接的数据包都被唯一地分配给这个连接,并标识连接的状态。
连接跟踪是防火墙模块的状态检测的基础,同时也是地址转换中实现SNAT和DNAT的前提。
2.2.1连接跟踪表
用于实现连接跟踪的hook函数分别注册到了NF_IP_PRE_ROUTING、NF_IP_LOCAL_OUT、NF_IP_LOCAL_IN和NF_IP_POST_ROUTING四个hook上,见图2-5。
入口函数以比较高的优先级在PRE_ROUTING和LOCAL_OUT上,而出口函数以最低的优先级处在LOCAL_IN和POST_ROUTING上。
图2-5连接跟踪出入口
数据包的流向无非三种:
发给本地(网关)的数据包。
流程PRE_ROUTING->LOCAL_IN
图2-6到本地的数据包流程
经过本地的数据包(转发包)。
流程是PRE_ROUTING->FORWARD->POST_ROUTING
图2-7转发包
从本地(网关)发出的数据包。
流程是LOCAL_OUT->POST_ROUTING
图2-8本地发包
对于一个连接来说,使用structnf_conn来记录,稍后会介绍关于这个结构体的详细内容。
一个内核数据skb的结构体中有一个成员指针nfct其类型也是structnf_conn,该指针指向一个连接。
换句话说,Netfilter框架使用结构体nf_conn来记录一个数据包和其连接的关系。
在连接跟踪内部,收到的每个skb首先被转换成一个nf_conntrack_tuple{}结构,也就是说nf_conntrack_tuple{}结构才是连接跟踪系统所“认识”的数据包。
那么skb和nf_conntrack_tuple{}结构之间是如何转换的呢?
这个问题没有一个统一的答案,与具体的协议息息相关。
例如,对于TCP/UDP协议,根据“源、目的IP+源、目的端口”再加协议号就可以唯一的标识一个数据包了;对于ICMP协议,根据“源、目的IP+类型+代号”再加序列号才可以唯一确定一个ICMP报文等等。
对于诸如像FTP这种应用层的“活动”协议来说情况就更复杂了。
现在从连接跟踪大的流程上,以简短的方式给以简单的概括。
图2-9,说明了连接跟踪入口都做了些什么。
图2-9连接跟踪入口
对于每个到来的skb,连接跟踪都将其转换成一个tuple结构,然后用该tuple去查连接跟踪表。
如果该类型的数据包没有被跟踪过,将为其在连接跟踪的hash表里建立一个连接记录项,对于已经跟踪过了的数据包则不用此操作。
紧接着,调用该报文所属协议的连接跟踪模块的所提供的packet()回调函数,最后根据状态改变连接跟踪记录的状态。
连接跟踪表是一个用于记录所有数据包连接信息的hash散列表,其实连接跟踪表就是一个以数据包的hash值组成的一个双向循环链表数组,每条链表中的每个节点都是nf_conntrack_tuple_hash{}类型的一个对象。
连接跟踪表是由一个全局的双向链表指针变量net->ct.hash[]来表示,如图2-11所示。
根据来的包skb生成tuple,然后根据哈希函数对tuple进行计算得到哈希值,把哈希值作为连接跟踪数组的下标可以得到哈希的头指针,从而可以插入、删除、查询等操作。
图2-11哈希链表
net->ct.hash数组是使用的哈希链表,这里内核巧妙地使用了一个辨别‘null’的方法,由于指针都是4字节对齐(取决于CPU,也有8字节对齐的),所以最后一位被占用来标识是不是’null’,当最后一位是1的时候,就被认为是’null’。
可以看下初始化的时候,遍历net->ct.hash数组,然后调用宏:
#defineINIT_HLIST_NULLS_HEAD(ptr,nulls)\
((ptr)->first=(structhlist_nulls_node*)(1UL|(((long)nulls)<<1)))
当去判断一个哈希链表指针是否是一个’null’,同样也只是判断最后一位是否是1。
源码如下:
staticinlineintis_a_nulls(conststructhlist_nulls_node*ptr){
return((unsignedlong)ptr&1);/*逻辑与1*/
}
2.2.2数据描述
从下面开始介绍几个关键的数据结构,及数据结构之间的关系。
图2-12描述关于’tuple’的结构体structnf_conntrack_tuple:
src:
表示源相关,ip地址、端口/id/key、3层协议号
dst:
表示目的相关,ip地址、端口/icmp(type,code)/key、协议号、方向
nf_conntrack_tuple_hash{}仅仅是对nf_conntrack_tuple{}的封装而已,将其组织成了一个双向链表结构。
因此,在理解层面上我们可以认为它们是同一个东西。
图2-12数据结构tuple
图2-13数据结构structnf_conn
连接跟踪主要的数据结构structnf_conn见图2-13所示,防火墙、策略等相关安全的扩展都是在这个结构体的基础上进行的扩展。
具体每一项的含义如下:
ct_general:
该连接记录被引用的次数
lock:
自旋锁
status:
数据连接的状态,是一个比特位图
master:
该成员指向另外一个ip_conntrack{}。
一般用于期望连接场景。
即如果当前连接是另外某条连接的期望连接的话,那么该成员就指向那条我们所属的主连接。
timeout:
不同协议的每条连接都有默认超时时间,如果在超过了该时间且没有属于某条连接的数据包来刷新该连接跟踪记录,那么会调用这种协议类型提供的超时函数。
mark:
secmark:
ext:
扩展使用
ct_net:
proto:
为其他模块保留的存储空间
tuple_hash:
该结构是个nf_conntrack_tuple_hash{}类型的数组,大小为2。
tuplehash[0]表示一条数据流“初始”方向上的连接情况,tuplehash[1]表示该数据流“应答”方向的响应情况。
对于不同的协议,其连接跟踪的实现是不一样的,所以每种协议如果要开发自己的连接跟踪模块,那么它首先必须实例化一个nf_conntrack_l3proto{}结构体类型的变量,对其进行必要的填充,然后调用nf_conntrack_l3proto_register()函数将该结构进行注册,其实就是根据协议类型将其设置到全局数组nf_ct_l3protos[]中的相应位置上,如图2-14所示。
同时,也存在一个4层协议的全局数组,各个协议保存着对应的处理函数,如图2-15所示。
图2-14三层协议处理全局数组
图2-15协议数组
pkt_to_tuple():
把包转换成tuple
invert_tuple():
根据已知tuple,推出反向tuple
print_tuple():
打印tuple
get_l4proto():
获取4层协议号
netlink相关(具体啥作用,还不清楚):
tuple_to_nlattr():
nlattr_tuple_size():
nlattr_to_tuple():
packet():
判断数据包是否合法,并调整相应连接的信息,也就是实现各协议的状态检测,对于UDP等本身是无连接的协议的判断比较简单,netfilter建立一个虚拟连接,每个新发包都是合法包,只等待回应包到后连接都结束;但对于TCP之类的有状态协议必须检查数据是否符合协议的状态转换过程,这是靠一个状态转换数组实现的。
返回数据报的verdict值
new():
当此协议的一个新连接发生时,调用其指向的这个函数
destroy():
删除一个连接状态
print_conntrack():
打印整个连接记录
to_nlattr():
nlattr_size():
from_nlattr():
2.2.3主要处理部分
再来回顾一下Netfilter的框架,如图2-16所示。
如果不存在Netfilter框架,则包的流程就是图2-16中的函数调用,比如到本地的是:
ip_rcv()->ip_rcv_finish()->ip_local_deliver()->ip_local_deliver_finish()
当加上Netfilter的时候,就会在钩子点的地方先调用注册的函数,然后再调用正常流程中的函数。
还拿到本地的包举例:
ip_rcv()->ipv4_conntrack_defrag()->nf_conntrack_in()->其他钩子处理……->ip_rcv_finish()->ip_local_deliver()->ipv4_confirm()->其他钩子处理……->ip_local_deliver_finish
图2-16Netfilter
从会话跟踪的角度上看,当一个数据包流经Netfilter的时候,主要是经历了几个函数,如图2-17所示:
图2-17Netfilter入口和出口示意图
当数据包流经LOCAL_OUT钩子点的时候,调用的是ipv4_conntrack_local,只不过ipv4_conntrack_local最终还是调用了函数nf_conntrack_in函数,所以这里没画出ipv4_conntrack_local函数。
ipv4_conntrack_defrag主要是完成ip报文分片的重新组装,将属于一个IP报文的多个分片组成一个真正的报文。
连接跟踪只跟踪完整的IP报文,这里不关心IP分片重组的函数ipv4_conntrack_defrag,所以这里着重分析nf_conntrack_in和ipv4_confirm函数。
来自网络的数据包(本地、内网、外网),都会经过函数nf_conntrack_in,首先来看nf_conntrack_in都做了哪些操作,如图2-17和2-18:
图2-17nf_conntrack_in简短流程图
图2-18nf_conntrack_in函数流程图
structsk_buff{}中有两个元素与连接跟踪有关系,分别是nfct和nfctinfo。
nf_conntrack.h中提供了一个函数staticinlinestructnf_conn*nf_ct_get(skb,*ctinfo),此函数可获取到skb的指针nfct(与此数据相关联的连接),从而得知该数据包的连接状态和该连接状态的相关信息ctinfo。
从连接跟踪的角度来看,这个ctinfo表示了每个数据包的几种连接状态:
IP_CT_ESTABLISHED
Packet是一个已建连接的一部分,在其初始方向。
IP_CT_RELATED
Packet属于一个已建连接的相关连接,在其初始方向。
IP_CT_NEW
Packet试图建立新的连接
IP_CT_ESTABLISHED+IP_CT_IS_REPLY
Packet是一个已建连接的一部分,在其响应方向。
IP_CT_RELATED+IP_CT_IS_REPLY
Packet属于一个已建连接的相关连接,在其响应方向。
流程中间有一部分是涉及到期望连接的部分,下文再讲。
再来看下出口做的操作:
图2-19函数__nf_conntrack_confirm流程图
出口的函数主要是从‘非确认’的链表中摘下来,然后加入到确认的链表中(net->ct.hash[])。
入口函数nf_conntrack_in中,查找是否存在tuple就是从net->ct.hash[]这个哈希链表中查找的。
定时器超