[转帖]Linux网络之 netfilter 框架学习研究

https://zhuanlan.zhihu.com/p/567556545

 

netfilter 框架

netfilter 是 linux 内核中的一个数据包处理框架,用于替代原有的 ipfwadm 和 ipchains 等数据包处理程序。

netfilter 的功能包括数据包过滤,修改,SNAT/DNAT 等。

netfilter 在内核协议栈的不同位置实现了 5 个 hook 点,其它内核模块 (比如 ip_tables) 可以向这些 hook 点注册处理函数,这样当数据包经过这些 hook 点时,其上注册的处理函数就被依次调用,用户层工具像 iptables 一般都需要相应内核模块 ip_tables 配合以完成与 netfilter 的交互。

netfilter hooksip {6}_tablesconnection tracking、和 NAT 子系统一起构成了 netfilter 框架的主要部分。

netfilter 在内核协议中的 5 个 hook 点:

  • NF_IP_PRE_ROUTING:刚刚进入网络层的数据包通过此点(刚刚进行完版本号,校验和等检测), 源地址转换在此点进行。
  • NF_IP_LOCAL_IN:经路由查找后,送往本机的通过此检查点,INPUT 包过滤在此点进行。
  • NF_IP_FORWARD:要转发的包通过此检测点,FORWARD 包过滤在此点进行。
  • NF_IP_POST_ROUTING:所有马上要通过 NIC 出去的包通过此检测点,内置的目的地址转换功能(包括地址伪装)在此点进行。
  • NF_IP_LOCAL_OUT:本机进程发出的包通过此检测点,OUTPUT 包过滤在此点进行。
netfilter 架构: 

可能这么直接去讲 netfilter 还是不太容易理解,为了更直观理解,我们使用 iptables 举例。

Iptables 是一个配置 IPv4 包过滤和 NAT 的管理工具,iptables 包含 4 个表,5 个链。

其中表是按照对数据包的操作区分的,链是按照不同的 Hook 点来区分的,表和链实际上是 netfilter 的两个维度。

iptables 4表

  • filter:一般的过滤功能
  • nat: 用于 nat 功能(端口映射,地址映射等)
  • mangle: 用于对特定数据包的修改
  • raw: 优先级最高,设置 raw 时一般是为了不再让 iptables 做数据包的链接跟踪处理,提高性能

注意:iptables 表处理是有优先级的(raw > mangle > nat > filter)。

iptables 5链

  • PREROUTING: 数据包进入路由表之前
  • INPUT: 通过路由表后目的地为本机
  • FORWARDING: 通过路由表后,目的地不为本机
  • OUTPUT: 由本机产生,向外转发
  • POSTROUTIONG: 发送到网卡接口之前

所以,对于 iptables 而言,5 个链正对应在了 netfilter 框架中的 5 个 hook 点,以此来实现各种防火墙和数据包处理功能。

下面这个图展示了 netfilter 框架在协议栈的位置,它可以清楚地看到 netfilter 框架是如何处理通过不同协议栈路径上的数据包。

Packet flow in Netfilter
Packet flow in Netfilter

我们知道 iptables 的定位是 IPv4 packet filter,它只处理 IP 数据包,而 ebtables 只工作在链路层 Link Layer 处理的是以太网帧 (比如修改源目 mac 地址)。

图中用有颜色的长方形方框表示 iptables 或 ebtables 的表和链,绿色小方框表示 network level,即 iptables 的表和链。蓝色小方框表示 bridge level,即 ebtables 的表和链,由于处理以太网帧相对简单,因此链路层的蓝色小方框相对较少。

我们还注意到一些代表 iptables 表和链的绿色小方框位于链路层,这是因为 bridge_nf 代码的作用 (从 2.6 kernel 开始),bridge_nf 的引入是为了解决在链路层 Bridge 中处理 IP 数据包的问题 (需要通过内核参数开启),那为什么要在链路层 Bridge 中处理 IP 数据包,而不等数据包通过网络层时候再处理呢,这是因为不是所有的数据包都一定会通过网络层,比如外部机器与主机上虚拟机的通信流量,bridge_nf 也是 openstack 中实现安全组功能的基础。

