热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Linux内核那些事之连接跟踪

“本文分析了Linux内核连接跟踪的关键实现”连接跟踪(也叫会话管理)是状态防火墙关键核心,也是很多网元设备必不可少的一部分。各厂商的实

 本文分析了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等。希望后面有机会跟大家分享更多的内核知识,争取把这个做成系列文章。


推荐阅读
  • 本文介绍了pack布局管理器在Perl/Tk中的使用方法及注意事项。通过调用pack()方法,可以控制部件在显示窗口中的位置和大小。同时,本文还提到了在使用pack布局管理器时,应注意将部件分组以便在水平和垂直方向上进行堆放。此外,还介绍了使用Frame部件或Toplevel部件来组织部件在窗口内的方法。最后,本文强调了在使用pack布局管理器时,应避免在中间切换到grid布局管理器,以免造成混乱。 ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • 模板引擎StringTemplate的使用方法和特点
    本文介绍了模板引擎StringTemplate的使用方法和特点,包括强制Model和View的分离、Lazy-Evaluation、Recursive enable等。同时,还介绍了StringTemplate语法中的属性和普通字符的使用方法,并提供了向模板填充属性的示例代码。 ... [详细]
  • Java程序设计第4周学习总结及注释应用的开发笔记
    本文由编程笔记#小编为大家整理,主要介绍了201521123087《Java程序设计》第4周学习总结相关的知识,包括注释的应用和使用类的注释与方法的注释进行注释的方法,并在Eclipse中查看。摘要内容大约为150字,提供了一定的参考价值。 ... [详细]
  • 本文整理了Java面试中常见的问题及相关概念的解析,包括HashMap中为什么重写equals还要重写hashcode、map的分类和常见情况、final关键字的用法、Synchronized和lock的区别、volatile的介绍、Syncronized锁的作用、构造函数和构造函数重载的概念、方法覆盖和方法重载的区别、反射获取和设置对象私有字段的值的方法、通过反射创建对象的方式以及内部类的详解。 ... [详细]
  • 上图是InnoDB存储引擎的结构。1、缓冲池InnoDB存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理。因此可以看作是基于磁盘的数据库系统。在数据库系统中,由于CPU速度 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 目录实现效果:实现环境实现方法一:基本思路主要代码JavaScript代码总结方法二主要代码总结方法三基本思路主要代码JavaScriptHTML总结实 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 本文介绍了Redis的基础数据结构string的应用场景,并以面试的形式进行问答讲解,帮助读者更好地理解和应用Redis。同时,描述了一位面试者的心理状态和面试官的行为。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
author-avatar
关圣钊
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有