您的位置:首页 > 运维架构 > 网站架构

linux netfilter/iptables 架构分析及nelink的使用

2013-09-29 11:14 417 查看
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目录某个方便访问的地方。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: