>,如果你想获取更多关于Linux对数据包的相关处理知识的话,我强烈建议你也阅读一下这篇文章。目前,就认为数据包只是经过了Linux内核的网络堆栈,它穿过几层钩子,在经过这些钩子时,数据包被解析,保留或者丢弃。这就是所谓的Netfilter 钩子。
[size=3]2.2 Ipv4中的Netfilter钩子[/size]
Netfilter为IPV4定义了5个钩子。可以在 linux/netfilter-ipv4.h里面找到这些符号的定义,表2.1列出了这些钩子。
表 2.1. ipv4中定义的钩子
钩子名称 调用时机
NF_IP_PRE_ROUTING 完整性校验之后,路由决策之前
NF_IP_LOCAL_IN 目的地为本机,路由决策之后
NF_IP_FORWARD 数据包要到达另外一个接口去
NF_IP_LOCAL_OUT 本地进程的数据,发送出去的过程中
NF_IP_POST_ROUTING 向外流出的数据上线之前
NF_IP_PRE_ROUTING 钩子称为是数据包接收后第一个调用的钩子程序,这个钩子在我们后面提到的模块当中将会被用到。其他的钩子也很重要,但是目前我们只集中探讨NF_IP_PRE_ROUTING这个钩子。
不管钩子函数对数据包做了哪些处理,它都必须返回表2.2中的一个预定义好的Netfilter返回码。
表2.2 Netfilter 返回码
返回码 含义
NF_DROP 丢弃这个数据包
NF_ACCEPT 保留这个数据包
NF_STOLEN 忘掉这个数据包
NF_QUEUE 让这个数据包在用户空间排队
NF_REPEAT 再次调用这个钩子函数
NF_DROP 表示要丢弃这个数据包,并且为这个数据包申请的所有资源都要得到释放。NF_ACCEPT告诉Netfilter到目前为止,这个数据包仍然可以被接受,应该将它移到网络堆栈的下一层。NF_STOLEN是非常有趣的一个返回码,它告诉Netfilter让其忘掉这个数据包。也就是说钩子函数会在这里对这个数据包进行完全的处理,而Netfilter就应该放弃任何对它的处理了。然而这并不意味着为该数据包申请的所有资源都要释放掉。这个数据包和它各自的sk_buff结构体依然有效,只是钩子函数从Netfilter夺取了对这个数据包的掌控权。不幸的是,我对于NF_QUEUE这个返回码的真实作用还不是很清楚,所在目前不对它进行讨论。最后一个返回值NF_REPEAT请求Netfilter再次调用这个钩子函数,很明显,你应该慎重的应用这个返回值,以免程序陷入死循环。
[size=4]第三章 注册和注销NetFilter 钩子[/size]
注册一个钩子函数是一个围绕nf_hook_ops结构体的很简单的过程,在linux/netfilter.h中有这个结构体的定义,定义如下:
struct nf_hook_ops
{
struct list_head list;
/* User fills in from here down. */
nf_hookfn *hook;
int pf;
int hooknum;
/* Hooks are ordered in ascending priority. */
int priority;
};
这个结构体的成员列表主要是用来维护注册的钩子函数列表的,对于用户来说,在注册时并没有多么重要。hook是指向nf_hookfn函数的指针。也就是为这个钩子将要调用的所有函数。nf_hookfn同样定义在linux/netfilter.h这个文件中。pf字段指定了协议簇(protocol family)。Linux/socket.h中定义了可用的协议簇。但是对于IPV4我们只使用PF_INET。hooknum 域指名了为哪个特殊的钩子安装这个函数,也就是表2.1中所列出的条目中的一个。Priority域表示在运行时这个钩子函数执行的顺序。为了演示例子模块,我们选择NF_IP_PRI_FIRST这个优先级。
注册一个Netfilter钩子要用到nf_hook_ops这个结构体和nf_register_hook()函数。nf_register_hook()函数以一个nf_hook_ops结构体的地址作为参数,返回一个整型值。如果你阅读了net/core/netfilter.c中nf_register_钩子()的源代码的话,你就会发现这个函数只返回了一个0。下面这个例子注册了一个丢弃所有进入的数据包的函数。这段代码同时会向你演示Netfilter的返回值是如何被解析的。
代码列表1. Netfilter钩子的注册
/* Sample code to install a Netfilter hook function that will
* drop all incoming packets. */
#define __KERNEL__
#define MODULE
#include
#include
#include
#include
/* This is the structure we shall use to register our function */
static struct nf_hook_ops nfho;
/* This is the hook function itself */
unsigned int hook_func(unsigned int hooknum,
struct sk_buff **skb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
return NF_DROP; /* Drop ALL packets */
}
/* Initialisation routine */
int init_module()
{
/* Fill in our hook structure */
nfho.hook = hook_func; /* Handler function */
nfho.hooknum = NF_IP_PRE_ROUTING; /* First hook for IPv4 */
nfho.pf = PF_INET;
nfho.priority = NF_IP_PRI_FIRST; /* Make our function first */
nf_register_hook(&nfho);
return 0;
}
/* Cleanup routine */
void cleanup_module()
{
nf_unregister_hook(&nfho);
}
这就是注册所要做的一切。从代码列表1你可以看到注销一个Netfilter钩子也是很简单的一件事情,只需要调用nf_unregister_hook()函数,并将注册时用到的结构体地址再次作为注销函数参数使用就可以了。
[size=4]第四章 基本的NetFilter数据包过滤技术[/size]
[size=3]4.1 钩子函数近距离接触[/size]
现在是我们来查看获得的数据如何传入钩子函数并被用来进行过滤决策的时候了。所以,我们需要更多的关注于nf_hookfn函数的模型。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 *));
nf_hookfn函数的第一个参数指定了表2.1给出的钩子类型中的一种。第二个参数更有趣,它是一个指向指针(这个指针指向一个sk_buff类型的结构体)的指针,它是网络堆栈用来描述数据包的结构体。这个结构体定义在linux/skbuff.h中,由于这个结构体的定义很大,这里我只着重于它当中更有趣的一些域。
或许sk_buff结构体中最有用的域就是其中的三个联合了,这三个联合描述了传输层的头信息(例如 UDP,TCP,ICMP,SPX),网络层的头信息(例如ipv4/6, IPX, RAW)和链路层的头信息(Ethernet 或者RAW)。三个联合相应的名字分别为:h,nh和mac。根据特定数据包使用的不同协议,这些联合包含了不同的结构体。应当注意,传输层的头和网络层的头极有可能在内存中指向相同的内存单元。在TCP数据包中也是这样的情况,h和nh都是指向IP头结构体的指针。这就意味着,如果认为h->th指向TCP头,从而想通过h->th来获取一个值的话,将会导致错误发生。因为h->th实际指向IP头,等同于nh->iph。
其他比较有趣的域就是len域和data域了。len表示包中从data开始的数据总长度。因此,现在我们就知道如何通过一个skbuff结构体去访问单个的协议头或者数据包本身的数据。还有什么有趣的数据位对于Netfilter的钩子函数而言是有用的呢?
跟在sk_buff之后的两个参数都是指向net_device结构体的指针。net_devices结构体是Linux内核用来描述各种网络接口的。第一个结构体,in,代表了数据包将要到达的接口,当然 out就代表了数据包将要离开的接口。有很重要的一点必须认识到,那就是通常情况下这两个参数最多只提供一个。 例如,in通常情况下只会被提供给NF_IP_PRE_ROUTING和NF_IP_LOCAL_IN钩子。out通常只被提供给NF_IP_LOCAL_OUT和NF_IP_POST_ROUTING钩子。在这个阶段,我没有测试他们中的那个对于NF_IP_FORWARD是可用的。如果你能在废弃之前确认它们(in和out)不空的话,那么你很优秀。
最后,传给钩子函数的最后一个参数是一个名为okfn的指向函数的指针,这个函数有一个sk_buff的结构体作为参数,返回一个整型值。我也不能确定这个函数做什么,在net/core/netfilter.c中有两处对此函数的调用。这两处调用就是在函数nf_hook_slow()和函数nf_reinject()里,在这两个调用处当Netfilter钩子的返回值为NF_ACCEPT时,此函数被调用。如果有谁知道关于okfn更详细的信息,请告诉我。
现在我们已经对Netfilter接收到的数据中最有趣和最有用的部分进行了分析,下面就要开始介绍如何利用这些信息对数据包进行各种各样的过滤。
[size=3]4.2 基于接口的过滤[/size]
这将是我们能做的最简单的过滤技术。是否还记得我们的钩子函数接收到的net_device结构体?利用net_device结构体中的name键值,我们可以根据数据包的目的接口名或者源接口名来丢弃这些数据包。为了抛弃所有发向”eth0”的数据,我们只需要比较一下“in->name”和“eth0”,如果匹配的话,钩子函数返回NF_DROP,然后这个数据包就被销毁了。它就是这样的简单。列表2给出了示例代码。请注意轻量级防火墙(LWFW)会使用到这里提到的所有过滤方法。LWFW同时还包含了一个IOCTL方法来动态改变自身的行为。
列表2. 基于源接口(网卡名)的数据过滤技术
/* Sample code to install a Netfilter hook function that will
* drop all incoming packets from an IP address we specify */
#define __KERNEL__
#define MODULE
#include
#include
#include
#include /* For IP header */
#include
#include
/* This is the structure we shall use to register our function */
static struct nf_hook_ops nfho;
/* IP address we want to drop packets from, in NB order */
static unsigned char *drop_ip = "\x7f\x00\x00\x01";
/* This is the hook function itself */
unsigned int hook_func(unsigned int hook_num,
struct sk_buff **skb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
struct sk_buff *sb = *skb;
if (sb->nh.iph->saddr == drop_ip) {
printk("Dropped packet from... %d.%d.%d.%d\n",
*drop_ip, *(drop_ip + 1),
*(drop_ip + 2), *(drop_ip + 3));
return NF_DROP;
} else {
return NF_ACCEPT;
}
}
/* Initialisation routine */
int init_module()
{
/* Fill in our hook structure */
nfho.hook = hook_func;
/* Handler function */
nfho.hook_num = NF_IP_PRE_ROUTING; /* First for IPv4 */
nfho.pf = PF_INET;
nfho.priority = NF_IP_PRI_FIRST; /* Make our func first */
nf_register_hook(&nfho);
return 0;
}
/* Cleanup routine */
void cleanup_module()
{
nf_unregister_hook(&nfho);
}
现在看看,是不是很简单?下面让我们看看基于IP地址的过滤技术。
[size=3]4.3 基于IP地址的过滤[/size]
类似基于接口的数据包过滤技术,基于源/目的IP地址的数据包过滤技术也很简单。这次我们对sk_buff结构体比较感兴趣。现在应该记起来,Skb参数是一个指向sk_buff结构体的指针的指针。为了避免运行时出现错误,通常有一个好的习惯就是另外声明一个指针指向sk_buff结构体的指针,把它赋值为双重指针所指向的内容,像这样:
struct sk_buff *sb = *skb; /* Remove 1 level of indirection* /
然后你只需要引用一次就可以访问结构体中的成员了。可以使用sk_buff结构体中的网络层头信息来获取此数据包的IP头信息。这个头包含在一个联合中,可以通过sk_buff->nh.iph来获取。列表3的函数演示了当给定一个数据包的sk_buff结构时,如何根据给定的要拒绝的IP对这个数据包进行源IP地址的检验。这段代码是直接从LWFW中拉出来的。唯一的不同之处就是LWFW中对LWFW统计量的更新被去掉了。
列表3.检测接收到数据包的源IP地址
unsigned char *deny_ip = "\x7f\x00\x00\x01"; /* 127.0.0.1 */
...
static int check_ip_packet(struct sk_buff *skb)
{
/* We don't want any NULL pointers in the chain to
* the IP header. */
if (!skb )return NF_ACCEPT;
if (!(skb->nh.iph)) return NF_ACCEPT;
if (skb->nh.iph->saddr == *(unsigned int *)deny_ip)
{
return NF_DROP;
}
return NF_ACCEPT;
}
如果源IP地址与我们想抛弃数据包的IP地址匹配的话,数据包就会被丢弃。为了使函数能正常工作,deny_ip的值应该以网络字节序的方式存储(与intel相反的Big-endian格式)。尽管这个函数在被调用的时候有一个空指针作参数这种情况不太可能,但是稍微偏执(小心)一点总不会有什么坏处。当然,如果调用时出现了差错的话,函数将会返回一个NF_ACCEPT值,以便于Netfilter能够继续处理这个数据包。列表4 展现了一个简单的基于IP地址的数据包过滤的模块,这个模块是由基于接口的过滤模块修改得到的。你可以修改IP地址来实现对指定IP地址发来的数据包的丢弃。
列表4. 基于数据包源IP地址的过滤技术
/* Sample code to install a Netfilter hook function that will
* drop all incoming packets from an IP address we specify */
#define __KERNEL__
#define MODULE
#include
#include %3