一、研究背景

1.1项目背景

     防火墙是网络安全的重要组成部分,用于保护网络免受未经授权的访问和攻击。随着网络技术的不断发展,防火墙的设计和实现也需要不断更新和完善。本报告中,我们小组的成员将利用netfilter编程实现一网络防火墙,让其具有基本功能和一定的操作界面。

1.2 防火墙介绍

     防火墙的发展历程如下:

图1  防火墙的发展历程

     防火墙的主要技术:包过滤、代理、状态检测,现在最常见的防火墙为状态检测防火墙。

     包过滤防火墙:包过滤是指在网络层对每一个数据包进行检查,根据配置的安全策略转发或丢弃数据包。其基本原理是,通过配置访问控制列表(ACL)实施数据包的过滤,主要基于数据包中的五元组,即源目IP源目端口和协议号。因为是逐包过滤,随着ACL复杂程度和长度的增加,过滤性能明显下降;且静态ACL规则难以适应动态的安全需求;未对会话状态和数据进行检查和分析,容易被绕过;无法适用多通道协议,如FTP协议。

     代理防火墙:代理服务作用于网络的应用层,代理防火墙其实就是将内部用户于外部服务彻底隔离开,通过自身进行数据转发。但其处理速度慢,容易收到DOS或者DDOS攻击;需要针对每一种协议开发应用层代理,开发周期长,升级困难;所以除了重要的如财务等部门需要部署代理技术的网闸以外,代理防火墙已经被彻底淘汰。

     状态检测防火墙:是基于包过滤技术实现的,但状态检测防火墙考虑前后报文的历史相关性,当检测到第一个数据包符合安全策略后,该数据包的后续数据包都直接转发放行;其在网络层截获数据包,然后从各应用层提取出安全策略所需要的状态信息,并保存到会话表中,通过分析这些会话表和与该数据包有关的后续连接请求来做出恰当决定。只对首包进行ACL检测,后续包无需再进行ACL检查,只根据会话表决定丢弃还是转发,提高了传输效率,且安全性较高,连接状态清单是动态管理的。会话完成后防火墙上所创建的临时返回报文入口随即关闭,保障了内部网络的实时安全。

二、系统设计

1.设计目标

1.1安全性

(1)防护能力:指防火墙能否拦截网络攻击,防止病毒、木马等恶意程序传播,预防网络被攻击和数据泄露。这也包括抵御攻击的种类和数量,特别是对DOS和DDOS攻击的抵抗力。

(2)访问控制:指防火墙的访问控制能力,能够实现对网络访问的控制,以便实现可信的网络安全。也是防火墙的核心功能。这涉及到控制细度,例如能够控制哪些内容,如地址、协议、端口、时间、用户、命令、附件等。同时,控制强度也很关键,即应限制的内容必须全部阻断,应通过的内容不应有任何阻断。

(3)安全评估:指对系统安全性进行评估的能力,以便了解系统的安全性状况,并及时排查隐患。

(4)安全审计:指能够从日志中提取系统安全事件的能力,以便及时发现安全问题,提出有效的解决办法。

(5)自身安全性:指防火墙系统的健壮性,即防火墙本身应难以被攻入。此外,防火墙的管理方式也很重要,如采用telnet还是web方式管理,是否加密和认证等。

1.2扩展性

(1)硬件的可扩展性:随着对网络性能和安全性的需求的增加,需要对防火墙的硬件进行升级的情况也会出现。具有良好扩展性的防火墙应具备对硬件的扩展能力,如增加处理器、内存、存储等,以适应业务的发展。

(2)功能的可扩展性:随着网络安全威胁的不断演变,可能需要更多的安全功能来提高网络安全性。因此,防火墙应具备功能的扩展能力,如支持入侵检测、病毒防护、内容过滤等功能的扩展。

(3)连接的可扩展性:随着网络规模的扩大,防火墙需要支持更多的网络连接。因此,具有良好扩展性的防火墙应具备连接的扩展能力,如支持更多的网络接口和更多的VPN隧道等。

(4)管理策略的可扩展性:随着企业网络架构的复杂化,防火墙的管理策略也需要更加精细和全面。具有良好扩展性的防火墙应支持管理策略的扩展,如支持基于用户、应用、时间等维度的管理策略。

(5)随着云计算技术的发展,越来越多的企业将业务迁移到云端。因此,具有良好扩展性的防火墙应支持云端服务的扩展,如与各大云服务商的集成和安全防护等。

1.3可靠性

