本文分析了Linux内核连接跟踪的关键实现

连接跟踪(也叫会话管理)是状态防火墙关键核心,也是很多网元设备必不可少的一部分。各厂商的实现原理基本雷同,只是根据各自的业务进行修改和优化。其中,还有不少厂商干脆是基于Linux内核实现的。下面,我们就来看看Linux内核中连接跟踪的几个要点。

注:本文对应的Linux源码为最新的5.9.12

00

基础知识

  1. 一个连接由两个tuple组成,分别代表两个方向的报文信息。

  2. 一个tuple一般由报文的五元组构成,分别是源地址、目的地址,源端口、目的端口和协议号(四层)。

  3. 连接跟踪表一般为hash表。该表可能是全局的,也可能是per cpu的,Linux内核选择的是全局表。

  4. 每个连接根据自己的状态,都有自己的生命周期,到期会销毁。

  5. 网元设备一般会在连接中增加扩展,来实现带状态的业务。

01

连接跟踪的匹配和创建

对于拥有连接跟踪的网元设备来说,数据报文一定是先尝试匹配已有连接,如果找到对应的连接则报文属于该连接,如果没有找到,则创建新连接。所以,连接的匹配和创建一般都是相邻的。

nf_conntrack_in是连接匹配的入口函数,其会被netfitler处理brdige(etables)、ipv4和ipv6的hook函数调用。

在nf_conntrack_in中,

这里先调用get_l4proto,根据三层协议获取四层协议号和数据偏移。然后,调用resolve_normal_ct进行连接的匹配和创建。

在resolve_normal_ct中,

首先调用nf_ct_get_tuple根据报文生成这个方向的tuple,然后调用__nf_conntrack_find_get通过tuple进行连接的查找。如果没有找到,则调用init_conntrack生成新的连接。

也许有的同学会有疑问,在基础知识小节中,tuple是包含源端口和目的端口。那么如果报文不是UDP或者TCP,没有源端口和目的端口怎么办?答案很简单,内核会根据4层协议使用不同字段来填充tuple。在nf_ct_get_tuple中,有如下代码:

以ICMP报文为例,见icmp_pkt_to_tuple

所以tuple的五元组只是一种粗略说法,实际上内核会根据不同协议填充不同字段。因为tuple的匹配时包含4层协议号本身,所以这样做完全没有问题。

连接的查找比较简单,根据tuple确定hash桶,然后遍历桶中元素查找拥有相等tuple的连接。

连接的创建同样简单,在init_conntrack中,

首先调用__nf_conntrack_alloc申请一块conntrack的内存,然后在根据需求增加相应的扩展(extension),如这里的timeout_ext,acct_ext,tstamp_ext等等。在很多厂商的实现中,都会把自己的业务数据直接保存在conntrack结构中,这就造成了conntrack的结构越来越大,且会保存一些没有必要的数据。比如一共有三个业务功能的数据保存在conntrack中,但实际上用户只使用了功能1,结果功能2和功能3虽然没有使用,但依然占用了内存。同时,越来越大的conntrack结构也越来越难以维护。Linux内核最早也是采取的这种方式(简单直接),后来其抽象了nf_ct_ext结构用于做业务扩展。conntrack不再直接保存扩展数据,当业务扩展被启用时,会动态申请nf_ct_ext,并追加到conntrack的扩展结构中。

因为本文只讨论连接跟踪,所以在此不细述conntrack的extension了。以后有机会再和大家分享这块儿内容。

在init_conntrack中的结尾,还有一块儿代码值得大家注意:

前两个语句,增加了conntrack的引用计数,然后将conntrack添加到unconfirmed_list中。

这里的unconfirmed list是一个per cpu 变量。

02

连接如何插入全局连接跟踪表

前一节中,我们看到了内核创建了一个新的连接conntrack,并将其插入到unconfirmed list中。那么为什么不直接将其插入到全局连接跟踪表中呢?其原因有二:

  1. 在基础知识一节中,我们提到一个conntrack有两个tuple。当我们创建conntrack时,实际上只有一个方向的报文,也就只能够生成这个方向的tuple。虽然我们可以根据tuple的定义,将当前方向的tuple做个反向处理来得到反向tuple。但这里会有一个问题,当有NAT规则时,此时此刻我们并不知道后面会如何进行NAT处理,生成的反向tuple自然不正确。那么,是否可以先插入一个tuple呢?答案也是否定的。这可能会引发并发竞争的问题。试想,一个连接的两个方向的报文,有可能由两个CPU进行处理,他们都根据当前报文生成了conntrack和tuple并插入到全局表中。这就意味着同一个连接被插入表两次,自然是一个错误。如果要增加这种情况的检查,逻辑会更加复杂。

  2. 在创建连接的时候,属于比较早期的阶段,很有可能在后面的处理中报文会被丢弃,比如命中了防火墙drop规则等等。如果先把连接插入了全局表,到时候还要进行删除处理,这无疑是一种浪费。Linux内核会在最后阶段,才会把连接插入到全局表中。

