如何理解连接跟踪机制

如何理解连接跟踪机制,第1张

连接跟踪定义很简单:用来记录和跟踪连接的状态。

为什么又需要连接跟踪功能呢?因为它是状态防火墙和NAT的实现基础。

Neftiler为了实现基于数据连接状态侦测的状态防火墙功能和NAT地址转换功能才开发出了连接跟踪这套机制。那就意思是说:如果编译内核时开启了连接跟踪选项,那么Linux系统就会为它收到的每个数据包维持一个连接状态用于记录这条数据连接的状态。接下来我们就来研究一下Netfilter的连接跟踪的设计思想和实现方式。

之前有一副图,我们可以很明确的看到:用于实现连接跟踪入口的hook函数以较高的优先级分别被注册到了netfitler的NF_IP_PRE_ROUTING和NF_IP_LOCAL_OUT两个hook点上;用于实现连接跟踪出口的hook函数以非常低的优先级分别被注册到了netfilter的NF_IP_LOCAL_IN和NF_IP_POST_ROUTING两个hook点上。

其实PRE_ROUTING和LOCAL_OUT点可以看作是整个netfilter的入口,而POST_ROUTING和LOCAL_IN可以看作是其出口。在只考虑连接跟踪的情况下,一个数据包无外乎有以下三种流程可以走:

一、发送给本机的数据包

流程:PRE_ROUTING----LOCAL_IN---本地进程,如果是新的包,在PREROUTING处生成连接记录,通过POSTROUTING后加到hash表

二、需要本机转发的数据包

流程:PRE_ROUTING---FORWARD---POST_ROUTING---外出,在PREROUTING处生成连接记录,在LOCAL_IN处把生成的连接记录加到hash表

三、从本机发出的数据包

流程:LOCAL_OUT----POST_ROUTING---外出,在LOCAL_OUT处生成连接记录,在POSTROUTING处把生成的连接记录加到hash表。

我们都知道在INET层用于表示数据包的结构是大名鼎鼎的sk_buff{}(后面简称skb),如果你不幸的没听说过这个东东,那么我强烈的建议你先补一下网络协议栈的基础知识再继续阅读这篇文章。在skb中有个成员指针nfct,类型是struct nf_conntrack{},该结构定义在include/linux/skbuff.h文件中。该结构记录了连接记录被公开应用的计数,也方便其他地方对连接跟踪的引用。连接跟踪在实际应用中一般都通过强制类型转换将nfct转换成指向ip_conntrack{}类型(定义在include/linux/netfilter_ipv4/ip_conntrack.h里)来获取一个数据包所属连接跟踪的状态信息的。即:Neftilter框架用ip_conntrack{}来记录一个数据包与其连接的状态关系。

同时在include/linux/netfilter_ipv4/ip_conntrack.h文件中还提供了一个非常有用的接口:struct ip_conntrack *ip_conntrack_get(skb, ctinfo)用于获取一个skb的nfct指针,从而得知该数据包的连接状态和该连接状态的相关信息ctinfo。从连接跟踪的角度来看,这个ctinfo表示了每个数据包的几种连接状态:

l IP_CT_ESTABLISHED

Packet是一个已建连接的一部分,在其初始方向。

l IP_CT_RELATED

Packet属于一个已建连接的相关连接,在其初始方向。

l IP_CT_NEW

Packet试图建立新的连接

l IP_CT_ESTABLISHED+IP_CT_IS_REPLY

Packet是一个已建连接的一部分,在其响应方向。

l IP_CT_RELATED+IP_CT_IS_REPLY

Packet属于一个已建连接的相关连接,在其响应方向。