(1)可靠性:指防火墙本身可靠性,如软件和硬件的可靠性,能够抵抗外界的攻击,确保系统的正常运行。且防火墙应具备长时间稳定运行的能力,无频繁的重启、死机现象,并能够应对各种网络异常流量和病毒攻击。

(2)负载能力:指防火墙在处理多个网络连接的能力,保证高效的网络访问。防火墙应具备高效的数据处理能力,能够应对大流量的网络数据,不会造成网络的拥堵。

(3)数据传输安全:防火墙应能够确保数据传输的安全,防止数据被非法窃取或篡改。

(4)易用性:防火墙应具备简单易用的管理界面,方便管理员进行配置和管理。

(5)高可用性:指防火墙能够在故障时自动恢复,以保证长时间的网络运行。

2.系统设计

2.1系统功能

     我们的防火墙代码主要有3部分,头文件代码(myfw.h)、内核层代码(myfw.c)和用户层代码(myfwctl.c),另外还有一个Makefile工程文件,用于将这3个代码编译并生成内核模块,之后加载该模块,就可以运行用户层代码,进行防火墙的测试了。

(1)头文件代码(myfw.h)

#define CMD_MIN		0x6000//十进制:24576
 
#define CMD_DEBUG	    CMD_MIN+1
#define CMD_RULE	    CMD_MIN+2
#define CMD_RULE_DEL    CMD_MIN+3
 
#define CMD_MAX		0x6100//十进制:24832
 
#define MYFW_ICMP   1  //IPPROTO_ICMP
#define MYFW_TCP    6  //IPPROTO_TCP
#define MYFW_UDP    17 //IPPROTO_UDP
 
typedef struct{
    unsigned int sip;//信息来源ip地址
    unsigned int dip;//信息目的ip地址
    unsigned short sport;//信息的发出端口
    unsigned short dport;//发送信息的目的端口
    unsigned short protocol;//使用的协议
    unsigned short allow;//是否接收信息
}Rule;
 
typedef struct{
    unsigned int count;//表示规则的个数
    Rule rule;//保存规则
}RuleTable;

(2)内核层代码(myfw.c)

      这段代码描述了一个Linux内核模块,用于实现网络过滤和防火墙功能。该模块定义了五个钩子点:local_in、local_out、pre_routing、forward和post_routing,用于在不同阶段对数据包进行处理。同时,它还定义了一个规则集,用于匹配和控制数据包的传输。

      该模块提供了一些函数,用于操作规则集,如添加规则、删除规则和匹配规则。它还提供了一些钩子函数,用于处理数据包。例如,在本地输入阶段,它会调用hookLocalIn函数来检查数据包是否满足规则集中的条件。如果满足条件,则允许数据包通过;否则,丢弃数据包。

     此外,该模块还提供了一些用户空间接口,用于设置和获取规则集以及调试等级。例如,用户可以通过setsockopt系统调用来设置调试等级,或者通过getsockopt系统调用来获取当前的规则集。

涉及到的数据结构:

Rule:规则集,用于存储防火墙的规则。每个规则包含源IP、目标IP、协议类型、源端口、目标端口和允许/禁止标志等字段。

nf_hook_ops:钩子操作结构体,用于注册和注销网络过滤钩子。

struct nf_hook_state:网络过滤钩子状态结构体,用于保存钩子处理过程中的状态信息。

addRule():添加规则的函数,将新规则添加到规则集的开头,并更新规则集的大小。

delRule():删除规则的函数,根据输入的规则编号删除对应的规则。

matchRule():匹配规则的函数,遍历规则集,判断数据包是否满足某个规则的条件。

hookLocalIn、hookLocalOut、hookPreRouting、hookForward、hookPostRouting:五个钩子函数,分别对应本地输入、本地输出、路由前、转发和路由后阶段的数据包处理。这些函数在相应的阶段被调用,对数据包进行处理。

netfilter五个钩子点在协议栈的位置如下:

  图2 netfilter五个钩子点在协议栈的位置

该部分的算法流程图如下:

图3 内核层算法流程图

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/skbuff.h>//包含sk_buff结构
#include <net/tcp.h>
#include <linux/netdevice.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
 
#include "myfw.h"
//内核层的代码
 
//使用静态变量更适合与模块化编程,使用static进行定义的结构或变量只能在本文件中使用
//定义detfilter的5个钩子点:
static struct nf_hook_ops nfhoLocalIn;
static struct nf_hook_ops nfhoLocalOut;
static struct nf_hook_ops nfhoPreRouting;
static struct nf_hook_ops nfhoForward;
static struct nf_hook_ops nfhoPostRouting;
//创建套接字选项,与用户层通信
static struct nf_sockopt_ops nfhoSockopt;
 
