# tiny firewall **Repository Path**: pvz122/tiny-firewall ## Basic Information - **Project Name**: tiny firewall - **Description**: An eBPF-based firewall - **Primary Language**: Unknown - **License**: GPL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 4 - **Forks**: 2 - **Created**: 2022-06-15 - **Last Updated**: 2025-12-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 网络程序设计课程设计-防火墙实现 ## 选题背景 出于对Linux内核的兴趣,我注意到现今Linux社区中流行着一种和这门课程密切相关的新技术——eBPF。这项技术有着诱人的前景,被安全界广泛推崇。我希望借完成这次课程设计的机会加深对eBPF技术的掌握。 eBPF(https://ebpf.io)的全称是extended Berkeley Packet Filter,它是运行在Linux内核中的一个小型虚拟机。与内核模块类似,它旨在在不修改内核的情况下扩充内核功能。 它的前身BPF早在1992年就被提出,专门用于网络过滤,被几乎所有的UNIX操作系统支持。但2014年之后Linux提出了新的eBPF,使BPF不再局限于网络栈,成为了内核顶级的子系统。与传统技术相比,eBPF功能强大,并且还在快速发展当中。它轻量、高效、安全,得益于它独特的设计: ![img](https://ebpf.io/static/overview-bf463455a5666fc3fb841b9240d588ff.png) 它被设计为只能确定性执行,在加载入内核时就会进行严格的时间和空间确定性检查,以确保不对内核稳定性造成影响。它还包含一个运行时JIT编译器,使得它的字节码可以平台无关且高效地运行。事实上,微软也正在开发eBPF for Windows,赋予了eBPF优越的可移植性。 eBPF可以被用于负载均衡、内核监测、故障排除、运行时安全等等领域。本次课程设计中,我使用eBPF实现了一个功能精简的防火墙。 ## 技术原理 ### XDP 防火墙的底层使用的是基于eBPF的XDP数据包处理器。XDP技术是Cloudflare提出的,它旨在以最优性能完成网络数据包过滤。netfilter与其它钩子的一个缺点是,必须将网络数据包从内核复制到用户空间加以处理。而XDP采用的思路是在尽量贴近物理层的早期阶段完成数据包过滤。它有三种工作模式: - 原生XDP:在网卡驱动的早期阶段运行 - 卸载XDP:直接在网卡上运行,不占用任何CPU时间 - 通用XDP:在内核处理的早期阶段运行 当eBPF程序捕获到一个原始的数据包,经过规则过滤后,可以返回以下结果码,指示网络驱动处理: - XDP_DROP:丢弃 - XDP_TX:转发,用于修改包内容后发回原网卡 - XDP_REDIRECT:重定向,发给其它网卡 - XDP_PASS:传递,移交正常的网络栈 - XDP_ABORTED:错误,被视为丢包 事实上,被赋予了修改并重定向网络数据包的能力后,XDP可以用来实现任何网络功能。譬如路由、NAT、负载均衡等等。 ### BCC eBPF程序有严格的语法要求,譬如只能调用规定的函数、只能使用哈希表、数组等规定的数据结构、不能含有不确定次数的循环。为了便于开发,社区贡献了一个类似GCC的开发套件BCC(https://github.com/iovisor/bcc)。它封装了一系列便于调用的eBPF helper函数,并为高级编程语言提供接口。 本次课程设计中使用了BCC套件,基于BCC的python接口实现了防火墙的控制器和服务器。 ## 实验目标和实验环境 ### 实验目标 1. 在内核中实现eBPF包过滤器,能按照规则过滤不同网卡上的IPv4、ICMP、TCP、UDP数据包。 2. 用户态实现防火墙的控制器,它能设定过滤规则,同时接受用户的交互请求。 3. 用户客户端,处理用户交互。 ### 实验环境 实验使用的是Ubuntu 21.10主机,Linux内核版本为5.13.0。 要运行本实验的源代码,需要额外安装以下软件包: clang、llvm、libcap-dev、libelf-dev、libbfd-dev、gcc-multilib、linux-headers-$(uname -r)、libbpf-dev、linux-tools-common、linux-tools-$(uname -r)、linux-tools-generic、bpfcc-tools 同时,python程序依赖于以下pip包: bcc、ctypes、regex ## 程序设计 ### 程序框架 如实验目标所称,程序主要分为三部分: - firewall.bcc.c:XDP bpf源程序,负责内核中的包过滤 - firewall.py:防火墙管理器,负责加载bpf程序到内核,并根据用户请求更改过滤规则 - firewall-cli.py:防火墙客户端,防火墙配置命令行工具,直接接受用户输入 程序框架如下: ![firewall](https://s2.loli.net/2022/06/16/3ontOq2B4rgDyRI.png) eBPF过滤器附加在网络接口上,使用eBPF maps结构与用户态的防火墙管理器同步过滤规则。防火墙管理器建立一个UNIX Socket的本地服务器,供客户端连接。客户端从命令行接受用户输入,并将配置交与防火墙管理器。 ### 交互与功能设计 程序应当具有相当的规则可配置性。它应该可以配置规则生效的网卡、规则生效的协议、协议报文中的关键字段。 为此,设计了三层命令,分别是基础命令、设备命令和协议命令: **基础命令** | 命令 | 参数 | 功能 | | ------- | ---------- | ---------------------- | | help | | 显示帮助信息 | | exit | | 退出程序 | | devices | | 显示所有网络接口的状态 | | use | \ | 设置指定的网络接口 | | back | | 返回上级 | 基础命令在所有情况下都可用 **设备命令** | 命令 | 参数 | 功能 | | -------- | -------------------- | ------------------------------------- | | show | | 显示当前网络接口的状态 | | enable | | 在当前网络接口上运行防火墙 | | disable | | 在当前网络接口上关闭防火墙 | | default | [allow\|deny] | 设定防火墙的默认规则(黑/白名单机制) | | protocol | [ip\|icmp\|tcp\|udp] | 设置指定的协议 | 设备命令只有在运行过use \指令后有效 **协议命令** | 命令 | 参数 | 功能 | | ----- | ------------------------------------------- | -------------------------- | | clear | | 清除本协议下的所有过滤规则 | | rule | | Pv4协议新建规则 | | rule | | ICMP协议新建规则 | | rule | | TCP协议新建规则 | | rule | | UDP协议新建规则 | 有一些值得提到的点: - 防火墙只对IPv4数据包有效,其它包会被放行 - 防火墙开启后默认是黑名单机制 - 如果设置为黑名单机制,则设定的规则就是允许通行的规则;如果设置为白名单机制,则设定的规则就是禁止通行的规则 - IPv4包中的protocol_type值取0-255. 0代表所有协议 - ICMP包中的icmp_type可以取以下值: - 0: echo reply - 3: destination unreachable - 4: source quench - 5: redirect - 8: echo request - 11: time exceeded - 12: parameter problem - 14: timestamp request - 15: timestamp reply - 16: address mask request - 17: address mask reply - 30: traceroute - src_addr和dst_addr都包含掩码,是用于范围匹配的。格式如192.168.1.1/24,就是匹配该子网下的所有IP。如果想匹配单个IP,可以将掩码设为全1 - src_port和dst_port取0-65535之间的值,0代表所有端口 ### 服务器-客户端通信设计 防火墙管理器与CLI客户端之间的通信使用本地的UNIX套接字实现。作为一个功能精简的防火墙,服务器没有支持多客户端并发请求。它们之间的通信采用简单的字符串传输,具体参数和用户输入参数类似。 ### eBPF过滤器通信设计 eBPF程序工作在内核空间,而且以沙箱的形式运行。它与用户空间的进程交换信息需要采取规定的特殊方法。目前Linux内核提供了三种用户程序与eBPF程序的数据交换方式:maps、perf和ring_buffer。 这里使用maps来实现。maps可以当作是一段eBPF程序和用户程序的共享内存,双方都可以借助一系列eBPF helper函数对其中的数据进行操作。 为此,在eBPF程序中定义了5个数组型的map,其中4个分别是四种协议的访问控制列表(access control list),它们每个数组成员包含以下一些元素: | 名称 | 含义 | | ----------- | --------------------- | | used | 数组中该项是否被使用 | | src_ip | 源ip地址 | | src_msk | 源ip地址的子网掩码 | | dst_ip | 目的ip地址 | | dst_msk | 目的ip地址的子网掩码 | | proto | IPv4过滤的协议字段 | | type | ICMP过滤的类型字段 | | src_port | TCP/UDP过滤的源端口 | | dst_port | TCP/UDP过滤的目的端口 | | match_count | 命中这条规则的包数 | 另一个用于设定白名单或黑名单机制。 ## 程序实现 ### eBPF包过滤器 包过滤器的程序逻辑是这样的:逐层解析XDP捕获的数据包,在每层中遍历访问控制列表(map结构),根据匹配情况和黑/白名单机制做出丢弃或传递的决策。 以过滤TCP包为例: ![image-20220616074652325](https://s2.loli.net/2022/06/16/hAsdHog62TRat8f.png) 当发现ip包头中的protocol是TCP时就解析TCP包头。然后遍历acl_tcp_list中的访问控制规则。acl_tcp_list是这样定义的map: ![image-20220616074958550](https://s2.loli.net/2022/06/16/bMhK2jus9WVeI7F.png) 针对这些规则,程序逐个匹配。它计算数据包中地址和规则地址经子网掩码位与后的值,以及对端口进行比较。当最终匹配成功时,根据策略选择丢弃或传递包。 ### CLI客户端 前文所述,客户端接受用户命令行输入并转化为管理命令发送给防火墙管理器。这一过程事实上有三个步骤:解析输入字符串->检查输入参数的有效性->构造包发送给管理器。 以icmp 协议的rule命令为例,程序接收到用户输入后会根据空格split为token。接下来进入一个大条件判断,如果当前选择的协议为icmp并且输入的是rule命令,就会进入该命令的处理过程: ![image-20220616080139673](https://s2.loli.net/2022/06/16/oJT8mjOnzQAIDYy.png) 它解析用户输入的参数,并调用相关validate函数验证有效性,最后构造一个形如icmprule device src_addr src_mask dst_addr dest_mask icmp_type的包发给服务器。 ### 防火墙管理器 #### 数据结构 管理器维护着一个字典bpf_handlers,它的形式如下: ![image-20220616080939841](https://s2.loli.net/2022/06/16/bTK8eJimFMCEAjk.png) 这个字典分网卡记录着挂附在其上的bpf程序的句柄和map对象。 #### 建立服务器 由于不考虑并发,服务器的建立是简单的。UNIX Sokcet是对应虚拟文件系统中的文件的,使用起来与普通套接字略有不同: ![image-20220616081424078](https://s2.loli.net/2022/06/16/GXPbq2miLU4NOSW.png) 套接字创建好后保持监听并接收客户端连接,连接后一个循环处理客户端的命令。这些处理只是简单重复劳动,不再赘述。 #### 装载eBPF程序 用户在网卡上使用enable命令时,管理器会把eBPF程序编译为字节码并装载、附加到网卡上。 ![image-20220616081807079](https://s2.loli.net/2022/06/16/abETCSj5siUGBl4.png) 有了BCC的帮助,它将llvm编译、libbpf生成、BPF系统调用封装为一个简单的函数,只需要告知eBPF源程序路径就能校验装载了。装载后,需要将XDP函数(即eBPF程序中负责处理数据包的关键函数)附加到网卡上。最后再处理map并维护bpf_handlers字典。 #### 增加过滤规则 以增加一条IPv4包过滤规则为例: ![image-20220616082401725](https://s2.loli.net/2022/06/16/1htf2Fw3z6Al7Pd.png) 它先由参数构造出map中的成员结构体,然后在map array中找到一个空闲位置放入。 程序逻辑是容易想到的,其中的困难主要在于数据类型和数据结构的对接。C语言的强类型和Python的弱类型是不可调和的,这里引入了ctypes库来处理c语言类型。 #### 查看过滤规则 查看过滤规则就是遍历读取map中的访问控制项,并将其打印出来。以获取所有IPv4规则为例: ![image-20220616083143671](https://s2.loli.net/2022/06/16/G9ow8bNtHqgu1AV.png) 它与增加过滤规则类似,主要做数据类型间的转换。 ## 程序测试 运行防火墙需要sudo执行firewall.py,并为其指定eBPF程序路径: ![image-20220616083515272](https://s2.loli.net/2022/06/16/N1mwAC8xejEYk7g.png) 接下来运行客户端的CLI程序: ![image-20220616083604418](https://s2.loli.net/2022/06/16/6z2oNhnUKkdfRbc.png) 它自动连接上了防火墙管理器。 当前系统中有三个网卡,enp0s3、enp0s8和lo: ![image-20220616083737814](https://s2.loli.net/2022/06/16/KSYOfragGp746EW.png) 接下来依次构造三种过滤规则来进行测试: 1. 禁止本机ping 127.0.0.1 2. 在enp0s8上只允许来自192.168.56.1的ssh连接,其它远程端口一律禁止 3. 禁止本机访问网段192.168.1.1/24 ### 禁止本机ping 127.0.0.1 依次执行use lo、enable、protocol icmp、rule 0.0.0.0/0 127.0.0.1/32 8,使用show查看: ![image-20220616084900720](https://s2.loli.net/2022/06/16/v8M1E4tjOILDw9Z.png) 可以看到这条规则已经被写入访问控制列表了。 此时尝试ping 127.0.0.1,无反应: ![image-20220616085047722](https://s2.loli.net/2022/06/16/4kaJOPgY7hBdzwf.png) 而再次查看show,被拒绝的包已经记录下来了: ![image-20220616085145254](https://s2.loli.net/2022/06/16/gsDNcTCwoijd3UY.png) 再Ping其它地址: ![image-20220616085240725](https://s2.loli.net/2022/06/16/mE1fx5gYWTqC3PJ.png) 都没问题,甚至127.0.0.2都可以ping通。 ### 只允许来自192.168.56.1的ssh连接 依次执行use enp0s8、enable、default deny、protocol tcp、rule 192.168.56.1/32 0 0.0.0.0/0 22 ![image-20220616085749506](https://s2.loli.net/2022/06/16/XFh83fNOjqy9pZL.png) 可以看到,这条规则已经被记录,默认也改为了白名单模式。 192.168.56.1也的确可以ssh上.101: ![image-20220616085950964](https://s2.loli.net/2022/06/16/bMpCGDfYT7AFWr9.png) 但想访问80端口就不行了: ![image-20220616090400351](https://s2.loli.net/2022/06/16/3CSNLhJxPZuW5j6.png) ### 禁止访问网段192.168.1.1/24 在执行操作之前,先尝试访问该网段的一个web服务器: ![image-20220616090746015](https://s2.loli.net/2022/06/16/ckPnXswaWoTpf7D.png) 没有问题。接下来依次执行use enp0s3、enable、protocol ip、rule 192.168.1.1/24 0.0.0.0/0 0: ![image-20220616091217882](https://s2.loli.net/2022/06/16/mVRc3wi4ItxQUpO.png) 再次访问192.168.1.200服务器: ![image-20220616091600204](https://s2.loli.net/2022/06/16/WIhBeLrmqDQGVER.png) 已经访问不了。 ![image-20220616091627553](https://s2.loli.net/2022/06/16/Wjy6cFUmQfSDwZs.png) ping 192.168.1.1也无法ping通。被DROP的包也记录下来了: ![image-20220616091753329](https://s2.loli.net/2022/06/16/7efH38BNXycrwmh.png) ## 实验感想 本次实验我尝试使用eBPF实现了一个简易的防火墙。使用新技术的难点在于它几乎没有资料,只能靠自己阅读源代码和示例学习。在编写过程中我翻遍了Linux内核源码中的相关部分,阅读了许多Linux社区的邮件通信列表。另一个问题是发展中的组件并不会有稳定的API,许多资料已经过时,而要解决某个函数的某个特性在哪个版本被引入此类的问题,就会非常艰难。在过程中我走了不少弯路,主要代码重构了两次,都是因为对框架不十分熟悉,写到中途发现实现不了。网上也几乎没有相关工作,摸不到石头过河的情况让人不禁对自己产生怀疑,我一度认为我不可能实现了。最后跑通那一刻简直让人不敢相信。让人觉得这一切又有意义了起来。