基于Netfilter框架设计的软件防火墙过滤系统

1. 题目要求

基于netfilter框架,编写内核防火墙模块(驱动)和用户态防火墙控制程序,实现服务协议、IP地址、端口等的控制和过滤。

2. 基本设计思路

本次实验主要是针对于防火墙过滤功能的设计,通过在5个检查点NF_INET_PRE_ROUTING、NF_INET_LOCAL_IN、NF_INET_FORWARD、NF_INET_LOCAL_OUT、NF_INET_POST_ROUTING中hook自定义的对数据包的检查函数,即可实现对数据包的检查过滤。

本次实验基于实践学习的目的,对5个检查点分别注册相对应的过滤函数,最后对防火墙对的过滤功能进行检测。

  • 检查点NF_INET_PRE_ROUTING
    数据包刚刚进入网络层还没有进行路由前会通过这个检查点,并执行该检查点上注册的函数。

    本次实验在这个检查点中执行的对数据包的IP版本和校验和进行检测,若版本为非IPv4或者校验和错误,则直接丢弃该数据包。

  • 检查点NF_INET_LOCAL_IN
    在接收到的报文做路由后,确实是本机接收的报文之后,则会经过该检查点。

    本次实验在这个检查点中对数据包的源、目的IP地址和源、目的端口号以及协议类型进行黑名单过滤,若存在规则拒绝接收该数据包,则将该数据包丢弃。

  • 检查点NF_INET_FORWARD
    在接收到的数据包向另一个网卡进行转发之前会经过该检查点。

    本次实验中在该点对数据包的源、目的IP地址和协议类型进行黑名单过滤。若存在规则拒绝对该数据包进行转发,则直接丢弃该数据包。

  • 检查点NF_INET_LOCAL_OUT
    在本地数据包做发送路由之前会经过该检查点。

    本次实验在该点进行了对数据包的源、目的IP地址和协议类型进行白名单过滤。只有规则中允许发送的数据包,才能够从该检查点经过,否则直接将数据包丢弃。

  • 检查点NF_INET_POST_ROUTING
    任何马上要通过网络设备出去的包都会经过该检查点,这也是netfilter的最后一个检查点,在该点能够进行内置的目的地址的转换(地址伪装)等。

    本次实验不对该检查点进行数据包的过滤。

3. 开发过程记录

3.0 开发基本介绍

本此的防火墙设计是基于Netfilter框架设计的软件防火墙过滤系统

基于学习和实践的目的,本次的防火墙设计主要面向于初学者,简单入门防火墙设计。

所以本次只对源IP、目的IP地址、源端口、目的端口以及一些简单协议进行过滤。

欢迎大家学习交流,如有不足之处,敬请批评指正!

3.1 头文件simpleFw.h

在前期根据可能的需要定义最开始的头文件simpleFw.h

其中包含一些关键的参数,如DEBUF模式、RULE模式、RULE_DEL模式参数

以及一些协议的标识符,如ICMP、TCP和UDP协议

和规则Rule结构体和规则表RuleTable结构体

#define CMD_MIN		0X6000

#define CMD_DEBUG		CMD_MIN+1
#define CMD_RULE		CMD_MIN+2
#define CMD_RULE_DEL	CMD_MIN+3

#define CMD_MAX		0X6100

#define SPFW_ICMP	1	//IPPROTO_ICMP
#define SPFW_TCP	2	//IPPROTO_TCP
#define SPFW_UDP	3	//TPPROTO_UDP


typedef struct{
    unsigned int src_ip;	//源IP地址
    unsigned int dst_ip;	//目标IP地址
    unsigned short src_port;	//源端口
    unsigned short dst_port;	//目的端口
    unsigned int protocol;		//使用的协议号
    int action;		//动作是否允许
} Rule;

typedef struct{
    unsigned int count;
    Rule rule;
}RuleTable;

3.2 simpleFw.c编写-初步框架

首先导入simpleFw.h头文件

定义5个struct nf_hook_ops结构体对应5个hook点,对每个hook设置相应的处理函数

还定义了1个struct nt_sockopt_ops结构体,用于设置 Socket Option 的属性。

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/skbuff.h>
#include <net/tcp.h>
#include <linux/netdevice.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>

#include "simpleFw.h"

static struct nf_hook_ops nfhoLocalIn;		//设置 NF_INET_LOCAL_IN 的 hook 钩子点函数
static struct nf_hook_ops nfhoLocalOut;		//设置 NF_INET_LOCAL_OUT 的 hook 钩子点函数
static struct nf_hook_ops nfhoPreRouting;	//设置 NF_INET_PRE_ROUTING 的 hook 钩子点函数
static struct nf_hook_ops nfhoForward;		//设置 NF_INET_FORWARD 的 hook 钩子点函数
static struct nf_hook_ops nfhoPostRouting;	//设置 NF_INET_POST_ROUTING 的 hook 钩子点函数

static struct nf_sockopt_ops nfhoSockopt;	//设置 Socket Option 的属性

定义每个hook点的函数,函数名分别为hookLocalInhookLocalOuthookPreRoutinghookForwardhookPostRouting

没有定义详细的函数功能,先把基本的框架完成,最后在实现函数的功能。

在函数定义的形式参数中,有两个新的结构体形式参数==struct sk_buffstruct nf_hook_state==,请点击超链接参看附录。

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 hookForward(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)
{

}

在定义hookSockoptSethookSockoptGet函数来实现Socket套接字相应的处理函数

int hookSockoptSet(struct sock *sock,
				   int cmd,
				   sockptr_t userPtr,
				   unsigned int len)
{

}

int hookSockoptGet(struct sock *sock,
				   int cmd,
				   void __user *user
				   int *len)
{

}

这里有个比较奇怪的点,就是hookSockoptSet函数的第三个形式参数的类型是sockptr_t,而不是与hookSockoptGet函数相同的void __user *,至于为什么是sockptr_t,还不清楚,查阅相关资料也没查到相关信息。下图是在内核的linu/netfilter.h头文件查阅到的struct nt_sockopt_ops结构体,可以看到确实是sockptr_t类型

image-20230422164659070

定义了hook函数以及相应的结构体,现在开始注册hook钩子,定义一个init_module函数,内核中初始化模块默认调用的函数,将相应的函数注册到对应的钩子

int init_module(){
	//将 hookLocalIn 函数注册到 NF_INET_LOCAL_IN 的 hook 钩子点
	nfhoLocalIn.hook = hookLocalIn;
	ntholocalIn.hooknum = NF_INET_LOCAL_IN;
	nfhoLocalIn.pf = PF_INET;
	nfhoLocalIn.priority = NF_IP_PRI_FIRST;
	nf_register_net_hook(&init_net, &nfhoLocalIn);
	//将 hookLocalOut 函数注册到 NF_INET_LOCAL_OUT 的 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);
	//将 hookPreRouting 函数注册到 NF_INET_PRE_ROUTING 的 hook 钩子点
	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);
	//将 hookForward 函数注册到 NF_INET_FORWARD 的 hook 钩子点
	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);
	//将 hookPostRouting 函数注册到 NF_INET_POST_ROUTING 的 hook 钩子点
	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
	nfhoSockopt.pf = 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);
}

