浅谈Linux内核与用户通信(基于netlink):以基于netfilter的防火墙为例

发布于 2020-11-30  154 次阅读


本文防火墙部分参考了《鸟哥》,netlink部分参考了这个文档

0.前序知识:netfilter防火墙

Netfilter/IPTables是Linux2.4.x之后新一代的Linux防火墙机制,是linux内核的一个子系统。Netfilter采用模块化设计,是Linux内核中的一个模块,具有良好的可扩充性。其重要工具模块IPTables从用户态的iptables连接到内核态的Netfilter的架构中,提供接口给用户进行防火墙操作,并允许使用者对数据报进行过滤、地址转换、处理等操作。

iptables 的表格与相关链示意图

iptables有许多的table,其中每个表格与其中链的用途分别是这样的:

  • filter (过滤器):主要跟进入 Linux 本机的封包有关,这个是预设的 table 喔!
    • INPUT:主要与想要进入我们 Linux 本机的封包有关;
    • OUTPUT:主要与我们 Linux 本机所要送出的封包有关;
    • FORWARD:这个咚咚与 Linux 本机比较没有关系, 他可以『转递封包』到后端的计算机中,与下列 nat table 相关性较高。
  • nat (地址转换):是 Network Address Translation 的缩写, 这个表格主要在进行来源与目的之 IP 或 port 的转换,与 Linux 本机较无关,主要与 Linux 主机后的局域网络内计算机较有相关。
    • PREROUTING:在进行路由判断之前所要进行的规则(DNAT/REDIRECT)
    • POSTROUTING:在进行路由判断之后所要进行的规则(SNAT/MASQUERADE)
    • OUTPUT:与发送出去的封包有关
  • mangle (破坏者):这个表格主要是与特殊的封包的路由旗标有关, 早期仅有 PREROUTING 及 OUTPUT 链,不过从 kernel 2.4.18 之后加入了 INPUT 及 FORWARD 链。 由于这个表格与特殊旗标相关性较高,所以像咱们这种单纯的环境当中,较少使用 mangle 这个表格。

基于filter和nat两个表可以实现本机的单机防火墙,或是专门的nat防火墙。

但是配置的规则和防火墙的日志都在内核(netfilter中),那么我们要如何才能得到日志和已经配置的规则呢?

这就需要netlink实现用户态和内核态之间的通信得到这些信息。

netlink的相关信息

netlink是什么?

Netlink是Linux特有的一个socket,其是用以实现用户进程与内核进程通信的一种特殊的进程间通信(IPC) 。

Netlink传递信息的格式

netlink传递的信息有两个部分,分别为:netlink信息头信息payload

Netlink Message Header
netlink信息头内容如上

netlink的head存入了如下信息:

总长度(32位):

消息的总长度(以字节为单位),包括netlink消息头。

讯息类型(16位):

消息类型指定消息承载的有效负载类型。Netlink协议定义了几种标准消息类型。每个协议族都可以定义其他消息类型。有关其他信息,请参见 消息类型

消息标志(16位):

消息标志可用于修改消息类型的行为。有关标准消息标志的列表,请参见消息标志部分。

序列号(32位):

序列号是可选的,可用于允许引用先前的消息,例如错误消息可以引用导致错误的原始请求。

端口号(32位):

端口号指定了将消息传递到的对等方。如果未指定,则消息将传递到相同协议系列的第一个匹配内核侧套接字

信息payload可以由任意数据组成,但通常包含一个固定大小的特定于协议的头,后面跟着一个属性流。

如何使用Netlink?

1.关于接收数据

netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg)
{
        return __netlink_kernel_create(net, unit, THIS_MODULE, cfg);
}

其中,netlink_kernel_create的第一个参数net一般使用&init_net这个全局量;第二个参数unit可使用include/uapi/linux/netlink.h文件中以NETLINK_开头的宏定义,其对应Netlink支持的协议类型,也可以是自定义的类型(可自行添加相应的宏定义,等会的例子里会使用),而cfg则是netlink配置的结构体。