注意:这也是在各种使用 2 层网络的容器环境中,我们需要打开 bridge-nf 相关内核参数的原因。(sysctl -a |grep 'bridge-nf-' 可以查看到)

bridge_nf 代码有时候会引起困惑,就像我们在图中看到的那样,代表 iptables 表和链的绿色小方框跑到了链路层,netfilter 文档对此也有说明 ebtables/iptables interaction on a Linux-based bridge

It should be noted that the br-nf code sometimes violates the TCP/IP Network Model. As will be seen later, it is possible, f.e., to do IP DNAT inside the Link Layer.

ok,图中长方形小方框已经解释清楚了,还有一种椭圆形的方框 conntrack,即 connection tracking,这是 netfilter 提供的连接跟踪机制,此机制允许内核” 审查” 通过此处的所有网络数据包,并能识别出此数据包属于哪个网络连接 (比如数据包 a 属于 IP1:8888->IP2:80 这个 tcp 连接,数据包 b 属于 ip3:9999->IP4:53 这个 udp 连接)。

因此,连接跟踪机制使内核能够跟踪并记录通过此处的所有网络连接及其状态。

图中可以清楚看到连接跟踪代码所处的网络栈位置,如果不想让某些数据包被跟踪 (NOTRACK), 那就要找位于椭圆形方框 conntrack 之前的表和链来设置规则。

注意: conntrack 机制是 iptables 实现状态匹配 (-m state) 以及 NAT 的基础,它由单独的内核模块 nf_conntrack 实现

接着看图中左下方 bridge check 方框,数据包从主机上的某个网络接口进入 (ingress)``, 在 bridge check 处会检查此网络接口是否属于某个 Bridge 的 port,如果是就会进入 Bridge 代码处理逻辑 (下方蓝色区域 bridge level), 否则就会送入网络层 Network Layer 处理。

图中下方中间位置的 bridging decision 类似普通二层交换机的查表转发功能,根据数据包目的 MAC 地址判断此数据包是转发还是交给上层处理。

图中中心位置的 routing decision 就是路由选择,根据系统路由表 (ip route 查看), 决定数据包是 forward,还是交给本地处理 。

总的来看,不同 packet 有不同的 packet flow,packet 总是从主机的某个接口进入 (左下方 ingress), 然后经过 check/decision/ 一系列表和链处理,最后,目的地或是主机上某应用进程 (上中位置 local process),或是需要从主机另一个接口发出 (右下方 egress)。这里的接口即可以是物理网卡 em1,也可以是虚拟网卡 tun0/vnetx,还可以是 Bridge 上的一个 port。

如上就是对在 netfilter 中数据包的流向的整体介绍。

链接跟踪 (connection tracking)

当加载内核模块 nf_conntrack 后,conntrack 机制就开始工作,如上图,椭圆形方框 conntrack 在内核中有两处位置 (PREROUTING 和 OUTPUT 之前) 能够跟踪数据包。

对于每个通过 conntrack 的数据包,内核都为其生成一个 conntrack 条目用以跟踪此连接,对于后续通过的数据包,内核会判断若此数据包属于一个已有的连接,则更新所对应的 conntrack 条目的状态 (比如更新为 ESTABLISHED 状态),否则内核会为它新建一个 conntrack 条目。

所有的 conntrack 条目都存放在一张表里,称为连接跟踪表。

Conntrack table

在 linux 系统中,我们可以使用如下指令来查看 conntrack table 相关信息:

# 查看 当前conntrack 的统计信息
$ conntrack -S 
cpu=0           found=5508 invalid=6125 ignore=106657121 insert=0 insert_failed=1 drop=1 early_drop=0 error=0 search_restart=443826 
cpu=1           found=5579 invalid=6477 ignore=104343231 insert=0 insert_failed=2 drop=2 early_drop=0 error=0 search_restart=473732 
cpu=2           found=5583 invalid=6132 ignore=107286285 insert=0 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=501842 
cpu=3           found=5507 invalid=6414 ignore=106874847 insert=0 insert_failed=1 drop=1 early_drop=0 error=0 search_restart=504806 
cpu=4           found=5459 invalid=6813 ignore=105410436 insert=0 insert_failed=3 drop=3 early_drop=0 error=0 search_restart=473754 
cpu=5           found=5348 invalid=6347 ignore=110859551 insert=0 insert_failed=1 drop=1 early_drop=0 error=0 search_restart=486828 
cpu=6           found=5446 invalid=6648 ignore=107420817 insert=0 insert_failed=3 drop=3 early_drop=0 error=0 search_restart=484902 
cpu=7           found=5404 invalid=6500 ignore=105923893 insert=0 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=472292 