上述中注册了注销函数,下面解释一下注销函数结构体nfhoSockopt中的相关成员变量
这段代码用于注册一个 Netfilter 的 Socket Option 操作。

  • nfhoSockopt 是一个 struct nf_sockopt_ops 类型的结构体,用于设置 Socket Option 的属性。其中:

  • nfhoSockopt.pf = PF_INET; 表示要注册的 Socket Option 操作只针对 IPv4 协议族。

  • nfhoSockopt.set_optminnfhoSockopt.set_optmax 分别表示在设置 Socket Option 操作时,允许传入的操作码的最小值和最大值。

  • nfhoSockopt.set 是一个回调函数,当设置 Socket Option 操作时会自动调用该函数。

  • nfhoSockopt.get_optminnfhoSockopt.get_optmax 分别表示在获取 Socket Option 操作时,允许传入的操作码的最小值和最大值。

  • nfhoSockopt.get 是一个回调函数,当获取 Socket Option 操作时会自动调用该函数。

最后,使用 nf_register_sockopt() 函数将 Socket Option 操作注册到 Netfilter 框架中。其中,nfhoSockopt 是一个指向 struct nf_sockopt_ops 类型的结构体的指针,表示要注册的 Socket Option 操作的属性。

到这里,基本的钩子注册操作就已经完成了,我们可以开始进行相关函数的实现了。

在实现相关函数之前,我们需要先考虑如何规则Rule的数据结构处理问题,即如何对规则进行增、删、改、查。

规则Rule应是由用户自由定义的,那么应该通过控制台程序使用户能够自定义过滤规则Rule。

故需要定义一个程序供用户定义相关规则Rule,即读取用户传入参数对软件防火墙中的过滤规则进行定义。

故我们创建一个simpleFwctl.c文件编写相关功能,首先即是读取用户执行时输入的相关参数等的处理形式。

3.3 simpleFwctl.c编写

本次设计设计五种命令供用户使用

分别是添加规则Rule、删除规则Rule、查看规则、设计Debug等级、查看Debug等级

使用方式:./simpoleFwctl rule add 192.168.1.1 80 any any ICMP r(any 代表 任意)

  • ./simpleFwctl rule add src_ip src_port dst_ip dst_port protocol action //添加规则, aciton指拦截block还是放行forward
  • ./simpleFwctl rule del rule_number //删除规则
  • ./simpleFwctl rule //查看规则
  • ./simpleFwctl debug debug_level //设计debug等级
  • ./simpleFwctf debug //查看debug等级
#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 "simpleFw.h"

void usage(char *program)
{
	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);
}

void printError(char *msg)
{
	printf("%s error %d: %s\n", msg, errno, strerror(errno));
}

void printSuccess(char *msg)
{
	printf("%s success\n", msg);
}

unsigned int str2Ip(char *ipstr)
{
	unsigned int ip;
	if (!strcmp(ipstr, "any"))
	{
		ip = 0;
	}
	else
	{
		inet_pton(AF_INET,ipstr, &ip);
	}
	return ip;
}

char *ip2Str(unsigned int ip, char buf[32])
{
	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 SPFW_ICMP:
		strcpy(buf, "ICMP");
		break;
	case SPFW_TCP:
		strcpy(buf, "TCP");
		break;
	case SPFW_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;
	}
	else if (!strcmp(protstr, "ICMP"))
	{
		protocol = SPFW_ICMP;
	}
	else if (!strcmp(protstr, "TCP"))
	{
		protocol = SPFW_TCP;
	}
	else if (!strcmp(protstr, "UDP"))
	{
		protocol = SPFW_UDP;
	}

	return protocol;
}

int parseArgs(int argc, char** argv, int *cmd, void *val, int *val_len){
	int ret = 0;

	//若用户近输入了两个参数,则一定是查看命令
	if(argc == 2){
		if(!strcmp(argv[1], "debug")){
			*cmd = CMD_DEBUG;
			ret = -1;
		}
		else if(!strcmp(argv[1], "rule")){
			*cmd = CMD_RULE;
			ret = -1;
		}
	}
	// 若用户输入的参数大于2,则应该是添加规则Rule、删除规则Rule、修改debug等级其中之一
	else if(argc > 2){
		//若argc等于3且第2个参数为 debug,则应为修改debug等级
		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")){
			//若输入的参数数量为4,则只可能是规则删除指令
			if(argc == 4){
				if(!strcmp(argv[2], "del")){
					*cmd = CMD_RULE_DEL;
					*(int *)val = atoi(argv[3]);
					ret = 1;
				}
			}
			//若输入的参数数量为9,则只可能是添加规则Rule的指令
			else if(argc == 9){
				if(!strcmp(argv[2], "add")){
					*cmd = CMD_RULE;
					Rule *r = (Rule *)val;
					*val_len = sizeof(Rule);
					r->src_ip = str2Ip(argv[3]);
					r->src_port = str2Port(argv[4]);
					r->dst_ip = str2Ip(argv[5]);
					r->dst_port = str2Ip(argv[6]);
					r->protocol = str2Protocol(argv[7]);
					r->action = strcmp(argv[8], "a") ? 0 : 1;
					ret = 1;
				}
			}
		}
	}
	return ret;
}

void printRuleTable(RuleTable *rtb)
{
	char src_ip[32], dst_ip[32], src_port[16], dst_port[16], protocol[16];
	Rule *r = &(rtb->rule);
	printf("Rules count: %d\n", rtb->count);
	for (int i = 0; i < rtb->count; i++)
	{
		ip2Str(r->src_ip, src_ip);
		ip2Str(r->dst_ip, dst_ip);
		port2Str(r->src_port, src_port);
		port2Str(r->dst_port, dst_port);
		protocol2Str(r->protocol, protocol);
		printf("%d\t%s:%s -> %s:%s, %s is %s\n", i + 1, src_ip, src_port, dst_ip, dst_port, protocol, r->action ? "allow" : "reject");
		r = r + 1;
	}
}

int set(int cmd ,void* val, int val_len, int sockfd){
	int ret = -1;

	if (setsockopt(sockfd, IPPROTO_IP, cmd, val, val_len))
	{
		printError("setsockopt()");
	}
	else
	{
		printf("setsockopt() success\n");
		ret = 0;
	}

	return ret;
}

int get(int cmd, int sockfd){
	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:
			printf("debug level=%d\n", *(int *)val);
			break;
		case CMD_RULE:
			printRuleTable((RuleTable *)val);
			break;
		}
	}
	return ret;
}

int main(int argc, char** argv){
	int ret = -1;
	int cmd;				//记录用户输入的命令的值
	char val[sizeof(Rule)];	//存储用户输入相关数据	规则、debug等级或者规则索引
	int val_len = 0;		//记录相关数据长度
	int get_set = parseArgs(argc, argv, &cmd, &val, &val_len);
	if(get_set){
		int sockfd;
		if((sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW)) == -1){
			printError("socket()");
		}
		else {
			// get_set > 0, 表示需要向内核中发送数据
			if(get_set > 0){
				ret = set(cmd, val, val_len, sockfd);
			}
			else { 
				ret = get(cmd, sockfd);
			}
		}
		close(sockfd);
	}
	else{
		usage(argv[0]);
	}
	return ret;
}

以上是simpleFwctl.c程序中对用户输入参数进行获取的函数,为后面的功能做准备。

在main函数中,根据获取参数函数parseArgs返回的值,判断用户是需要向内核中发送数据还是从内核中接收数据。

分别调用set函数和get函数,进行相应的操作;

3.4 simpleFw.c编写-接收数据

在我们编写完毕用户的simpleFwctl.c程序后,我们需要在内核中编写相应的函数来接受处理用户发送过来的数据,即编写被注册到注销函数结构体nfhoSockopt的set变量中的函数hookSockoptSet,设计自定义的setSockopt()函数

由于对应的功能是增、删、查规则和 DEBUG Level 那么我们应该先实现对应功能的函数;