static int debug_level = 0;
static int nfcount = 0;
 
static Rule* g_rules;//规则集,用指针形式来记录可以更省空间
static int g_rules_count = 0;//用来记录规则的个数
 
//------函数声明------
void addRule(Rule* rule);//增加规则的函数
void delRule(int rule_num);//删除规则,输入的参数表示要删除的规则的编号
int matchRule(void* skb);//进行规则比较的函数,判断是否能进行通信
void debugInfo(char* msg);//记录操作次数,将每次的操作信息输出到日志
//sk_buff就是传入的数据包,*skb
unsigned int hookLocalIn(void* priv, struct sk_buff* skb, const struct nf_hook_state* state);
unsigned int hookLocalOut(void* priv, struct sk_buff* skb, const struct nf_hook_state* state);
unsigned int hookPreRouting(void* priv, struct sk_buff* skb, const struct nf_hook_state* state);
unsigned int hookPostRouting(void* priv, struct sk_buff* skb, const struct nf_hook_state* state);
unsigned int hookForward(void* priv, struct sk_buff* skb, const struct nf_hook_state* state);
//用于接收用户端数据的函数
int hookSockoptSet(struct sock* sock, int cmd, void __user* user, unsigned int len);
//用与将数据传到用户层的函数
int hookSockoptGet(struct sock* sock, int cmd, void __user* user, int* len);
int init_module();//内核模块初始化,初始化五个钩子(进行钩子的注册)
void cleanup_module();//将钩子注销
 
//------函数实现------
void addRule(Rule* rule)//增加规则的函数
{
	//这一个函数是先将要添加的规则放入新的规则集中,再把原来的规则集放到新的规则集中
	//所以每次添加规则时,添加的规则都会放到第一位
	int r_c = g_rules_count + 1;//将规则个数+1
	Rule* g_r = (Rule*)vmalloc(r_c * sizeof(Rule));//开辟相应大小的空间
	memcpy(g_r, rule, sizeof(Rule));//将要增加的规则放到新开辟的规则集中
 
	if (g_rules_count > 0){//如果原规则集中有规则,则先将原来的规则集赋值给新规则集
		memcpy(g_r + 1, g_rules, g_rules_count * sizeof(Rule));
		vfree(g_rules);//回收之前的rule,释放空间
	}
 
	g_rules = g_r;//将新的规则集赋值给全局规则集
	g_rules_count = r_c;//更新规则集中规则的数量
}
 
void delRule(int rule_num)//删除规则,输入的参数表示要删除的规则的编号
{
	int i;
	if (rule_num > 0 && rule_num <= g_rules_count){//如果输入的规则编号有效,则进行删除
		for (i = rule_num; i < g_rules_count; i++){
			//把要删除的规则的位置之后的规则都往前移一个位置,将要删除的规则覆盖掉
			memcpy(g_rules + i - 1, g_rules + i, sizeof(Rule));
		}
		g_rules_count--;//最后一个空间闲着,不用管
	}
}
 
int matchRule(void* skb)//进行规则比较的函数,判断是否能进行通信
{
	//增加了端口控制的规则匹配
	int sport = 0;
	int dport = 0;
	struct iphdr* iph = ip_hdr(skb);
	struct tcphdr* tcph;
	struct udphdr* udph;
	int act = 1, i;
	Rule* r;
	for (i = 0; i < g_rules_count; i++){//遍历规则集
		r = g_rules + i;//用r来遍历
		if ((!r->sip || r->sip == iph->saddr) &&
			(!r->dip || r->dip == iph->daddr) &&
			(!r->protocol || r->protocol == iph->protocol)){
			switch (iph->protocol){//对协议类型进行判断进行判断
			case MYFW_TCP:
				tcph = (struct tcphdr*)skb_transport_header(skb);
				sport = tcph->source;
				dport = tcph->dest;
				break;
			case MYFW_UDP:
				udph = (struct udphdr*)skb_transport_header(skb);
				sport = udph->source;
				dport = udph->dest;
				break;
			}
			if ((!r->sport || !sport || r->sport == sport) &&
				(!r->dport || !dport || r->dport == dport)){
				act = r->allow;
			}
		}
	}
	return act;
}
 
void debugInfo(char* msg)//记录操作次数,将每次的操作信息输出到日志
{
	if (debug_level){//如果等级符合要求,才进行+1和输出到日志
		nfcount++;
		printk("%s, nfcount: %d\n", msg, nfcount);
	}
}
 
