解永亮,付国楷,房利国
(中国电子科技集团公司第三十研究所,四川 成都 610041)
在实际的网络环境中,防火墙是网络设备所提供的非常重要的功能之一。一般情况下,对性能要求较高的防火墙使用现场可编程门阵列(Field Programmable Gate Array,FPGA)对网络包进行处理,而对于那些需要提供防火墙功能的普通网络设备则由运行在中央处理器(Central Processing Unit,CPU)上的软件完成这项工作。在各种防火墙软件中,Linux 内核中的Netfilter 子系统又是一个经常被选择的方案。由于Netfilter 子系统所提供的数据包过滤功能放在了整个网络包处理路径中比较靠后的位置,因此如果在实际运行中防火墙需要大量丢弃接收到的网络包,就会导致CPU 运算能力被极大浪费,这在处理能力本就比较低的国产处理器上表现得尤为明显。
快速数据路径(eXpress Data Path,XDP)是一套内核态、高性能、可编程柏克莱封包过滤器(Berkeley Packet Filter,BPF)包处理框架[1]。该框架是在网络包处理路径上添加“处理点”(HOOK),这一点与Netfilter 相同,但XDP HOOK 位于更早的位置。这个位置可以是网卡驱动程序甚至是网卡内部,因此XDP程序可以直接从接收缓冲区中将网络包拿下来,无须执行任何耗时的操作,比如分配套接字缓冲区(Socket Buffer,SKB),然后将包推送到网络协议栈,或者将包推送给通用接收分担(Generic Receive Offload,GRO)引擎等[1]。除此之外,XDP的可编程特性可以实现在不修改Linux 内核的情况下重构包处理逻辑,这为防火墙软件升级提供了便利。
本文将简单介绍XDP 核心要素,并在此基础上结合现有Netfilter 子系统设计出一种新的框架结构,以达到高速网络包分析以及对现有防火墙上层软件不造成任何影响的目的,最后通过在飞腾处理器平台上的性能测试对两种方案进行了对比和分析。
XDP 程序只在网络数据包的接收(Ingress)路径上被触发执行。它可以运行在从硬件到协议栈的3 个不同位置,分别对应着3 种工作模式[2]:
(1)通用XDP(Generic XDP):XDP 程序运行在内核协议栈。
(2)XDP 分 担(Offloaded XDP):XDP 程 序运行在网卡硬件上。
(3)本地XDP(Native XDP):XDP 程序运行在网卡驱动中。本文将使用运行在该工作模式的XDP 对Netfilter 进行改进。由其所参与的内核协议栈数据接收路径[3]如图1 所示。
图1 XDP 网络包接收路径
网卡驱动从硬件上接收到网络包后首先交给XDP 程序处理,XDP 程序对网络包进行分析,并通过返回值的方式告诉内核接下来该如何操作:如果返回值为XDP_DROP,则直接丢弃该网络包;如果返回XDP_PASS[4],则由网卡驱动为该数据包分配SKB 数据结构,并提交内核协议栈做进一步的处理。用户空间的进程通过套接字等手段与协议栈交互,获取来自网卡的网络包数据。
通常所说的XDP 程序指的是BPF 程序,它是一个由C 语言子集编写的程序。
XDP 程序由源代码变为最终可执行的二进制代码需要以下两个步骤:
(1)预编译:通过低级虚拟机(Low Level Virtual Machine,LLVM)将C 语言源代码翻译成BPF 指令集,该指令集是一个通用目的精简指令集(Reduced Instruction Set Computer,RISC)。
(2)加载:通过内核中的即时编译器(Just In Time Compiler)将BPF 指令映射成最终处理器可以理解的原生指令。
XDP 程序与用户空间的应用程序之间只能使用Map 进行通信。Map 是由核心内核(core kernel)提供的驻留在内核空间的高效键值仓库(key/value store)[1],BPF 程序可以直接对其访问,而应用程序需要通过文件描述符(File Descriptor,FD)访问该数据。XDP 与应用程序之间的通信方式,如图2所示。
图2 XDP 与应用程序之间的通信方式
Map 可分为per-CPU 及non-per-CPU两种类型[1],也可以按照其组织数据的方式分为通用型和非通用型两种。经常使用的通用Map 有[5]:
(1)BPF_MAP_TYPE_HASH
(2)BPF_MAP_TYPE_ARRAY
(3)BPF_MAP_TYPE_PERCPU_HASH
(4)BPF_MAP_TYPE_PERCPU_ARRAY
(5)BPF_MAP_TYPE_LRU_HASH
(6)BPF_MAP_TYPE_LRU_PERCPU_HASH
新构架由Netfilter 命令分析和处理模块、XDP处理模块、XDP 后台守护进程、网卡驱动XDP 支持模块4 部分组成。整体构架如图3 所示,其中虚线为控制流,实线为网络数据包。
图3 整体框架结构及数据流
下面分别介绍每个模块需要完成的功能、所处的位置、实现方式以及模块间的关系。
Netfilter 命令分析和处理模块位于内核Netfilter子系统与用户进程进行交互的控制接口处,用于对来自用户进程的控制命令进行分流:如果操作的是FILTER 表的INPUT 或FORWARD 链(网络数据包目的地址为本机时触发INPUT 链,不是本机但需要本机转发时触发FORWARD 链[6]),则拦截该命令,并把用户实际的操作意图转换为XDP 规则,交由XDP 后台守护进程处理;如果操作的是其他表或者链,比如NAT 表、MINGLE 表、PREROUTING 链、POSTROUTING 链等,则放行,依旧由NetFilter 子系统进行处理。
XDP 处理模块是一个位于内核层的XDP 程序,它根据XDP 后台守护进程配置的规则对网络包进行分析和匹配,并根据对应规则决定后续处理方式。它由XDP 后台守护进程加载,通过Map 与后台进程进行交互。
XDP 后台守护进程是一个应用层程序,系统开机后自动运行,接收来自Netfilter 命令分析和处理模块的请求,根据请求的内容向指定网口安装或拆卸XDP 处理模块,并通过Map 向其添加、修改或删除规则。
网卡驱动XDP 支持模块属于内核层网卡驱动的一部分。该模块在从网卡硬件上接收到网络包之后,以及在分配SKB 之前按照Linux 内核提供的XDP 支持功能调用XDP 处理模块,并根据返回值决定后续处理方式。
整个构架所涉及的应用层、内核层软件开发和测试使用的硬件平台:CPU 为飞腾1500A(四核,主频1.5 GHz),内存为DDR3-800(4GB)。网口使用CPU 自带的千兆以太网控制器。操作系统版本为Linux-4.14.10。
本节只具体描述整个框架中最核心的Netfilter命令分析和处理模块和XDP 处理模块。
该模块从应用层传递到Linux 内核的参数中提取关键数据,然后根据现有的规则形成规则增量信息。整个过程如图4 所示。
图4 处理流程
下面对每一步如何从内核数据结构中提取相关信息进行说明,详细数据结构定义请参考内核源代码。
(1)提取Table 名称。用户层传递的数据结构为struct ipt_replace,其中name 字段保存的就是需要操作的Netfilter 表名。
(2)提取Chain 名称。因为不管是对Netfilter表的添加、删除还是修改,应用层都会把相应表中所有的Chain 及其包含的所有规则传递给内核层。因此,想要知道这次调用操作的是哪个Chain,就必须对比本次调用与上次调用哪些数据发生了变化。利用struct ipt_replace 中的valid_hooks 字段可以知道当前表中哪些Chain 是合法的及这些Chain在表中出现的顺序,再使用内核提供的xt_entry_foreach()宏遍历所有的struct ipt_entry(Netfilter 规则用该数据结构表示)进行比较。图5 展示了Filter 表中Chain的组织方式,需要注意的是每一个Chain 都是以一个全0的struct ipt_entry 结束。
图5 Filter 表中链的组织方式
(3)提取输入/输出接口名称。struct ipt_entry中的ip 字段保存了这些信息,它是个struct ipt_ip数据结构,其中iniface、outiface、iniface_mask、outiface_mask 字段指示接口信息。
(4)提取源/目的IP 地址。struct ipt_ip 数据结构中的src、dst、smsk、dmsk 字段保存这些信息。
(5)提取协议号。struct ipt_ip 数据结构中的proto 字段保存这些信息。
(6)提取Target信息。Target 表示的是规则匹配成功后的处理方式。使用ipt_get_target()函数可以获取规则对应的struct xt_entry_target 数据结构,其成员user 中的name 字段以字符串的形式表示这些信息。
(7)提取Match信息。如果规则涉及4 层协议,比如用户数据报协议(User Datagram Protocol,UDP)、传输控制协议(Transmission Control Protocol,TCP)等,那么struct ipt_entry 中的elems字段保存这些数据,然后使用xt_ematch_foreach()宏遍历所有struct xt_entry_match(Netfilter 表 示Match的数据结构)。该结构中user 成员的name字段用于指示Match的名称。不同的4 层协议,xt_entry_match 关联的数据(data 字段)并不一样,比如UDP,关联的数据结构是struct xt_udp,从中可以提取源/目的端口号等信息。
(8)计算规则增量信息。使用的方法与(2)中介绍的一样。增量信息包括添加、删除、修改3种类型。
4.2.1 数据结构定义
XDP 后台守护进程通过两个Map 与XDP 处理模块交互,一个用于向XDP 处理模块发送命令并接收响应(控制Map),另一个用于存储规则表(规则Map)。
控制Map的参数如表1 所示。通过索引值0 来访问256 Bytes的命令/应答空间。命令帧与响应帧的区分使用自定义帧头来实现。因为使用的是命令/应答方式,在接收到响应之前不会发送下一条命令,因此最大条目只需要一个。
表1 控制Map
规则Map的参数如表2 所示。
表2 规则Map
规则定义如表3 所示。其中,next 和prev 字段主要用于解决大业务量时,XDP 处理模块频繁读取规则表,而XDP 后台处理进程会对规则表进行修改的问题,比如,需要删除规则时,先修改后一条规则的prev 指向前一条规则,再修改前一条规则的next 字段为被删除规则的next。
表3 规则定义
4.2.2 数据接收和处理
XDP 所使用的网络包数据结构是struct xdp_md/xdp_buff,该结构只有data 和data_end[7]两个字段,分别用于指示网络包在连续内存空间中的起始和结束位置。XDP 处理模块通过分析这段空间提取网络包的ip 地址、协议、端口号等信息,然后在规则Map 中匹配规则,并根据规则的target 字段决定返回值。需要注意的是,遍历规则Map 是使用规则中的next 字段来完成的,prev 字段仅在修改规则表时使用。
另外,XDP 中网络包处理函数需要放在名称为“xdp”开头的“节”中,这样加载程序才能自动找到该函数。
网络防火墙在实际运行过程中会丢弃大量不合法的网络包,因此测试中使用以下命令添加一条对目的端口为5126的UDP 包进行丢弃的规则,来评估两种方式下每秒的丢包数(package per second,pps):
iptables -A INPUT -p udp --dport 5126 -j DROP[8]
测试结果如表4 所示。
表4 吞吐量对比测试结果
从表中的测试结果可以得出以下几个结论:
(1)随着包长的变大性能提升率逐渐降低,这是因为协议分析逻辑对大包和小包的处理时间基本相同(都只是分析位于网络包前端的协议头),而CPU的处理性能远低于千兆网口小包的理论传输速率,这时CPU 是性能瓶颈。当包长为1 500 Bytes时,CPU的处理性能已经超过了千兆网口大包传输的理论性能,这时千兆网口是性能瓶颈,网络包处理达到饱和。
(2)如果在实际运行过程中大量的网络包通过规则匹配后的结果是“放行”,那么本文设计的新框架不会对防火墙的吞吐率有很大的提升,这是因为不管是内核中的Netfilter 还是XDP 程序对网络包的分析原理基本上是一致的,并且分析后依旧是通过协议栈交给相应模块处理,在这种情况下,XDP 程序的优势有二:一是处理的包在线性空间中,而内核中使用SKB 描述的网络包可能是分段的;二是XDP 程序没有复杂的调用关系。
(3)如果不考虑对上层软件的兼容性等问题,可以考虑利用基于应用层的数据平面开发套件(Data Plane Development Kit,DPDK)来实现网络防火墙,也可以通过内存大页以及POLL 模式的网卡驱动能够进一步提高网络包处理性能[9]。
本文设计了一种不局限于某种特定处理器和Linux 操作系统的中低端防火墙框架,并对其原理、软件模块组成及与其他子系统之间的关系和实现要点做了详细介绍。本设计对所有基于内核Netfilter的防火墙软件完全透明,在实际部署过程中不要求上层软件进行相应适配,并且可以在不修改Linux内核的情况下对所支持的协议进行扩充和升级,具有较强的适应性和扩展性。通过与传统方式的吞吐量对比测试可以看出,目前该框架能在不增加硬件成本的情况下显著提高防火墙的性能,满足现有网络设备对高性能防火墙的需求。