首先是增加规则的函数void addRule(Rule *rule)

  • void addRule(Rule *rule)
void addRule(Rule *rule){
	int cnt = g_rules_cnt + 1;	//记录添加一条规则后的规则总数
    //开辟一个必规则数量大 1 的空间, 使之能够添加一条新规则
	Rule *rules_t = (Rule *)vmalloc(cnt*sizeof(Rule));
    //将新的规则先添加到开辟的空间中
	memcpy(rules_t, rule, sizeof(Rule));
	if(g_rules_cnt > 0){
        //再将原先的规则也添加的新开辟的空间中
		memcpy(rules_t, g_rules, g_rules_cnt*sizeof(Rule));	
		vfree(g_rules);		//释放原先的规则空间
	}
	g_rules = rules_t;	//令规则数组的首地址为新开辟的空间
	g_rules_cnt = cnt;	//更新规则数
}

随后是删除规则的函数void delRule(int rule_num)

  • void delRule(int rule_num)
void delRule(int rule_num){
	//判断规则序号是否存在
	if(rule_num>0 && rule_num<g_rules_cnt){
		//存在则将该序号及其以后的规则用下一条规则的内容覆盖
		for(int i = rule_num; i < g_rules_cnt; i++){
			memcpy(g_rules+i-1, g_rules+i, sizeof(Rule));
		}
		g_rules_cnt++;	//规则数减 1
	}
}

再定义修改debug_level的函数void setDebug_Level(int level)

  • void setDebug_Level(int level)
void setDebug_Level(int level){
	debug_level = level;
}

完成上述工作后,可以开始着手编写hookSockoptSet()函数处理用户发送到内核中的数据。

int hookSockoptSet(struct sock *sock,
				   int cmd,
				   sockptr_t userPtr,
				   unsigned int len)
{
	int ret = 0;
	Rule r;
	int r_num;

	debugInfo("hookSockoptSet");
	//根据 cmd 的不同,接受不同大小的数据,并执行相应的操作
	switch(cmd){
		case CMD_DEBUG:
			//从用户空间中拷贝接收用户设置的用户等级,即一个整形int数据
			ret = copy_from_user(&debug_level, userPtr.user, sizeof(debug_level));
			setDebug_Level(debug_level);	//执行debug等级修改函数
			printk("set debug level to %d", debug_level);
			break;
		case CMD_RULE:
			//从用户空间中拷贝接收用户设置的规则Rule内容
			ret = copy_from_user(&r, userPtr.user, sizeof(Rule));
			addRule(r);	//执行添加规则函数
			printk("add rule!");
			break;
		case CMD_RULE_DEL:
			//从用户空间中拷贝接收用户所需要删除的规则序号,一个整形int数据
			ret = copy_from_user(&r_num, userPtr.user, sizeof(r_num));
			delRule(r_num);	//执行删除规则函数
			printk("del rule");
			break;
	}

	if(ret != 0){
		printk("copy_from_user error!");
		ret = -EINVAL;
	}
	return ret;
}

3.5 simpleFw.c编写-发送数据

根据用户的需要我们需要编写相应的程序向用户反馈信息;例如,用户需要查询所有定义的规则,则我们需要将规则内容发送到用户空间,供用户查看;以及用户查看debug_level,需要向用户空间发送debug_level。即编写被注册到注销函数结构体nfhoSockopt的get变量中的函数hookSockoptGet。

int hookSockoptGet(struct sock *sock,
				   int cmd,
				   void __user *user
				   int *len)
{
	int ret;

	debugInfo("hookSockoptGet");
	//根据用户的命令,向用户空间发送相应的数据
	switch(cmd){
		case CMD_DEBUG:
			//向用户空间发送debug_level
			ret = copy_to_user(user, &debug_level, sizeof(debug_level));
			break;
		case CMD_RULE:
			//向用户空间发送规则条数
			ret = copy_to_user(user, &g_rules_cnt, sizeof(g_rules_cnt));
			//向用户空间发送规则数组(注意:由于规则条数发送过一次数据,所以再次发送数据需要添加已发送的数据偏移)
			ret = copy_to_user(user+sizeof(g_rules_cnt), g_rules, g_rules_cnt*siezof(Rule));
			break;
	}

	if(ret != 0){
		ret = -EINVAL;
		debugInfo("copy_to_user error");
	}

	return ret;
}

3.6 simpleFw.c编写-入站过滤

对入站数据包的过滤一般可以考虑在NF_INET_PRE_ROUTINGNF_INET_LOCAL_IN这两个钩子点进行过滤。

NF_INET_PRE_ROUTING 钩子点是 Netfilter 防火墙的一个预处理钩子点,它可以在路由表处理之前对该报文进行过滤和处理。

NF_INET_LOCAL_IN 钩子点是 Netfilter 防火墙的一个入站钩子点,它可以在 IP 报文到达主机并确定是由本机接收的数据包被内核接收之后,对该报文进行过滤和处理。

本次的简单实验,选择在钩子点NF_INET_LOCAL_IN中进行对入站数据包的过滤。

根据钩子函数的不同返回值,内核会对数据执行不同的处理

  1. NF_ACCEPT:表示允许数据包通过,不进行过滤。
  2. NF_DROP:表示拒绝数据包通过,并释放数据包占用的资源。
  3. NF_STOLEN:表示拒绝数据包通过,但是不释放数据包占用的资源,而是交给了其他模块处理。
  4. NF_QUEUE:表示把数据包放入队列中,等待用户空间程序处理。
  5. NF_REPEAT:表示重新调用此钩子函数。

详细的实现代码如下:

  • matchRule
int matchRule(struct iphdr *iph){
	int action = 1;
	int i;
	Rule *r;
	//对规则进行逐一检查, 若存在相应的规则则返回相应的动作
	for (i = 0; i < g_rules_cnt; i++) { 
		r = g_rules + i;
		//若规则中未定义源IP和目的IP, 则默认规则值为0
		if((!r->src_ip || r->src_ip==iph->saddr) &&
		   (!r->dst_ip || r->dst_ip==iph->daddr) && 
		   (!r->protocol || r->protocol==iph->protocol))
		{
			action = r->action;
			break;
		}
	}
	return action;
}
  • hookLocalIn
unsigned int hookLocalIn(void *priv,
						 struct sk_buff *skb,
						 const struct nf_hook_state *state)
{
	unsigned int ret = NF_ACCEPT;
	//将数据包结构体skb转换成struct iphdr
	struct iphdr *iph = ip_hdr(skb);
	//与规则逐一匹配, 判断规则是否拦截该数据包, 默认数据包不被拦截
	//黑名单过滤方式
	if(matchRule(iph) <= 0){
        //若存在规则拒绝接收该数据包, 则返回NF_DROP丢弃该数据包
        printk("NF_IP_LOCAL_IN检查点过滤了来自数据包\n源地址:%d\t目的地址:%d\t协议:%d\n", iph->saddr, iph->daddr, iph->protocol);	//若出现数据包的过滤可以在内核输出区得到消息提示
		ret = NF_DROP;
	}
	debugInfo("hookLocalIn");
	return ret;
}

3.7 中期效果测试

在完成上述代码的编辑工作后,可以对LOCAL_IN检查点的功能进行简单的测试

先编辑一个Makefile文件,进行对内核模块的编辑工作

# Makefile 4.0
obj-m := simpleFw.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

按照正常的内核编译和安装命令(若没有学习过内核模块的编译安装,请先去内核Hello World中学习),编译安装内核模块

image-20230504170105165