unsigned int hookLocalIn(void* priv,
	struct sk_buff* skb,//sk_buff就是传入的数据包,*skb
	const struct nf_hook_state* state)
{
	unsigned rc = NF_ACCEPT;//默认继续传递,保持和原来输出的一致
 
	if (matchRule(skb) <= 0)//查规则集,如果返回值<=0,那么不允许进行通信
		rc = NF_DROP;//丢弃包,不再继续传递
 
	debugInfo("hookLocalIn");
 
	return rc;//返回是是否允许通信,是否丢包
}
 
unsigned int hookLocalOut(void* priv,
	struct sk_buff* skb,
	const struct nf_hook_state* state)
{
	debugInfo("hookLocalOut");
	return NF_ACCEPT;//接收该数据
}
 
unsigned int hookPreRouting(void* priv,
	struct sk_buff* skb,
	const struct nf_hook_state* state)
{
	debugInfo("hookPreRouting");
	return NF_ACCEPT;//接收该数据
}
 
unsigned int hookPostRouting(void* priv,
	struct sk_buff* skb,
	const struct nf_hook_state* state)
{
	debugInfo("hookPostRouting");
	return NF_ACCEPT;//接收该数据
}
 
unsigned int hookForward(void* priv,
	struct sk_buff* skb,
	const struct nf_hook_state* state)
{
	debugInfo("hookForwarding");
	return NF_ACCEPT;//接收该数据
}
 
int hookSockoptSet(struct sock* sock,
	int cmd,
	void __user* user,
	unsigned int len)//用于接收用户端数据的函数
{
	int ret = 0;
	Rule r;
	int r_num;
 
	debugInfo("hookSockoptSet");
 
	switch (cmd){
	case CMD_DEBUG:
		//copy_from_user函数的作用:用于将用户空间的数据拷贝到内核空间
		ret = copy_from_user(&debug_level, user, sizeof(debug_level));
		printk("set debug level to %d", debug_level);//设置debug等级
		break;
	case CMD_RULE:
		ret = copy_from_user(&r, user, sizeof(Rule));//如果是添加规则
		addRule(&r);
		printk("add rule");//输出到日志
		break;
	case CMD_RULE_DEL:
		ret = copy_from_user(&r_num, user, sizeof(r_num));//删除规则
		delRule(r_num);
		printk("del rule");//输出到日志
		break;
	}
	if (ret != 0)//说明赋值失败,进行输出
	{
		printk("copy_from_user error");
		ret = -EINVAL;
	}
 
	return ret;
}
 
int hookSockoptGet(struct sock* sock,
	int cmd,
	void __user* user,
	int* len)//用与将数据传到用户层的函数
{
	int ret;
 
	debugInfo("hookSockoptGet");
 
	switch (cmd){
	case CMD_DEBUG:
		ret = copy_to_user(user, &debug_level, sizeof(debug_level));
		break;
	case CMD_RULE:
		//copy_to_user函数的作用:将内核空间的数据拷贝到用户空间
		//拷贝成功返回0
		ret = copy_to_user(user, &g_rules_count, sizeof(g_rules_count));
		ret = copy_to_user(user + sizeof(g_rules_count), g_rules, sizeof(Rule) * g_rules_count);
		break;
	}
 
	if (ret != 0){
		ret = -EINVAL;
		debugInfo("copy_to_user error");
	}
 
	return ret;
}
 
int init_module()//内核模块初始化,初始化五个钩子(进行钩子的注册)
{
	nfhoLocalIn.hook = hookLocalIn;//设置一些参数
	nfhoLocalIn.hooknum = NF_INET_LOCAL_IN;//注册回调函数
	nfhoLocalIn.pf = PF_INET;
	nfhoLocalIn.priority = NF_IP_PRI_FIRST;
	nf_register_net_hook(&init_net, &nfhoLocalIn);//注册hook
 
	nfhoLocalOut.hook = hookLocalOut;
	nfhoLocalOut.hooknum = NF_INET_LOCAL_OUT;
	nfhoLocalOut.pf = PF_INET;
	nfhoLocalOut.priority = NF_IP_PRI_FIRST;
	nf_register_net_hook(&init_net, &nfhoLocalOut);
 
	nfhoPreRouting.hook = hookPreRouting;
	nfhoPreRouting.hooknum = NF_INET_PRE_ROUTING;
	nfhoPreRouting.pf = PF_INET;
	nfhoPreRouting.priority = NF_IP_PRI_FIRST;
	nf_register_net_hook(&init_net, &nfhoPreRouting);
 
	nfhoForward.hook = hookForward;
	nfhoForward.hooknum = NF_INET_FORWARD;
	nfhoForward.pf = PF_INET;
	nfhoForward.priority = NF_IP_PRI_FIRST;
	nf_register_net_hook(&init_net, &nfhoForward);
 
	nfhoPostRouting.hook = hookPostRouting;
	nfhoPostRouting.hooknum = NF_INET_POST_ROUTING;
	nfhoPostRouting.pf = PF_INET;
	nfhoPostRouting.priority = NF_IP_PRI_FIRST;
	nf_register_net_hook(&init_net, &nfhoPostRouting);
 
	nfhoSockopt.pf = PF_INET;//PF_INET表示协议族
	nfhoSockopt.set_optmin = CMD_MIN;
	nfhoSockopt.set_optmax = CMD_MAX;
	nfhoSockopt.set = hookSockoptSet;
	nfhoSockopt.get_optmin = CMD_MIN;
	nfhoSockopt.get_optmax = CMD_MAX;
	nfhoSockopt.get = hookSockoptGet;
	nf_register_sockopt(&nfhoSockopt);//注册扩展套接字选项
 
	printk("myfirewall started\n");//将信息输出到日志中
 
	return 0;
}
 
