概念
Netfilter 是 Linux 内核中进行数据包过滤,连接跟踪(Connect Track),网络地址转换(NAT)等功能的主要实现框架;该框架在网络协议栈处理数据包的关键流程中定义了一系列钩子点(Hook 点),并在这些钩子点中注册一系列函数对数据包进行处理。这些注册在钩子点的函数即为设置在网络协议栈内的数据包通行策略,也就意味着,这些函数可以决定内核是接受还是丢弃某个数据包,换句话说,这些函数的处理结果决定了这些网络数据包的“命运”。
内核协议栈各 hook 点位置和 iptables 规则优先级的经典配图:
Netfilter Hooks
netfilter 提供了 5 个 hook 点。包经过协议栈时会触发内核模块注册在这里的处理函数 。触发哪个 hook 取决于包的方向(是发送还是接收)、包的目的地址、以及包在上一个 hook 点是被丢弃还是拒绝等等,也就是说通过hook机制可以对包实现放行、修改和丢弃的目的:
- 放行:不对包进行任何修改,退出检测逻辑,继续后面正常的包处理
- 修改:例如修改 IP 地址进行 NAT,然后将包放回正常的包处理逻辑
- 丢弃:安全策略或防火墙功能
IPv4 网络层的Netfilter Hook点
- NF_IP_PRE_ROUTING: 接收到的包进入协议栈后立即触发此 hook,在进行任何路由判断 (将包发往哪里)之前
- NF_IP_LOCAL_IN: 接收到的包经过路由判断,如果目的是本机,将触发此 hook
- NF_IP_FORWARD: 接收到的包经过路由判断,如果目的是其他机器,将触发此 hook
- NF_IP_LOCAL_OUT: 本机产生的准备发送的包,在进入协议栈后立即触发此 hook
- NF_IP_POST_ROUTING: 本机产生的准备发送的包或者转发的包,在经过路由判断之后, 将触发此 hook
==注册处理函数时必须提供优先级==,以便 hook 触发时能按照优先级高低调用处理函数。这使得多个模块(或者同一内核模块的多个实例)可以在同一hook
点注册,并且有确定的处理顺序。内核模块会依次被调用,每次返回一个结果给netfilter
框架,提示该对这个包做什么操作。
21 static const struct nf_hook_ops nf_nat_ipv4_ops[] = {
20 {
19 .hook = iptable_nat_do_chain,
18 .pf = NFPROTO_IPV4,
17 .hooknum = NF_INET_PRE_ROUTING,
16 .priority = NF_IP_PRI_NAT_DST,
15 },
...
};
其中,矩形方框中的即为 Netfilter 的钩子节点。从图中可以看到,三个方向的数据包需要经过的钩子节点不完全相同:
发往本地:
NF_INET_PRE_ROUTING-->NF_INET_LOCAL_IN
转发:
NF_INET_PRE_ROUTING-->NF_INET_FORWARD-->NF_INET_POST_ROUTING
本地发出:
NF_INET_LOCAL_OUT-->NF_INET_POST_ROUTING
了解了Netfilter
的hook
点,我们看一下ipv4的在内核协议栈基本的收包流程
网络层(IPv4)收发包流程
- 绿色方框内的IP_PRE_ROUTING为Netfilter框架的Hook点,该节点会根据预设的规则对数据包进行判决并根据判决结果做相关的处理,比如执行 NAT 转换;
- IP_PRE_ROUTING节点处理完成后,数据包将交由ip_rcv_finish处理,该函数根据路由判决结果,决定数据包是交由本机上层应用处理,还是需要进行转发;如果是交由本机处理,则会交由ip_local_deliver走本地上交流程;如果需要转发,则交由ip_forward函数走转发流程;
- 在数据包上交本地的流程中,IP_LOCAL_INPUT节点用于监控和检查上交到本地上层应用的数据包,该节点是 Linux 防火墙的重要生效节点之一;
- 在数据包转发流程中,Netfilter 框架的IP_FORWARD节点会对转发数据包进行检查过滤;
- 而对于本机上层发出的数据包,网络层通过注册到上层的ip_local_out函数接收数据处理,处理 OK 进一步交由IP_LOCAL_OUT节点检测;
- 对于即将发往下层的数据包,需要经过IP_POST_ROUTING节点处理;网络层处理结束,通过dev_queue_xmit函数将数据包交由 Linux 内核中虚拟网络设备做进一步处理,从这里数据包即离开网络层进入到下一层;
PS: ==绿色方框内的为Netfilter框架的Hook点==
conntrack
概念
当加载了内核模块nf_conntrack后,conntrack机制就开始工作,对于通过ct的数据包,内核都会新生成一个ct的条目用来跟踪此连接,对于后续通过的数据包,内核会判断若此数据包属于一个已有的连接,则更新所对应的conntrack条目的状态(比如更新为ESTABLISHED状态),否则内核会为它新建一个conntrack条目。所有的conntrack条目都存放在一张表里,称为连接跟踪表。
- 从数据包中提取元组(tuple)信息,辨别数据流(flow)和对应的连接(connection)
- 为所有连接维护一个状态数据库(conntrack table)
- 回收过期的连接(流表老化时间)
- 为更上层的功能(例如 NAT)提供服务
例如:
icmp 1 29 src=192.168.0.23 dst=192.168.0.15 type=8 code=0 id=4050 src=192.168.0.15 dst=192.168.0.23 type=0 code=0 id=4050 mark=0 zone=3 use=1
内核识别出协议类型:icmp, 协议id 为 1
协议的生存时间是 30s;
提供了src和dst的ip地址, type code id号已经replay的相关信息;
mark信息等信息,mark被标记为1说明该流是不可用,匹配到该rule的流量会被drop
我们看一下ipv4_conntrack_ops
175 /* Connection tracking may drop packets, but never alters them, so
176 make it the first hook. */
177 static const struct nf_hook_ops ipv4_conntrack_ops[] = {
178 {
179 .hook = ipv4_conntrack_in,
180 .pf = NFPROTO_IPV4,
181 .hooknum = NF_INET_PRE_ROUTING,
182 .priority = NF_IP_PRI_CONNTRACK,
183 },
184 {
185 .hook = ipv4_conntrack_local,
186 .pf = NFPROTO_IPV4,
187 .hooknum = NF_INET_LOCAL_OUT,
188 .priority = NF_IP_PRI_CONNTRACK,
189 },
190 {
191 .hook = ipv4_helper,
192 .pf = NFPROTO_IPV4,
193 .hooknum = NF_INET_POST_ROUTING,
194 .priority = NF_IP_PRI_CONNTRACK_HELPER,
195 },
196 {
197 .hook = ipv4_confirm,
198 .pf = NFPROTO_IPV4,
199 .hooknum = NF_INET_POST_ROUTING,
200 .priority = NF_IP_PRI_CONNTRACK_CONFIRM,
201 },
202 {
203 .hook = ipv4_helper,
204 .pf = NFPROTO_IPV4,
205 .hooknum = NF_INET_LOCAL_IN,
206 .priority = NF_IP_PRI_CONNTRACK_HELPER,
207 },
208 {
209 .hook = ipv4_confirm,
210 .pf = NFPROTO_IPV4,
211 .hooknum = NF_INET_LOCAL_IN,
212 .priority = NF_IP_PRI_CONNTRACK_CONFIRM,
213 },
214 };
在不同的hook点都有一个不同的conntrack的函数,我们再回顾一下上面netfilter的流程,与之对应的hook点流程:
- 发往本地:NF_INET_PRE_ROUTING–>NF_INET_LOCAL_IN
ipv4_conntrack_in --> ipv4_confirm - 转发:NF_INET_PRE_ROUTING–>NF_INET_FORWARD–>NF_INET_POST_ROUTING
ipv4_conntrack_in --> ipv4_confirm - 本地发出:NF_INET_LOCAL_OUT–>NF_INET_POST_ROUTING
ipv4_conntrack_local(nf_conntrack_in) --> ipv4_confirm- NF_INET_PRE_ROUTING 和 NF_INET_LOCAL_OUT:调用 nf_conntrack_in() 开始连接跟踪,正常情况 下会创建一条新连接记录,然后将 conntrack entry 放到 unconfirmed list。
- NF_INET_POST_ROUTING 和 NF_INET_LOCAL_IN:调用 nf_conntrack_confirm() 将 nf_conntrack_in() 创建的连接移到 confirmed list。
那么内核如何判断一个数据包是否属于已有连接呢?
连接跟踪表存放于系统内存中,可以用cat /proc/net/nf_conntrack
查看当前跟踪的所有conntrack条目。如下是代表一个tcp连接的conntrack条目,根据连接协议不同,下面显示的字段信息也不一样,比如icmp协议
ipv4 2 tcp 6 431955 ESTABLISHED src=172.16.207.231 dst=172.16.207.232 sport=51071 dport=5672 src=172.16.207.232 dst=172.16.207.231 sport=5672 dport=51071 [ASSURED] mark=0 zone=0 use=2
每个conntrack条目表示一个连接,连接协议可以是tcp,udp,icmp等,它包含了数据包的原始方向信息和期望的响应包信息,这样内核能够在后续到来的数据包中识别出属于此连接的双向数据包,并更新此连接的状态,各字段意思的具体分析后面会说。连接跟踪表中能够存放的conntrack条目的最大值,即系统允许的最大连接跟踪数记作CONNTRACK_MAX
在内核中,连接跟踪表是一个二维数组结构的哈希表(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越大,遍历时间越长
具体在内核代码实现:
首先看一下conntrack_table的定义和数据结构体:
50 struct nf_conn {
...
64 #ifdef CONFIG_NF_CONNTRACK_ZONES
65 struct nf_conntrack_zone zone;
66 #endif
67 /* XXX should I move this to the tail ? - Y.K */
68 /* These are my tuples; original and reply */
69 struct nf_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX];
...
100 union nf_conntrack_proto proto;
101 };
struct nf_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX];
其中nf_conntrack_tuple_hash
如下:
118 /* Connections have two entries in the hash table: one for each way */
119 struct nf_conntrack_tuple_hash {
120 struct hlist_nulls_node hnnode;
121 struct nf_conntrack_tuple tuple;
122 };
表明如上面的图标所示是一个二维的hash链表,使用tuple的hash值找到对应的Bucket的entry,然后遍历该链表查找是否已经存在连接;
例如如下代码
unsigned int bucket = hash_bucket(hash, net);
/* Disable BHs the entire time since we normally need to disable them
* at least once for the stats anyway.
*/
local_bh_disable();
begin:
hlist_nulls_for_each_entry_rcu(h, n, &net->ct.hash[bucket], hnnode) {
if (nf_ct_key_equal(h, tuple, zone)) {
NF_CT_STAT_INC(net, found);
local_bh_enable();
return h;
}
NF_CT_STAT_INC(net, searched);
}
以后可以再说ct的代码实现
参考链接
https://zhuanlan.zhihu.com/p/93630586
https://opengers.github.io/openstack/openstack-base-netfilter-framework-overview/
http://arthurchiao.art/blog/deep-dive-into-iptables-and-netfilter-arch-zh/