网络十二:linux网络与netfilter
skaiuijing
引言
Netfilter的作用就是让用户空间程序能够注册对应的网络协议栈处理规则,从而实现高效的网络转发和过滤。许多常见的主机防火墙应用程序就是使用netfilter实现的。
iptables 是 Linux 系统中用于配置防火墙规则的命令行工具,它是 Netfilter 框架的用户空间接口。
netfilter工作在ip层。
概述
Netfilter:提供钩子点和框架(内核机制)。
iptables:提供规则管理和配置接口(用户工具 + 内核模块)。
协作关系:用户通过 iptables 定义规则 → 内核模块存储并执行规则 → 数据包在经过钩子点时被规则处理。
数据包的处理
在前面我们已经知道了数据包是如何进入内核的,通过DMA写入sk_buff对应的内存区域,然后一步步被拆解包头,分析字段,例如,如果我们确认一个数据包是在ip层后,它就会调用对应的函数进行处理。
输入:
1 | graph LR |
路由这里继续:
1 | graph LR |
输出:
1 | graph LR |
继续划分:
1 | graph LR |
hook
在netfilter这里,共有这么几个hook点:
1 | enum nf_inet_hooks { |
每个钩子对应于内核网络堆栈中的特定触发点位置:
| Hook 点 | 位置函数 | 触发时机 | 常见用途 |
|---|---|---|---|
| PRE_ROUTING | ip_rcv() |
收包后、路由前 | DNAT、早期丢弃 |
| LOCAL_IN | ip_local_deliver() |
路由后、交付前 | INPUT 链,本机防火墙 |
| FORWARD | ip_forward() |
路由后、转发时 | FORWARD 链,路由器防火墙 |
| LOCAL_OUT | __ip_local_out() |
本机发包、路由后 | OUTPUT 链,限制本机流量 |
| POST_ROUTING | ip_output() |
发包前、出接口时 | SNAT、TTL 修改 |
| NUMHOOKS | - | - | 计数器 |
iptables
基于内核 Netfilter 提供的钩子回调机制,Netfilter 的作者进一步开发了 iptables,作为用户空间与内核空间之间的桥梁,用于管理和应用自定义的数据包处理规则。通过 iptables,用户无需直接编写内核代码,就能灵活地控制数据包的过滤、转发和地址转换。
iptables 的体系结构可以分为两部分:
用户空间(user-space)
1.提供 iptables 命令行工具,作为管理接口。
2.用户通过该工具定义规则(如允许、拒绝、重定向数据包),并将规则下发给内核。
3.本质上是一个配置层,负责与内核通信,而不直接处理数据包。
内核空间(kernel-space)
1.内核中的 iptables 模块依托 Netfilter 框架,在内存中维护规则表(tables)、链(chains)和规则(rules)。
2.不同的表对应不同的功能:
filter:数据包过滤(ACCEPT、DROP 等)nat:网络地址转换(SNAT、DNAT、MASQUERADE)mangle:修改数据包头部字段(如 TTL、TOS)raw:控制是否启用连接跟踪
3.当数据包经过协议栈中的钩子点(如 PRE_ROUTING、LOCAL_IN、FORWARD、LOCAL_OUT、POST_ROUTING)时,内核会调用相应链上的规则进行匹配和处理。
eBPF
我们关注的是netfilter对应的这几个hook点,通过eBPF技术,我们可以将编写的模块注入到内核中。
挂载
我们需要指定hook的函数、优先级、挂载类型这些,然后加载到内核中,例如:
1 |
|
netfilter层难道只能获取到ip网际层的信息吗?
获取上层信息
在每个eBPF的hook点,获取到的上下文都是不同的,不同层访问的信息都是有限制的。
在内核层,可以对数据包执行许多操作,例如丢弃、修改、重定向等等,但是在应用层,当数据包要被递交给具体进程时,就无法做到这些了,但是内核层能获取到的信息又非常有限,这该怎么办呢?
我们可以想到,tcp在传输层实际上已经与具体进程绑定了,那么在传输层进行具体操作不就可以了吗?
但是,越往上层,我们使用eBPF程序修改数据包的权限就小,性能也越差,但是底层又无法直接与进程相关联,这该怎么办?
例如,我们知道五元组):源ip,目的ip,源端口,目的端口,协议.可以代表一个链接,也可以直接代表一个进程。
但是在传输层虽然可以解析到协议及端口,但是ip层又相对变得透明了,难道我们还要进行逆向解析数据包吗?
在netfilter这里其实有一个很有意思的机制,也就是early demux机制。
early demux机制
设计哲学
我们都知道如果一件事情经常发生,那么它大概率在任何时间尺度下都经常发生。
所以,如果数据包经常交付给某个进程,那么接下来到达网络协议栈的数据包,也大概率是交付给这个进程。
如果我们能准确判断出一类数据包,那么对于这所有的一类数据包,我们只需要进行同一类的处理即可。
例如,如果我们在数据包刚到达网卡时就知道这个数据包是给某个进程的,而且协议栈只需要对这一类数据包进行简单的检查处理,并没有严格的约束。那么我们完全可以绕过协议栈,直接把包的内容丢给该进程。
从时间局部性我们可以得知,如果某个数据在某时刻被访问,那么它很可能在不久后再次被访问。这体现在:
- TCP 连接是持久的:一旦建立连接,后续的数据包会频繁使用同一个 socket和路由。
- socket 和 dst 缓存在内核中:这些结构在连接期间保持不变,适合被提前复用。
所以:既然你刚刚用过这个 socket,那我就假设你还会继续用它。
内核基于这个原理,实现了early demux机制。
内核实现
在传统路径中,IP 层处理完数据包后,才进入传输层查找 socket(如 TCP 的连接四元组匹配)并设置 skb->sk 和 skb->dst。这一步通常涉及:
- 哈希查找 socket(可能是红黑树或哈希表)
- 查找路由缓存或重新计算路由
而 early demux 的优化在于:
如果数据包属于一个已建立连接(如 TCP ESTABLISHED),那么我们可以在 IP 层就直接绑定 socket 和路由信息,跳过后续查找。
下面是Linux6.6内核源码,传输层的early demux相关代码,我们可以看见,其实在ip层,内核就已经获取了传输层相关信息:
1 | int tcp_v4_early_demux(struct sk_buff *skb); |
eBPF程序编写
既然知道了这一点,那么我们完全可以建立数据包与进程信息的映射:
1 | struct { |
通过map,在应用层,我们建立sock与进程信息的map结构,在ip层,就能直接通过sock查找到数据包关联的进程信息。
总结
对netfilter进行了一个简单的介绍,具体用法留给感兴趣的读者自行探索。
值得一提的是,如果传输层发送端的目标地址是本地地址时,数据包会绕过netfilter,我们的hook程序无法捕捉到这些数据包,这是为什么呢?
这个等以后有机会再写吧。