1.6 Netfilter
1.6.1 简介
Netfilter/IPTables是Linux2.4.x之后新一代的Linux防火墙机制,是一套融入到linux内核网络协议栈的框架,是linux内核的一个子系统。
Netfilter采用模块化设计,具有良好的可扩充性。其重要工具模块IPTables从用户态的iptables连接到内核态的Netfilter的架构中,在报文流经的关键位置处,基于不同协议(ipv4/ipv6)的hook方式,使用hook列表中对应的钩子函数匹配处理,以实现过滤、修改报文、跟踪等功能;最后处理过的报文会被决定以“放行”或者“丢弃”行为。
主要源代码文件
-
Linux内核版本:2.4.x以上
-
Netfilter主文件:net/core/netfilter.c
-
Netfilter主头文件:include/linux/netfilter.h
-
IPv4相关:
-
c文件:net/ipv4/netfilter/*.c
-
头文件:include/linux/netfilter_ipv4.h
include/linux/netfilter_ipv4/*.h
-
IPv4协议栈主体的部分c文件,特别是与数据报传送过程有关的部分:ip_input.c,ip_forward.c,ip_output.c,ip_fragment.c等
具体功能模块
- 数据报过滤模块
- 连接跟踪模块(Conntrack)
- 网络地址转换模块(NAT)
- 数据报修改模块(mangle)
- 其他高级功能模块
Netfilter主要通过表、链实现规则,可以这么说,Netfilter是表的容器,表是链的容器,链是规则的容器,最终形成对数据报处理规则的实现。
1.6.2 理解
Netfilter是一个用于Linux操作系统的网络过滤框架,它提供了一种在Linux内核中处理网络数据包的机制。
Netfilter允许Linux管理员通过定义一组规则来控制网络数据包的流动,例如阻止或允许某些IP地址或端口的数据包通过。
这种过滤机制可以用于网络安全、流量控制和网络监控等应用。
Netfilter的核心是iptables,它是一个命令行工具,可用于在Linux系统中配置Netfilter规则。iptables提供了一组命令,使管理员可以定义、修改、删除和显示规则集。这些规则集可以被用于控制网络流量,例如阻止来自特定IP地址或端口的流量、限制连接速率或转发流量等。此外,iptables还支持网络地址转换(NAT)功能,允许Linux系统充当路由器,将私有网络连接到公共网络。
除了iptables之外,Netfilter还提供了其他工具和扩展,如ip6tables(IPv6版本的iptables)、ebtables(以太网框架过滤器)和nflog(用于捕获Netfilter事件的模块)。这些工具和扩展可以帮助管理员更精细地控制网络流量,并提供更灵活的配置选项。
Netfilter 还被广泛应用于防火墙、VPN、路由器、负载均衡、反向代理、网络监控等各种网络应用和安全系统中。
1.6.3 Netfilter框架基本原理
Netfilter框架是Linux操作系统中的一个内核模块,提供了一种机制,可以在网络数据包通过Linux内核的过程中截获和处理数据包。Netfilter的核心是钩子(hook)机制,该机制允许内核在特定的网络数据包到达某个阶段时,调用已注册的函数,对该数据包进行处理。
Netfilter框架的基本原理和架构如下:
-
Netfilter钩子(hook):Netfilter钩子是一组特定的内核函数,它们会在网络数据包经过Linux内核的不同阶段时被调用。Netfilter钩子被称为“钩子点”,钩子点包括PRE_ROUTING、LOCAL_IN、FORWARD、LOCAL_OUT和POST_ROUTING等。这些钩子点与Linux内核的不同阶段相关联,数据包在经过内核的不同阶段时会被调用相应的钩子点。在钩子点上注册的回调函数会在数据包到达时被调用,以执行特定的操作。
例如,可以在PREROUTING钩子点上注册一个回调函数,用于更改数据包的源地址或目标地址。
-
Netfilter表(table):Netfilter表是一组具有相同功能的规则集合。Linux内核中定义了五个表:raw、mangle、nat、filter和security。每个表包含一些特定的钩子点,用于在数据包到达时调用特定的回调函数。
每个表都有自己的规则集,可以用于控制网络流量、实现安全策略和管理网络连接等。
-
Netfilter链(chain):Netfilter链是一个由规则集组成的列表,它定义了在特定的钩子点上执行的规则集。每个链都与特定的表相关联,并可以在链中注册自定义规则。
例如,在INPUT链中可以注册一个规则,用于阻止来自特定IP地址的数据包通过。
-
Netfilter规则(rule):Netfilter规则是指定义特定网络流量的过滤或修改规则。规则由匹配条件和对应的动作组成。
例如,一个规则可以是“如果数据包的源IP地址是192.168.0.1,那么阻止这个数据包通过”。可以将多个规则添加到Netfilter链中,以构建复杂的规则集。
1.6.4 Netfilter实现
Netfilter主要通过表、链实现规则,可以这么说,Netfilter是表的容器,表是链的容器,链是规则的容器,最终形成对数据报处理规则的实现。
数据在协议栈里的发送过程中,从上至下依次是“加头”的过程,每到达一层数据就被会加上该层的头部;与此同时,接受数据方就是个“剥头”的过程,从网卡收上包来之后,在往协议栈的上层传递过程中依次剥去每层的头部,最终到达用户那儿的就是裸数据了。
接收:
对于收到的每个数据包,都从“A”点进来,经过路由判决,如果是发送给本机的就经过“B”点,然后往协议栈的上层继续传递;否则,如果该数据包的目的地是不本机,那么就经过“C”点,然后顺着“E”点将该包转发出去。
发送:
对于发送的每个数据包,首先也有一个路由判决,以确定该包是从哪个接口出去,然后经过“D”点,最后也是顺着“E”点将该包发送出去。
协议栈那五个关键点A,B,C,D和E就是我们Netfilter大展拳脚的地方了。
如下图:
Netfilter是Linux 2.4.x引入的一个子系统,它作为一个通用的、抽象的框架,提供一整套的hook函数的管理机制,使得诸如数据包过滤、网络地址转换(NAT)和基于协议类型的连接跟踪成为了可能。
Netfilter在内核中位置如下图所示:
这幅图很直观的反应了用户空间的iptables和内核空间的基于Netfilter的ip_tables模块之间的关系和其通讯方式,以及Netfilter在这其中所扮演的角色。
Netfilter在netfilter_ipv4.h中将这个五个点重新命了个名
如下图所示:
在每个关键点上,有很多已经按照优先级预先注册了的回调函数(称为“钩子函数”)埋伏在这些关键点,形成了一条链。对于每个到来的数据包会依次被那些回调函数“审视”一番再视情况是将其 放行or丢弃or其他。
但是无论如何,这些回调函数最后必须向Netfilter报告一下该数据包的情况,因为毕竟每个数据包都是Netfilter从协议栈那儿借调过来的。
每个钩子函数最后必须向Netfilter框架返回下列几个值其中之一:
- NF_ACCEPT继续正常传输数据报。这个返回值告诉Netfilter:到目前为止,该数据包还是被接受的并且该数据包应当被递交到网络协议栈的下一个阶段。
- NF_DROP丢弃该数据报,不再传输。
- NF_STOLEN模块接管该数据报,告诉Netfilter“忘掉”该数据报。该回调函数将从此开始对数据包的处理,并且Netfilter应当放弃对该数据包做任何的处理。但是,这并不意味着该数据包的资源已经被释放。这个数据包以及它独自的sk_buff数据结构仍然有效,只是回调函数从Netfilter获取了该数据包的所有权。
- NF_QUEUE对该数据报进行排队(通常用于将数据报给用户空间的进程进行处理)
- NF_REPEAT 再次调用该回调函数,应当谨慎使用这个值,以免造成死循环。
为了让我们显得更专业些,我们开始做些约定:上面提到的五个关键点后面我们就叫它们为hook点,每个hook点所注册的那些回调函数都将其称为hook函数。
Linux 2.6版内核的Netfilter目前支持IPv4、IPv6以及DECnet等协议栈,这里我们主要研究IPv4协议。关于协议类型,hook点,hook函数,优先级,通过下面这个图给大家做个详细展示:
对于每种类型的协议,数据包都会依次按照hook点的方向进行传输,每个hook点上Netfilter又按照优先级挂了很多hook函数。这些hook函数就是用来处理数据包用的。
hook注册的钩子函数具有优先级顺序的执行, 值越小,优先级越高
enum nf_ip_hook_priorities {
...
NF_IP_PRI_RAW = -300, /*对应 raw table*/
...
NF_IP_PRI_MANGLE = -150, /*对应 mangle table*/
NF_IP_PRI_NAT_DST = -100, /*对应 nat table*/
NF_IP_PRI_FILTER = 0, /*对应 filter table*/
...
NF_IP_PRI_NAT_SRC = 100, /*对应 nat table*/
...
};
hook被划分成5个关键位置处:PREROUTING、INPUT、FORWARD、OUTPUT、POSTROUTING
enum nf_inet_hooks {
NF_INET_PRE_ROUTING, /*对应 PREROUTING*/
NF_INET_LOCAL_IN, /*对应 INPUT*/
NF_INET_FORWARD, /*对应 FORWARD*/
NF_INET_LOCAL_OUT, /*对应 OUTPUT*/
NF_INET_POST_ROUTING,/*对应 POSTROUTING*/
NF_INET_NUMHOOKS
};
hook与chain(链)是一个东西,linux系统环境的iptables命令帮助信息(iptables -h),使用术语为chain(链)
iptables v1.4.21
Usage: iptables -[ACD] chain rule-specification [options]
iptables -I chain [rulenum] rule-specification [options]
iptables -R chain rulenum rule-specification [options]
iptables -D chain rulenum [options]
...
一个报文会经过的路径:
PRE ROUTING->INPUT:报文会在路由选择之前,简单的健全性检查,然后先经过PRE ROUTING点,再进行路由选择,若是本地的报文,途经INPUT点,然后交由上层处理。当然路由选择时可以丢弃不可达的报文。
PRE ROUTING->FORWARD->POST ROUTING:报文会在路由选择之前,简单的健全性检查,然后先经过PRE ROUTING点,再进行路由选择,若不是本地的报文,则进入FORWARD点,然后途经最后一个POST ROUTING点,选择对应子网的出口发送出去。
OUTPUT->POST ROUTING:上层处理完后,创建报文,先经过OUTPUT点,然后途经最后一个POST ROUTING点,根据子网选择对应网口发送出去。
**注意:**网络上有文章描述OUTPUT点之前或者之后会调用”路由选择“行为,刚接触的同学们会被整懵;实际上,报文先处理路由选择,计算源地址和IP选项 ,然后才进入该hook点。如下的流程图关注的是Hook点的经过,因此没有把 “本地发送->route ->OUTPUT”流程描述出来。
1.6.6 hook点和table的关系
每个hook点对应的功能繁多,需要配置N条规则,并且有序的执行;如果过多规则被配置的话,不好维护,所以table就诞生了。
table一方面细分功能,另一方面把不同的规则划分到不同table里,加以限定执行的顺序。
比如:用户基于PRE ROUTING点,在RAW表、MANGLE表配置多条规则;然后当报文经过PRE ROUTING点时,因RAW表优先级比MANGLE表高(hook优先级列表),所以RAW表的规则先执行,再执行MANGLE表的规则。
因此所谓的”四表五链“,就是基于4张table和5个hook的关系,来管理诸多规则。
4张表神奇的作用:raw表用于连接跟踪、 mangle表用于修改报文信息、filter表用于过滤报文信息、nat表用于映射地址信息。
1.6.7 hook的数据结构、注册钩子列表、调用关键函数
如下hook定义的结构体信息,可以看出它存在于不同net namespace的 struct net结构中,由于net namespace方面可以在同一个系统中建立不同的网络环境,因此hook上的table规则是独立于net namespace中的。
/*安装在hook点的钩子函数结构体*/
struct nf_hook_ops {
/* User fills in from here down. */
nf_hookfn *hook; /*钩子函数,报文经过hook点时调用*/
struct net_device *dev; /*对应的网络设备*/
void *priv; /*每个hook点独有的私有数据*/
u_int8_t pf; /*特定的协议族:NFPROTO_IPV4/NFPROTO_IPV6/...*/
unsigned int hooknum;/*钩子安装到哪个hook点*/
/* Hooks are ordered in ascending priority. */
int priority; /*优先级设置,决定该钩子函数的执行位置,优先级高,则先执行*/
};
/*以下结构体基于net namespace方面而定义*/
struct nf_hook_entry {
nf_hookfn *hook;/*隐式的指向nf_hook_ops结构体,仅显式的暴露钩子函数*/
void *priv;
};
struct nf_hook_entries {
u16 num_hook_entries;/*hook点的钩子函数注册的数量*/
/* padding */
struct nf_hook_entry hooks[];/*指向一堆hook点对应的钩子函数*/
};
/*定义包含不同协议族的hook点钩子函数的结构体*/
struct netns_nf {
...
struct nf_hook_entries __rcu *hooks_ipv4[NF_INET_NUMHOOKS];
struct nf_hook_entries __rcu *hooks_ipv6[NF_INET_NUMHOOKS];
#ifdef CONFIG_NETFILTER_FAMILY_ARP
struct nf_hook_entries __rcu *hooks_arp[NF_ARP_NUMHOOKS];
#endif
#ifdef CONFIG_NETFILTER_FAMILY_BRIDGE
struct nf_hook_entries __rcu *hooks_bridge[NF_INET_NUMHOOKS];
#endif
#if IS_ENABLED(CONFIG_DECNET)
struct nf_hook_entries __rcu *hooks_decnet[NF_DN_NUMHOOKS];
#endif
...
};
/*net namespace的结构体,系统可以有多个不同网络空间,隔离网络资源。*/
struct net {
...
struct netns_nf nf;/*每个net namespace独有的hook点列表*/
...
}
ipv4协议族的hook函数注册列表:仅把4张表的安装的钩子函数罗列出来,其它的表可自行查看。
4张表的内核源文件:iptable_raw.c、iptable_mangle.c、iptable_nat.c、iptable_filter.c
nf_hook函数和NF_HOOK宏定义,它们会在所有hook点的位置被调用, 即是入口点。
static inline int
NF_HOOK(uint8_t pf, unsigned int hook, struct net *net, struct sock *sk, struct sk_buff *skb,
struct net_device *in, struct net_device *out,
int (*okfn)(struct net *, struct sock *, struct sk_buff *))
{
/*pf = 协议族, hook = 使用哪个hook点, skb = 报文*/
int ret = nf_hook(pf, hook, net, sk, skb, in, out, okfn);
if (ret == 1)
ret = okfn(net, sk, skb);
return ret;
}
/**
* nf_hook - call a netfilter hook
*
* Returns 1 if the hook has allowed the packet to pass. The function
* okfn must be invoked by the caller in this case. Any other return
* value indicates the packet has been consumed by the hook.
*/
static inline int nf_hook(u_int8_t pf, unsigned int hook, struct net *net,
struct sock *sk, struct sk_buff *skb,
struct net_device *indev, struct net_device *outdev,
int (*okfn)(struct net *, struct sock *, struct sk_buff *))
{
struct nf_hook_entries *hook_head = NULL;
int ret = 1;
...
/*选择协议族的hook列表*/
rcu_read_lock();
switch (pf) {
case NFPROTO_IPV4:
hook_head = rcu_dereference(net->nf.hooks_ipv4[hook]);
break;
case NFPROTO_IPV6:
hook_head = rcu_dereference(net->nf.hooks_ipv6[hook]);
break;
case NFPROTO_ARP:
#ifdef CONFIG_NETFILTER_FAMILY_ARP
if (WARN_ON_ONCE(hook >= ARRAY_SIZE(net->nf.hooks_arp)))
break;
hook_head = rcu_dereference(net->nf.hooks_arp[hook]);
#endif
break;
case NFPROTO_BRIDGE:
#ifdef CONFIG_NETFILTER_FAMILY_BRIDGE
hook_head = rcu_dereference(net->nf.hooks_bridge[hook]);
#endif
break;
#if IS_ENABLED(CONFIG_DECNET)
case NFPROTO_DECNET:
hook_head = rcu_dereference(net->nf.hooks_decnet[hook]);
break;
#endif
default:
WARN_ON_ONCE(1);
break;
}
if (hook_head) {
struct nf_hook_state state;
/*把相关信息,设置到state结构里*/
nf_hook_state_init(&state, hook, pf, indev, outdev,
sk, net, okfn);
/*找到对应的hook函数列表 hook_head,遍历有序的函数进行匹配*/
ret = nf_hook_slow(skb, &state, hook_head, 0);
}
rcu_read_unlock();
return ret;
}
以上源代码以及内容基于内核4.19.1产出,不同的内核版本,表定义的hook点会有不同,如:nat表 原来只定义了pre routing/output/post routing,4.19.1中的源码新增input。
1.6.8 使用Netfilter开发基本步骤
使用Netfilter框架开发程序可以实现自定义的网络数据包过滤和修改功能,以满足特定的网络安全需求或网络管理要求。
下面是使用Netfilter框架开发程序的一般步骤:
- 了解Netfilter框架:首先需要了解Netfilter框架的基本原理和架构,包括Netfilter的hook机制、内核模块和用户空间程序之间的通信方式等。
- 编写内核模块:Netfilter框架主要是一个内核模块,需要编写C语言代码实现需要的过滤或修改功能。内核模块的代码需要包含模块初始化和清理函数,以及实现Netfilter钩子函数来处理数据包。
- 注册Netfilter钩子函数:在内核模块中注册Netfilter钩子函数,使其能够在特定的网络数据包到达时被调用。Netfilter提供了多个钩子点(hook points)来处理不同的数据包类型和阶段,例如INPUT、OUTPUT、FORWARD、PREROUTING、POSTROUTING等。
- 编译和加载内核模块:编译内核模块并加载到Linux内核中。这可以使用标准的make和insmod命令进行操作。
- 测试和调试:测试和调试内核模块以确保它可以正确地处理网络数据包,并按预期方式过滤或修改数据包。
1.6.9 代码案例
通过hook注册函数,实现向内核中相应的hook点注册hook回调函数,结合icmp数据包的格式,实现对icmp数据包的处理。
1、接收过滤
主要是数据接收方向的NF_IP_LOCAL_IN点注册回调函数,该回调函数对接收到的icmp reply报文,将序列号是9的倍数的reply报文丢弃掉。
1.6.10 参考文章
http://t.csdnimg.cn/LAqQ0
http://t.csdnimg.cn/rekEo
http://t.csdnimg.cn/SliJQ
http://t.csdnimg.cn/n8LLR
1.7 eBPF(未完待续…)
伯克利包过滤器,是一种基于伪汇编的底层数据包过滤器,他可以根据配置的规则过滤数据包,但好像不能对数据包做修改。我也不太清楚这种方法,这里就放一个链接(eBPF用于Linux防火墙数据包过滤https://blog.csdn.net/dog250/article/details/102884567),自己去研究吧。
1.8 ftrace框架
看这篇文章吧,他解释的更好。简单来说就是ftrace是hook内核函数的方法,他本身的作用是用于跟踪和调试内核,但后来被用来拦截网络数据包了。
https://www.apriorit.com/dev-blog/546-hooking-linux-functions-2
源代码下载
这是githup上找到的,分享给大家,对照源代码去看上一个链接的文章说明,更容易明白。
https://github.com/ilammy/ftrace-hook.git