void cleanup_module()//将钩子注销
{
	nf_unregister_net_hook(&init_net, &nfhoLocalIn);//将5个hook注销
	nf_unregister_net_hook(&init_net, &nfhoLocalOut);
	nf_unregister_net_hook(&init_net, &nfhoPreRouting);
	nf_unregister_net_hook(&init_net, &nfhoForward);
	nf_unregister_net_hook(&init_net, &nfhoPostRouting);
 
	nf_unregister_sockopt(&nfhoSockopt);
 
	printk("myfirewall stopped\n");//输出相应的信息
}
 
MODULE_LICENSE("GPL");//模块的许可证声明,防止收到内核被污染的警告

(3)用户层代码(myfwctl.c)

       这段代码描述了一个基于Linux系统的防火墙规则设置程序,它允许用户添加、删除和查看防火墙规则。程序的主要功能包括创建套接字与内核通信、解析用户输入的命令和参数、根据命令调用相应的函数、输出错误和成功信息,以及提供一些辅助函数。其中,get函数用于获取规则集并进行输出或输出debug_level,而parseArgs函数则用于解析用户输入的参数并进行处理。这两个函数共同实现了对规则集的查询、修改和输出功能,为用户提供了灵活的规则管理工具。总之,这是一个简单易用的防火墙规则设置程序,能够帮助用户轻松地管理防火墙规则。

     这段代码主要涉及到的数据结构有:

Rule:规则结构体,用于保存单个规则的信息。包含以下字段:

allow:允许或禁止标志,表示是否允许通过此规则。

dip:目的IP地址,表示数据包的目标IP地址。

dport:目的端口号,表示数据包的目标端口号。

protocol:协议类型,表示数据包使用的协议类型。

sport:源端口号,表示数据包的源端口号。

sip:源IP地址,表示数据包的源IP地址。

RuleTable:规则表结构体,用于保存多个规则的信息。包含以下字段:

count:规则数量,表示规则表中的规则个数。

rule:规则数组,用于存放规则结构体指针。

RuleSet:规则集结构体,用于保存整个规则集的信息。包含以下字段:

rules:规则表指针,指向一个RuleTable结构体,用于保存规则集的所有规则。

该部分的算法流程图如下:

      

图4 用户层算法流程图

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>
#include <errno.h>
#include "myfw.h"
//这一个是用户层代码
 
//------全局变量------
//创建一个全局变量,将要添加规则时的信息进行保留
Rule *pd_temp;
 
//------函数声明------
void printError(char* msg);//输出错误信息的函数
void printSuccess(char* msg);//输出成功信息的函数
void usage(char* program);//输出一些信息
unsigned int str2Ip(char* ipstr);//字符串类型的ip转换为整型
char* ip2Str(unsigned int ip, char buf[32]);//将整型的ip转为字符型的ip
unsigned short str2Port(char* portstr);//将端口转为整型
char* port2Str(unsigned short port, char buf[16]);//将整型端口构造为字符
char* protocol2Str(unsigned short protocol, char buf[16]);//将整型协议进行转为字符串
unsigned short str2Protocol(char* protstr);//将字符串类型的协议转为短整型的协议
void printRuleTable(RuleTable* rtb);//将规则集中的规则输出
int set(int cmd, void* val, int val_len, int sockfd);//将信息传到内核对规则进行修改
int get(int cmd, int sockfd);//将规则集进行输出或输出debug_level
int parseArgs(int argc, char* argv[], int* cmd, void* val, int* val_len);//获取用户端输入的信息并进行处理
 