在连接跟踪内部,收到的每个skb首先被转换成一个ip_conntrack_tuple{}结构,也就是说ip_conntrack_tuple{}结构才是连接跟踪系统所“认识”的数据包。那么skb和ip_conntrack_tuple{}结构之间是如何转换的呢?这个问题没有一个统一的答案,与具体的协议息息相关。例如,对于TCP/UDP协议,根据“源、目的IP+源、目的端口”再加序列号就可以唯一的标识一个数据包了;对于ICMP协议,根据“源、目的IP+类型+代号”再加序列号才可以唯一确定一个ICMP报文等等。对于诸如像FTP这种应用层的“活动”协议来说情况就更复杂了。本文不试图去分析某种具体协议的连接跟踪实现,而是探究连接跟踪的设计原理和其工作流程,使大家掌握连接跟踪的精髓。因为现在Linux内核更新的太快的都到3.4.x,变化之大啊。就算是2.6.22和2.6.21在连接跟踪这块还是有些区别呢。一旦大家理解了连接跟踪的设计思想,掌握了其神韵,它再怎么也万变不离其宗,再看具体的代码实现时就不会犯迷糊了。俗话说“授人一鱼,不如授人一渔”,我们教给大家的是方法。有了方法再加上自己的勤学苦练,那就成了技能,最后可以使得大家在为自己的协议开发连接跟踪功能时心里有数。这也是我写这个系列博文的初衷和目的。与君共勉。

在开始分析连接跟踪之前,我们还是站在统帅的角度来俯视一下整个连接跟踪的布局。这里我先用比较粗略的精简流程图为大家做个展示,目的是方便大家理解,好入门。当然,我的理解可能还有不太准确的地方,还请大牛们帮小弟指正。

我还是重申一下:连接跟踪分入口和出口两个点。谨记:入口时创建连接跟踪记录,出口时将该记录加入到连接跟踪表中。我们分别来看看。

入口:

整个入口的流程简述如下:对于每个到来的skb,连接跟踪都将其转换成一个tuple结构,然后用该tuple去查连接跟踪表。如果该类型的数据包没有被跟踪过,将为其在连接跟踪的hash表里建立一个连接记录项,对于已经跟踪过了的数据包则不用此 *** 作。紧接着,调用该报文所属协议的连接跟踪模块的所提供的packet()回调函数,最后根据状态改变连接跟踪记录的状态。

出口:

整个出口的流程简述如下:对于每个即将离开Netfilter框架的数据包,如果用于处理该协议类型报文的连接跟踪模块提供了helper函数,那么该数据包首先会被helper函数处理,然后才去判断,如果该报文已经被跟踪过了,那么其所属连接的状态,决定该包是该被丢弃、或是返回协议栈继续传输,又或者将其加入到连接跟踪表中。

连接跟踪的协议管理:

我们前面曾说过,不同协议其连接跟踪的实现是不相同的。每种协议如果要开发自己的连接跟踪模块,那么它首先必须实例化一个ip_conntrack_protocol{}结构体类型的变量,对其进行必要的填充,然后调用ip_conntrack_protocol_register()函数将该结构进行注册,其实就是根据协议类型将其设置到全局数组ip_ct_protos[]中的相应位置上。

ip_ct_protos变量里保存连接跟踪系统当前可以处理的所有协议,协议号作为数组唯一的下标,如下图所示。

结构体ip_conntrack_protocol{}中的每个成员,内核源码已经做了很详细的注释了,这里我就不一一解释了,在实际开发过程中我们用到了哪些函数再具体分析。

连接跟踪的辅助模块:

Netfilter的连接跟踪为我们提供了一个非常有用的功能模块:helper。该模块可以使我们以很小的代价来完成对连接跟踪功能的扩展。这种应用场景需求一般是,当一个数据包即将离开Netfilter框架之前,我们可以对数据包再做一些最后的处理。从前面的图我们也可以看出来,helper模块以较低优先级被注册到了Netfilter的LOCAL_OUT和POST_ROUTING两个hook点上。

你不记得如何在代码中插入探针点了吗? 没问题!了解如何使用uprobe和kprobe来动态插入它们吧。 基本上,程序员需要在源代码汇编指令的不同位置插入动态探针点。

探针点