cfg结构体定义如下:

/* optional Netlink kernel configuration parameters */
struct netlink_kernel_cfg {
        unsigned int    groups;
        unsigned int    flags;
        void            (*input)(struct sk_buff *skb);
        struct mutex    *cb_mutex;
        void            (*bind)(int group);
        bool            (*compare)(struct net *net, struct sock *sk);
};

其中input为一个指针,指向一个回调函数。当socket建立之后,就开始监听对应协议的数据,当收到数据时,就可以调用上面的input回调函数,其参数skb为接收到的 sk_buff 结构体。

我们可以用nlmsg_hdr函数从skb 获得指向当前信息头的指针。

static inline struct nlmsghdr *nlmsg_hdr(const struct sk_buff *skb)
{
        return (struct nlmsghdr *)skb->data;
}

通过如下的宏,我们可以完成数据处理的操作

#define NLMSG_ALIGNTO   4U
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )
#define NLMSG_HDRLEN     ((int) NLMSG_ALIGN(sizeof(struct nlmsghdr)))
#define NLMSG_LENGTH(len) ((len) + NLMSG_HDRLEN)
#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))
#define NLMSG_DATA(nlh)  ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))
#define NLMSG_NEXT(nlh,len)      ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len), \
                (struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len)))

#define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr) && \
                (nlh)->nlmsg_len >= sizeof(struct nlmsghdr) && \
                (nlh)->nlmsg_len <= (len))

#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len)))

其中NLMSG_DATA()的参数就是指向之前得到的信息头的指针,使用这个宏后,能够得到一个指向payload首位的指针。

2.关于发送数据

首先,我们先构建一个skb结构体:

skb_1 = alloc_skb(NLMSG_SPACE(len), GFP_KERNEL);

然后,通过include/net/netlink.h头文件中的如下函数,构建要发送的netlink信息:

static inline struct nlmsghdr *nlmsg_put(struct sk_buff *skb, u32 portid, u32 seq,
                int type, int payload, int flags)
{
        if (unlikely(skb_tailroom(skb) < nlmsg_total_size(payload)))
                return NULL;

        return __nlmsg_put(skb, portid, seq, type, payload, flags);
}

其中这里的portid设置为0(因为是内核)。

