Linux下状态检测防火墙的设计与实现
Linux下状态检测防火墙的设计与实现基于Linux Netfilter框架,使用C语言的内核态编程,实现了状态检测防火墙。
Linux下状态检测防火墙的设计与实现
本文改写自网络安全课程设计报告
全部代码见: Linux下状态检测防火墙的设计与实现.
1 实验目的和要求
1.1 实验目的
计算机网络安全是信息安全专业的一门核心课程,网络安全课程设计是计算机网络安全课程的一个综合实践环节。通过本课程设计要使学生达到以下目标:
(1)结合理论课程学习,深入理解计算机网络安全的基本原理与协议,巩固计算机网络安全基本理论知识;
(2)熟练掌握计算机网络编程方法,拓展学生的应用能力;
(3)加强对网络协议栈的理解;
(4)提高分析、设计软件系统以及编写文档的能力;
(5)培养团队合作能力。
1.2 实验要求
1.正确理解题意;
2.具有良好的编程规范和适当的注释;
3.有详细的文档,文档中应包括设计题目涉及的基础知识、设计思路、程序流程图、程序清单、开发中遇到的问题及解决方法、设计中待解决的问题及改进方向。
课程设计的防火墙系统包括两个部分,一个是防火墙内核模块,一个是应用程序。
内核模块用来截获通过防火墙的报文,进行规则过滤,根据规则来决定是禁止还是放行,若放行,是直接路由转发还是NAT之后转发,维护连接表。规则由应用程序通过跟内核的接口写入;
应用程序用来设置规则(向内核模块写入),查看日志(从内核模块获得,内核模块不要直接写文件,效率低),查看连接(从内核模块获得)等操作,需要跟内核模块之间通过一定的接口来进行数据交换。
(1) 系统运行
系统启动以后插入模块,防火墙以内核模块方式运行;
应用程序读取配置,向内核写入规则,报文到达,按照规则进行处理;
(2) 界面
采用图形或者命令行方式进行规则配置,界面友好;
(3) 功能要求
能对TCP、UDP、ICMP协议的报文进行状态分析和过滤
每一条 过滤规则至少包含:报文的源IP(带掩码的网络地址)、目的IP(带掩码的网络地址)、源端口、目的端口、协议、动作(禁止/允许),是否记录日志;
过滤规则可以进行添加、删除、保存,配置的规则能立即生效;
过滤日志可以查看;
具有NAT功能,转换地址分按接口地址转换和指定地址转换(能实现源或者目的地址转换的任一种即可);
能查看所有连接状态信息。
(4)测试
测试系统是否符合设计要求
系统运行稳定
性能分析
2 实验原理(简述)
2.1 状态检测技术
对于TCP连接,一次连接总以3次握手开始,以4次挥手或Reset报文结束,由于TCP协议是询问-应答类型的可靠数据传输协议,故TCP连接存在状态,对于同一连接的报文,只需对其握手报文进行规则匹配即可,若允许通过,则同一TCP连接的所有报文均可通过,无需遍历规则表。
这些状态信息可由状态检测表存储,状态检测表包括所有通过防火墙的连接,并维护所有连接。每条连接的信息包括源地址、目的地址、协议类型、协议相关信息(如TCP/UDP协议的端口、ICMP协议的ID号)、连接状态(如TCP连接状态)和超时时间(若进行NAT转换,还需记录转换前后地址端口信息)等,防火墙把这些信息叫做状态。
状态检测表通常由非线性数据结构,Hash表、树等构成,直接在内核执行匹配,速度快,有的防火墙采用硬件支持,速度更快。
通过状态检测,可实现比简单包过滤防火墙更大的安全性及更高的性能。
对于非TCP连接,防火墙为其建立虚拟连接。
UDP:一方发出UDP包后,如DNS请求,防火墙会将从目的地址和端口返回的、到达源地址源端口的包作为状态相关的包而允许通过。
ICMP:信息查询报文:如ping(echo)包,其状态相关包就是来自目的地址的echo reply包,而没有与echo包对应的echo reply包则认为是状态非法的。
这些连接在防火墙的状态检测表中的超时时间比较短
2.2 Linux Netfilter架构
Netfilter是Linux 2.4.x引入的一个子系统,它作为一个通用的、抽象的框架,提供一整套的hook函数的管理机制,使得诸如数据包过滤、网络地址转换(NAT)和基于协议类型的连接跟踪成为了可能。
Netfilter的架构就是在整个网络流程的若干位置放置了一些检测点(HOOK),而在每个检测点上登记了一些处理函数进行处理。
2.3 Linux字符设备驱动
Linux把设备和文件统一抽象为文件,Linux设备有字符设备、块设备和网络设备3种。Linux字符设备是一种按字节来访问的设备,字符驱动则负责驱动字符设备,这样的驱动通常实现open、close、read和write系统调用。例如:串口、Led、按键等。通过字符设备文件(/dev/xxx),应用程序可以使用相应的字符设备驱动来控制字符设备
Linux 操作系统和驱动程序运行在内核空间(比如:报文过滤的内核模块),应用程序(比如:配置规则的程序)运行在用户空间,两者不能简单地使用指针传递数据,因为Linux使用的虚拟内存机制,用户空间的数据可能被换出,当内核空间使用用户空间指针时,对应的数据可能不在内存中。
3 实验环境和采用的工具
操作系统:Ubuntu1604、Windows 10
Linux内核版本:4.8.0
编译环境:gcc5.4.0 Java1.8
IDE:Eclipse 3.1.8 for Ubuntu
4 系统设计
4.1 内核模块
内核模块由访问控制处理逻辑和字符设备驱动构成,访问控制处理逻辑负责对报文进行过滤分析,实现状态监测;字符设备驱动负责内核态与用户态的交互。
访问控制处理逻辑维护了4张表和1个链,其中规则表记录了规则信息,包含报文5元组、动作、是否记录日志等信息,日志表记录了最近匹配到的规则记录信息,Hash链和Hash表共同实现了状态检测,NAT表实现NAT地址转换。
其中Filter In和Filter Out 2个Hook函数维护了Hash链和Hash表,通过查询规则表的访问控制规则决定是否允许报文通过,同时更新Hash链和Hash表的状态信息,并且对匹配的规则记录日志。
DNAT和SNAT 2个Hook函数维护了NAT表,进行NAT地址转换,可动态添加删除NAT规则,也可根据用户态写入的静态NAT规则进行地址转换。
由于防火墙需要对超时的连接进行删除,故定时器中断处理函数将遍历Hash链,对连接进行时间控制,删除超时的连接。Hash链和Hash表为定时器中断处理函数与规则匹配Hook函数的临界资源,需要加入锁机制防止资源争用,避免死锁发生。
规则表、日志表、状态表、NAT表均可被用户态程序读取,其中用户态程序还可写入规则表和NAT表,故设置内核数据缓冲区实现与用户态的数据交互。用户态调用写函数时,先将写入的数据存入缓冲区,根据用户态的命令确定写入数据的类型,再将缓冲区数据拷贝到相应的数据结构。用户态调用读函数时,根据用户态程序的命令将相应数据结构的数据拷贝到缓冲区,再将数据送到用户态程序。其中涉及到状态表的读操作也属于临界资源的争用,也需要加入锁机制避免死锁的发生。
图4.1展示了内核模块的构成,箭头表示各模块间的数据流向或调用关系,其中内核数据缓冲区为字符设备驱动和访问控制处理模块之间交互的通道。规则表、Hash链、Hash表、NAT表等提供了状态检测防火墙重要数据结构的支持,SNAT、DNAT Hook函数实现了NAT地址转换,Filter In、Filter Out实现了报文过滤,状态监测,记录日志的主要功能,定时器实现了连接的时间控制和超时处理。
4.2 用户模块
用户模块由Java程序实现,完成了从文件读写规则,添加删除规则,从内核读取规则表,状态表,NAT表,日志表以及向内核写入规则等功能,根据用户输入的命令,完成相应的功能,并加入了错误检测机制,对错误输入不作处理。
用户态程序将内核模块的数据结构抽象成了类,各个类对象的成员变量记录的相应的信息,其中内核态Hash链和Hash表共同抽象成了Connection类,记录了连接的状态信息。
Rule类记录了规则信息,包含规则五元组,动作和是否记录日志的标志位,提供了多种构造函数供外部调用,提供print函数打印该对象的规则信息。
Connection类记录了连接状态信息,除五元组外还包含连接剩余时间,连接剩余时间记录了获得该条连接记录时的剩余时间;调用print函数将输出该连接的状态信息。
Log类记录了从内核态获取的日志信息,包含五元组和动作,可通过print函数输出日志信息。
Nat类记录了NAT地址转换信息,包含被转换的地址,被转换的端口和防火墙端口,由print函数输出NAT信息。
以上各类都聚合在Main类的List中,有Main类的各个静态函数调用,实现相应的功能。用户模块的类图由图4.2所示。
5 系统详细设计
5.1 关键数据结构
5.1.1 内核态
(1) 规则表
规则表为结构体struct Rule的数组,考虑到规则的快速匹配,故内核态规则表的IP地址全部采用二进制形式,由用户态实现点分十进制到二进制的转换,内核态直接进行二进制的IP地址匹配。
表5.1 Rule结构体定义
类型 | 变量名 | 描述 |
---|---|---|
unsigned | src_ip | 源地址 |
unsigned | dst_ip | 目的地址 |
unsigned | src_mask | 源地址的掩码 |
unsigned | dst_mask | 目的地址的掩码 |
int | src_port | 源端口 |
int | dst_port | 目的端口 |
int | protocol | 协议 |
int | action | 动作 |
int | log | 是否记录日志 |
(2) 连接表
Hash链表的节点由struct Connection定义,链表头为空节点conHead。Hash链表记录当前的所有连接信息,其中剩余时间由字节数组形式的Hash表hashTable记录。hashTable为1000001大小的字节数组,记录了所有连接的剩余时间,实现了Hash表的功能。Hash链上的节点中保存了当前连接的剩余时间在Hash表中的索引,遍历Hash链时可通过索引访问到当前连接的剩余时间。
表5.2 Connection结构体定义
类型 | 变量名 | 描述 |
---|---|---|
unsigned | src_ip | 源地址 |
unsigned | dst_ip | 目的地址 |
int | src_port | 源端口 |
int | dst_port | 目的端口 |
int | protocol | 协议 |
int | indx | 索引 |
Hash链和Hash表的关系如图5.1所示,Hash链的每个节点记录了连接的五元组信息和连接剩余时间在Hash表中的索引,通过索引可以在Hash表中获得连接的剩余时间,若剩余时间为0,则连接超时,删除Hash链中的节点,索引值即为五元组的Hash值。
(3) NAT表
NAT表为结构体Nat类型的数组,记录了被地址转换的地址,被转换的端口和转换后的端口,struct Nat的结构如表5.3所示。
表5.3 Nat结构体定义
类型 | 变量名 | 描述 |
---|---|---|
unsigned | nat_ip | 被转换的地址 |
unsigned short | firewall_port | 转换后的端口 |
unsigned short | nat_port | 被转换的端口 |
(4) 日志表
日志表记录了匹配到规则的五元组和动作等信息,由结构体Log类型的数组存储内核态日志记录,struct Log的结构如表5.4所示。
表5.4 Log结构体定义
类型 | 变量名 | 描述 |
---|---|---|
unsigned | src_ip | 源地址 |
unsigned | dst_ip | 目的地址 |
int | src_port | 源端口 |
int | dst_port | 目的端口 |
int | protocol | 协议 |
int | action | 动作 |
5.1.2 用户态
表5.5 Main类
类名: Main
描述 :实现用户态程序功能,是程序运行的主类,实现了用户态与内核态的交互,规则和日志的读取和保存,提供了用户交互接口,执行用户输入的命令。
变量:
类型 | 名称 | 描述 |
---|---|---|
static List | ruleList | 规则列表 |
static List | connectionList | 状态列表 |
static List | natList | NAT列表 |
static List | logList | 日志列表 |
static String | DEV_NAME | 设备名 |
方法 :
返回值 | 名称 | 参数 | 描述 |
---|---|---|---|
static void | getRules | 无 | 从文件中获取规则,导入到ruleList中 |
static void | getConnection | 无 | 从内核态获取当前连接信息,导入到connectionList中 |
static void | getLogs | 无 | 从内核态获取当前日志信息,导入到logList中 |
static void | getNat | 无 | 从内核态获取当前NAT信息,导入到natList中 |
static void | commitRules | 无 | 将当前ruleList中的规则写入到内核并保存到文件中 |
static void | commitNat | 无 | 将当前natList中的规则写入到内核 |
static void | addRule | String[] | 添加一条规则 |
static void | delRule | String[] | 删除一条规则 |
static void | addNat | String[] | 添加一条Nat规则 |
static void | delNat | String[] | 删除一条Nat规则 |
static void | main | String[] | 主函数,读取用户输入,执行相应的命令 |
表5.6 Util类
类名:Util
描述:工具类,提供了必需的数据类型转换接口
方法:
返回值 | 名称 | 参数 | 描述 |
---|---|---|---|
static void | intTobyte | byte[] b, int x, int offset | 将int类型的值x以小端字节序的形式逐字节写到字符数组b偏移量offset处 |
static int | byteToint | byte[] b, int offset | 将字符数组b偏移量offset处的4个字节转为int型变量 |
static int | strToIP | String | 将IP地址点分十进制形式转为int类型 |
static String | ipToString | int | 将int类型的IP地址转为点分十进制字符串 |
表5.7 Rule类
类名: Rule
描述 :规则类,保存了规则信息,提供生成和打印规则的接口
变量:
类型 | 名称 | 描述 |
---|---|---|
static int | TCP | TCP协议号 |
static int | UDP | UDP协议号 |
static int | ICMP | ICMP协议号 |
static int | ANY | 任意端口对应的端口号,为-1 |
static int | ICMP_PORT | 假定ICMP的端口号,为65530 |
static int | RULE_SIZE | 一条规则的大小,为36 |
int | src_ip | 源地址 |
int | dst_ip | 目的地址 |
int | src_mask | 源地址掩码 |
int | dst_mask | 目的地址掩码 |
int | src_port | 源端口 |
int | dst_port | 目的端口 |
int | protocol | 协议 |
int | action | 动作 |
int | log | 是否记录日志 |
方法:
返回值 | 名称 | 参数 | 描述 |
---|---|---|---|
void | Rule | byte[] bs | 构造函数,以字符数组初始化对象,用于从内核态读取规则时构造 |
void | Rule | String sip, int smask, String dip, int dmask, int sp, int dp, int ptl, boolean act, boolean logged | 构造函数,测试用 |
void | Rule | String sip, String smask, String dip, String dmask, String sp, String dp, String ptl, String act,String logged | 构造函数,以字符串初始化对象,用于从用户添加规则时构造 |
void | 无 | 打印当前规则 | |
byte[] | getStruct | 无 | 将当前规则转为字符数组 |
表5.8 Connection类
类名 :Connection
描述: 连接类,保存了连接信息,提供生成和打印连接信息的接口
变量 :
类型 | 名称 | 描述 |
---|---|---|
static int | CONNECTION_SIZE | 一条连接记录的大小,为24 |
int | src_ip | 源地址 |
int | dst_ip | 目的地址 |
int | src_port | 源端口 |
int | dst_port | 目的端口 |
int | protocol | 协议 |
int | time | 剩余时间 |
方法 :
返回值 | 名称 | 参数 | 描述 |
---|---|---|---|
void | Connection | byte[] data, int offset | 构造函数,以字符数组初始化对象,用于从内核态读取连接时构造 |
void | 无 | 打印当前连接 |
表5.9 Nat类
类名: Nat
描述 :NAT类,保存了NAT信息,提供生成和打印NAT信息的接口
变量 :
类型 | 名称 | 描述 |
---|---|---|
static int | NAT_SIZE | 一条NAT记录的大小,为12 |
int | nat_ip | 被转换的地址 |
int | firewall_port | 转换后的端口 |
int | nat_port | 被转换的端口 |
方法 :
返回值 | 名称 | 参数 | 描述 |
---|---|---|---|
void | Nat | byte[] data, int offset | 构造函数,以字符数组初始化对象,用于从内核态读取Nat记录时构造 |
void | 无 | 打印当前Nat |
表5.10 Log类
类名: Log
描述 :日志类,保存了日志信息,提供生成和打印日志信息的接口
变量 :
类型 | 名称 | 描述 |
---|---|---|
static | int | LOG_SIZE |
int | src_ip | 源地址 |
int | dst_ip | 目的地址 |
int | src_port | 源端口 |
int | dst_port | 目的端口 |
int | protocol | 协议 |
int | action | 动作 |
方法:
返回值 | 名称 | 参数 | 描述 |
---|---|---|---|
void | Log | byte[] data, int offset | 构造函数,以字符数组初始化对象,用于从内核态读取日志时构造 |
void | 无 | 打印当前日志 |
5.2 关键模块的流程设计
5.2.1 报文过滤函数
报文过滤函数是实现状态检测防火墙功能的主要函数,在PRE_ROUTING和POST_ROUTING检查点均有Hook,对网卡收到的报文进行检查,判断是否允许报文通过。
首先获得报文五元组的Hash值,其Hash值作为索引在Hash表中查询,由于Hash表中保存的是连接的剩余时间,若索引得到的时间值不为0,则为已存在的连接,报文通过;若索引到的时间值为0,则不存在连接或连接已超时,则开始进行逐条规则匹配。
匹配规则时首先判断报文的源地址和目的地址是否在规则的范围内,再判断协议,然后判断端口,若全部满足,则规则匹配成功,根据规则的logged标志位判断是否记录日志,根据规则的action标志位判断是否允许报文通过。若action为1,则报文通过,同时将连接信息加入Hash链表,并在对应的Hash表上设置超时时间;若action为0,则禁止报文通过。
若所有规则都不匹配,则根据default标志位进行默认规则处理,若default标志位为1,则默认通过,将连接信息加入到连接表中;若default标志位为0,则默认禁止。default标志位可由用户手动设置,默认为1。
报文过滤函数流程图如图5.2所示。
图5.2 报文过滤函数流程图
5.2.2 状态检测
当接收到一个报文,首先获得报文五元组信息,将报文的源端口、目的端口和协议转为int类型变量,与源地址和目的地址异或,得到报文特征码。将特征码通过Hash函数获得Hash值,Hash值即为该连接在Hash表中的索引。
为减小碰撞概率,考虑到实际情况下的连接数,同时考虑存储空间的开销,Hash表采用10000001大小的字节数组,只存储连接剩余时间,而避免将连接所有信息存入Hash表。
连接的五元组信息保存在另一个链表中,链表的每个节点保存连接的五元组和连接的Hash值,即连接剩余时间在Hash表中的索引。当新建连接时,在Hash表对应位置初始化剩余时间,默认时间为60秒,同时采用头插法方式将连接节点插入到链表中,并初始化连接信息。
Hash函数定义如下,参数为连接的特征码,返回连接在Hash表中的索引。
static unsigned getHash(int k)
{
unsigned a, b, c=4;
a = b = 0x9e3779b9;
a+=k;
a -= b; a -= c; a ^= (c>>13); b -= c; b -= a; b ^= (a<<8); c -= a; c -= b; c ^= (b>>13);
a -= b; a -= c; a ^= (c>>12); b -= c; b -= a; b ^= (a<<16); c -= a; c -= b; c ^= (b>>5);
a -= b; a -= c; a ^= (c>>3); b -= c; b -= a; b ^= (a<<10); c -= a; c -= b; c ^= (b>>15);
return c%HASH_SIZE;
}
5.2.3 时间控制
当接收到一个报文,经过Hash后判断其为已存在的连接,防火墙将根据Hash值找到Hash表中对应的位置,重新设定剩余时间,默认为60秒。
考虑到超时连接需要删除,防火墙引入了定时器机制,通过定时器中断函数判断连接是否超时,对超时的连接进行删除。
由于Hash表为状态检测与中断处理争用的临界资源,故进入定时器中断函数后,首先等待临界资源解锁,并立即上锁,遍历Hash链;根据每个节点保存的Hash值,在Hash表找到对应的超时时间进行自减,若超时时间减为0,则连接超时,删除该节点并继续遍历。
默认定时器中断时间为1秒,即防火墙每秒将遍历一次Hash链进行超时检测,中断处理函数结束后将重新设定中断时间为1秒,等待下次中断。
5.2.4 NAT地址转换
防火墙提供NAT地址转换功能,将对内网与外网通信的报文进行地址转换。当防火墙收到一个报文时,首先判断该报文的源地址是否为内网地址或目的地址是否是防火墙外网地址。若报文源地址是内网地址,则进行SNAT地址转换;若报文目的地址为防火墙外网地址,则进行DNAT地址转换。
进行SNAT地址转换时,首先遍历NAT表查询有无此条NAT地址转换记录,只要NAT记录的源地址和被转换的端口与报文的源地址和源端口相同,则存在此条NAT记录。若存在NAT记录,则根据NAT地址转换记录将报文的源地址替换为防火墙外网地址,源端口设置为NAT记录的源端口,重新计算网络层和运输层校验和。若不存在NAT转换记录,则动态添加一条记录,防火墙端口将递增。
进行DNAT地址转换时,遍历NAT记录,若报文的目的地址为防火墙外网地址且目的端口为NAT记录保存的防火墙端口,则存在此条NAT记录。若存在NAT记录,则将报文的目的地址替换为NAT记录的目的地址,将报文的目的端口替换为NAT记录的目的端口,重新计算校验和并允许报文通过。
图5.3 NAT转换
5.2.5 字符设备驱动
为实现内核态与用户态的交互,防火墙提供了字符设备读写接口,用户态程序可将防火墙视为普通文件进行读写。进行读写操作时,总是由用户态先写入数据,防火墙根据写入数据的最后一个字节,判定用户态的命令。若为0x00则为用户态写入规则,若为0x01则为用户态读取连接表,若为0x02则为用户态读取日志表,若为0x03则为用户态读取NAT表,若为0x04则为用户态写入NAT表。
内核态设置全局缓冲区和全局标志位,用户态调用写函数后,将根据写入数据的最后一个字节为全局标志位赋值。当用户态调用读函数时,内核态根据全局标志位决定将哪些数据送入用户态。
字符设备驱动逻辑如图5.4所示。
图5.4 字符设备驱动
5.2.6 用户态程序
用户态程序由Java实现,程序运行时首先从文件读取规则导入到ruleList中,同时写入到内核中,进入消息处理循环,每次处理一条用户命令,用户命令定义如表5.11所示。
表5.11 用户命令定义
命令 | 格式 | 描述 |
---|---|---|
rules | ||
connection | 从内核态读取并打印连接表 | |
logs | 从内核态读取并打印日志表 | |
nat | 从内核态读取并打印NAT表 | |
add | add id sip dip smask dmask sp dp ptl act log | 添加一条规则,放在第id位 |
delete | delete id | 删除第id条规则 |
commit | commit | 向内核写入规则并保存到文件 |
default | default 0 | 向内核写入默认规则 |
exit | exit | 退出程序 |
6 系统测试
6.1 测试环境
实验使用2台Ubuntu1604虚拟机和Windows物理机,其中一台虚拟机为防火墙,另一台为内网主机,Windows物理机为外网主机,网络拓扑图如图6.1所示。
6.2 功能测试
6.2.1 规则过滤
防火墙启动时设置了2条静态规则,其中第2条为禁止192.168.100.0/24网段与221.159.191.18所有TCP端口的通信。221.159.191.18为sohu.com经DNS解析后的地址,执行命令curl sohu.com后,发现无法获得任何报文,同时防火墙打印了日志,匹配到该规则并进行阻断,如图6.2所示。
第1条规则为禁止任何IP源地址、源端口到202.114.0.245地址80端口的TCP通信,202.114.0.245为hust.edu.cn经DNS解析后的地址,执行命令curl hust.edu.cn后,发现无法获得报文,防火墙打印了日志信息,而执行curl https://www.hust.edu.cn后,发现可以获得HTML报文,表明防火墙只阻断了80端口,而未阻断443端口的TCP通信。
6.2.2 规则的添加、删除、查看、保存
执行用户态程序,首先读取文件,将规则导入内核中,内核同时更新规则。
执行添加规则命令,添加一条规则,add 1 192.168.100.0 255.255.255.0 222.20.94.23 255.255.255.255 any 2238 TCP deny loged,即禁止源地址为192.168.100.0/24到222.20.94.23目的端口为2238的TCP通信,将此条规则添加到规则表的第1条,添加此条规则并写入到内核态。
已知222.20.94.23的2238端口提供了ssh服务,在终端输入命令ssh test@222.20.94.23 -p 2238后,发现无法建立ssh连接,防火墙阻断该连接并打印了日志信息。
删除刚才添加的规则,在用户态执行delete 1,删除规则,再次提交并重新建立ssh连接,发现可以连接上。
查看规则只需输入命令print rules,保存规则可输入命令commit,上图已经演示。
6.2.3 默认策略设置
用户态执行default 0,将设置内核态默认动作为禁止,若没有规则匹配,防火墙将禁止报文通过。设置默认规则后,在终端执行命令ping 222.20.94.23,发现无法ping通,防火墙打印匹配默认规则的日志。
在防火墙主机上用tcpdump抓取外网网卡的报文,发现进行了NAT转换。
在防火墙主机上用tcpdump抓取内网网卡的报文。
7 心得体会
7.1 遇到的问题
(1) Makefile文件格式不明确
在最初尝试编译内核时,仿照指导书上的Makefile文件自行编写,发现系统报命令错误。经研究发现指导书上的Makefile是基于Linux2.4内核版本的,而测试系统为Linux4.8.0,文件格式已有了较大改变。在查阅一些资料后重新编写了Makefile文件,实现了内核代码的编译。
(2) 字符设备驱动读写的问题
在安装好字符设备驱动后,尝试进行内核读写,发现每次运行read函数总会无限读取数据,审查代码后发现是内核态的read函数总返回0,而不是返回读取的字节数,修改为返回实际读取的字节数,程序可正常运行。
(3) 大小端字节序的问题
此问题是贯彻整个课程设计的问题,在多个阶段均有遇到,Hook函数捕获的报文头部均为大端字节序,而内核态变量为小端字节序,起初没有考虑到这个问题,导致打印出的IP地址和端口等信息全为乱序。后来特别注意这个问题,但在用户态处理数据,向内核态发送规则,打印IP地址的点分十进制时又遇到了此问题,最终通过反复审查代码解决了字节序的问题。
(4) 字节对齐的问题
由于用户态采用了Java程序,起初不知道Java的InputStream和OutStream类读写的数据为4字节对齐,故用户态与内核态进行数据交互时总是读取到错误的数据。后来将内核态与用户态数据结构保持一致,无论地址、掩码、端口、动作、标志位统一使用4字节的int型或unsigned型,使得内核态与用户态的数据始终4字节对齐,避免数据交互时出现错误。
(5) 定时器只定时一次的问题
对Linux内核定时器机制不了解,不知道执行完定时中断函数后操作系统会将定时处理函数从事务处理链表中摘下,查阅相关资料后修改了中断函数的代码,在执行完中断逻辑后再次将定时器加到事务处理链表中。
(6) NAT转换的问题
起初没考虑到外网发来的包和内网发来的包都会进入DNAT函数,以及从防火墙发出去的包都会进入SNAT函数,导致所有的报文都进行NAT转换,防火墙和内网主机均无法收到报文。后来对报文的地址进行了判断,仅对源地址为内网地址或目的地址为防火墙外网地址的报文进行NAT转换。
(7) 内核崩溃死机的问题
由于内核态的规则表,日志表,NAT表等都为固定大小,且连接表的处理涉及到链表操作,导致经常出现数组越界,野指针,空指针等问题。内核经常内存泄露以致死机,反复审查代码,优化代码逻辑可解决此问题。
7.2 总结收获
本次课程设计实现了一个状态检测防火墙,能够对同一连接的报文直接放行,对新建连接进行规则匹配,同时支持NAT地址转换,超时处理,记录日志以及与用户态交互等功能,基本实现了一个较为完善的状态检测防火墙。
在实验中需要进行Linux内核编程,由于之前对Linux内核编程经验不足,我前期查阅了大量的资料了解Linux内核编程,对Makefile文件格式有了深入的了解,并尝试以字符设备驱动的方式进行内核态与用户态的交互,积累了内核编程的经验。
由于前期思路不明确,我在规则匹配的阶段走了不少弯路,起初的内核态规则结构体使用字符串存储IP地址,但发现这种方式需要在内核态处理字符串,不仅效率不高,占用空间较大而且容易出错。于是在中途重构了代码,全部采用二进制方式进行匹配,在用户态实现点分十进制到整数的转换,方便了用户态与内核态的交互同时提高了内核态处理的效率。
在构建连接表数据结构时我遇到了较大的问题,不知道如何运用Hash的方式提高效率并支持超时删除,跟同学经过了细致的讨论后决定采用链表和数组结合的方式实现连接表,既保证了Hash的快速访问,又支持超时连接的删除。
实验过程中每增加一些新功能,特别是构建连接表和实现内核态与用户态交互时,虚拟机经常崩溃死机,有时花费几个小时也找不到问题所在,导致心态一度崩溃,在仔细审查代码,不断尝试修改错误后,终于使得内核程序得以正常运行。
希望能够吸取以上经验教训,在以后的学习中不断提升自我。
更多推荐
所有评论(0)