linux 内核编程是我大三网络安全这门课中黄老师讲解的,这个简单的防火墙是当时的实验作业,由 netfilter 相关的知识实现,理论部分参考了一些《Linux网络编程》的内容,这本书的下载地址 https://www.jb51.net/books/626304.html
虽然现在的工作还用不到相关的技术,但是越来越觉得 netfilter 在开发安全工具方面有很多优势,它可以把数据包过滤、网络地址转换和基于协议类型的连接跟踪等在内核层面实现,所以现在把这个小实例翻出来温故而知新,完整的代码在 https://github.com/BEIWO778/netfilter-firewall
一、netfilter架构
netfilter 是在 Linux 内核中一组钩子,这些钩子允许在网络协议栈中使用内核模块来注册回调函数,可以处理协议栈中经过钩子的每一个报文。因此可以利用它来实现许多功能,如过滤报文,修改报文等。netfilter 的架构就是在整个网络流程的若干位置放置了一些检测点(HOOK),而在每个检测点上登记了一些处理函数进行处理。在 IPv4 的协议栈中,netfilter 在 IP 数据包的路线上仔细选取了 5 个挂接点(HOOK)。这 5 个点中,在合适的位置对 NF_HOOK() 宏函数进行了调用,这 5 个点的含义如下所述
- NF IP PRE ROUTING:刚刚进入网络层而没有进行路由之前的网络数据会通过此点(进行完版本号、校验和等检测)
- NF_IP_FORWARD:在接收到的网络数据向另一个网卡进行转发之前通过此点
- NF IP_POST_ROUTING:任何马上要通过网络设备出去的包通过此检测点,这是 netfilter 的最后一个设置检测的点,内置的目的地址转换功能(包括地址伪装)在此点进行
- NF _IP_LOCAL_IN:在接收到的报文做路由,确定是本机接收的报文之后
- NF_ IP LOCAL_OUT:在本地报文做发送路由之前
这个小实例就是处理 NF_IP_LOCAL_IN 这个检测点,对 Linux 主机接收的数据包进行过滤。而对于用户与内核之间通信功能,则使用 sockopt 来实现,5 个挂接点书中的流程图如下:
在网上冲浪又找到一个更详细的图:
二、内核层代码实现
头文件初始化一些状态宏,设定驱动程序处理的最小值与最大值,然后在这之间设置防火墙不同功能的编号,尽量设置大一点避免与内核中某些命令值重复。结构体则用于存储防火墙的过滤规则信息
firewall/filter/fwfilter.h
#define SOE_MIN 0x6000 //驱动程序处理最小值 #define BANPING 0x6001 //过滤程序和驱动程序对话时禁ping功能编号 #define BANIP 0x6002 //禁ip功能编号 #define BANPORT 0x6003 //禁port功能编号 #define NOWRULE 0x6004 //获取防火墙当前规则功能编号 #define SOE_MAX 0x6100 //驱动程序处理最大值 typedef struct ban_status{ int ping_status; //是否禁ping,1禁止,0未设置 int ip_status; //是否禁ip,1禁止,0未设置 int port_status; //是否禁port,1禁止,0未设置 unsigned int ban_ip; //禁ip数值 unsigned short ban_port; //禁port数值 }ban_status;
过滤网络报文功能的实现主要使用 hookLocalIn 函数。ban ping 功能的实现是如果检测到是 ICMP 数据包,并且设置了禁用 ping 命令规则,则丢弃该 ICMP 数据包
ban port 功能的实现是如果设置了禁用端口规则,并且源端口符合用户设置的端口,则丢弃发往该端口的所有的数据包
ban ip 功能的实现是如果设置了禁用 ip 规则,并且源 ip 地址符合,丢弃该源 ip 发送的数据包
从《Linux网络编程》书中截取的这三个功能流程图
内核空间和用户空间的通信则是使用 hookSockoptSet() 函数和 hookSockoptGet() 函数,hookSockoptSet() 函数获取用户空间的命令信息,主要用到 copy_from_user() 函数,复制用户空间的数据到内核模块,然后按照用户的命令执行过滤规则
hookSockoptGet() 函数将内核模块的防火墙的规则情况传输给用户空间,主要用到 copy_to_user() 函数,复制内核空间的数据到用户空间
三、应用层代码实现
get_status() 函数分别获取当前防火墙三个功能的状态,若有禁止的 ip 或端口则打印出来详细禁止信息
改变 ping 规则时函数将当前 ping 状态取反,再把信息传到内核
改变 ip 规则函数,首先判断当前是否有 ip 已封禁,若当前无封禁 ip,则改变 ip 状态,由用户输入需封禁 ip,这里需要注意使用 inet_addr 函数转换 ip 格式。若当前已有封禁 ip,则直接取消封禁,再把信息传到内核
改变端口规则函数,首先判断当前是否有端口已封禁,若当前无封禁端口,则改变端口状态,由用户输入需封禁端口。若当前已有封禁端口,则直接取消封禁,再把信息传到内核
四、编译运行
编译内核的 Makefile 文件中指定内核模块的编译文件和头文件路径,编译模块的名称,当前模块的路径
firewall/filter/Makefile
# Makefile 4.0 obj-m := fwfilter.o CURRENT_PATH := $(shell pwd) LINUX_KERNEL := $(shell uname -r) LINUX_KERNEL_PATH := /usr/src/linux-headers-$(LINUX_KERNEL) all: make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules clean: make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean
在 /firewall/filter 路径下编译、加载内核
make //编译 insmod fwfilter.ko //加载内核(rmmod 卸载内核)
在 /firewall/app 路径下编译应用层测试代码,运行应用层程序
gcc fwctl.c -o fwctl ./fwctl
五、总结与思考
vps 版本为 ubuntu 18.04,内核版本为 4.15.0-136-generic,程序运行效果就不测试了,再补充一点内核中打印使用 printk() 函数,dmesg 命令可以得到内核中打印的信息,他们可以配合着进行简单的内核调试
范师傅对于 netfilter 防火墙和 iptables 防火墙的理解我一直记着,觉得很形象生动,这里给大家参考,iptables 就是自行车的脚踏板,netfilter 就是自行车轮子,自行车动起来底层都是使用了轮子推进,即防火墙的底层都是使用 netfilter 来进行数据包过滤
内核编程很依赖内核版本,经常网上的代码因为内核版本问题跑不起来,听说 ebpf 可以解决这个问题,打算学习一下
参考文章:
https://www.ebpf.top/post/iptalbes_ebpf/