# 实时查看 conntrack 表中的链接信息
$ conntrack -E 
    [NEW] tcp      6 120 SYN_SENT src=100.126.47.26 dst=9.144.119.20 sport=26428 dport=32178 [UNREPLIED] src=9.144.119.20 dst=100.126.47.26 sport=443 dport=26428
 [UPDATE] tcp      6 60 SYN_RECV src=100.126.47.26 dst=9.144.119.20 sport=26428 dport=32178 src=9.144.119.20 dst=100.126.47.26 sport=443 dport=26428
 [UPDATE] tcp      6 10 CLOSE src=100.126.47.26 dst=9.144.119.20 sport=26428 dport=32178 src=9.144.119.20 dst=100.126.47.26 sport=443 dport=26428
[DESTROY] tcp      6 src=9.144.85.79 dst=11.166.65.134 sport=15498 dport=7443 src=11.166.65.134 dst=9.144.85.79 sport=7443 dport=15498
[DESTROY] tcp      6 src=9.144.119.182 dst=11.166.65.131 sport=33905 dport=50052 src=11.166.65.131 dst=9.144.119.182 sport=50052 dport=33905
[DESTROY] tcp      6 src=9.144.85.39 dst=11.166.65.130 sport=33682 dport=8008 src=11.166.65.130 dst=9.144.85.39 sport=8008 dport=33682
[DESTROY] icmp     1 src=100.126.0.94 dst=9.144.119.20 type=8 code=0 id=15060 src=9.144.119.20 dst=100.126.0.94 type=0 code=0 id=15060
[DESTROY] udp      17 src=9.144.119.20 dst=9.146.24.15 sport=41863 dport=53 src=9.146.24.15 dst=9.144.119.20 sport=53 dport=41863
[DESTROY] tcp      6 src=11.166.65.129 dst=11.166.65.131 sport=37122 dport=50052 src=11.166.65.131 dst=11.166.65.129 sport=50052 dport=37122 [ASSURED]
[DESTROY] tcp      6 src=9.144.85.153 dst=11.166.65.131 sport=64221 dport=50052 src=11.166.65.131 dst=9.144.85.153 sport=50052 dport=64221
[DESTROY] tcp      6 src=9.144.85.79 dst=11.166.65.130 sport=56999 dport=8008 src=11.166.65.130 dst=9.144.85.79 sport=8008 dport=56999
    [NEW] tcp      6 120 SYN_SENT src=9.144.119.97 dst=11.166.65.134 sport=22611 dport=7443 [UNREPLIED] src=11.166.65.134 dst=9.144.119.97 sport=7443 dp
    ....
    ....

连接跟踪表存放于系统内存中,因此也可以直接查看文件 /proc/net/nf_conntrack 查看当前跟踪的所有 conntrack 条目。

如下是代表一个 tcp 连接的 conntrack 条目,根据连接协议不同,下面显示的字段信息也不一样,比如 icmp 协议。

ipv4     2 icmp     1 3 src=169.254.128.93 dst=9.144.119.20 type=8 code=0 id=13037 src=9.144.119.20 dst=169.254.128.93 type=0 code=0 id=13037 mark=0 zone=0 use=2

每个 conntrack 条目表示一个连接,连接协议可以是 tcp,udp,icmp 等,它包含了数据包的原始方向信息 和期望的响应包信息,这样内核能够在后续到来的数据包中识别出属于此连接的双向数据包,并更新此连接的状态,各字段意思的具体分析后面会说。

连接跟踪表中能够存放的 conntrack 条目的最大值,即系统允许的最大连接跟踪数记作 CONNTRACK_MAX

Conntrack Table