我们在执行对simpleFwctl.c编译,用来对防火墙进行规则的添加和修改

image-20230504170254377

由于我使用的wsl2的Linux系统,所以可以通过使用wireshark软件监听相应的网卡的数据包观察主机Windows10向wsl2发送的数据包,与此同时,实现一个简单的禁ping的功能。

image-20230504170555779

在正常情况下使用主机是可以ping通wsl2的linux子系统的。

那么我们现在对我们创建的防火墙中加入相应的规则

image-20230504170729961

该规则Rule的意思即为,禁止接收来自172.23.144.1的ICMP类型数据包,我们此时再使用主机尝试ping一下wsl2的linux子系统。

image-20230504170950605

发现此时,windows10主机确实发送了相应的数据包,但是却无法收到来自linux系统的回应,从而可知确实是linux系统的防火墙,即我们所设计的防火墙将来自windows10(172.23.144.1)的ICMP数据包给过滤了。

通过查看内核输出区的输出内容可以发现,导致请求超时的原因确实是由于我们设计的防火墙将来自Windows10的ICMP类型的询问数据包过滤了。可自行将主机端数值型IP地址修改成字符型IP地址;

image-20230505084242183

3.8 其他检查点过滤规则续写

其实上述的代码已经可以实现一个简单功能的防火墙了。通过上述的代码其实也基本能够理解一个简单的防火墙的实现原理和设计过程了。

下面继续完善其他检查点的规则过滤代码,使防火墙的功能更加完善,同时也去理解每个检查点的大致工作是什么。

  • NF_INET_PRE_ROUTING

在5个检查点中NF_INET_PRE_ROUTING检查点主要完成的工作是对版本号、校验和的检测。

对IP数据包版本号的检查,若数据包为非IPv4的数据包则丢弃;对数据包检验和的检测,查看数据包是否存在差错,若存在丢弃该数据包。

unsigned int hookPreRouting(void *priv,
						 struct sk_buff *skb,
						 const struct nf_hook_state *state)
{
	unsigned int ret = NF_ACCEPT;
	struct iphdr *iph = ip_hdr(skb);
	//判断IP数据包是否为IPv4版本数据包, 不是则丢弃
	if(iph->version != 4){
		ret = NF_DROP;	
	}
	//校验 IPv4 数据报头的正确性, 不正确则丢弃
	if(iph->check){
		if(ip_fast_csum((unsigned char *)iph, iph->ihl)){
			ret = NF_DROP;
		}
	}
	debugInfo("hookPreRouting");
	return ret;
}
  • NF_INET_FORWARD

你可以根据你的需要设置一些过滤规则,在本次实验中,该检查点中执行的自定义过滤函数仅对数据包的源、目的IP地址和协议类型进行过滤,不对端口号进行过滤。

对于该检查点的功能验证其实也比较难进行,由于需要数据包经过该linux主机进行转发,所以需要linux进行相关配置,这里我们便不在进行实验。

代码实现如下:

int matchRule_IP_PROTOCOL(struct iphdr *iph){
	int action = 1;
	int i;
	Rule *r;
	//对规则进行逐一检查, 若存在相应的规则则返回相应的动作
	for (i = 0; i < g_rules_cnt; i++) { 
		r = g_rules + i;
		//若规则中未定义源IP和目的IP, 则默认规则值为0
		if((!r->src_ip || r->src_ip==iph->saddr) &&
		   (!r->dst_ip || r->dst_ip==iph->daddr) &&
		   (!r->protocol || r->protocol==iph->protocol))
		{
			action = r->action;	
			break;
		}
	}
	return action;
}

unsigned int hookForward(void *priv,
						 struct sk_buff *skb,
						 const struct nf_hook_state *state)
{
	unsigned int ret = NF_ACCEPT;
	//将数据包结构体skb转换成struct iphdr
	struct iphdr *iph = ip_hdr(skb);
	//与规则逐一匹配, 判断规则是否拦截该数据包, 默认数据包不被拦截
	//黑名单过滤方式
	if(matchRule_IP_PROTOCOL(iph) <= 0){
		ret = NF_DROP;
	}
	debugInfo("hookForwoad");
	return ret;
}
  • NF_INET_LOCAL_OUT

这个检查点与NF_INET_LOCAL_IN检查点恰好相反,是对出站数据包进行过滤,是对本机数据包做路由的过滤规则。

在该检查点也可以使用与入站过滤相同的过滤方式,这里便不在设计新的过滤方式了,直接使用与入站过滤相同的过滤方式。

代码实现如下:

unsigned int hookLocalOut(void *priv,
						 struct sk_buff *skb,
						 const struct nf_hook_state *state)
{
	unsigned int ret = NF_ACCEPT;
	//将数据包结构体skb转换成struct iphdr
	struct iphdr *iph = ip_hdr(skb);
	//与规则逐一匹配, 判断规则是否拦截该数据包, 默认数据包不被拦截
	//黑名单过滤方式
	if(whiteListFilterRule(iph) >= 1){
		ret = NF_DROP;
	}
	debugInfo("hookLocalOut");
	return ret;
}

3.9 总结

其实到这里,这个基于Netfilter框架设计的软件防火墙过滤系统就已经设计完毕了。啪啪啪啪啪啪!!!!

4. 源代码

由于在编写过程中,没有进行及时的功能测试,导致最后debug的时候对代码进行了修改,方便实现一个完整的效果。

后续部分的测试效果也是根据该源代码进行测试的

4.1 simpleFwctl.c

#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 "simpleFw.h"

void usage(char *program)
{
	printf("%s debug\n", program);
	printf("%s debug debug_level\n", program);
	printf("%s rule add src_ip src_port dst_ip dst_port protocol a|r\n", program);
	printf("%s rule del rule_number\n", program);
	printf("%s rule\n", program);
}

void printError(char *msg)
{
	printf("%s error %d: %s\n", msg, errno, strerror(errno));
}

void printSuccess(char *msg)
{
	printf("%s success\n", msg);
}

unsigned int str2Ip(char *ipstr)
{
	unsigned int ip;
	if (!strcmp(ipstr, "any"))
	{
		ip = 0;
	}
	else
	{
		inet_pton(AF_INET,ipstr, &ip);
	}
	return ip;
}

char *ip2Str(unsigned int ip, char buf[32])
{
	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 SPFW_ICMP:
		strcpy(buf, "ICMP");
		break;
	case SPFW_TCP:
		strcpy(buf, "TCP");
		break;
	case SPFW_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;
	}
	else if (!strcmp(protstr, "ICMP"))
	{
		protocol = SPFW_ICMP;
	}
	else if (!strcmp(protstr, "TCP"))
	{
		protocol = SPFW_TCP;
	}
	else if (!strcmp(protstr, "UDP"))
	{
		protocol = SPFW_UDP;
	}

	return protocol;
}