基于以上原因,Linux内核会在最后时刻才会将新建的conntrack插入到全局表中。那么这个最后的时刻是什么时候呢?Linux内核的连接跟踪是由netfilter模块的功能,而netfilter的原理主要是通过五个阶段(prerouting、forward、postrouting、localin和localout),并在每个阶段根据优先级执行hook函数或者规则。关于这块儿的资料已经很多,在此不做重复说明。

以IPv4报文为例,

其分别在postrouting和localin两个阶段,以优先级NF_IP_PRI_CONNTRACK_CONFIRM(INT_MAX)来调用hook函数ipv4_confirm。这就保证了无论是转发的,本机发出的(最后也会走到postrouting),还是发给本机的,都会在最后阶段(也就是即将离开netfilter模块)时执行ipv4_confirm。而ipv4_confirm经过层层还是会最终调用到__nf_conntrack_confirm,其负责将conntrack插入到全局表中。

前文说过,一个连接有两个tuple,根据不同tuple计算的hash bucket自然也不同,也就是说,内核需要将conntrack插入到了两个bucket中。前面在__nf_conntrack_find_get中进行连接查找匹配时,使用的是rcu_read_lock进行保护。现在要进行插入操作(写操作),自然要使用锁了。在老版本内核中,全局连接表的写入操作使用了全局唯一一个spinlock,这无疑降低并发性能。后来内核对此做了改进,使用了CONNTRACK_LOCKS(1024)个锁,来减小锁的粒度。

对于一个连接涉及两把锁的时候,就需要注意上锁的顺序,不然就会引起死锁。比如连接1上锁顺序是lock A,lock B,而连接2上锁顺序则是lock B,lock A。当连接1持有了lock A,然后尝试获取lock B,连接2持有了lock B,然后尝试获取lock A。这时,两个CPU就陷入了死锁状态。为了避免这种问题,就需要保证上锁的顺序,即使是不同连接,也要使用同一个顺序上锁。为此,内核特意封装了一个函数解决这一问题。

上面代码中h1和h2分别对应conntrack两个tuple计算的hash值,分别与CONNTRACK_LOCKS进行模操作得到两个锁的索引。然后比较h1和h2,永远保证先对索引小的lock进行上锁,然后再锁索引大的lock。其中特殊情况是两个锁索引相同时,那么只锁一次。

然后先检查是否已经有CPU插入了相同连接,

如果两个tuple中的任何一个已经被插入,则认为已有CPU插入了相同连接,则放弃当前连接的插入。

通过一系列检查后,__nf_conntrack_confirm调用__nf_conntrack_hash_insert把conntrack两个tuple插入到全局表中。

03

连接跟踪的生命周期

如何处理淘汰(或者叫做删除)过期连接,最直接的做法就是为每个连接增加一个定时器,定时器过期时间即为连接的生命周期。早期内核版本也是采取的这一方式。但随着支持的并发连接数量的增多,过期timer的数量也成为了一个巨大的值。这种海量的timer,对timer机制是一个挑战,同时每个timer(struct timer_list)会占用80个字节(x86_64)。在海量的连接下,定时器内存的消耗也不容忽视。于是,内核做了一个优化,使用了一个u32 变量timeout作为conntrack的过期时间。但是,没有了定时器触发,如何判定conntrack过期呢?

首先,在nf_conntrack_in函数中调用nf_conntrack_handle_packet根据不同协议处理报文,更新连接状态。以TCP报文为例,会调用nf_conntrack_tcp_packet进行处理。

这里根据不同的TCP状态确定不同的timeout值,然后调用nf_ct_refresh_acct设置到conntrack上。

然后,在连接查找匹配时,即____nf_conntrack_find函数中。

在遍历桶中连接时,在匹配前调用nf_ct_is_expired判断连接是否过期,如果过期则调用nf_ct_gc_expired淘汰该连接。这样就保证了大部分过期连接可以得到及时淘汰。

如果在最坏的情况下,某个桶始终不会被遍历时,那个桶中的连接如何淘汰呢?为应对这种情况,内核还有一个补救措施 —— 定义了一个deferable work,gc_worker。其周期性(1s)执行,按顺序遍历全局连接表,淘汰过期连接。

以上三点是连接跟踪中比较大块和重要的部分,除此之外,还有关联连接、扩展支持等。内核基于连接跟踪又实现了很多有趣实用的功能,如NAT、ALG、SynProxy等。希望后面有机会跟大家分享更多的内核知识,争取把这个做成系列文章。