在内核中,连接跟踪表是一个二维数组结构的哈希表 (hash table),哈希表的大小记作 HASHSIZE,哈希表的每一项 (hash table entry) 称作 bucket,因此哈希表中有 HASHSIZE 个 bucket 存在,每个 bucket 包含一个链表 (linked list),每个链表能够存放若干个 conntrack 条目 (bucket size)。

对于一个新收到的数据包,内核使用如下步骤判断其是否属于一个已有连接:

  • 内核提取此数据包信息 (源目 IP,port,协议号) 进行 hash 计算得到一个 hash 值,在哈希表中以此 hash 值做索引,索引结果为数据包所属的 bucket (链表)。这一步 hash 计算时间固定并且很短
  • 遍历 hash 得到的 bucket,查找是否有匹配的 conntrack 条目,这一步是比较耗时的操作,bucket size 越大,遍历时间越长
需要注意的是,由于上面 netfilter 框架的作用,当 conntrack 表满之后,就可能造成丢包,因此在实际环境中,我们需要对 conntrack table 进行调整,特别是像在容器化环境中,svc 的规模直接会影响到 conntrack table 的容量。 在我们大规模的 K8S 环境中,还是经常会出现 nf conntrack 满了,而导致各种丢包,比较典型的场景就是 coredns 丢包超时。

conntrack max

根据上面对哈希表的解释,系统最大允许连接跟踪数 CONNTRACK_MAX = 连接跟踪表大小(HASHSIZE) * Bucket大小(bucket size)

从连接跟踪表获取 bucket 是 hash 操作时间很短,而遍历 bucket 相对费时,因此为了 conntrack 性能考虑,bucket size 越小越好,默认为 8。

#查看系统当前最大连接跟踪数CONNTRACK_MAX
$ sysctl -a | grep net.netfilter.nf_conntrack_max
net.netfilter.nf_conntrack_max = 262144

#查看当前连接跟踪表大小HASHSIZE
$ sysctl -a | grep net.netfilter.nf_conntrack_buckets
net.netfilter.nf_conntrack_buckets = 262144

conntrack_max 和 conntrack_buckets 的比值即为 bucket size = 262144/262144 。

如下,现在需求是设置系统最大连接跟踪数为 320w,由于 bucket size 不能直接设置,为了使 bucket size 值为 8,我们需要同时设置 CONNTRACK_MAX 和 HASHSIZE,因为他们的比值就是 bucket size

#HASHSIZE (内核会自动格式化为最接近允许值)       
echo 400000 > /sys/module/nf_conntrack/parameters/hashsize

#系统最大连接跟踪数
sysctl -w net.netfilter.nf_conntrack_max=3200000      

#注意nf_conntrack内核模块需要加载  

为了使 nf_conntrack 模块重新加载或系统重启后生效

echo "options nf_conntrack hashsize=400000" > /etc/modprobe.d/nf_conntrack.conf                  

只需要固化 HASHSIZE 值,nf_conntrack 模块在重新加载时会自动设置 CONNTRACK_MAX = hashsize * 8,当然前提是你 bucket size 使用系统默认值 8。

如果自定义 bucket size 值,就需要同时固化 CONNTRACK_MAX,以保持其比值为你想要的 bucket size。

上面我们没有改变 bucket size 的默认值 8,但是若内存足够并且性能很重要,你可以考虑每个 bucket 一个 conntrack 条目 (bucket size = 1),最大可能降低遍历耗时,即 HASHSIZE = CONNTRACK_MAX

#HASHSIZE
echo 3200000 > /sys/module/nf_conntrack/parameters/hashsize
#CONNTRACK_MAX
sysctl -w net.netfilter.nf_conntrack_max=3200000

如何计算连接跟踪所占内存

连接跟踪表存储在系统内存中,因此需要考虑内存占用问题,可以用下面公式计算设置不同的最大连接跟踪数所占最大系统内存

total_mem_used(bytes) = CONNTRACK_MAX * sizeof(struct ip_conntrack) + HASHSIZE * sizeof(struct list_head)

例如我们需要设置最大连接跟踪数为 320w,在 centos6/7 系统上,sizeof(struct ip_conntrack) = 376,sizeof(struct list_head) = 16,并且 bucket size 使用默认值 8,并且 HASHSIZE = CONNTRACK_MAX / 8,因此