int parseArgs(int argc, char** argv, int *cmd, void *val, int *val_len){
	int ret = 0;

	//若用户近输入了两个参数,则一定是查看命令
	if(argc == 2){
		if(!strcmp(argv[1], "debug")){
			*cmd = CMD_DEBUG;
			ret = -1;
		}
		else if(!strcmp(argv[1], "rule")){
			*cmd = CMD_RULE;
			ret = -1;
		}
	}
	// 若用户输入的参数大于2,则应该是添加规则Rule、删除规则Rule、修改debug等级其中之一
	else if(argc > 2){
		//若argc等于3且第2个参数为 debug,则应为修改debug等级
		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")){
			//若输入的参数数量为4,则只可能是规则删除指令
			if(argc == 4){
				if(!strcmp(argv[2], "del")){
					*cmd = CMD_RULE_DEL;
					*(int *)val = atoi(argv[3]);
					ret = 1;
				}
			}
			//若输入的参数数量为9,则只可能是添加规则Rule的指令
			else if(argc == 9){
				if(!strcmp(argv[2], "add")){
					*cmd = CMD_RULE;
					Rule *r = (Rule *)val;
					*val_len = sizeof(Rule);
					r->src_ip = str2Ip(argv[3]);
					r->src_port = str2Port(argv[4]);
					r->dst_ip = str2Ip(argv[5]);
					r->dst_port = str2Port(argv[6]);
					r->protocol = str2Protocol(argv[7]);
					r->action = strcmp(argv[8], "a") ? 0 : 1;
					ret = 1;
				}
			}
		}
	}
	return ret;
}

void printRuleTable(RuleTable *rtb)
{
	char src_ip[32], dst_ip[32], src_port[16], dst_port[16], protocol[16];
	Rule *r = &(rtb->rule);
	printf("Rules count: %d\n", rtb->count);
	for (int i = 0; i < rtb->count; i++)
	{
		ip2Str(r->src_ip, src_ip);
		ip2Str(r->dst_ip, dst_ip);
		port2Str(r->src_port, src_port);
		port2Str(r->dst_port, dst_port);
		protocol2Str(r->protocol, protocol);
		printf("%d\t%s:%s -> %s:%s, %s is %s\n", i + 1, src_ip, src_port, dst_ip, dst_port, protocol, r->action ? "allow" : "reject");
		r = r + 1;
	}
}

int set(int cmd ,void* val, int val_len, int sockfd){
	int ret = -1;

	if (setsockopt(sockfd, IPPROTO_IP, cmd, val, val_len))
	{
		printError("setsockopt()");
	}
	else
	{
		printf("setsockopt() success\n");
		ret = 0;
	}

	return ret;
}

int get(int cmd, int sockfd){
	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:
			printf("debug level=%d\n", *(int *)val);
			break;
		case CMD_RULE:
			printRuleTable((RuleTable *)val);
			break;
		}
	}
	return ret;
}

int main(int argc, char** argv){
	int ret = -1;
	int cmd;				//记录用户输入的命令的值
	char val[sizeof(Rule)];	//存储用户输入相关数据	规则、debug等级或者规则索引
	int val_len = 0;		//记录相关数据长度
	int get_set = parseArgs(argc, argv, &cmd, &val, &val_len);
	if(get_set){
		int sockfd;
		if((sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW)) == -1){
			printError("socket()");
		}
		else {
			// get_set > 0, 表示需要向内核中发送数据
			if(get_set > 0){
				ret = set(cmd, val, val_len, sockfd);
			}
			else { 
				ret = get(cmd, sockfd);
			}
		}
		close(sockfd);
	}
	else{
		usage(argv[0]);
	}
	return ret;
}

4.2 simpleFw.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/skbuff.h>
#include <net/tcp.h>
#include <linux/netdevice.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>

#include "simpleFw.h"

static struct nf_hook_ops nfhoLocalIn;		//设置NF_INET_LOCAL_IN的hook点函数
static struct nf_hook_ops nfhoLocalOut;		//设置NF_INET_LOCAL_OUT的hook点函数
static struct nf_hook_ops nfhoPreRouting;	//设置NF_INET_PRE_ROUTING的hook点函数
static struct nf_hook_ops nfhoForward;		//设置NF_INET_FORWARD的hook点函数
static struct nf_hook_ops nfhoPostRouting;	//设置NF_INET_POST_ROUTING的hook点函数

static struct nf_sockopt_ops nfhoSockopt;	//设置 Socket Option 的属性

static int debug_level = 0;
static int nfcount = 0;

static Rule *g_rules;			//规则数组的头地址
static int g_rules_cnt = 0;		//记录当前定义的规则数

void addRule(Rule *rule){
	int cnt = g_rules_cnt + 1;	//记录添加一条规则后的规则总数
	Rule *rules_t = (Rule *)vmalloc(cnt*sizeof(Rule));	//开辟一个必规则数量大 1 的空间, 使之能够添加一条规则
	memcpy(rules_t, rule, sizeof(Rule));	//将新的规则先添加到开辟的空间中
	if(g_rules_cnt > 0){
		memcpy(rules_t + 1, g_rules, g_rules_cnt*sizeof(Rule));	//再将原先的规则也添加的新开辟的空间中
		vfree(g_rules);		//释放原先的规则空间
	}
	g_rules = rules_t;	//令规则数组的首地址为新开辟的空间
	g_rules_cnt = cnt;	//更新规则数
}

void delRule(int rule_num){
	int i;
	//判断规则序号是否存在
	if(rule_num>0 && rule_num<g_rules_cnt){
		//存在则将该序号及其以后的规则用下一条规则的内容覆盖
		for(i = rule_num; i < g_rules_cnt; i++){
			memcpy(g_rules+i-1, g_rules+i, sizeof(Rule));
		}
		g_rules_cnt++;	//规则数减 1
	}
}

int matchRule_IP_PROTOCOL(struct iphdr *iph){
	int action = 1;
	int i;
	Rule *r;
	//对规则进行逐一检查, 若存在相应的规则则返回相应的动作
	for (i = 0; i < g_rules_cnt; i++) { 
		r = g_rules + i;
		//若规则中未定义源IP和目的IP, 则默认规则值为0
		if((!r->src_ip || r->src_ip==iph->saddr) &&
		   (!r->dst_ip || r->dst_ip==iph->daddr) &&
		   (!r->protocol || r->protocol==iph->protocol))
		{
			action = r->action;	
			break;
		}
	}
	return action;
}

int matchRule_IP_PORT_PROTOCOL(struct iphdr *iph){
	int action = 1;
	int i;
	Rule *r;
	for (i = 0; i < g_rules_cnt; i++) {
		r = g_rules + i;
		//判断是否符合源、目的地址IP地址
		if((!r->src_ip || r->src_ip == iph->saddr) &&
		   (!r->dst_ip || r->dst_ip == iph->daddr)){
			if(!r->protocol){
				action = r->action;
			}
			else {
				//ICMP协议不需要过滤端口
				if(r->protocol == SPFW_ICMP){
					action = r->action;
					break;
				}
				//对TCP协议的端口过滤
				else if(r->protocol == SPFW_TCP){
					struct tcphdr *tcph = (struct tcphdr *)((unsigned char *)iph + iph->ihl*4);	// 获取TCP头
					if((!r->src_port || r->src_port == ntohs(tcph->source)) &&
					   (!r->dst_port || r->dst_port == ntohs(tcph->dest))){
						action = r->action;
					}
					break;
				}
				//对UDP协议的端口过滤
				else if (r->protocol == SPFW_UDP) {
					struct udphdr *udph = (struct udphdr *)((unsigned char *)iph + iph->ihl*4);	// 获取UDP头
					if((!r->src_port || r->src_port == ntohs(udph->source)) &&
					   (!r->dst_port || r->dst_port == ntohs(udph->dest))){
						action = r->action;
					}
					break;
				}
				//若规则的协议未非TCP、UDP、ICMP其中之一, 则丢弃该数据包
				else{
					action = 0;
					break;
				}
			}
		}
	}
	return action;
}