之后再使用nlmsg_unicast函数进行单播发送数据(此函数中的portid为接收进程的端口号

static inline int nlmsg_unicast(struct sock *sk, struct sk_buff *skb, u32 portid)
{
        int err;
        err = netlink_unicast(sk, skb, portid, MSG_DONTWAIT);
        if (err > 0)
                err = 0;

        return err;
}

以防火墙为例说明如何实现通信

首先使用配置结构体对netlink进行配置(主要是指定了后面要自己实现的回调函数):

struct netlink_kernel_cfg nkc = {
	.groups = 0,
	.flags = 0,
	.input = netlink_input,
	.cb_mutex = NULL,
	.bind = NULL,
	//nkc.unbind = NULL;
	.compare = NULL
};

通过回调函数解析内核发来的信息,并将对应的信息(防火墙规则,nat规则,日志)打印出来,完成了内核态与用户态间的通信。让用户态上运行的用户程序可以得到内核中的信息。

static void netlink_input(struct sk_buff *__skb)
{
	int i;
    struct sk_buff *skb;
    char str[1000];
	char buff[20], buff2[20];
    struct nlmsghdr *nlh;
    if( !__skb ) {
        return;
    }
    skb = skb_get(__skb);
    if( skb->len < NLMSG_SPACE(0)) {
        return;
    }
    nlh = nlmsg_hdr(skb);
    memset(str, 0, sizeof(str));
    memcpy(str, NLMSG_DATA(nlh), 1000);
	//user_process.pid = nlh->nlmsg_pid;
	switch (str[0])
	{
	case 0:
		//flush rules
		rnum = str[1];
		memcpy(rules, str + 2, rnum * sizeof(Rule));
		for(i = 0; i < rnum; i++){
			printk("\n\n\n\n\nrnum:%d\n", i);
			printk("src_ip:%s\n", rules[i].src_ip);
			printk("dst_ip:%s\n", rules[i].dst_ip);
			printk("src_port:%d\n", rules[i].src_port);
			printk("dst_port:%d\n", rules[i].dst_port);
			printk("protocol:%d\n", rules[i].protocol);
			printk("log:%d\n", rules[i].log);
			printk("action:%d\n", rules[i].action);
		}
		break;
	case 1:
		//flush nat rules
		nnum = str[1];
		memcpy(&net_ip, str + 2, sizeof(unsigned));
		memcpy(&net_mask, str + 6, sizeof(unsigned));
		memcpy(&firewall_ip, str + 10, sizeof(unsigned));
		memcpy(&natTable[1], str + 14, nnum * sizeof(NatEntry));
		natTable[0].firewall_port = 30001;
		natTable[0].nat_port = 30001;
		natTable[0].nat_ip = ipstr_to_num("192.168.1.1");
		nnum++;
		printk("\n\n\n\n\nglobal_fireip:%s\nnet_ip:%s\nnet_mask:%x\n", addr_from_net(buff,ntohl(firewall_ip)), addr_from_net(buff2,ntohl(net_ip)), net_mask);
		for(i = 0; i < nnum; i++){
			printk("nnum:%d\n", i);
			printk("nat_ip:%s\nfirewall_port:%u\nnat_port:%u\n", addr_from_net(buff2, natTable[i].nat_ip), natTable[i].firewall_port, natTable[i].nat_port);
		}
		break;
	case 2:
		//get logs		
		for(i = 0; i < lnum; i++){
			printk("\n\n\n\n\nlnum:%d\n", i);
			printk("src_ip:%s\n", addr_from_net(buff,ntohl(logs[i].src_ip)));
			printk("dst_ip:%s\n", addr_from_net(buff,ntohl(logs[i].dst_ip)));
			printk("src_port:%hu\n", logs[i].src_port);
			printk("dst_port:%hu\n", logs[i].dst_port);
			printk("protocol:%hhu\n", logs[i].protocol);
			printk("action:%hhu\n", logs[i].action);
		}
		netlink_send(nlh->nlmsg_pid, (uint8_t *)logs, lnum * sizeof(Log));
		break;
	case 3:
		//get connections
		UpdateHashList();
		for(i = 0; i < cnum; i++){
			printk("\n\n\n\n\nlnum:%d\n", i);
			printk("src_ip:%s\n", addr_from_net(buff,ntohl(cons2[i].src_ip)));
			printk("dst_ip:%s\n", addr_from_net(buff,ntohl(cons2[i].dst_ip)));
			printk("src_port:%hu\n", cons2[i].src_port);
			printk("dst_port:%hu\n", cons2[i].dst_port);
			printk("protocol:%hhu\n", cons2[i].protocol);
		}
		netlink_send(nlh->nlmsg_pid, (uint8_t *)cons2, cnum * sizeof(Connection));
	default:
		break;
	}
    return;
}

基于单播函数封装了发出信息的过程:

static void netlink_send(int pid, uint8_t *message, int len)
{
    struct sk_buff *skb_1;
    struct nlmsghdr *nlh;
    if(!message || !nl_sk) {
        return;
    }
    skb_1 = alloc_skb(NLMSG_SPACE(len), GFP_KERNEL);
    if( !skb_1 ) {
        printk(KERN_ERR "alloc_skb error!\n");
    }
    nlh = nlmsg_put(skb_1, 0, 0, 0, len, 0);
    NETLINK_CB(skb_1).portid = 0;
    NETLINK_CB(skb_1).dst_group = 0;
    memcpy(NLMSG_DATA(nlh), message, len);
    netlink_unicast(nl_sk, skb_1, pid, MSG_DONTWAIT);
}

你好哇!欢迎来到雷公马碎碎念的地方:)