total_mem_used(bytes) = 3200000 * 376 + (3200000 / 8) * 16
# = 1153MB ~= 1GB


因此可以得到,在 centos6/7 系统上,设置 320w 的最大连接跟踪数,所消耗的内存大约为 1GB,对现代服务器来说,占用内存并不多,但 conntrack 实在让人又爱又恨。

conntrack 条目

conntrack 从经过它的数据包中提取详细的,唯一的信息,因此能保持对每一个连接的跟踪。

关于 conntrack 如何确定一个连接,对于 tcp/udp,连接由他们的源目地址,源目端口唯一确定。

对于 icmp,由 type,code 和 id 字段确定。

ipv4     2 tcp      6 33 SYN_SENT src=172.16.200.119 dst=172.16.202.12 sport=54786 dport=10051 [UNREPLIED] src=172.16.202.12 dst=172.16.200.119 sport=10051 dport=54786 mark=0 zone=0 use=2

如上是一条 conntrack 条目,它代表当前已跟踪到的某个连接,conntrack 维护的所有信息都包含在这个条目中,通过它就可以知道某个连接处于什么状态。

  • 此连接使用 ipv4 协议,是一条 tcp 连接 (tcp 的协议类型代码是 6)
  • 33 是这条 conntrack 条目在当前时间点的生存时间 (每个 conntrack 条目都会有生存时间,从设置值开始倒计时,倒计时完后此条目将被清除),可以使用 sysctl -a |grep conntrack | grep timeout 查看不同协议不同状态下生存时间设置值,当然这些设置值都可以调整,注意若后续有收到属于此连接的数据包,则此生存时间将被重置 (重新从设置值开始倒计时),并且状态改变,生存时间设置值也会响应改为新状态的值
  • SYN_SENT 是到此刻为止 conntrack 跟踪到的这个连接的状态 (内核角度),SYN_SENT 表示这个连接只在一个方向发送了一初始 TCP SYN 包,还未看到响应的 SYN+ACK 包 (只有 tcp 才会有这个字段)。
  • src=172.16.200.119 dst=172.16.202.12 sport=54786 dport=10051 是从数据包中提取的此连接的源目地址、源目端口,是 conntrack 首次看到此数据包时候的信息。
  • [UNREPLIED] 说明此刻为止这个连接还没有收到任何响应,当一个连接已收到响应时,[UNREPLIED] 标志就会被移除
  • src=172.16.202.12 dst=172.16.200.119 sport=10051 dport=54786 地址和端口和前面是相反的,这部分不是数据包中带有的信息,是 conntrack 填充的信息,代表 conntrack 希望收到的响应包信息。意思是若后续 conntrack 跟踪到某个数据包信息与此部分匹配,则此数据包就是此连接的响应数据包。注意这部分确定了 conntrack 如何判断响应包 (tcp/udp),icmp 是依据另外几个字段

上面是 tcp 连接的条目,而 udp 和 icmp 没有连接建立和关闭过程,因此条目字段会有所不同,后面 iptables 状态匹配部分我们会看到处于各个状态的 conntrack 条目。

注意 conntrack 机制并不能够修改或过滤数据包,它只是跟踪网络连接并维护连接跟踪表,以提供给 iptables 做状态匹配使用,也就是说,如果你 iptables 中用不到状态匹配,那就没必要启用 conntrack。

iptables 状态匹配

先明确下 conntrack 在内核协议栈所处位置,上面也提过 conntrack 跟踪数据包的位置在 PREROUTING 和 OUTPUT 这两个 hook 点,主机自身进程产生的数据包会通过 OUTPUT 处的 conntrack,从主机任意接口进入 (包括 Bridge 的 port) 的数据包会通过 PREROUTING 处的 conntrack,从 netfilter 框架图上可以看到 conntrack 位置很靠前,仅在 iptables 的 raw 表之后,raw 表主要作用就是允许我们对某些特定的数据包打上 NOTRACK 标记,这样后面的 conntrack 就不会记录此类带有 NOTRACK 标签的数据包。

conntrack 位置很靠前一方面是保证其后面的 iptables 表和链都能使用状态匹配,另一方面使得 conntrack 能够跟踪到任何进出主机的原始数据包 (比如数据包还未 NAT/FORWARD)。