int whiteListFilterRule(struct iphdr *iph){
	int action = 0;
	int i;
	Rule *r;
	//匹配所有规则, 若存在规则允许数据包通过则允许该数据包通过, 否则丢弃该数据包
	for (i = 0; i < g_rules_cnt; i++) {
		r = g_rules + i;
		//判断是否符合源、目的地址IP地址
		if((!r->src_ip || r->src_ip == iph->saddr) &&
		   (!r->dst_ip || r->dst_ip == iph->daddr)){
			if(!r->protocol){
				action = r->action;
			}
			else {
				//ICMP协议不需要过滤端口
				if(r->protocol == SPFW_ICMP){
					action = r->action;
				}
				//对TCP协议的端口过滤
				else if(r->protocol == SPFW_TCP){
					struct tcphdr *tcph = (struct tcphdr *)((unsigned char *)iph + iph->ihl*4);	// 获取TCP头
					if((!r->src_port || r->src_port == ntohs(tcph->source)) &&
					   (!r->dst_port || r->dst_port == ntohs(tcph->dest))){
						action = r->action;
					}
				}
				//对UDP协议的端口过滤
				else if (r->protocol == SPFW_UDP) {
					struct udphdr *udph = (struct udphdr *)((unsigned char *)iph + iph->ihl*4);	// 获取UDP头
					if((!r->src_port || r->src_port == ntohs(udph->source)) &&
					   (!r->dst_port || r->dst_port == ntohs(udph->dest))){
						action = r->action;
					}
				}
			}
		}
		if(action == 1)
			break;
	}
	return action;
}

void setDebug_Level(int level){
	debug_level = level;
}

void debugInfo(char *msg)
{
	if (debug_level)
	{
		nfcount++;
		printk("%s, nfcount: %d\n", msg, nfcount);
	}
}


unsigned int hookLocalIn(void *priv,
						 struct sk_buff *skb,
						 const struct nf_hook_state *state)
{
	unsigned int ret = NF_ACCEPT;
	//将数据包结构体skb转换成struct iphdr
	struct iphdr *iph = ip_hdr(skb);
	//与规则逐一匹配, 判断规则是否拦截该数据包, 默认数据包不被拦截
	//黑名单过滤方式
	if(matchRule_IP_PORT_PROTOCOL(iph) <= 0){
		printk("NF_INET_LOCAL_IN过滤了数据包");
		ret = NF_DROP;
	}
	debugInfo("hookLocalIn");
	return ret;
}

unsigned int hookLocalOut(void *priv,
						 struct sk_buff *skb,
						 const struct nf_hook_state *state)
{
	unsigned int ret = NF_DROP;
	//将数据包结构体skb转换成struct iphdr
	struct iphdr *iph = ip_hdr(skb);
	//与规则逐一匹配, 判断规则是否拦截该数据包, 默认数据包不被拦截
	//白名单过滤方式
	if(whiteListFilterRule(iph) >= 1){
		printk("NF_INET_LOCAL_OUT允许通过了数据包");
		ret = NF_ACCEPT;
	}
	debugInfo("hookLocalOut");
	return ret;
}

unsigned int hookPreRouting(void *priv,
						 struct sk_buff *skb,
						 const struct nf_hook_state *state)
{
	unsigned int ret = NF_ACCEPT;
	struct iphdr *iph = ip_hdr(skb);
	//判断IP数据包是否为IPv4版本数据包, 不是则丢弃
	if(iph->version != 4){
		printk("NF_INET_PRE_ROUTING过滤了数据包");
		ret = NF_DROP;	
	}
	//校验 IPv4 数据报头的正确性, 不正确则丢弃
	if(iph->check){
		if(ip_fast_csum((unsigned char *)iph, iph->ihl)){
			ret = NF_DROP;
		}
	}
	debugInfo("hookPreRouting");
	return ret;
}	

unsigned int hookForward(void *priv,
						 struct sk_buff *skb,
						 const struct nf_hook_state *state)
{
	unsigned int ret = NF_ACCEPT;
	//将数据包结构体skb转换成struct iphdr
	struct iphdr *iph = ip_hdr(skb);
	//与规则逐一匹配, 判断规则是否拦截该数据包, 默认数据包不被拦截
	//黑名单过滤方式
	if(matchRule_IP_PROTOCOL(iph) <= 0){
		printk("NF_INET_FORWARD过滤了数据包");
		ret = NF_DROP;
	}
	debugInfo("hookForwoad");
	return ret;
}

unsigned int hookPostRouting(void *priv,
						 	 struct sk_buff *skb,
						 	 const struct nf_hook_state *state)
{
	debugInfo("hookPostRouting");
	return NF_ACCEPT;
}

int hookSockoptSet(struct sock *sock,
				   int cmd,
				   sockptr_t userPtr,
				   unsigned int len)
{
	int ret = 0;
	int level;
	Rule r;
	int r_num;

	debugInfo("hookSockoptSet");
	//根据 cmd 的不同,接受不同大小的数据,并执行相应的操作
	switch(cmd){
		case CMD_DEBUG:
			//从用户空间中拷贝接收用户设置的用户等级,即一个整形int数据
			ret = copy_from_user(&level, userPtr.user, sizeof(debug_level));
			setDebug_Level(level);	//执行debug等级修改函数
			printk("set debug level to %d", debug_level);
			break;
		case CMD_RULE:
			//从用户空间中拷贝接收用户设置的规则Rule内容
			ret = copy_from_user(&r, userPtr.user, sizeof(Rule));
			addRule(&r);	//执行添加规则函数
			printk("add rule!");
			break;
		case CMD_RULE_DEL:
			//从用户空间中拷贝接收用户所需要删除的规则序号,一个整形int数据
			ret = copy_from_user(&r_num, userPtr.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:
			//向用户空间发送debug_level
			ret = copy_to_user(user, &debug_level, sizeof(debug_level));
			break;
		case CMD_RULE:
			//向用户空间发送规则条数
			ret = copy_to_user(user, &g_rules_cnt, sizeof(g_rules_cnt));
			//向用户空间发送规则数组(注意:由于规则条数发送过一次数据,所以再次发送数据需要添加已发送的数据偏移)
			ret = copy_to_user(user+sizeof(g_rules_cnt), g_rules, g_rules_cnt*sizeof(Rule));
			break;
	}

	if(ret != 0){
		ret = -EINVAL;
		debugInfo("copy_to_user error");
	}

	return ret;
}

int init_module(){
	//将 hookLocalIn 函数注册到 NF_INET_LOCAL_IN 的 hook 钩子点
	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);
	//将 hookLocalOut 函数注册到 NF_INET_LOCAL_OUT 的 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);
	//将 hookPreRouting 函数注册到 NF_INET_PRE_ROUTING 的 hook 钩子点
	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);
	//将 hookForward 函数注册到 NF_INET_FORWARD 的 hook 钩子点
	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);
	//将 hookPostRouting 函数注册到 NF_INET_POST_ROUTING 的 hook 钩子点
	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
	nfhoSockopt.pf = 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("simpleFw started!\n");
	return 0;
}

void cleanup_module(){
	nf_unregister_net_hook(&init_net, &nfhoLocalIn);
	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("simpleFw stopped!\n");
}

MODULE_LICENSE("GPL");
MODULE_AUTHOR("o_o'"); 
MODULE_DESCRIPTION("It's a simple software filter firewall!"); 
MODULE_VERSION("0.0.1"); 

5. 测试效果

  • simpleFwctl.c
gcc simpleFwctl.c -o simpleFwctl

image-20230505202956744

  • simpleFw编译安装防火墙过滤模块
make
sudo insmod simpleFw.ko
lsmod

image-20230505203109827

查看防火墙过滤规则Rule和debug_level,并设置debug_level为1

image-20230505203356267