//------主函数------
int main(int argc, char *argv[])
{
	int ret = -1;
	int cmd;
	char val[sizeof(Rule)];
	int val_len;
	int get_set = parseArgs(argc, argv, &cmd, val, &val_len);//将输入的参数进行处理
	if (get_set)//如果函数返回值不为零
	{
		int sockfd;
		//创建套接字,SOCK_RAW是RAW类型,提供原始网络协议访问
		//AF_INET设置地址族,IPPRPTP_RAW:原始IP数据包
		if ((sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW)) == -1)
			printError("socket()");//创建套接字失败输出错误信息
		else
		{
			if (get_set > 0)
				ret = set(cmd, val, val_len, sockfd);
				//将信息传到内核层,对规则集进行修改
			else ret = get(cmd, sockfd);
			close(sockfd);//关闭套接字
		}
	}
	else usage(argv[0]);//到这里说明是输入不对,对输入的格式进行提示
	return ret;//结束用户端程序
}
 
//------函数实现------
void printError(char* msg)//输出错误信息的函数
{
	printf("%s error %d: %s\n", msg, errno, strerror(errno));
}
 
void printSuccess(char* msg)//输出成功信息的函数
{
	printf("%s success\n", msg);
}
 
void usage(char* program)//输出一些输入格式信息
{
	printf("please check your input,the correct input format is:\n");
	printf("%s debug\n", program);
	printf("%s debug debug_level\n", program);
	printf("%s rule add sip sport dip dport protocol a|r\n", program);
	printf("%s rule del rule_number\n", program);
	printf("%s rule\n", program);
}
 
unsigned int str2Ip(char* ipstr)//字符串类型的ip转换为整型
{
	unsigned int ip;
	if (!strcmp(ipstr, "any"))ip = 0;//如果是any,表示任何任何端口或ip都可以,用0表示
	else inet_pton(AF_INET, ipstr, &ip);//将ip地址转换为用于网络传输的数值格式
	return ip;
}
 
char* ip2Str(unsigned int ip, char buf[32])//将整型的ip转为字符型的ip
{
	if (ip){
		unsigned char* c = (unsigned char*)&ip;
		sprintf(buf, "%d.%d.%d.%d", *c, *(c + 1), *(c + 2), *(c + 3));
	}
	else sprintf(buf, "any");
	return buf;
}
 
unsigned short str2Port(char* portstr)//将端口转为整型
{
	unsigned short port;
	if (!strcmp(portstr, "any"))port = 0;
	else port = atoi(portstr);
	return port;
}
 
char* port2Str(unsigned short port, char buf[16])//将整型端口构造为字符
{
	if (port)sprintf(buf, "%d", port);
	else sprintf(buf, "any");
	return buf;
}
 
char* protocol2Str(unsigned short protocol, char buf[16])//将整型协议进行转为字符串
{
	switch (protocol){
	case 0:
		strcpy(buf, "any");
		break;
	case MYFW_ICMP:
		strcpy(buf, "ICMP");
		break;
	case MYFW_TCP:
		strcpy(buf, "TCP");
		break;
	case MYFW_UDP:
		strcpy(buf, "UDP");
		break;
	default:
		strcpy(buf, "Unknown");
	}
	return buf;
}
 
unsigned short str2Protocol(char* protstr)//将字符串类型的协议转为短整型的协议
{
	unsigned short protocol = 0;
	if (!strcmp(protstr, "any")) protocol = 0;//0表示任何端口
	else if (!strcmp(protstr, "ICMP"))protocol = MYFW_ICMP;
	else if (!strcmp(protstr, "TCP"))protocol = MYFW_TCP;
	else if (!strcmp(protstr, "UDP"))protocol = MYFW_UDP;
	return protocol;
}
 
void printRuleTable(RuleTable* rtb)//将规则集中的规则输出
{
	char sip[32], dip[32], sport[16], dport[16], protocol[16];
	Rule* r = &(rtb->rule);
	printf("Rules count: %d\n", rtb->count);
	for (int i = 0; i < rtb->count; i++){//遍历规则集
		ip2Str(r->sip, sip);//将源ip进行转换
		ip2Str(r->dip, dip);//将目的地址进行转换
		port2Str(r->sport, sport);//将源端口进行转换
		port2Str(r->dport, dport);//将目的端口进行转换
		protocol2Str(r->protocol, protocol);//将协议进行转换
		//进行输出
		printf("%d\t%s:%s -> %s:%s, %s is %s\n", i + 1, sip, sport, dip, dport, protocol, r->allow ? "allow" : "reject");
		r = r + 1;//移到下一个规则
	}
}
 