探针点是一个调试语句,有助于探索软件的执行特性(即,执行流程以及当探针语句执行时软件数据结构的状态)。printk是探针语句的最简单形式,也是黑客用于内核攻击的基础工具之一。

因为它需要重新编译源代码,所以printk插入是静态的探测方法。内核代码中重要位置上还有许多其他静态跟踪点可以动态启用或禁用。 Linux内核有一些框架可以帮助程序员探测内核或用户空间应用程序,而无需重新编译源代码。Kprobe是在内核代码中插入探针点的动态方法之一,并且uprobe在用户应用程序中执行此 *** 作。

使用uprobe跟踪用户空间

可以通过使用thesysfs接口或perf工具将uprobe跟踪点插入用户空间代码。

使用sysfs接口插入uprobe

考虑以下简单测试代码,没有打印语句,我们想在某个指令中插入探针:

[source,c\n.test.c

#include <stdio.h>\n#include <stdlib.h>\n#include <unistd.h>

编译代码并找到要探测的指令地址:

# gcc -o test test.\n# objdump -d test

假设我们在ARM64平台上有以下目标代码:

0000000000400620 <func_1>: 400620\t90000080\tadr\tx0, 410000 <__FRAME_END__+0xf6f8>

并且我们想在偏移量0x620和0x644之间插入探针。执行以下命令:

# echo 'p:func_2_entry test:0x620' >/sys/kernel/debug/tracing/uprobe_event\n# echo 'p:func_1_entry test:0x644' >>/sys/kernel/debug/tracing/uprobe_event\n# echo 1 >/sys/kernel/debug/tracing/events/uprobes/enable# ./test&

在上面的第一个和第二个echo语句中,p告诉我们这是一个简单的测试。(探测器可以是简单的或返回的。)func_n_entry是我们在跟踪输出中看到的名称,名称是可选字段,如果没有提供,我们应该期待像p_test_0x644这样的名字。test 是我们要插入探针的可执行二进制文件。如果test 不在当前目录中,则需要指定path_to_test / test。

0x620或0x640是从程序启动开始的指令偏移量。请注意>>在第二个echo语句中,因为我们要再添加一个探针。所以,当我们在前两个命令中插入探针点之后,我们启用uprobe跟踪,当我们写入events/ uprobes / enable时,它将启用所有的uprobe事件。程序员还可以通过写入在该事件目录中创建的特定事件文件来启用单个事件。一旦探针点被插入和启用,每当执行探测指令时,我们可以看到一个跟踪条目。

读取跟踪文件以查看输出:

# cat /sys/kernel/debug/tracing/trac\n# tracer: no\n\n# entries-in-buffer/entries-written: 8/8\n#P:\n\n# _-----=>irqs-of\n# / _----=>need-resche\n# | / _---=>hardirq/softir\n# || / _--=>preempt-dept\n# ||| / dela\n# TASK-PID CP\n# |||| TIMESTAMP FUNCTION# | | | |||| | |

我们可以看到哪个CPU完成了什么任务,什么时候执行了探测指令。

返回探针也可以插入指令。当返回该指令的函数时,将记录一个条目:

# echo 0 >/sys/kernel/debug/tracing/events/uprobes/enabl\n# echo 'r:func_2_exit test:0x620' >>/sys/kernel/debug/tracing/uprobe_event\n# echo 'r:func_1_exit test:0x644' >>/sys/kernel/debug/tracing/uprobe_event\n# echo 1 >/sys/kernel/debug/tracing/events/uprobes/enable

这里我们使用r而不是p,所有其他参数是相同的。请注意,如果要插入新的探测点,需要禁用uprobe事件:

test-3009 [002] .... 4813.852674: func_1_entry: (0x400644)

上面的日志表明,func_1返回到地址0x4006b0,时间戳为4813.852691。

# echo 0 >/sys/kernel/debug/tracing/events/uprobes/enabl\n# echo 'p:func_2_entry test:0x630' >/sys/kernel/debug/tracing/uprobe_events count=%x\n# echo 1 >/sys/kernel/debug/tracing/events/uprobes/enabl\n# echo >/sys/kernel/debug/tracing/trace# ./test&

当执行偏移量0x630的指令时,将打印ARM64 x1寄存器的值作为count =。

输出如下所示:

test-3095 [003] .... 7918.629728: func_2_entry: (0x400630) count=0x1

使用perf插入uprobe

找到需要插入探针的指令或功能的偏移量很麻烦,而且需要知道分配给局部变量的CPU寄存器的名称更为复杂。 perf是一个有用的工具,用于帮助引导探针插入源代码中。

除了perf,还有一些其他工具,如SystemTap,DTrace和LTTng,可用于内核和用户空间跟踪;然而,perf与内核配合完美,所以它受到内核程序员的青睐。

# gcc -g -o test test.c# perf probe -x ./test func_2_entry=func_\n# perf probe -x ./test func_2_exit=func_2%retur\n# perf probe -x ./test test_15=test.c:1\n# perf probe -x ./test test_25=test.c:25 numbe\n# perf record -e probe_test:func_2_entry -e\nprobe_test:func_2_exit -e probe_test:test_15\n-e probe_test:test_25 ./test

如上所示,程序员可以将探针点直接插入函数start和return,源文件的特定行号等。可以获取打印的局部变量,并拥有许多其他选项,例如调用函数的所有实例。 perf探针用于创建探针点事件,那么在执行./testexecutable时,可以使用perf记录来探测这些事件。当创建一个perf探测点时,可以使用其他录音选项,例如perf stat,可以拥有许多后期分析选项,如perf脚本或perf报告。

使用perf脚本,上面的例子输出如下:

# perf script

使用kprobe跟踪内核空间

与uprobe一样,可以使用sysfs接口或perf工具将kprobe跟踪点插入到内核代码中。

使用sysfs接口插入kprobe

程序员可以在/proc/kallsyms中的大多数符号中插入kprobe;其他符号已被列入内核的黑名单。还有一些与kprobe插入不兼容的符号,比如kprobe_events文件中的kprobe插入将导致写入错误。 也可以在符号基础的某个偏移处插入探针,像uprobe一样,可以使用kretprobe跟踪函数的返回,局部变量的值也可以打印在跟踪输出中。

以下是如何做:

disable all events, just to insure that we see only kprobe output in trace\n# echo 0 >/sys/kernel/debug/tracing/events/enabledisable kprobe events until probe points are inseted\n# echo 0 >/sys/kernel/debug/tracing/events/kprobes/enableclear out all the events from kprobe_events\n to insure that we see output foronly those for which we have enabled

[root@pratyush ~\n# more /sys/kernel/debug/tracing/trace# tracer: no\n\n# entries-in-buffer/entries-written: 9037/9037\n#P:8\n# _-----=>irqs-of\n# / _----=>need-resche\n# | / _---=>hardirq/softirq#\n|| / _--=>preempt-depth#\n ||| / delay# TASK-PID CPU#\n |||| TIMESTAMP FUNCTION#\n | | | |||| | |

使用perf插入kprobe

与uprobe一样,程序员可以使用perf在内核代码中插入一个kprobe,可以直接将探针点插入到函数start和return中,源文件的特定行号等。程序员可以向-k选项提供vmlinux,也可以为-s选项提供内核源代码路径:

# perf probe -k vmlinux kfree_entry=kfre\n# perf probe -k vmlinux kfree_exit=kfree%retur\n# perf probe -s ./ kfree_mid=mm/slub.c:3408 \n# perf record -e probe:kfree_entry -e probe:kfree_exit -e probe:kfree_mid sleep 10

使用perf脚本,以上示例的输出:

关于Linux命令的介绍,看看《linux就该这么学》,具体关于这一章地址3w(dot)linuxprobe/chapter-02(dot)html


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/yw/8565504.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-04-18
下一篇 2023-04-18

发表评论

登录后才能评论

评论列表(0条)

保存