在为添加任何规则的情况下,我们尝试使用Linux系统ping主机Windows系统,发现无法ping通。相反地,我们使用windows系统pingLinux系统,发现同样也无法ping通。

在此我们可以进行初步的判定是由于检查点NF_INET_LOCAL_OUT的白名单过滤导致,Linux系统中无法发出任何数据包导致系统双方无法ping通。通过wireshark抓包,可以发现windows系统确实将数据包发送出去了。

注:由于使用的是wsl2子系统的linux,主机pingLinux时,主机的数据包会经过一个网关发送给Linux,Linux的回应数据包也是发送到网关处,故主机的数据包的源地址会被修改为相应的网关地址。

image-20230505203851845

image-20230505203903936

image-20230505204558082

此时我们向防火墙中加入允许双方向windows10(10.3.160.48)发送ICMP协议数据包的规则,再使用windows10尝试是否能够pingLinux系统。image-20230505204517414

image-20230505215705446

发现Windows10主机已经能够成功ping通Linux系统,但是Linux系统仍然无法ping通Windows10主机,通过wireshark抓包发现Linux系统ping主机Windows的ICMP数据包并没有发出,可以推测是被检查点NF_INET_LOCAL_OUT过滤(原因是,目的地址为10.3.160.48并没有加入规则中)。

将相应目的地址允许通过加入规则中,可以发现Linux也成功ping通主机Windows10

image-20230505220054179

image-20230505220132496

查看此时的防火墙系统的规则

image-20230505220216892

接下来,测试检查点NF_INET_LOCAL_IN的过滤效果,我们向规则中添加过滤来自Window10主机的数据包的规则。尝试使用Windows10主机pingLinux系统。

sudo ./simpleFwctl rule add 172.23.144.1 any 172.23.157.70 any ICMP r

image-20230505220512184

image-20230505220641355

image-20230505220626661

通过wireshark抓包发现,windows10主机ping命令的数据包确实已经发送,但是却无法ping通Linux系统,通过查看Linux内核输出缓冲区,可以发现,确实存在四个数据包被过滤了。

image-20230505220724344

但是此时使用Linux系统pingwindows10主机,是能够ping通的,原因是因为Windows10回应的数据包的源IP地址不是172.23.144.1,不会被检查点NF_INET_LOCAL_IN过滤。

image-20230505221010271

image-20230505221424828

在添加来自10.3.160.48的数据包过滤规则,再尝试使用Linux系统pingWindows10主机。

sudo ./simpleFwctl rule add 10.3.160.48 any 172.23.157.70 any ICMP r

image-20230505221916233
在这里插入图片描述

image-20230505221952177

可见Linux发送ICMP数据确实已经发出,但是来自Windows10操作系统的数据包被Linux的防火墙过滤。

由此可知,我们所设计的防火墙能够实现基本的过滤功能。

开发防火墙基本步骤

基于Netfilter框架编写内核防火墙模块和用户态防火墙控制程序,可以实现服务协议、IP地址、端口等的控制和过滤。下面是一个简单的实现步骤:

  1. 编写内核防火墙模块:创建一个内核模块,使用Netfilter框架的hook函数拦截网络数据包,并根据需要进行过滤、修改或重定向。可以使用Netfilter框架提供的五个hook点(PREROUTING、INPUT、FORWARD、OUTPUT、POSTROUTING)来控制不同阶段的数据包处理。
  2. 实现用户态防火墙控制程序:创建一个用户态程序,用于配置内核防火墙模块的规则和策略。该程序可以使用Netlink套接字来与内核防火墙模块进行通信,并通过发送命令来添加、删除或修改规则。
  3. 实现规则匹配和过滤:内核防火墙模块和用户态防火墙控制程序需要共同实现规则匹配和过滤的功能。例如,当接收到一个数据包时,内核防火墙模块需要将其与用户态防火墙控制程序中的规则进行匹配,并根据规则进行相应的处理。
  4. 实现日志记录:在防火墙模块中,可以添加日志记录功能,记录被过滤或拒绝的数据包,以便后续分析和调试。
  5. 测试和调试:在完成防火墙模块和防火墙控制程序的编写后,需要进行测试和调试。可以使用各种网络工具和测试工具来模拟网络流量,并验证防火墙的规则和策略是否正确。

需要注意的是,基于Netfilter框架编写防火墙模块和控制程序需要具有一定的内核编程和网络知识,同时还需要考虑安全性和性能等方面的问题。因此,在实现防火墙功能时,需要谨慎考虑各种因素,并严格遵循最佳实践。

Netfilter的5个钩子(hook)点

Netfilter框架提供了五个不同的hook点(也称为"hook函数"),用于处理网络数据包。这些hook点可以在不同的网络协议栈层上进行注册,以便在网络数据包经过该层时拦截和处理数据包。

下面是五个hook点的作用:

  1. NF_INET_PRE_ROUTING:此hook点位于网络协议栈的最顶层,可以用来处理接收到的数据包。例如,可以在此hook点上进行基于源地址的过滤、端口转发等操作。
  2. NF_INET_LOCAL_IN:此hook点位于协议栈中的IP层,用于处理本地主机上的数据包。例如,可以在此hook点上进行基于源地址的过滤、基于服务协议进行过滤或在本地主机上安装代理服务器。
  3. NF_INET_FORWARD:此hook点位于IP层,用于处理通过本地主机的数据包转发。例如,可以在此hook点上进行基于源地址目标地址的过滤、基于服务协议进行过滤或进行数据包的NAT转换。
  4. NF_INET_LOCAL_OUT:此hook点位于协议栈的IP层,用于处理本地主机发送的数据包。例如,可以在此hook点上进行基于目的地址的过滤或进行数据包的NAT转换。
  5. NF_INET_POST_ROUTING:此hook点位于网络协议栈的最底层,用于处理发送到网络的数据包。例如,可以在此hook点上进行基于目标地址的过滤或进行SNAT转换。

通过使用这些hook点,可以灵活地控制网络数据包的流动,并在需要时拦截和处理数据包。

网络编程-包过滤防火墙简单实现 - PIAOMIAO1 - 博客园 (cnblogs.com)

Netfilter框架代码实现IP过滤

示例代码

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/ip.h>

static struct nf_hook_ops nfho;
static char *allowed_ip = "192.168.0.1";

