1 Netfilter/iptables
1.1 Netfilter/iptables概述
Netfilter实际上为linux开发的第三代网络防火墙,其之前的版本与linux内核版本对应的关系如表1所示:
内核版本 |
Linux 防火墙版本 |
Kernel-2.0.x |
Ipfwadm |
Kernel-2.2.x |
Ipchains |
Kernel-2.4.x/Kernel-2.6.x |
Netfilter/Iptables |
表1:linux内核与linux防火墙之间的关系
Netfilter的实现采用高度的模块化结构,内核开发者可以很容易的基于Netfilter提供的架构实现定制模块,完成需要的功能。2.4之后netfilter开发了iptables用户控制工具,用户通过iptables连接到内核态的netfilter处理架构,完成网络数据包的过滤、修改、NAT及其他复杂的功能。
Netfilter源码结构如表2所示:(linux内核版本:2.6.32)
项目名称 |
位置 |
Netfilter主文件 |
/net/netfilter/ |
Netfilter主头文件 |
/linux/net/netfilter/ |
Netfilter Ipv4相关文件 |
/net/ipv4/netfilter/ |
Netfilter Ipv6相关文件 |
/net/ipv6/netfilter/ |
Linux2.6.14之后,Netfilter在架构设计上做了比较的改变,其希望Netfilter的模块与协议是无关的。其实现了部分模块与协议无关,这些模块存放在/net/netfilter/目录下,一般以xt_前缀开头。
1.2 Netfilter/iptables总体架构
Netfilter/iptables 通过一系列的表、链实现规则的管理,表、链相当于Netfilter规则的数据库,Netfilter通过该数据库的规则完成数据包的匹配、修改、NAT等功能。Iptables功能模块通过修改内存中的表、链中的规则已完成其对内核中Netfilter的控制。
Netfilter/iptables总体架构主要分为以下几部分:
Ø Netfilter HOOK
Ø Netfilter /Iptables 基础模块
Ø 基于Netfilter实现具体功能模块
1.2.1 Netfilter hook
Netfilter最为核心的机制就是对外提供了五个Hook点,分别为PREROUTING、INPUT、FORWARD、OUTPUT、POSTROUTING,其实现与具体协议无关。具体功能模块一般都讲处理模块挂载到某个或某几个Hook点上完成相应数据包的处理工作。例如,iptables filter内核功能模块,该模块分别挂载到了INPUT、OUTPUT、FORWARD三个hook点上,Netfilter通过该模块配合iptables工具下发的管理规则完成数据包的具体处理工作。
图1展示了Netfilterhook在linux网络协议栈的具体位置
图1 netfilter hook挂载位置
对照图1,简单介绍一下数据包在netfilter中各个HOOK点的处理流程。
数据报从进入系统,进行IP校验以后,首先经过第一个HOOK点 NF_IP_PRE_ROUTING进行处理;然后就进入路由代码,其决定该数据报是需要转发还是发给本机的;若该数据报是发被本机的,则该数据经过HOOK点NF_IP_LOCAL_IN处理以后然后传递给上层协议;若该数据报应该被转发则它被NF_IP_FORWARD处理;经过转发的数据报经过最后一个HOOK点NF_IP_POST_ROUTING处理以后,再传输到网络上。本地产生的数据经过HOOK点NF_IP_LOCAL_OUT 处理后,进行路由选择处理,然后经过NF_IP_POST_ROUTING处理后发送出去。
1.2.2 Netfilter/iptables 基础模块
基于Netfilter提供的基础架构,iptables在内核中实现了四种模块分别为:filter、mangle、nat、raw。其主要完成hook处理函数的实现和注册工作。Linux-2.6.x内核中其主要对应于iptable_filter.c、iptables_mangle.c、iptables_nat.c、iptables_raw.c四个文件。
1.2.3具体功能模块
Linux -2.6.32 下Netfilter/iptables提供的功能模块主要如下:
Ø 数据包过滤模块(filter)
Ø 数据包修改模块(mangle)
Ø 网络地址转换模块(nat)
Ø 数据包穿越防火墙加速模块(raw)
Ø 链接跟踪模块(connection track)
Ø … …
1.3 Netfilter 模块扩展方式
Netfilter实现的基础架构是协议和功能无关的,其主要就是在linux网络协议栈中挂载了五个HOOK处理点。开发人员可以根据具体的功能需求,编写不同功能HOOK处理函数,然后将其挂载到Netfilter相应的HOOK点上,当数据包流经改HOOK点时,相应的HOOK函数就会被回调并执行一系列的定义好的处理流程。
下面主要介绍一下netfilter模块扩展的基本方式:
1.3.1 nf_hook_ops
首先介绍一下netfilter hook机制的核心数据结构:nf_hook_ops(/include/linux/netfilter.h)
struct nf_hook_ops { struct list_head list; /* User fills in from here down. */ nf_hookfn *hook;/*hook function*/ struct module *owner; u_int8_t pf; unsigned int hooknum;/*hook */ /* Hooks are ordered in ascending priority. */ int priority; }; |
下面对nf_hook_ops成员具体介绍一下:
l hooknum 代表netfilter中五个hook点,其定义在(/include/linux/netfilter.h)中
enum nf_inet_hooks { NF_INET_PRE_ROUTING, NF_INET_LOCAL_IN, NF_INET_FORWARD, NF_INET_LOCAL_OUT, NF_INET_POST_ROUTING, NF_INET_NUMHOOKS }; |
l nf_hookfn *hook,为hook处理函数的的指针,其原型定义在(/include/linux/netfilter.h)
typedef unsigned int nf_hookfn(unsigned int hooknum, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)); |
Ø struct sk_buff *skb sk_buff表示linux内核中数据包的缓冲结构,网卡在接收到一个数据包之后,就会将数据缓存到sk_buff结构中,然后将其递送给网络协议栈,之后在协议栈的整个处理流程中基本上都是围绕sk_buff展开的。该结构具体定义在(/include/linux/skbuff.h)中。
Ø const struct net_device *in,表示输入设备,即网络数据包进入网络协议栈时的网卡设备。
Ø const struct net_device *out, 表示输出设别,即网络数据包离开网络协议栈输出时的网卡设备。
Hook函数完成处理后,必须返回特定的值,该值定义在(/include/linux/netfilter.h)
/* Responses from hook functions. */ #define NF_DROP 0 #define NF_ACCEPT 1 #define NF_STOLEN 2 #define NF_QUEUE 3 #define NF_REPEAT 4 #define NF_STOP 5 #define NF_MAX_VERDICT NF_STOP |
Ø NF_DROP(0):丢弃此数据报,禁止包继续传递,不进入此后的处理流程;
Ø NF_ACCEPT(1):接收此数据报,允许包继续传递,直至传递到链表最后,而进入okfn函数;以上两个返回值最为常见
Ø NF_STOLEN(2):数据报被筛选函数截获,禁止包继续传递,但并不释放数据报的资源,这个数据报及其占有的sk_buff仍然有效(e.g. 将分片的数据报一一截获,然后将其装配起来再进行其他处理);
Ø NF_QUEQUE(3):将数据报加入用户空间队列,使用户空间的程序可以直接进行处理;
Ø NF_REPEAT(4):再次调用当前这个HOOK的筛选函数,进行重复处理。
Ø NF_STOP(5):2.6内核中的NF动作增加了NF_STOP,功能和NF_ACCEPT类似但强于NF_ACCEPT,一旦挂接链表中某个hook节点返回NF_STOP,该skb包就立即结束检查而接受,不再进入链表中后续的hook节点,而NF_ACCEPT则还需要进入后续hook点检查。
l priority,该值越小,优先级越高,其定义在(/include/linux/netfilter_ipv4.h)
enum nf_ip_hook_priorities { NF_IP_PRI_FIRST = INT_MIN, NF_IP_PRI_CONNTRACK_DEFRAG = -400, NF_IP_PRI_RAW = -300, NF_IP_PRI_SELINUX_FIRST = -225, NF_IP_PRI_CONNTRACK = -200, NF_IP_PRI_MANGLE = -150, NF_IP_PRI_NAT_DST = -100, NF_IP_PRI_FILTER = 0, NF_IP_PRI_SECURITY = 50, NF_IP_PRI_NAT_SRC = 100, NF_IP_PRI_SELINUX_LAST = 225, NF_IP_PRI_CONNTRACK_CONFIRM = INT_MAX, NF_IP_PRI_LAST = INT_MAX, }; |
1.3.2 hook的注册和注销
Netfilter提供了hook函数的注册和注销的函数,(/include/linux/netfilter.h)分别定义如下:
l int nf_register_hook(structnf_hook_ops *reg);
intnf_register_hooks(struct nf_hook_ops *reg, unsigned int n);//完成多个nf_hook_ops注册
l void nf_unregister_hook(structnf_hook_ops *reg);
voidnf_unregister_hooks(struct nf_hook_ops *reg, unsigned int n);//完成多个nf_hook_ops注销
1.4 Netfilter 示例模块
下面简单编写了一个hook处理模块,完成的功能是:禁止ping命令。该hook函数首先会解析数据包的协议类型,如果为imcp数据包就会将其丢弃。
l hook函数定义如下:
static unsigned int nf_test_hook(unsigned int hook, struct sk_buff *pskb, const struct net_device *in, const struct net_device *out, int(*okfn)(struct sk_buff*) ) { struct iphdr *ip; /*Initialization*/ ip = ip_hdr(pskb); switch(ip->protocol) { case IPPROTO_ICMP: { struct icmphdr *icmp = NULL; struct icmphdr icmp_hdr; icmp = skb_header_pointer(pskb, ip->ihl * 4, sizeof(struct icmphdr), &icmp_hdr);//Get icmp header printk(KERN_INFO "src_ip:%d.%d.%d.%d dst_ip:%d.%d.%d.%d\n", NIPQUAD(ip->saddr), NIPQUAD(ip->daddr)); return NF_DROP; } default : return NF_ACCEPT; } return NF_ACCEPT; } |
l nf_hook_ops结构定义如下:
static struct nf_hook_ops nf_test_ops = { .hook = nf_test_hook, .owner = THIS_MODULE, .pf = PF_INET, .hooknum = NF_INET_LOCAL_OUT, .priority = NF_IP_PRI_FIRST, }; |
l Makefile
MODULE_NAME := nf_ping obj-m :=$(MODULE_NAME).o KERNELDIR ?= /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: $(MAKE) -C $(KERNELDIR) M=$(PWD) clean: rm -fr *.ko *.o *.cmd |
l 源文件见压缩包
2 Netlinksocket
Netlinksocket 为内核空间与用户空间之间的通信 IPC机制, 其经常被用来完成IP 网络相关的设置工作,RFC3485对其实现进行详细的介绍。
Netlinksocket 能够利用标准的sockets APIs完成从socket 打开、关闭、传输、接收的操作。例如,系统调用socket:
int socket(int domain, int type, int protocol); |
我们可以通过man socket获得非常详细的关于TCP/IP下的socket系统调用的参数的说明信息。
对于netlinksocket,socket系统调用的三个参数的定义如下:
l domain: PF_NETLINK
l type:SOCK_DGRAM
l protocol:可以是自定义的或系统提供的,其定义在(include/linux/netlink.h)
#define NETLINK_ROUTE 0 /* Routing/device hook */ #define NETLINK_UNUSED 1 /* Unused number */ #define NETLINK_USERSOCK 2 /* Reserved for user mode socket protocols */ #define NETLINK_FIREWALL 3 /* Firewalling hook */ #define NETLINK_INET_DIAG 4 /* INET socket monitoring */ #define NETLINK_NFLOG 5 /* netfilter/iptables ULOG */ #define NETLINK_XFRM 6 /* ipsec */ #define NETLINK_SELINUX 7 /* SELinux event notifications */ #define NETLINK_ISCSI 8 /* Open-iSCSI */ #define NETLINK_AUDIT 9 /* auditing */ #define NETLINK_FIB_LOOKUP 10 #define NETLINK_CONNECTOR 11 #define NETLINK_NETFILTER 12 /* netfilter subsystem */ #define NETLINK_IP6_FW 13 #define NETLINK_DNRTMSG 14 /* DECnet routing messages */ #define NETLINK_KOBJECT_UEVENT 15 /* Kernel messages to userspace */ #define NETLINK_GENERIC 16 /* leave room for NETLINK_DM (DM Events) */ #define NETLINK_SCSITRANSPORT 18 /* SCSI Transports */ #define NETLINK_ECRYPTFS 19
#define MAX_LINKS 32 |
在使用Netlink socket时,通信端点之间通过进程ID进行识别,特别的内核的标识为0。Netfilter socket可以完成消息的单播和多播发送:发送的目的地可以是一个进程PID,或者一个组ID,或者两者之间的结合体。内核定义了一些列的广播组,其目的是为一些特殊的事件发送通知,用户空间的程序可以关注其感兴趣的组ID,内核中这些组的定义位于/include/linux/rtnetlink.h.
相比于ioctl netlink的优势为:内核可以初始化一个传输过程,完成其与用户空间的通信。而ioctl只能被动的回应用户空间的请求。
2.1 Netlink 使用方式
下面通过使用netlink实现的用户空间与内核空间通信的简单例子介绍netlink相关接口函数的使用方式。
该测试demo完成的功能如下:首先,用户空间向内核空间发送一条信息“Hello kernel”,内核在收到以后会向用户空间发送“Hello user ”。
下面介绍用户空间和内核空间程序的编写步骤:
2.1.1 用户空间
l 创建netlink socket
sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST); |
其中,NETLINK_TEST是自定义的,前面介绍了系统定义的一些其他的类型。
l 初始化本地sockaddr_nl结构
//init & set src_addr memset(&src_addr, 0, sizeof(struct sockaddr_nl)); src_addr.nl_family = AF_NETLINK; src_addr.nl_pid = getpid(); /* self pid */ src_addr.nl_groups = 0; /* not in mcast groups */ |
l 将创建的socket_fd与本地socketaddr_nl绑定
(bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr)) |
l 初始化对端socketaddr_nl结构
memset(&dest_addr, 0, sizeof(struct sockaddr_nl)); dest_addr.nl_family = AF_NETLINK; dest_addr.nl_pid = 0; /* For Linux Kernel */ dest_addr.nl_groups = 0; /* unicast */ |
l 初始化netlink消息头
nlh = (struct nlmsghdr*)malloc(sizeof(struct nlmsghdr)); nlh->nlmsg_len = NLMSG_LENGTH(MAX_PAYLOAD); nlh->nlmsg_flags = 0; nlh->nlmsg_type = IPM2_HELLO;//自定义类型 nlh->nlmsg_pid = getpid(); |
l 初始化消息体
memcpy(NLMSG_DATA(nlh), "Hello Kernel", 12);
iov.iov_base = (void*)nlh; iov.iov_len = nlh->nlmsg_len; msg.msg_name = (void*)&dest_addr; msg.msg_namelen = sizeof(dest_addr); msg.msg_iov = &iov; msg.msg_iovlen = 1; |
l 发送消息,使用的是sendmsg,也已可以使用sendto
sendmsg(sock_fd, &msg, 0) |
l 接收内核发送的消息,使用的是recvform,也可以使用recvmsg
recvfrom(sock_fd, &msg_kernel, sizeof(msg_from_kernel), 0, (struct sockaddr*)&dest_addr, &destaddr_len) |
2.1.2 内核空间
内核空间比用户空间的实现稍微附在一些,主要分为模块初始化函数、netlink消息接收处理函数、消息发送函数、模块退出函数。
l 模块初始化函数
Netlink内核socket的创建比较复杂,主要是通过netlink_kernel_create()函数完成,其原型如下:
struct sock *netlink_kernel_create(struct net *net, int unit,unsigned int groups, void (*input)(struct sk_buff *skb), struct mutex *cb_mutex, struct module *module) |
Netlink_kernel_create函数在内核的各个版本中的定义稍有不同,在2.6.32中其参数的个数为6个,其中第一个参数net一般使用的系统全局的变量,我们不需要定义。input函数指针表示netlink收到消息后调用的回调函数,我们主要在里面完成消息的解析处理工作。
其返回指向struct sock结构的指针,该结构相当于用户空间的socket文件句柄。
l 消息接收处理函数
static void kernel_receive(struct sk_buff *skb) { kernel_msg msg; struct nlmsghdr *nlh = NULL; printk(KERN_INFO "in kernel_receive\n"); //while(skb->len >= nlmsg_total_size(0)) { nlh = (struct nlmsghdr *)skb->data; if(nlh->nlmsg_len >= sizeof(struct nlmsghdr) && (skb->len >= nlh->nlmsg_len)) { if(nlh->nlmsg_type == IPM2_HELLO) { user_pid = nlh->nlmsg_pid; printk(KERN_INFO "user pid:%d\n", user_pid); printk(KERN_INFO "data from user space:%s\n", (char *)NLMSG_DATA(nlh)); memset(&msg, 0, sizeof(kernel_msg)); memcpy(msg.msg, "Hello user", 10); send_to_user(&msg, sizeof(kernel_msg)); } else if(nlh->nlmsg_type == IPM2_CLOSE) { if(nlh->nlmsg_pid== user_pid) { user_pid = 0; } } } //kfree_skb(skb);*/ } } |
该函数主要完成的工作就是解析netlink 消息,保存用户空间的进程pid,之后向用户空间发送消息。
l 消息发送函数
static int send_to_user(const void *buff, unsigned int size) { struct sk_buff *skb; struct nlmsghdr *nlh; int len = NLMSG_SPACE(100); if(user_pid == 0) { printk(KERN_ERR "user pid is 0\n"); return -1; } skb = alloc_skb(len, GFP_ATOMIC); if(!skb) { printk(KERN_ERR "alloc skb err\n"); return -1; } nlh = __nlmsg_put(skb, 0, 0, 0, len - sizeof(struct nlmsghdr), 0); nlh->nlmsg_len = len; NETLINK_CB(skb).pid = 0; NETLINK_CB(skb).dst_group = 0;
memcpy(NLMSG_DATA(nlh), buff, len); if(netlink_unicast(nl_sk, skb, user_pid, MSG_DONTWAIT) < 0) { printk(KERN_ERR "netlink unicast err\n"); return -1; } return 0; } |
该函数主要完成发送消息的组装,然后利用netlink_unicast函数将消息发送到进程号为user_pid的用户进程。
l 模块退出函数
退出模块主要完成一些资源清理、释放的工作,最后释放内核netlink通信socket
sock_release(nl_sk->sk_socket); |
3 内核模块编译
Linux2.6内核中,由于采用了新的“kbuild”构建系统,现在构建系统模块相比于以前容易了很多。构建过程的第一步就是决定在哪里管理模块源码。一般有两种选择:放在内核源码树中,或者作为一个补丁或者最终把代码合并到内核源码树中;放在内核源码树之外构建、维护你的模块代码。
3.1 内核源码树中构建
首先,我们需要明确我们的模块代码应该放在哪个内核目录下面,设备驱动程序一般放置在/drivers目录下,在其内部,设备驱动程序被进一步按照类别、类型或特殊驱动程序等更有序的方式组织在一起。如字符设备存在于/drivers/char 目录下,块设备存在于/drivers/block/目录下等。
假如你编写一个netfilter模块文件,而且希望将他存放于/net/netfilter/目录下,那么要注意,在该目录下存在大量的.c源码文件。如果你的模块文件仅仅只有一两个源文件,你可以直接将其放在该目录下,如果你的模块包含的源文件比较多的话,也许你应该建立一个单独的文件夹,用于专门维护你的模块程序源文件。假如创建一个目录名为:mynetfilter/子目录。接下来需要修改/net/netfilter/目录下的Makefile文件:
Obj-m += mynetfilter/ |
这行编译指令告诉模块构建系统,在编译模块时需要进入mynetfilter/子目录。如果你的模块程序依赖于一个特殊的配置选项。比如,CONFIG_ MYNETFILTER_TEST(该选项在编译内核时,执行make menuconfig命令时用于配置该模块的编译选项),你需要修改/net/netfilter/目录下的Kconfig文件
config “MYNETFILTER_TEST” tristate “netfilter test module” |
编译内核时,执行make menucofnig之后,我们会在配置菜单上看到此选项:
随之,需要修改Makefile文件,用下面的指令替换之前的:
Obj-$(CONFIG_MYNETFILTER_TEST) += mynetfilter/ |
最后,在/net/netfilter/mynetfilter/目录下添加一个Makefile文件,其中需要添加下面的指令:
Obj –m += mynetfilter.o |
准备就绪了,现在构建系统会进入到mynetfilter/目录下,将mynetfilter.c编译为mynetfilter.ko模块。
3.2 内核源码树之外构建
如果将模块代码放在内核源码树之外单独构建的话,你只需要在你的模块目录下创建一个Makefile文件,添加一行指令:
Obj-m := mynetfilter.o |
如果你有多个源文件只需添加另一行指令:
mynetfilter-objs := mynetfiler-init.o mynetfiler-exit.o |
木块在内核内和内核外构建的最大的区别在于构建过程。当模块在内核源代码树之外构建时,你必须告诉make如何找到内核源代码文件和基础Makefile文件。通过下面的指令完成上述功能:
make –c /kernel/source/location SUBDIRS=$PWD modules |
其中,/kernel/source/location/ 即为你配置的内核源代码树的位置。注意不要将你的内核源码树放在/usr/src/linux 目录下,而是放在/home目录某个方便访问的地方。