int set(int cmd, void* val, int val_len, int sockfd)//将信息传到内核对规则进行修改
{
	//val_len表示的是数据类型的长度,不是规则集的个数
	int ret = -1;
	//判断要添加的规则或要删除的规则是否在规则集中
	int new_val_len = 1024 * 1024;
	void* new_val = malloc(new_val_len);
	//从内核层获取规则表
	if (getsockopt(sockfd, IPPROTO_IP, CMD_RULE, new_val, &new_val_len))
		printError("getsockopt");//输出错误信息,说明从内核层获取信息失败
	else {
		RuleTable* rules = (RuleTable*)new_val;
		Rule* r = &(rules->rule);
		if (cmd == CMD_RULE_DEL) {//要进行删除操作
			RuleTable* rules2 = (RuleTable*)val;
			if (rules2->count > rules->count) {//说明规则集中不存在该规则
				printf("failed to delete:the rule to delete does not exist\n");
				return 0;//不存在不需要删除
			}
		}
		else if (cmd == CMD_RULE) {
			for (int i = 0; i < rules->count; i++) {//遍历规则集,判断是否已经存在要添加的规则。
				if (pd_temp->allow == r->allow && pd_temp->dip == r->dip &&
					pd_temp->dport == r->dport && pd_temp->protocol == r->protocol
					&& pd_temp->sip == r->sip && pd_temp->sport == r->sport) {
					printf("failed to add:rule to add already exists\n");
					return 0;//已存在不需要添加
				}
				r = r + 1;//移到下一个规则
			}
		}
		
	}
	//若无错误发生,setsockopt返回0,否则返回socket_error错误
	if (setsockopt(sockfd, IPPROTO_IP, cmd, val, val_len))
		printError("setsockopt()");//输出错误信息,说明消息传到内核失败或创建套接字失败
	else{
		printf("setsockopt() success\n");//前后的长度不相同说明添加或删除成功
		//获取当前的规则集并进行输出
		//从内核层获取规则表
		int new_val_len1 = 1024 * 1024;
		void* new_val1 = malloc(new_val_len1);
		if (getsockopt(sockfd, IPPROTO_IP, CMD_RULE, new_val1, &new_val_len1))
			printError("getsockopt");//输出错误信息,说明从内核层获取信息失败
		else {
			printf("Current rule set:\n");
			printRuleTable(new_val1);//将规则集进行输出
		}
		ret = 0;
	}
	return ret;
}
 
int get(int cmd, int sockfd)//将规则集进行输出或输出debug_level
{
	int ret = -1;
	int val_len = 1024 * 1024;
	void* val = malloc(val_len);
	if (getsockopt(sockfd, IPPROTO_IP, cmd, val, &val_len))printError("getsockopt");//输出错误信息
	else{
		switch (cmd){
		case CMD_DEBUG://如果输入的是debug,那么输出debug等级
			printf("debug level=%d\n", *(int*)val);
			break;
		case CMD_RULE://输入的是rule,则将规则集中的数据进行输出
			printRuleTable((RuleTable*)val);
			break;
		}
	}
	return ret;
}
 
int parseArgs(int argc, char* argv[], int* cmd, void* val, int* val_len)//获取用户端输入的信息并进行处理
{
	int ret = 0;//初始将ret设为0
	//argc是输入的参数个数
	//argv数组是存放输入的参数
	if (argc == 2){//说明不需要添加或者删除规则
		if (!strcmp(argv[1], "debug")){
			*cmd = CMD_DEBUG;
			ret = -1;
		}
		else if (!strcmp(argv[1], "rule")){
			*cmd = CMD_RULE;
			ret = -1;
		}
	}
	else if (argc > 2){
		if (!strcmp(argv[1], "debug") && argc == 3){
			*cmd = CMD_DEBUG;
			*(int*)val = atoi(argv[2]);
			*val_len = sizeof(int);
			ret = 1;
		}
		else if (!strcmp(argv[1], "rule")){//说明要对规则进行操作
			if (argc == 4){
				if (!strcmp(argv[2], "del")){//删除规则
					*cmd = CMD_RULE_DEL;
					*(int*)val = atoi(argv[3]);
					*val_len = sizeof(int);
					ret = 1;
				}
			}
			else if (argc == 9){
				if (!strcmp(argv[2], "add")){//添加规则
					*cmd = CMD_RULE;
					Rule* r = (Rule*)val;
					*val_len = sizeof(Rule);
					r->sip = str2Ip(argv[3]);//下面都是对输入的参数进行处理,将字符串转为数字或其他类型
					r->sport = str2Port(argv[4]);
					r->dip = str2Ip(argv[5]);
					r->dport = str2Port(argv[6]);
					r->protocol = str2Protocol(argv[7]);
					r->allow = strcmp(argv[8], "a") ? 0 : 1;//如果是a,说明是允许通信,赋值为1,否则赋值为0
					pd_temp = r;//将变量保存,用于后面添加规则时进行判断
					ret = 1;
				}
			}
		}
	}
	return ret;
}

