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目录某个方便访问的地方。