Linux内核那些事之连接跟踪相关推荐

  1. linux内核协议栈 netfilter 之连接跟踪子系统的L3 L4协议栈模块初始化与自定义注册

    目录 1 L3.L4协议跟踪初始化 nf_conntrack_proto_init() 1.1 L3协议管理 1.1.1 struct nf_conntrack_l3proto 1.1.2 L3协议注 ...

  2. linux内核那些事之buddy(慢速申请内存__alloc_pages_slowpath)(5)

    内核提供__alloc_pages_nodemask接口申请物理内存主要分为两个部分:快速申请物理内存get_page_from_freelist(linux内核那些事之buddy(快速分配get_p ...

  3. linux内核那些事之buddy(anti-fragment机制)(4)

    程序运行过程中,有些内存是短暂的驻留 用完一段时间之后就可以将内存释放以供后面再次使用,但是有些内存一旦申请之后,会长期使用而得不到释放.长久运行有可能造成碎片.以<professional l ...

  4. linux内核那些事之mmap_region流程梳理

    承接<linux内核那些事之mmap>,mmap_region()是申请一个用户进程虚拟空间 并根据匿名映射或者文件映射做出相应动作,是实现mmap关键函数,趁这几天有空闲时间 整理下mm ...

  5. linux内核那些事之buddy

    buddy算法是内核中比较古老的一个模块,很好的解决了相邻物理内存碎片的问题即"内碎片问题",同时有兼顾内存申请和释放效率问题,内核从引入该算法之后一直都能够在各种设备上完好运行, ...

  6. linux内核那些事之pg_data_t、zone结构初始化

    free_area_init 继续接着<linux内核那些事之ZONE>,分析内核物理内存初始化过程,zone_sizes_init()在开始阶段主要负责对各个类型zone 大小进行计算, ...

  7. linux内核那些事之Sparse vmemmap

    <inux内核那些事之物理内存模型之SPARCE(3)>中指出在传统的sparse 内存模型中,每个mem_section都有一个属于自己的section_mem_map,如下图所示: 而 ...

  8. linux内核那些事之buddy(anti-fragment机制-steal page)(5)

    继<linux内核那些事之buddy(anti-fragment机制)(4)>,在同一个zone内指定的migrate type中没有足够内存,会启动fallback机制,从fallbac ...

  9. 一文读懂 Linux 下单机实现百万并发的内核黑科技:连接跟踪(Conntrack)

    公众号关注 「奇妙的 Linux 世界」 设为「星标」,每天带你玩转 Linux ! 本文介绍连接跟踪(connection tracking,conntrack,CT)的原理,应用,及其在 Linu ...

  10. 介绍Calico eBPF数据平面:Linux内核网络、安全性和跟踪(Kubernetes、kube-proxy)

    目录 性能测试环境 Pod到Pod的吞吐量和CPU 改善kube-proxy 第一包延迟 保留外部源IP和直接服务器返回 高效的数据平面更新 "技术预览"是什么意思? 结论 推荐阅 ...

最新文章

  1. APT(高级可持续性威胁)
  2. 征服Perl——输入输出相关知识——里程碑M6
  3. uos安装方法_UOS(最美操作系统)安装与使用体验
  4. React Native开发环境搭建记录
  5. word文档出现方格乱码
  6. 推荐:Java性能优化系列集锦
  7. 为什么计算机语言都是英语,编程为什么都是英语 编程为什么没有汉语
  8. 斗地主AI算法——第十章の被动出牌(4)
  9. break和continue的区别和执行过程
  10. SAP License:对不起,“下一代ERP”仍旧是现在的ERP
  11. NLP知识包--语义分析-语义角色标注
  12. linux-qcow2格式安装虚拟机及脚本一键克隆qcow2虚拟机
  13. 怎么用计算机解方程,计算器怎么解方程
  14. Modown V1.9 WordPress资源素材付费下载Erphpdown主题模板原版
  15. css3背景颜色渐变属性(Gradients)
  16. 企业最大的危机是来自于内部
  17. 类的封装与对象数组-随机点名器
  18. 数据科学导论——数据预处理进阶
  19. 用C语言打印一个等腰三角形
  20. 《The One!团队》第八次作业:ALPHA冲刺(二)

热门文章

  1. python3--命名空间字典
  2. JAVA学习第六十三课 — 关于client服务端 amp;amp; URL类 amp; URLConnection
  3. BZOJ-3227 红黑树(tree) 树形DP
  4. WEB架构师成长之路 二
  5. 从程序员到项目经理(16):原来一切问题都是可以解决的【转载】
  6. Serv-U权限提升再提升--记一次虚拟主机入侵
  7. [转] 面向对象软件开发和过程(四)重用
  8. 以太坊DAO之股东协会智能合约
  9. Postfix邮件服务系统
  10. PostgreSQL 全文检索 - 词频统计