netfilter
内核网络编程
网络协议数据结构inet_protosw
在Linux-2.6.26.3/net/ipv4/af_inet.c
文件中有一个名为inet_init()的函数对协议进行了初始化。inet_init()函数使用proto_register()函数来注册每个内嵌协议。
软中断CPU报文队列及其处理
- Linux内核网络协议层的层间传递手段——软中断。
软中断机制的核心元素:
- 软中断状态:是否有触发的软中断未处理
- 软中断向量表:包含两个成员变量,一个是处理此软中断的回调函数,另一个是处理时所需的参数。
- 软中断守护内核线程:内核建立一个内核线程ksoftirqd来轮询软中断状态,调用软中断向量表中的软中断回调函数处理中断。
中断事件处理过程:
- 中断事件发生
- 调用raise_softirq()函数设置对应的中断标记位,触发中断事务
- 检测中断状态寄存器的状态
- ksoftirqd通过查询发现某一软中断事务发生之后,通过软中断向量表调用软中断程序action
软中断使用方法
Linux系统最用同时注册32个软中断,目前系统使用了6个软中断。其中一个为taskle机制,该机制用来实现下半部,描述软中断的核心数据结构为中断向量表,其定义如下:
struct softirq_action
{
void (*action)(struct softirq_action *);
void *data;
};
- action:软中断服务程序
- data:服务程序输入参数
sk_buff结构
因为内核层和用户层在网络方面的差别很大,在内核的网络层的sk_buff结构占有重要的地位,几乎所有的处理与此结构有关系。
sk_buff主要成员:
socket数据在内核中接收和发送
socket数据在内核中的流程主要包括初始化、销毁、接收和发送网络数据源。其过程设计网卡驱动、网络协议栈和应用层的接口函数。
- socket()初始化:创建socket()需要传递family、type、protocol这三个参数。
创建socket()其实就是创建一个socket实例,然后创建一个文件描述符结构。创建套接字文件描述符会互相建立关联,即建立相互连接的的指针,并且初始化这些文件的读写操作映射到read()、write()函数上来。
在初始化套接字的时候,同时初始化socket的操作函数(proto_ops结构)
创建socket的同时还创建sock结构的数据空间。还会初始化三个队列:receive_queue,send_queue,backlog_queue。
- 接收网络数据recv():
网络数据接收依次经过网卡驱动和协议栈程序。
- 发送网络数据send():
linux对网络数据的发送过程的处理与接收过程相反。在一端对socket进行write()的过程中。首先会把write的字符串缓冲区整理成msghdr的数据结构形式,然后调用sock_sendmsg()把msghdr的数据传送至inet层。
msghdr结构中数据区的每个数据包,创建sk_buff结构,填充数据,挂至发送队列。一层层往下层协议传递。以下的每层协议将不再对数据进行复制,而是对sk_buff结构直接进行操作。
内核模块编程
内核模块编程和典型的应用程序的区别:
典型应用有一个main程序,而内核模块需要一个初始化函数和清理函数,在向内核中插入模块时调用初始化函数,卸载内核模块时调用清理函数。
加载内核相对于直接编写内核模块有很大方便性:
- 不用重新编译内核
- 可以动态加载和卸载,调试使用方便。
Hello,World
编写C语言程序:
//必要的头文件
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
//模块许可证声明(必须)
MODULE_LICENSE("Dual BSD/GPL");
//模块加载函数(必须)
static int hello_init(void)
{
printk(KERN_ALERT "Hello World enter/n");
return 0;
}
//模块卸载函数(必须)
static void hello_exit(void)
{
printk(KERN_ALERT "Hello World exit/n");
}
//模块的注册
module_init(hello_init);
module_exit(hello_exit);
//声明模块的作者(可选)
MODULE_AUTHOR("XXX");
//声明模块的描述(可选)
MODULE_DESCRIPTION("This is a simple example!/n");
//声明模块的别名(可选)
MODULE_ALIAS("A simplest example");
编写makefile文件:
obj-m += hello.o
#generate the path
CURRENT_PATH:=$(shell pwd)
#the current kernel version number
LINUX_KERNEL:=$(shell uname -r)
#the absolute path
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)
#complie object
all:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
#clean
clean:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean
执行make命令能得到
以上这些文件,使用sudo insmod Hello.ko
来加载内核:
但是能发现并没有任何消息提示,这是因为printk函数并不是在命令行将信息打印而是在系统日志里面,需要使用指令dmesg
进入系统日志:
也可以使用lsmod查看内核依赖关系:
最后,输入rmmod
指令卸载内核
内核模块的基本架构
- 模块初始化
自动调用模块的的初始化函数,进行模块的初始化,主要是资源申请。
- 模块清楚函数
使用命令rmmod卸载内核模块,模块清除函数自动调用。主要状态重置和资源释放。
- 模块许可声明、作者、模块描述等信息
并不是强制要求必须声明。内核可以识别以下四种许可方式:GPLa Dual BSD/GPLa Dual MPL/GPLa Proprietary。如果没有采用以上许可证方式的声明则假定为私有的,内核加载这种模块会被“污染”。
- MODULE_AUTHOR:作者描述模块
- MODULE_DESCRIPTION:模块用途的简短描述
- MODULE_VERSION:模块版本号
- MODULE_AVIAS:模块别名
内核加载模块过程
内核卸载模块过程
netfilter的五个钩子函数
在IP包的IPv4协议栈上的传递过程中,有五个检查点,并且各引入了一对NF_HOOK()宏函数的一个响应的调用:
- PREROUTING:在报文做路由以前执行
- LOCAK-IN:在报文转向另一个NIC(网卡)以前执行
- FORWARD:在报文流出以前执行
- LOCAL-OUT:在流入本地的报文做路由以后执行
- POSTROUTING:在本地报文做流出路由之前执行
利用这5个参考点,查阅用户注册的回调函数,根据用户定义的回调函数来监视进出的网络数据包,是netfilter的基本实现框架。
netfilter定义了一个全局变量来存储用户的回调函数:
struct list_head nf_hooks[NPROTO][NF_MAX_HOOKS];
其中,NPROTO是协议类型,可以是TCP、UDP或者IP协议。NF_MAX_HOOKS是挂接的钩子最大数量。
以上介绍的五个检查点对应五个钩子函数。
- NF_IP_PRE_ROUTING:
- NF_IP_FORWARD:
- NF_IP_POST_ROUTING:
- NF_IP_LOCAL_IN:
- NF_IP_LOACL_OUT:
如果是要做包过滤,需要用到NF_IP_LOCAL_IN钩子函数。
NF_HOOK宏
netfilter的框架是在协议栈处理过程中调用NF_HOOK(),插入处理过程来实现的。
#ifdef CONFIG_NETFILTER
#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) \
nf_hook_slow((pf), (hook), (skb), (indev), (outdev), (okfn)))
#else
#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) (okfn)(skb)
#endif /*CONFIG_NETFILTER*/
以上为NF_HOOK()函数的定义。从该函数宏可知,当编译了netfilter时(#ifdef CONFIG_NETFILTER )就会调用nf_hook,slow()函数。如果没有的话则调用NF_HOOK宏中的参数okfn。
钩子的处理规则
netfilter的钩子函数的返回值可以为NF_ACCEPT、 NF_DROP、 NF_STOLEN、NF_QUERE、 NF_REPEAT
- nf_accept:继续传递,保持和原来传输的一致
- nf_drop:丢弃包,不再继续传递
- nf_stolen:接管包,不再继续传递
- nf_quere:队列化包(通常是为用户空间处理做准备)
- nf_repeat:再次调用这个钩子
注册/注销钩子
注册和注销钩子函数的接口主要有:nf_register_hook、nf_unregister_hook、nf_register_sockopt 、nf_unregister_sockopt
nf_hook_ops结构
struct nf_hook_ops
{
struct list_head list; //钩子链表
nf_hookfn *hook; //钩子处理函数
struct module //模块所有者
int pf; //钩子的协议族
int hooknum; //钩子的位置值
int priority; //钩子的优先级,默认情况为继承优先级
};
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 *));
okfn()函数是当回调函数为空时,netfilter调用的处理函数。
- 注册钩子
void nf_register_net_hook(struct net *net, const struct nf_hook_ops *ops);
- 注销钩子
void nf_unregister_net_hook(struct net *net, const struct nf_hook_ops *ops);
钩子函数的Hello,World
#include <linux/init.h>
#include <linux/module.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/skbuff.h>
#include <net/tcp.h>
#include <linux/netdevice.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
static struct nf_hook_ops nfhoLocalIn; //定义实现钩子函数的结构体
MODULE_LICENSE("Dual BSD/GPL");
unsigned int hello_hookfn(void* priv, struct sk_buff* skb, const struct nf_hook_state* state) //回调函数
{
printk(KERN_ALERT "Hello World Hook\n");
return NF_ACCEPT;
}
int init_module() //初始化模块
{
nfhoLocalIn.hook = hello_hookfn; //构建结构体
nfhoLocalIn.pf = PF_INET;
nfhoLocalIn.priority = NF_IP_PRI_FIRST;
nf_register_net_hook(&init_net, &nfhoLocalIn); //注册钩子
printk("My nf register\n");
return 0;
}
void cleanup_module()
{
nf_unregister_net_hook(&init_net, &nfhoLocalIn); //注销钩子
printk("My nf unregister\n");
}