欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

基于Netfilter从零开始写一个Linux防火墙,为我们的迷你防火墙编写代码

最编程 2024-01-02 21:03:32
...

介绍

Firewalls是一个重要的工具,可以配置为保护您的服务器和基础设施。防火墙的主要功能是过滤数据、重定向流量和防止网络攻击。既有基于硬件的防火墙,也有基于软件的防火墙。在这里我不会过多讨论背景,因为您可以在网上找到很多关于它的文档。

你有没有想过从头开始实现一个简单的迷你防火墙?听起来很疯狂?但借助 Linux 的强大功能,您可以做到这一点。看完本系列文章,你会发现其实很简单。

您可能曾经在 Linux 上使用过各种防火墙,例如iptables、nftables、UFW等。所有这些防火墙工具都是用户空间实用程序,它们都依赖于Netfilter. Netfilter是允许实现各种与网络相关的操作的 Linux 内核子系统。Netfilter允许您使用Linux Kernel Module. 如果您不了解 Linux 内核模块和 Netfilter 等技术,请不要担心。在本文中,让我们基于 Netfilter 从零开始编写一个 Linux 防火墙。您可以了解到以下有趣的观点:

  • Linux内核模块开发。

  • Linux内核网络编程。

  • Netfilter模块开发。
    这篇文章会有点长,分为五个部分:

  • Netfilter 和内核模块的背景:介绍Netfilter 和内核模块的理论。

  • 制作第一个内核模块:学习如何编写一个简单的内核模块。

  • Netfilter 架构和 API:回顾 Netfilter 挂钩架构和源代码。

  • 实施迷你防火墙:为我们的迷你防火墙编写代码。

Netfilter 和内核模块的背景

Netfilter 基础知识

Netfilter可以认为是firewallLinux上的第三代。在 Linux Kernel 2.4 引入之前Netfilter,Linux 上有两个老一代的防火墙如下:

  • ipfw第一代是 BSD UNIX 早期版本到 Linux 1.1的移植。
  • 第二代ipchains开发于Linux Kernel 2.2系列。

正如我们上面提到的,Netfilter旨在为各种网络操作提供 Linux 内核内部的基础设施。Sofirewall只是提供的多种功能之一,Netfilter如下所示:

image.png
  • 数据包过滤:负责根据规则过滤数据包。这也是本文的主题。
  • NAT(网络地址转换):负责转换网络数据包的IP地址。NAT是一个重要的协议,已成为conserving global address space in the face of IPv4 address exhaustion. 不懂NAT协议的可以参考其他文档。我将在以后的其他文章中对其进行研究。
  • Packet mangling : 负责修改数据包内容(其实NAT就是packet mangling的一种,修改源或目的IP地址)。例如,MSS (Maximum Segment Size)可以更改 TCP SYN 数据包的值以允许通过网络传输大尺寸数据包。

注意:本文将重点介绍基于Netfilter构建一个简单的防火墙来过滤数据包。所以NAT和Packet Mangling部分不在本文讨论范围内。

包过滤只能在Linux内核中完成(Netfilter的代码也在内核中),如果我们要写一个迷你防火墙,它必须运行在内核空间。正确的?这是否意味着我们需要将我们的代码添加到内核中并重新编译内核?想象一下,每次要添加新的数据包过滤规则时都必须重新编译内核。这是个坏主意。好消息是Netfilter允许您使用Linux kernel modules.

Linux 内核模块基础

虽然 Linux 是一个 . monolithic kernel,但它可以使用内核模块进行扩展。模块可以插入到内核中,也可以按需删除。Linux 隔离内核,但允许您通过模块动态添加特定功能。通过这种方式,Linux 在稳定性和可用性之间保持了平衡。

driver我想在这里检查一个关于内核模块的混淆点:和之间有什么区别module:

  • 驱动程序是在内核中运行的一些代码,用于与某些硬件设备通信。它驱动硬件。标准做法是尽可能将驱动程序构建为内核模块,而不是将它们静态链接到内核,因为这样可以提供更大的灵活性。
  • 内核模块可能根本不是设备驱动程序。

在下一节中,我们将亲自动手并开始实施我们的迷你防火墙。我们将逐步完成整个过程。第一步,让我们使用一个简单的hello world演示编写我们的第一个 Linux 内核模块。然后让我们学习如何构建模块(这与在用户空间编译应用程序有很大不同)以及如何在内核中加载它。