iptables 状态匹配模块

iptables 是带有状态匹配的防火墙,它使用 -m state 模块从连接跟踪表查找数据包状态。

上面我们分析的那条 conntrack 条目处于 SYN_SENT 状态,这是内核记录的状态,数据包在内核中可能会有几种不同的状态,但是映射到用户空间 iptables,只有 5 种状态可用:

  • NEW
  • ESTABLISHED
  • RELATED
  • INVALID
  • UNTRACKED

注意这里说的状态不是 tcp/ip 协议中 tcp 连接的各种状态。下面表格说明了这 5 种状态分别能够匹配什么样的数据包,注意下面两点:

  • 用户空间这 5 种状态是 iptables 用于完成状态匹配而定义的,不关联与特定连接协议
  • conntrack 记录在前,iptables 匹配在后
状态解释
NEW NEW 匹配连接的第一个包。意思就是,iptables 从连接跟踪表中查到此包是某连接的第一个包。判断此包是某连接的第一个包是依据 conntrack 当前” 只看到一个方向数据包”([UNREPLIED]),不关联特定协议,因此 NEW 并不单指 tcp 连接的 SYN 包
ESTABLISHED ESTABLISHED 匹配连接的响应包及后续的包。意思是,iptables 从连接跟踪表中查到此包是属于一个已经收到响应的连接 (即没有 [UNREPLIED] 字段)。因此在 iptables 状态中,只要发送并接到响应,连接就认为是 ESTABLISHED 的了。这个特点使 iptables 可以控制由谁发起的连接才可以通过,比如 A 与 B 通信,A 发给 B 数据包属于 NEW 状态,B 回复给 A 的数据包就变为 ESTABLISHED 状态。ICMP 的错误和重定向等信息包也被看作是 ESTABLISHED,只要它们是我们所发出的信息的应答。
RELATED RELATED 匹配那些属于 RELATED 连接的包,这句话说了跟没说一样。RELATED 状态有点复杂,当一个连接与另一个已经是 ESTABLISHED 的连接有关时,这个连接就被认为是 RELATED。这意味着,一个连接要想成为 RELATED,必须首先有一个已经是 ESTABLISHED 的连接存在。这个 ESTABLISHED 连接再产生一个主连接之外的新连接,这个新连接就是 RELATED 状态了,当然首先 conntrack 模块要能” 读懂” 它是 RELATED。拿 ftp 来说,FTP 数据传输连接就是 RELATED 与先前已建立的 FTP 控制连接,还有通过 IRC 的 DCC 连接。有了 RELATED 这个状态,ICMP 错误消息、FTP 传输、DCC 等才能穿过防火墙正常工作。有些依赖此机制的 TCP 协议和 UDP 协议非常复杂,他们的连接被封装在其它的 TCP 或 UDP 包的数据部分 (可以了解下 overlay/vxlan/gre),这使得 conntrack 需要借助其它辅助模块才能正确” 读懂” 这些复杂数据包,比如 nf_conntrack_ftp 这个辅助模块
INVALID INVALID 匹配那些无法识别或没有任何状态的数据包。这可能是由于系统内存不足或收到不属于任何已知连接的 ICMP 错误消息。一般情况下我们应该 DROP 此类状态的包
UNTRACKED UNTRACKED 状态比较简单,它匹配那些带有 NOTRACK 标签的数据包。需要注意的一点是,如果你在 raw 表中对某些数据包设置有 NOTRACK 标签,那上面的 4 种状态将无法匹配这样的数据包,因此你需要单独考虑 NOTRACK 包的放行规则

状态的使用使防火墙可以非常强大和有效,来看下面这个常见的防火墙规则,它允许本机主动访问外网,以及放开 icmp 协议。

#iptables-save  -t filter
*filter
:INPUT DROP [1453341:537074675]
:FORWARD DROP [10976649:6291806497]
:OUTPUT ACCEPT [1221855153:247285484556]
-A INPUT -p icmp -j ACCEPT 
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT 