static unsigned int hook_func(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
{
    struct iphdr *iph;
    char *ip_str;
    
    iph = ip_hdr(skb);
    if (iph->protocol == IPPROTO_TCP)
    {
        ip_str = (char *)&iph->saddr;
        if (strcmp(ip_str, allowed_ip) != 0)
        {
            printk(KERN_INFO "Dropping packet from %pI4\n", &iph->saddr);
            return NF_DROP;
        }
    }
    return NF_ACCEPT;
}

static int __init init_nf_module(void)
{
    nfho.hook = hook_func;
    nfho.pf = PF_INET;
    nfho.hooknum = NF_INET_LOCAL_IN;
    nfho.priority = NF_IP_PRI_FIRST;

    nf_register_hook(&nfho);
    return 0;
}

static void __exit exit_nf_module(void)
{
    nf_unregister_hook(&nfho);
}

module_init(init_nf_module);
module_exit(exit_nf_module);

MODULE_LICENSE("GPL");

Netfilter框架的钩子点注册使用

#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>

static struct nf_hook_ops nfho1;
static struct nf_hook_ops nfho2;

static unsigned int my_hook_func1(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
{
    // 第一个 hook 处理函数的具体实现
    return NF_ACCEPT;
}

static unsigned int my_hook_func2(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
{
    // 第二个 hook 处理函数的具体实现
    return NF_ACCEPT;
}

static int __init my_init(void)
{
    nfho1.hook = my_hook_func1;
    nfho1.pf = PF_INET;
    nfho1.hooknum = NF_INET_POST_ROUTING;
    nfho1.priority = NF_IP_PRI_FIRST;

    nfho2.hook = my_hook_func2;
    nfho2.pf = PF_INET;
    nfho2.hooknum = NF_INET_POST_ROUTING;
    nfho2.priority = NF_IP_PRI_LAST;

    nf_register_hook(&nfho1);
    nf_register_hook(&nfho2);

    return 0;
}

static void __exit my_exit(void)
{
    nf_unregister_hook(&nfho1);
    nf_unregister_hook(&nfho2);
}

module_init(my_init);
module_exit(my_exit);

附录

struct sk_buff结构体

回到原处

struct sk_buff是Linux内核网络协议栈中的一个关键数据结构,代表了一个网络数据包。在数据包在网络协议栈中传输时,它会被存储在一个 struct sk_buff 结构体中,然后在不同的网络协议层之间传递。

struct sk_buff 包含了很多元数据信息,如数据包的协议类型、源地址、目的地址、传输层协议(如TCP、UDP)、数据包负载等等,同时还包含了用于在协议栈中传递数据包的指针和锁等。

开发者可以通过 struct sk_buff 结构体的成员变量来访问数据包中的各个字段信息,如 skb->protocol 访问协议类型、skb->ip_summed 访问校验和等等。

以下是一个简单的 struct sk_buff 的定义示例:

arduinoCopy codestruct sk_buff {
    struct sk_buff *next;
    struct sk_buff *prev;
    ktime_t tstamp;
    struct sock *sk;
    struct net_device *dev;
    char *head, *data;
    unsigned int len, data_len;
    unsigned int truesize;
    atomic_t users;
    unsigned char protocol;
    /* more fields... */
};

在此示例中,struct sk_buff 包含了一些关键的成员变量,如:

  • nextprev:双向链表指针,用于连接在接收队列中的数据包。
  • tstamp:时间戳,用于记录数据包接收或发送时间。
  • sk:指向协议控制块的指针。
  • dev:指向接收或发送数据包的网络设备的指针。
  • headdata:指向数据包缓冲区的指针,其中 head 指向缓冲区的起始地址,而 data 则指向有效负载数据的起始地址。
  • lendata_len:数据包的总长度和有效负载的长度。
  • truesize:数据包占用内存的实际大小。
  • users:引用计数器,用于跟踪 struct sk_buff 的使用情况。
  • protocol:协议类型,如 ETH_P_IP 表示IPv4协议,ETH_P_IPV6 表示IPv6协议等等。

struct nf_hook_state结构体

回到原处

struct nf_hook_state是Netfilter框架中用于表示hook调用状态的结构体。它在Netfilter的hook函数中作为参数传递,用于记录当前hook调用的状态和传递信息。该结构体的定义如下:

cCopy codestruct nf_hook_state {
    struct sk_buff *skb;  /* The socket buffer being considered */
    unsigned int hook;    /* Netfilter hook point */
    struct net_device *in;   /* Input device */
    struct net_device *out;  /* Output device */
    const void *targinfo;    /* Target-specific information */
    struct nf_conntrack *ct; /* Connection tracking state */
    enum ip_conntrack_info ctinfo; /* Status of the connection */
};

其中,成员变量的含义如下:

  • skb:指向当前处理的sk_buff结构体。
  • hook:当前hook的位置,即hook点。
  • in:输入网络设备的指针。
  • out:输出网络设备的指针。
  • targinfo:指向目标的信息。
  • ct:连接跟踪状态的指针。
  • ctinfo:连接的状态信息。

在hook函数中,可以根据struct nf_hook_state结构体中的信息进行过滤、修改等操作,然后通过返回值来决定是否将该数据包传递给下一个hook函数或目标处理函数。

struct nt_sockopt_ops结构体

回到原处

struct nf_sockopt_ops是Netfilter框架中用于处理套接字选项的结构体。该结构体包含一些函数指针,用于实现对套接字选项的处理。在注册Netfilter套接字选项时,需要创建一个struct nf_sockopt_ops结构体并填充相应的函数指针,然后通过nf_register_sockopt函数将其注册到Netfilter框架中。该结构体的定义如下:

cCopy codestruct nf_sockopt_ops {
    int family;                         /* Address family */
    int (*set_optmin)(void);            /* Minimum setsockopt() option */
    int (*set_optmax)(void);            /* Maximum setsockopt() option */
    int (*set)(struct sock *, int cmd, void __user *user, unsigned int len); /* Handler for setsockopt() */
    int (*get_optmin)(void);            /* Minimum getsockopt() option */
    int (*get_optmax)(void);            /* Maximum getsockopt() option */
    int (*get)(struct sock *, int cmd, void __user *user, int *len); /* Handler for getsockopt() */
};

其中,成员变量的含义如下:

  • family:套接字地址族。
  • set_optmin:支持的setsockopt最小选项值。
  • set_optmax:支持的setsockopt最大选项值。
  • setsetsockopt处理函数指针。
  • get_optmin:支持的getsockopt最小选项值。
  • get_optmax:支持的getsockopt最大选项值。
  • getgetsockopt处理函数指针。

通过填充struct nf_sockopt_ops结构体中的成员变量,可以实现自定义的套接字选项的处理。在处理函数中,可以通过void __user *user参数获取用户空间传递的选项值,并对其进行相应的处理。

struct iphdr结构体

struct iphdr 是 Linux 内核中用于表示 IPv4 数据报头的结构体,它的定义可以在 <linux/ip.h> 头文件中找到。

下面是 struct iphdr 结构体的成员变量:

rustCopy codestruct iphdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
    __u8    ihl:4,
            version:4;
#elif defined (__BIG_ENDIAN_BITFIELD)
    __u8    version:4,
            ihl:4;
#else
#error  "Please fix <asm/byteorder.h>"
#endif
    __u8    tos;
    __u16   tot_len;
    __u16   id;
    __u16   frag_off;
    __u8    ttl;
    __u8    protocol;
    __u16   check;
    __be32  saddr;
    __be3	2  daddr;
    /*The options start here. */
};

下面是每个成员变量的含义:

  • ihl: 该字段指示 IPv4 头部的长度,以 32 位字为单位计算。通常取值为 5,表示 IPv4 头部为 20 字节。如果头部中包含选项,则该字段的值会增加。
  • version: 该字段指示 IPv4 协议版本号,通常为 4。
  • tos: 该字段指示服务类型,包括优先级、延迟、吞吐量、可靠性等方面。
  • tot_len: 该字段指示整个 IPv4 数据报的长度,包括头部和数据部分。
  • id: 该字段指示数据报的标识符,用于 IP 分片和重组。
  • frag_off: 该字段用于 IP 分片和重组,指示分片的偏移量和分片标志。
  • ttl: 该字段指示生存时间,表示该数据报可以经过的路由器的最大数量。
  • protocol: 该字段指示上层协议类型,例如 TCP、UDP 或 ICMP。
  • check: 该字段用于校验 IPv4 数据报头的正确性,通常在发送数据报时自动计算并填充。
  • saddr: 该字段指示源 IP 地址。
  • daddr: 该字段指示目标 IP 地址。

需要注意的是,IPv4 头部的长度 ihl 和整个数据报的长度 tot_len 都以 32 位字为单位计算。这意味着,tot_len 包括头部和数据部分的总长度,而 ihl 只包括头部的长度。

  • 2
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值