制作第一个内核模块

首先,不得不承认Linux Kernel模块开发是一个庞大而复杂的技术课题。并且有很多关于它的很棒的在线资源。本系列文章着重于开发基于 Netfilter 的迷你防火墙,因此我们无法涵盖 Kernel 模块本身的所有方面。在以后的文章中,我将更深入地研究内核模块的知识。

编写模块

hello world您可以使用单个C源代码文件编写内核模块hello.c,如下所示:

#include <linux/init.h> /* Needed for the macros */
#include <linux/kernel.h> 
#include <linux/module.h> /* Needed by all modules */

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, world\n");
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, world\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");

我们可以用如此简单的方式编写内核模块,因为 Linux 内核为您施展了魔法。记住Linux(Unix)的设计理念:Design for simplicity;只在必须的地方增加复杂性。

让我们检查几个值得评论的技术点如下:

首先,内核模块必须至少有两个函数:一个是在模块加载到内核时调用的“开始”函数,另一个是在模块从内核中删除之前调用的“结束”函数。在内核 2.3.13 之前,这两个函数的名称被硬编码为init_module()和cleanup_module()。但是在新版本中,您可以使用module_init和module_exit宏为模块的开始和结束函数使用任何您喜欢的名称。宏在include/linux/module.h和中定义include/linux/init.h。您可以参考那里的详细信息。

通常,module_init要么向内核注册一个处理程序(例如,本文开发的迷你防火墙),要么用自己的代码替换其中一个内核函数(通常是代码做某事然后调用原始函数) . 该module_exit函数应该撤消module_init所做的任何操作,因此可以安全地卸载模块。

其次,printk函数提供与 类似的行为printf,它接受format string作为第一个参数。函数printk原型如下:

int printk(const char *fmt, ...);

printk函数允许调用者指定log level要发送到内核消息日志的消息的类型和重要性。例如,在上面的代码中,日志级别KERN_INFO是通过附加到格式字符串来指定的。在 C 编程中,这种语法称为[string literal concatenation](https://en.wikipedia.org/wiki/String_literal#String_literal_concatenation). (在其他高级编程语言中,字符串连接一般是用+运算符完成的)。对于函数printkand ,您可以在andlog level中找到更多信息。include/linux/kern_levels.h``include/linux/printk.h

注意:Linux 内核模块开发的头文件路径与您通常用于应用程序开发的头文件路径不同。不要试图在/usr/include/linux中查找头文件,而是请使用以下路径/lib/modules/uname -r/build/include/linuxuname -r命令返回您的内核版本)。

接下来,让我们构建这个 hello-world 内核模块。

构建模块

构建内核模块的方法与构建用户空间应用程序的方法略有不同。构建内核映像及其模块的有效解决方案是Kernel Build System(Kbuild).

Kbuild这是一个复杂的话题,我不会在这里详细解释。简单地说,Kbuild允许您创建高度定制的内核二进制映像和模块。从技术上讲,每个子目录都包含一个Makefile只编译其目录中的源代码文件。顶层 Makefile 递归执行每个子目录的 Makefile 以生成二进制对象。您可以通过定义来控制包含哪些子目录config files。具体可以参考其他文档。

以下是该hello world模块的 Makefile:

obj-m += hello.o
PWD := $(CURDIR)
all:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

make -C dir在读取 makefile 或执行任何其他操作之前,该命令更改为目录 dir。将使用/lib/modules/$(shell uname -r)/build中的* Makefile 。您会发现该命令make M=dir modules用于在指定目录中制作所有模块。

在模块级 Makefile 中,obj-m语法告诉系统从kbuild构建,链接后将生成内核模块。在我们的例子中,模块名称是.module_name.omodule_name.cmodule_name.kohello

构建过程如下:

chrisbao:~$ sudo make
make -C /lib/modules/4.15.0-176-generic/build M=/home/DIR/jbao6/develop/kernel/hello-1  modules
make[1]: Entering directory '/usr/src/linux-headers-4.15.0-176-generic'
  CC [M]  /home/DIR/jbao6/develop/kernel/hello-1/hello.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/DIR/jbao6/develop/kernel/hello-1/hello.mod.o
  LD [M]  /home/DIR/jbao6/develop/kernel/hello-1/hello.ko
make[1]: Leaving directory '/usr/src/linux-headers-4.15.0-176-generic'