主机进程与外网机器通信经历如下步骤,因为除了 filter 表,其它表没设置任何规则,所以下面步骤就省略其它表的匹配过程:

  • 1. 进程产生要发送的数据包,数据包通过 raw 表 OUTPUT 链 (可以决定是否要 NOTRACK)
  • 2. 数据包通过 conntrack,conntrack 记录此连接到连接跟踪表 ([UNREPLIED]) – NEW
  • 3. 通过 OUTPUT 链,然后从主机网卡发出 – NEW
  • 4. 外网目标主机收到请求,发出响应包
  • 5. 响应包从主机某个接口进入,到达 raw 表 PREROUTING 链 (可以决定是否要 NOTRACK) – NEW
  • 6. 响应包通过 conntrack,conntrack 发现此数据包为一个连接的响应包,更新对应 conntrack 条目状态 (去掉 [UNREPLIED],至此两个方向都看到包了) – ESTABLISHED
  • 7. 响应包到达 filter 表 INPUT 链,在这里匹配到 --state RELATED,ESTABLISHED,因此放行 – ESTABLISHED

像上面这种允许本机主动出流量的需求,如果不用 conntrack 会很难实现,那你可能会说我也可以使用 iptables

数据包在内核中的状态

从内核角度,不同协议有不同状态,这里我们来具体看下三种协议 tcp/udp/icmp 在连接跟踪表中的不同状态

tcp 链接

下面是 172.16.1.100 向 172.16.1.200 建立 tcp 通信过程中,/proc/net/nf_conntrack 中此连接的状态变化过程

ipv4     2 tcp      6 118 SYN_SENT src=172.16.1.100 dst=172.16.1.200 sport=36884 dport=8220 [UNREPLIED] src=172.16.1.200 dst=172.16.1.100 sport=8220 dport=36884 mark=0 zone=0 use=2

如上,首先 172.16.1.100 向 172.16.1.200 发送 SYN 包,172.16.1.200 收到 SYN 包但尚未回复,由于是新连接,conntrack 将此连接添加到连接跟踪表,并标记为 SYN_SENT 状态,[UNREPLIED] 表示 conntrack 尚未跟踪到 172.16.1.200 的响应包。注意上面这条 conntrack 条目存在于两台主机的连接跟踪表中 (当然,首先要两台主机都启用 conntrack),对于 172.16.1.100,数据包在经过 OUTPUT 这个 hook 点时触发 conntrack,而对于 172.16.1.200,数据包在 PREROUTING 这个 hook 点时触发 conntrack

随后,172.16.1.200 回复 SYN/ACK 包给 172.16.1.100,通过 conntrack 更新连接状态为 SYN_RECV,表示收到 SYN/ACk 包,去掉 [UNREPLIED] 字段

ipv4     2 tcp      6 59 SYN_RECV src=172.16.1.100 dst=172.16.1.200 sport=36884 dport=8220 src=172.16.1.200 dst=172.16.1.100 sport=8220 dport=36884 mark=0 zone=0 use=2

接着,172.16.1.100 回复 ACK 给 172.16.1.200,至此,三次握手完成,tcp 连接已建立,conntrack 更新连接状态为 ESTABLISHED

ipv4     2 tcp      6 10799 ESTABLISHED src=172.16.1.100 dst=172.16.1.200 sport=36884 dport=8220 src=172.16.1.200 dst=172.16.1.100 sport=8220 dport=36884 [ASSURED] mark=0 zone=0 use=2


连接跟踪表中的 conntrack 条目不可能是永久存在,每个 conntrack 条目都有超时时间,可以如下方式查看 tcp 连接各个状态当前设置的超时时间。

$ sysctl -a |grep 'net.netfilter.nf_conntrack_tcp_timeout_'
net.netfilter.nf_conntrack_tcp_timeout_close = 10
net.netfilter.nf_conntrack_tcp_timeout_close_wait = 60
net.netfilter.nf_conntrack_tcp_timeout_established = 432000
...

正常的 tcp 连接是很短暂的,不太可能查看到一个 tcp 连接所有状态变化的,那如何构造处于特定状态的 tcp 连接呢,一个方法是利用 iptables 的 --tcp-flags 参数,其可以匹配 tcp 数据包的标志位,比如下面这两条:

#对于本机发往172.16.1.200的tcp数据包,丢弃带有SYN/ACK flag的包       
$ iptables -A OUTPUT -o eth0 -p tcp --tcp-flags SYN,ACK SYN,ACK -d 172.16.1.200 -j DROP