(4)Makefile工程文件

# Makefile 4.0
obj-m := myfw.o
CURRENT_PATH := $(shell pwd)
LINUX_KERNEL := $(shell uname -r)
LINUX_KERNEL_PATH := /usr/src/linux-headers-$(LINUX_KERNEL)
KBUILD_CFLAGS += -w
 
all:
	make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
clean:
	make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean

       这段代码是一个用于操作Linux内核防火墙规则的程序。它允许用户添加、删除和查看规则,以及设置和获取当前的debug等级。主要功能包括:

该部分具体功能如下:

①定义目标模块名:obj-m := myfw.o,表示要编译的目标模块名为myfw.o。

②获取当前路径:CURRENT_PATH := $(shell pwd),将当前路径赋值给变量CURRENT_PATH。

③获取Linux内核版本:LINUX_KERNEL := $(shell uname -r),将当前系统使用的Linux内核版本赋值给变量LINUX_KERNEL。

④获取Linux内核头文件路径:LINUX_KERNEL_PATH := /usr/src/linux-headers-$(LINUX_KERNEL),根据LINUX_KERNEL的值拼接出Linux内核头文件的路径,并赋值给变量LINUX_KERNEL_PATH。

⑤编译选项:KBUILD_CFLAGS += -w,在编译过程中添加-w选项,用于关闭所有警告信息。

⑥定义all目标:all: make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules,当执行make命令时,会进入到LINUX_KERNEL_PATH指定的目录,并使用M参数指定当前路径,然后执行modules命令进行模块编译。

⑦定义clean目标:clean: make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean,当执行make clean命令时,会进入到LINUX_KERNEL_PATH指定的目录,并使用M参数指定当前路径,然后执行clean命令进行清理工作。

3.系统测试

(1)配置Makefile文件。

①输入命令vim makefile,这个文件主要作用就是编译.c文件,生成.ko文件。

图5 配置Makefile文件

(2)设置防火墙

①首先输入make命令进行编译、而后输入insmod myfw.ko命令加载内核(若要卸载内核,用rmmod)。

图6 编译、加载内核

②输入gcc myfwctl.c -o myfwctl命令,在该路径下编译应用层测试代码,运行应用层程序。

 图7 运行应用层程序

③输入sudo ./myfwctl rule命令,查看当前防火墙规则数量为0。

           

图8 查看防火墙规则数量

(3)这样防火墙就设置好了,但未添加规则,在此情形下,先输入ping命令,检测主机与虚拟机是否互通。

①先查看虚拟机和主机的IP地址,虚拟机为192.168.91.131,主机为192.168.91.1。

图9 查看虚拟机和主机的IP地址

②虚拟机Ping本地主机,可以ping通;

图10 虚拟机Ping本地主机

③本机ping虚拟机,可以ping通。

图11 本机ping虚拟机 

(4)增加防火墙配置

①输入sudo ./myfwctl rule add any any any any ICMP r命令,末尾的r代表拒绝ICMP协议通信,如果是a代表允许。

图12 查看是否允许ICMP通信

②此时再用虚拟机和主机互相ping,都ping不通,代表防火墙规则设置成功。若要删除规则,则用sudo ./myfwctl rule del rule-number。

图13 虚拟机和主机互ping

三、总结

     基于netfilter框架,编写了内核防火墙模块和用户态防火墙控制程序,实现服务协议、IP地址、端口等的控制和过滤。实验过程中遇到了一些问题:一开始缺少了一些库,而后下载导入;makefile文件中路径出错,也进行了相应的修改;命令语句与之前学习的不同,也查找资料进行学习;在设置防火墙规则时,尝试了多种规则,最后发现了一条可以成功实现的规则语句;代码的编写参考了很多网络上的资料,查询了相关资料对代码深入探讨和研究,再结合自己要实现的功能加以改进。通过本次实验,我对防火墙起到的重要作用有了更深刻的认识,也对防火墙的结构有了更深入的理解。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