构建完成后,您可以在同一目录中获得几个新文件:

chrisbao:~$ ls 
hello.c hello.ko hello.mod.c hello.mod.o hello.o Makefile modules.order Module.symvers

文件结尾.ko是内核模块。你现在可以忽略其他文件,我稍后会写另一篇文章来深入讨论内核模块系统。

加载模块

使用该file命令,您可以注意到内核模块是一个ELF(Executable and Linkable Format)格式化文件。ELF 文件通常是编译器或链接器的输出,并且是二进制格式。

chrisba:~$ file hello.ko
hello.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=f0da99c757751e7e9f9c4e55f527fb034a0a4253, not stripped

下一步,让我们尝试动态安装和删除模块。你需要知道以下三个命令:

  • lsmod:显示当前加载的内核模块列表。
  • insmod:通过运行将模块插入 Linux 内核sudo insmod module_name.ko
  • rmmod:通过运行从 Linux 内核中删除模块sudo rmmod module_name

由于hello world模块非常简单,您可以根据需要轻松安装和删除模块。具体的命令这里就不展示了,留给读者吧。

注意:这并不意味着您可以毫无问题地轻松安装和删除任何内核模块。如果您加载的模块有错误,整个系统可能会崩溃。

调试模块

下一步,让我们证明hello world模块已按预期安装和删除。我们将使用dmesg命令。dmesg(诊断消息)可以打印kernel ring buffer.

首先,aring buffer是一种数据结构,它使用单个固定大小的缓冲区,就好像它是端到端连接的一样。是kernel ring buffer一个环形缓冲区,记录与内核运行相关的消息。正如我们上面提到的,函数打印的内核日志printk将被发送到内核环形缓冲区。

我们可以使用命令找到我们的模块产生的消息,dmesg | grep world如下所示:

chrisbao:~$ dmesg | grep world

[2147137.177254] Hello, world
[3281962.445169] Goodbye, world
[3282008.037591] Hello, world
[3282054.921824] Goodbye, world

现在您可以看到已hello world正确加载到内核中。它也可以动态删除。伟大的。

基于对内核模块的这种理解,让我们继续我们的旅程,编写一个Netfilter模块作为我们的迷你防火墙。

Netfilter 架构

Netfilter 钩子的基础知识

该框架在 Linux 内核中Netfilter提供了一堆。hooks当网络数据包通过内核中的协议栈时,它们也会遍历这些钩子。Netfilter 允许您使用这些挂钩编写模块和注册回调函数。当钩子被触发时,回调函数将被调用。这是 Netfilter 架构背后的基本思想。不难理解吧?

image.png

目前,Netfilter 提供了以下 5 个钩子IPv4:

  • NF_INET_PRE_ROUTING:在网卡上接收到数据包后立即触发。routing decision这个钩子在创建之前被触发。然后内核确定这个数据包是否发往当前主机。根据条件,将触发以下两个钩子。

  • NF_INET_LOCAL_IN:为发往当前主机的网络数据包触发。

  • NF_INET_FORWARD:为应转发的网络数据包触发。

  • NF_INET_POST_ROUTING:为已路由且在发送到网卡之前的网络数据包触发。

  • NF_INET_LOCAL_OUT:为当前主机上的进程生成的网络数据包触发。
    您在模块中定义的挂钩函数可以处理或过滤数据包,但它最终必须向 Netfilter 返回一个状态码。代码有几个可能的值,但现在,您只需要了解其中两个:

  • NF_ACCEPT:这意味着钩子函数接受数据包并且它可以继续网络堆栈之旅。

  • NF_DROP:这意味着数据包被丢弃并且不会遍历网络堆栈的其他部分。
    Netfilter 允许您以不同的优先级将多个回调函数注册到同一个钩子。如果第一个钩子函数接受了数据包,那么数据包将被传递给下一个低优先级的函数。如果数据包被一个回调函数丢弃,则不会遍历下一个函数(如果存在)。

如您所见,Netfilter范围很大,我无法涵盖文章中的每个细节。所以这里开发的迷你防火墙会在 hook 上工作NF_INET_PRE_ROUTING,也就是说它是通过控制入站网络流量来工作的。但是注册钩子和处理数据包的方式可以应用于所有其他钩子。