#同样,这个是丢弃带有ACK flag的包 
$ iptables -A OUTPUT -o eth0 -p tcp --tcp-flags ACK ACK -d 172.16.1.100 -j DROP

同时利用 tcp 超时重传机制,待 cat /proc/net/nf_conntrack 获取到 conntrack 条目后,使用 iptables -D OUTPUT X 删除之前设置的 DROP 规则,这样 tcp 连接就会正常走下去,这个很容易测试出来。

udp 链接

UDP 连接是无状态的,它没有连接的建立和关闭过程,连接跟踪表中的 udp 连接也没有像 tcp 那样的状态字段,但这不妨碍用户空间 iptables 对 udp 包的状态匹配,上面也说过,iptables 中使用的各个状态与协议无关。

#只收到udp连接第一个包

ipv4     2 udp      17 28 src=172.16.1.100 dst=172.16.1.200 sport=26741 dport=8991 [UNREPLIED] src=172.16.1.200 dst=172.16.1.100 sport=8991 dport=26741 mark=0 zone=0 use=2

#收到此连接的响应包    

ipv4     2 udp      17 29 src=172.16.1.100 dst=172.16.1.200 sport=26741 dport=8991 src=172.16.1.200 dst=172.16.1.100 sport=8991 dport=26741 mark=0 zone=0 use=2

同样可以查看 udp 超时时间。

$ sysctl -a | grep 'net.netfilter.nf_conntrack_udp_'
net.netfilter.nf_conntrack_udp_timeout = 30
net.netfilter.nf_conntrack_udp_timeout_stream = 180

icmp

icmp 请求,在用户空间 iptables 看来,跟踪到 echo request 时连接处于 NEW 状态,当有 echo reply 时就是 ESTABLISHED 状态。

#icmp请求
ipv4     2 icmp     1 28 src=103.229.215.2 dst=113.31.136.7 type=8 code=0 id=35102 [UNREPLIED] src=113.31.136.7 dst=103.229.215.2 type=0 code=0 id=35102 mark=0 zone=0 use=2

#reply
ipv4     2 icmp     1 29 src=103.229.215.2 dst=113.31.136.7 type=8 code=0 id=35102 src=113.31.136.7 dst=103.229.215.2 type=0 code=0 id=35102 mark=0 zone=0 use=2

如何管理连接跟踪表

我们在一开始简单介绍了使用 conntrack 命令行工具来查看 链接跟踪信息,该工具提供了对连接跟踪表的增删改查功能。

#查看连接跟踪表所有条目  
$ conntrack -L
#清除连接跟踪表
$ conntrack -F
#删除连接跟踪表中所有源地址是1.2.3.4的条目
$ conntrack -D -s 1.2.3.4

Bridge 与 netfilter

从 netfilter 框架图中可以看到,最下层蓝色区域为 bridge level。

Bridge 的存在,使得主机可以充当一台虚拟的普通二层交换机来运作,这个虚拟交换机可以建多个 port,连接到多个虚拟机。

由此带来的问题是,外部机器与其上虚拟机通信流量只会经过主机二层 (靠 Bridge 转发,此时不经过主机 IP 层),主机上的网卡类型变得复杂 (物理网卡 em1,网桥 br0,虚拟网卡 vnetX),进入主机的数据包可选路径变多 (bridge 转发 / 交给主机本地进程)。

conntrack 与 LVS

LVS 的修改数据包功能也是依赖 netfilter 框架,在 LVS 机器上应用 iptables 时需要注意一个问题就是,LVS-DR (或 LVS-Tun) 模式下,不能在 director 上使用 iptables 的状态匹配 (NEW,ESTABLISHED,INVALID,…)。

LVS-DR 模式下,client 访问 director,director 转发流量到 realserver,realserver 直接回复 client,不经过 director,这种情况下,client 与 direcotr 处于 tcp 半连接状态。

因此如果在 director 机器上启用 conntrack,此时 conntrack 只能看到 client–>director 的数据包,因为响应包不经过 direcotr,conntrack 无法看到反方向上的数据包,就表示 iptables 中的 ESTABLISHED 状态永远无法匹配,从而可能发生 DROP。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章