注意Netfilter:还有另一个值得注意的问题:和之间有什么区别eBPF?如果你不了解eBPF,请参考我之前的文章。它们都是 Linux 内核中重要的网络特性。重要的是Netfilter,eBPF钩子位于内核的不同层。正如我在上图中所画的,eBPF位于较低的一层。

Netfilter 钩子的内核代码

为了清楚地了解Netfilter框架在协议栈内部是如何实现的,让我们稍微深入一点,看看内核源代码(不用担心,只展示了几个简单的功能)。我们以钩子NF_INET_PRE_ROUTING为例;因为迷你防火墙将基于它编写。

当接收到 IPv4 数据包时,ip_rcv将调用其处理函数,如下所示:

//In source code file /kernel-src/net/ipv4/ip_input.c
/*
 * IP receive entry point
 */
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt,
           struct net_device *orig_dev)
{
        struct net *net = dev_net(dev);

        skb = ip_rcv_core(skb, net);
        if (skb == NULL)
                return NET_RX_DROP;
        // run Netfilter NF_INET_PRE_ROUTING hook's callback function
        return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, 
                       net, NULL, skb, dev, NULL,
                       ip_rcv_finish);
}

在这个处理函数中,你可以看到钩子被传递给了函数NF_HOOK。根据名称NF_HOOK,您可以猜到它是用于触发 Netfilter 挂钩的。正确的?NF_HOOK下面我们继续考察它是如何实现的:

//In source code file /kernel-src/include/linux/netfilter.h
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 *))
{
        int ret = nf_hook(pf, hook, net, sk, skb, in, out, okfn);
        if (ret == 1)
                ret = okfn(net, sk, skb); // in our case: okfn is ip_rcv_finish
        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 *))
{
    // code omit
}

该函数NF_HOOK包含两个步骤:

  • 首先,通过调用底层函数来运行钩子的回调函数nf_hook。
  • 其次,如果数据包通过挂钩函数并且没有丢弃,则调用该函数okfn(作为参数传递给NF_HOOK )。

对于钩子NF_INET_LOCAL_IN,该函数ip_rcv_finish将在钩子函数通过后被调用。它的工作是将数据包传递给协议栈中的下一个协议处理程序(TCP 或 UDP)以继续其旅程!

其他 4 个钩子都使用相同的函数NF_HOOK来触发回调函数。下表显示了钩子在内核中的嵌入位置,我将它们留给读者。

image.png

接下来,让我们回顾一下Netfilter 的创建和注册钩子函数的API。

过滤器API

创建一个 Netfilter 模块很简单,包括三个步骤:

  • 定义钩子函数。
  • 在内核模块初始化过程中注册钩子函数。
  • 在内核模块清理过程中注销钩子函数。

让我们一一快速过一遍。

定义一个钩子函数
钩子函数名可以随意,但必须遵循下面的签名:

//In source code file /kernel-src/include/linux/netfilter.h
typedef unsigned int nf_hookfn(void *priv,
                               struct sk_buff *skb,
                               const struct nf_hook_state *state);

钩子函数可以破坏或过滤其数据存储在结构中的数据包sk_buff(我们可以忽略其他两个参数;因为我们不在我们的迷你防火墙中使用它们)。正如我们上面提到的,回调函数必须返回一个整数形式的 Netfilter 状态代码。例如,accepted和dropped状态定义如下:

// In source code file /kernel-src/include/uapi/linux/netfilter.h
/* Responses from hook functions. */
#define NF_DROP 0
#define NF_ACCEPT 1

注册和注销钩子函数

注册一个钩子函数,我们应该把定义好的钩子函数和相关信息,比如你要绑定哪个钩子,协议族,钩子函数的优先级等,包装成一个结构体,传递给struct nf_hook_ops函数nf_register_net_hook。

//In source code file /kernel-src/include/linux/netfilter.h
struct nf_hook_ops {
        /* User fills in from here down. */
        nf_hookfn               *hook;    // callback function
        struct net_device       *dev;     // network device interface
        void                    *priv; 
        u_int8_t                pf;       // protocol
        unsigned int            hooknum;  // Netfilter hook enum
        /* Hooks are ordered in ascending priority. */
        int                     priority; // priority of callback function
};

大多数字段都非常容易理解。需要强调的是 field hooknum,也就是上面讨论的Netfilter hooks。它们被定义为枚举数,如下所示:

// In source code file /kernel-src/include/uapi/linux/netfilter.h
enum nf_inet_hooks {
    NF_INET_PRE_ROUTING,
    NF_INET_LOCAL_IN,
    NF_INET_FORWARD,
    NF_INET_LOCAL_OUT,
    NF_INET_POST_ROUTING,
    NF_INET_NUMHOOKS,
    NF_INET_INGRESS = NF_INET_NUMHOOKS,
};

接下来我们来看看注册和注销钩子函数的函数如下:

//In source code file /kernel-src/include/linux/netfilter.h
/* Function to register/unregister hook points. */
int nf_register_net_hook(struct net *net, const struct nf_hook_ops *ops);
void nf_unregister_net_hook(struct net *net, const struct nf_hook_ops *ops);

第一个参数struct net与网络命名空间有关,我们暂时忽略它并使用默认值。

接下来,让我们基于这些 API 来实现我们的迷你防火墙。好的?

实施迷你防火墙

首先,我们需要澄清我们的迷你防火墙的要求。我们将在迷你防火墙中实现两个网络流量控制规则,如下所示:

网络协议规则:丢弃ICMP协议数据包。
IP 地址规则:丢弃来自一个特定 IP 地址的数据包。

丢弃 ICMP 协议数据包

ICMP是现实世界中广泛使用的网络协议。流行的诊断工具喜欢ping并traceroute运行 ICMP 协议。我们可以使用以下钩子函数根据 IP 标头中的协议类型过滤掉 ICMP 数据包:

// In mini-firewall.c 
static unsigned int nf_blockicmppkt_handler(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
{
    struct iphdr *iph;   // IP header
    struct udphdr *udph; // UDP header
    if(!skb)
        return NF_ACCEPT;
    iph = ip_hdr(skb); // retrieve the IP headers from the packet
    if(iph->protocol == IPPROTO_UDP) { 
        udph = udp_hdr(skb);
        if(ntohs(udph->dest) == 53) {
            return NF_ACCEPT; // accept UDP packet
        }
    }
    else if (iph->protocol == IPPROTO_TCP) {
        return NF_ACCEPT; // accept TCP packet
    }
    else if (iph->protocol == IPPROTO_ICMP) {
        printk(KERN_INFO "Drop ICMP packet \n");
        return NF_DROP;   // drop TCP packet
    }
    return NF_ACCEPT;
}

上述钩子函数中的逻辑很容易理解。首先,我们从网络数据包中检索 IP 标头。然后根据protocol标头中的类型字段,我们决定接受 TCP 和 UDP 数据包,但丢弃 ICMP 数据包。我们唯一需要注意的技术是 function ip_hdr,它是定义如下的核函数:

//In source code file /kernel-src/include/linux/ip.h
static inline struct iphdr *ip_hdr(const struct sk_buff *skb)
{
        return (struct iphdr *)skb_network_header(skb);
}
// In source code file /kernel-src/include/linux/skbuff.h
static inline unsigned char *skb_network_header(const struct sk_buff *skb)
{
        return skb->head + skb->network_header;
}

函数ip_hdr将任务委托给函数skb_network_header。它根据以下两个数据获取 IP 标头:

  • head:是指向数据包的指针;
  • network_header:是指向数据包的指针和指向网络层协议头的指针之间的偏移量。详细可以参考这篇文档。
    接下来,我们可以注册上面的钩子函数,如下:
// In mini-firewall.c 
static struct nf_hook_ops *nf_blockicmppkt_ops = NULL;

static int __init nf_minifirewall_init(void) {
    nf_blockicmppkt_ops = (struct nf_hook_ops*)kcalloc(1,  sizeof(struct nf_hook_ops), GFP_KERNEL);
    if (nf_blockicmppkt_ops != NULL) {
        nf_blockicmppkt_ops->hook = (nf_hookfn*)nf_blockicmppkt_handler;
        nf_blockicmppkt_ops->hooknum = NF_INET_PRE_ROUTING;
        nf_blockicmppkt_ops->pf = NFPROTO_IPV4;
        nf_blockicmppkt_ops->priority = NF_IP_PRI_FIRST; // set the priority
        
        nf_register_net_hook(&init_net, nf_blockicmppkt_ops);
    }
    return 0;
}

static void __exit nf_minifirewall_exit(void) {
    if(nf_blockicmppkt_ops != NULL) {
        nf_unregister_net_hook(&init_net, nf_blockicmppkt_ops);
        kfree(nf_blockicmppkt_ops);
    }
    printk(KERN_INFO "Exit");
}

module_init(nf_minifirewall_init);
module_exit(nf_minifirewall_exit);

上面的逻辑是不言自明的。我不会在这里花太多时间。

接下来,是时候演示我们的迷你防火墙是如何工作的了。

演示时间

在我们加载迷你防火墙模块之前,ping命令可以按预期工作:

chrisbao@CN0005DOU18129:~$ lsmod | grep mini_firewall
chrisbao@CN0005DOU18129:~$ ping www.google.com
PING www.google.com (142.250.4.103) 56(84) bytes of data.
64 bytes from sm-in-f103.1e100.net (142.250.4.103): icmp_seq=1 ttl=104 time=71.9 ms
64 bytes from sm-in-f103.1e100.net (142.250.4.103): icmp_seq=2 ttl=104 time=71.8 ms
64 bytes from sm-in-f103.1e100.net (142.250.4.103): icmp_seq=3 ttl=104 time=71.9 ms
64 bytes from sm-in-f103.1e100.net (142.250.4.103): icmp_seq=4 ttl=104 time=71.8 ms
^C
--- www.google.com ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3005ms
rtt min/avg/max/mdev = 71.857/71.902/71.961/0.193 ms

相反,在构建并加载迷你防火墙模块之后(基于我们之前讨论的命令):

chrisbao@CN0005DOU18129:~$ lsmod | grep mini_firewall
mini_firewall          16384  0
chrisbao@CN0005DOU18129:~$ ping www.google.com
PING www.google.com (142.250.4.105) 56(84) bytes of data.
^C
--- www.google.com ping statistics ---
6 packets transmitted, 0 received, 100% packet loss, time 5097ms

你可以看到所有的数据包都丢失了;因为它被我们的迷你防火墙丢弃了。我们可以通过运行命令来验证这一点dmesg:

chrisbao@CN0005DOU18129:~$ dmesg | tail -n 5
[ 1260.184712] Drop ICMP packet
[ 1261.208637] Drop ICMP packet
[ 1262.232669] Drop ICMP packet
[ 1263.256757] Drop ICMP packet
[ 1264.280733] Drop ICMP packet

但其他协议报文仍然可以通过防火墙。例如,命令wget 142.250.4.103可以正常返回如下:

chrisbao@CN0005DOU18129:~$ wget 142.250.4.103
--2022-06-25 10:12:39--  http://142.250.4.103/
Connecting to 142.250.4.103:80... connected.
HTTP request sent, awaiting response... 302 Moved Temporarily
Location: http://142.250.4.103:6080/php/urlblock.php?args=AAAAfQAAABAjFEC0HSM7xhfO~a53FMMaAAAAEILI_eaKvZQ2xBfgKEgDtwsAAABNAAAATRPNhqoqFgHJ0ggbKLKcdinR4UvnlhgAR4~YyrY4tAnroOFkE_IsHsOg9~RFPc7nEoj6YdiDgqZImAmb_xw9ZuFLvF91P2HzP5tlu1WX&url=http://142.250.4.103%2f [following]
--2022-06-25 10:12:39--  http://142.250.4.103:6080/php/urlblock.php?args=AAAAfQAAABAjFEC0HSM7xhfO~a53FMMaAAAAEILI_eaKvZQ2xBfgKEgDtwsAAABNAAAATRPNhqoqFgHJ0ggbKLKcdinR4UvnlhgAR4~YyrY4tAnroOFkE_IsHsOg9~RFPc7nEoj6YdiDgqZImAmb_xw9ZuFLvF91P2HzP5tlu1WX&url=http://142.250.4.103%2f
Connecting to 142.250.4.103:6080... connected.
HTTP request sent, awaiting response... 200 OK
Length: 3248 (3.2K) [text/html]
Saving to: ‘index.html’

index.html                                           100%[===================================================================================================================>]   3.17K  -.KB/s    in 0s

2022-06-25 10:12:39 (332 MB/s) - ‘index.html’ saved [3248/3248]

接下来,让我们尝试禁止来自该 IP 地址的流量。

丢弃来自一个特定 IP 地址的数据包源

正如我们上面提到的,允许在同一个 Netfilter 钩子上注册多个回调函数。所以我们将定义具有不同优先级的第二个钩子函数。这个钩子函数的逻辑是这样的:我们可以从 IP 头中获取源 IP 地址,并据此做出丢弃或接受的决定。代码如下:

// In mini-firewall.c 
#define IPADDRESS(addr) \
    ((unsigned char *)&addr)[3], \
    ((unsigned char *)&addr)[2], \
    ((unsigned char *)&addr)[1], \
    ((unsigned char *)&addr)[0]

static char *ip_addr_rule = "142.250.4.103";

static unsigned int nf_blockipaddr_handler(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
{
    if (!skb) {
        return NF_ACCEPT;
    } else {
        char *str = (char *)kmalloc(16, GFP_KERNEL);
        u32 sip;
        struct sk_buff *sb = NULL;
        struct iphdr *iph;

        sb = skb;
        iph = ip_hdr(sb);
        sip = ntohl(iph->saddr); // get source ip address; 
        
        sprintf(str, "%u.%u.%u.%u", IPADDRESS(sip)); // convert to standard IP address format
        if(!strcmp(str, ip_addr_rule)) {
            return NF_DROP;
        } else {
            return NF_ACCEPT;
        }
    }
}

这个钩子函数使用了两个有趣的技术:

  • ntohl: 是一个内核函数,用于将值从 转换network byte order为host byte order。Byte order与计算机科学概念有关Endianness。Endianness 定义计算机内存中数字数据字的顺序或字节序列。系统big-endian将字的最高有效字节存储在最小的内存地址。相反,系统little-endian将最低有效字节存储在最小地址。网络协议使用big-endian系统。但是不同的操作系统和平台运行不同的 Endianness 系统。所以它可能需要基于主机的这种转换。
  • IPADDRESS: 是一个宏,它从一个 32 位整数生成标准的 IP 地址格式(四个 8 位字段,用句点分隔)。它使用的技术the equivalence of arrays and pointers in C。我将写另一篇文章来研究它是什么以及它是如何工作的。请继续关注我的更新!
    接下来,我们可以用上面讨论的相同方式注册这个钩子函数。唯一值得注意的是这个回调函数
    应该有不同的优先级如下:
static int __init nf_minifirewall_init(void) {
    <-omit code->
    nf_blockipaddr_ops = (struct nf_hook_ops*)kcalloc(1, sizeof(struct nf_hook_ops), GFP_KERNEL);
    if (nf_blockipaddr_ops != NULL) {
        nf_blockipaddr_ops->hook = (nf_hookfn*)nf_blockipaddr_handler;
        nf_blockipaddr_ops->hooknum = NF_INET_PRE_ROUTING;  // register to the same hook
        nf_blockipaddr_ops->pf = NFPROTO_IPV4;
        nf_blockipaddr_ops->priority = NF_IP_PRI_FIRST + 1; // set a higher priority

        nf_register_net_hook(&init_net, nf_blockipaddr_ops);
    }
    <-omit code->
}

让我们通过演示看看它是如何工作的。

演示时间

重新构建并重新加载模块后,我们可以得到:

chrisbao@CN0005DOU18129:~$ wget 142.250.4.103
--2022-06-25 10:20:07--  http://142.250.4.103/
Connecting to 142.250.4.103:80... failed: Connection timed out.
Retrying.

无法wget 142.250.4.103返回响应。因为它被我们的迷你防火墙丢弃了。伟大的!

chrisbao@CN0005DOU18129:~$ dmesg | tail -n 5
[ 3162.064284] Drop packet from 142.250.4.103
[ 3166.089466] Drop packet from 142.250.4.103
[ 3166.288603] Drop packet from 142.250.4.103
[ 3174.345463] Drop packet from 142.250.4.103
[ 3174.480123] Drop packet from 142.250.4.103

更多扩展空间

您可以在文章底部找到完整的代码实现。但我不得不说,我们的迷你防火墙只触及了 Netfilter 可以提供的功能的表面。您可以继续扩展功能。例如,目前规则是硬编码的,为什么不可以动态配置规则呢。有很多很酷的想法值得尝试。我把它留给读者。

概括

在本文中,我们逐步实现了迷你防火墙,并研究了许多详细的技术。不仅是代码;但我们也通过运行真实的演示来验证迷你防火墙的行为。

项目文件
linux-mini-firewall-netfilter-main.zip
链接:https://pan.baidu.com/s/1BlbllHNmB5cCbvBFPjkLuw  密码:xid8