分类:
2011-07-01 11:08:39
本文分析ip_queue的内核态源码。文中如有任何疏漏和差错,欢迎各位朋友指正。
由于本文内容较多,本人将其分为上、中、下三篇。其中中篇和下篇的链接如下:
中篇:http://blog.chinaunix.net/u/33048/showart_2139494.html
下篇:http://blog.chinaunix.net/u/33048/showart_2139500.html
本文欢迎自由转载,但请标明出处,并保证本文的完整性。
作者:Godbach
日期:2010/01/04
本系列的前两篇文章如下:
1. Linux内核IP Queue机制的分析(一)——用户态接收数据包
http://blog.chinaunix.net/u/33048/showart_1678213.html
2. Linux内核IP Queue机制的分析(二)——用户态处理并回传数据包
http://blog.chinaunix.net/u/33048/showart_1839753.html
本文大纲如下:
(四)入队函数ipq_enqueue_packet —— 发送数据包到用户空间
IP Queue的生效
数据包能够进入ip_queue模块,需要两个动作:
(1)模块的加载:modprobe ip_queue
(2)NF上对数据包执行NF_QUEUE的动作,这个可以通过用户态配置一条iptables规则实现:
iptables -A INPUT -p tcp --dport 21 -j QUEUE
这里假设对发往本机的TCP报文端口为21的进行QUEUE。
有了以上两个步骤, 所有匹配到(2)中的报文将会调用IP Queue模块的相关函数。
IP报文进入IP Queue的流程
本文中分析的代码的内核版本为2.6.18.3.
这里我们以本地接收报文为例,如果是转发的报文,可比照着分析即可。
IP层接收报文的函数为:ip_rcv()(ip_input.c)。该函数对报文进行一些初步的检查后,就将报文交给PREROUTING Hook点注册的钩子函数处理:
return NF_HOOK(PF_INET, NF_IP_PRE_ROUTING, skb, dev, NULL, ip_rcv_finish);
由以上代码可见,报文经过钩子函数之后,由ip_rcv_finish()接着处理。该函数主要完成数据报文的路由查找。
如果是发往本地的报文,则会调用ip_local_deliver()函数:
return NF_HOOK(PF_INET, NF_IP_LOCAL_IN, skb, skb->dev, NULL,
ip_local_deliver_finish);
该函数主要功能就是将报文交给NF_IP_LOCAL_IN hook点的钩子函数进行处理。我们在第一部分中添加的一条iptables规则就是对于经过INPUT链的TCP报文且目的端口21执行动作QUEUE。如果此时用户空间已经开启socket等待接收IP Queue报文的话,那么对应的报文就会进入用户空间,然后就可以参照我们之前提供的用户空间例程进行处理。
这里,我们简单列出在NF_IP_LOCAL_IN中NF_HOOK宏的调用函数过程:
NF_HOOK()->NF_HOOK_THRESH()->nf_hook_thresh()->nf_hook_slow()
当nf_hook_slow函数返回值为NF_QUEUE时,进一步调用nf_queue()。该函数对所有动作为QUEUE的报文进行处理,其中关键的一行代码如下:
status = queue_handler[pf]->outfn(*skb, info, queuenum,
queue_handler[pf]->data);
这行代码就是将报文按照IP层的协议交给对应的queue handler。IPv4协议中注册的queue handler为ipq_enqueue_packet(),即我们要分写ip_queue模块的代码。
至此,需要QUEUE的报文已经走入了我们要分析ip_queue,我们下面就开始走入正题,分析ip_queue的代码。
ip_queue代码分析
ip_queue模块的代码较为简单,包含ip_queue.h和ip_queue.c。我们将分别该两个源文件进行分析。
ip_queue模块的数据结构定义在头文件ip_queue.h中,主要定义了用于在内核态和用户态传输数据的相关数据结构。其代码如下:
/*
* This is a module which is used for queueing IPv4 packets and
* communicating with userspace via netlink.
*
* (C) 2000 James Morris, this code is GPL.
*/
#ifndef _IP_QUEUE_H
#define _IP_QUEUE_H
#ifdef __KERNEL__
#ifdef DEBUG_IPQ
#define QDEBUG(x...) printk(KERN_DEBUG ## x)
#else
#define QDEBUG(x...)
#endif /* DEBUG_IPQ */
#else
#include
#endif /* ! __KERNEL__ */
/* 内核态发送到用户态的消息的数据结构*/
typedef struct ipq_packet_msg {
unsigned long packet_id; /* ID of queued packet */
unsigned long mark; /* Netfilter mark value */
long timestamp_sec; /* Packet arrival time (seconds) */
long timestamp_usec; /* Packet arrvial time (+useconds) */
unsigned int hook; /* Netfilter hook we rode in on */
char indev_name[IFNAMSIZ]; /* Name of incoming interface */
char outdev_name[IFNAMSIZ]; /* Name of outgoing interface */
unsigned short hw_protocol; /* Hardware protocol (network order) */
unsigned short hw_type; /* Hardware type */
unsigned char hw_addrlen; /* Hardware address length */
unsigned char hw_addr[8]; /* Hardware address */
size_t data_len; /* Length of packet data */
unsigned char payload[0]; /* Optional packet data */
} ipq_packet_msg_t;
/* 用户态发送到内核态的模式消息 */
typedef struct ipq_mode_msg {
unsigned char value; /* Requested mode */
size_t range; /* Optional range of packet requested */
} ipq_mode_msg_t;
/* 用户态发送到内核态的断言消息 */
typedef struct ipq_verdict_msg {
unsigned int value; /* Verdict to hand to netfilter */
unsigned long id; /* Packet ID for this verdict */
size_t data_len; /* Length of replacement data */
unsigned char payload[0]; /* Optional replacement packet */
} ipq_verdict_msg_t;
/*统一封装起来的用户态发内核态的信息的数据结构*/
typedef struct ipq_peer_msg {
union {
ipq_verdict_msg_t verdict;
ipq_mode_msg_t mode;
} msg;
} ipq_peer_msg_t;
/* 报文传输的模式*/
enum {
IPQ_COPY_NONE, /* Initial mode, packets are dropped */
IPQ_COPY_META, /* Copy metadata */
IPQ_COPY_PACKET /* Copy metadata + packet (range) */
};
#define IPQ_COPY_MAX IPQ_COPY_PACKET
/* IP Queue消息的类型 */
#define IPQM_BASE 0x10 /* standard netlink messages below this */
#define IPQM_MODE (IPQM_BASE + 1) /* Mode request from peer */
#define IPQM_VERDICT (IPQM_BASE + 2) /* Verdict from peer */
#define IPQM_PACKET (IPQM_BASE + 3) /* Packet from kernel */
#define IPQM_MAX (IPQM_BASE + 4)
#endif /*_IP_QUEUE_H*/
该头文件中定义的相关数据结构和宏应该和用户空间引用的头文件的内容是保持一致的。因此,对于该文件中的数据结构的详细解释,可以参考本系列文章的第一篇《Linux内核IP Queue机制的分析(一)——用户态接收数据包》中第二部分IP Queue编程接口。
ip_queue模块的加载和卸载
分析一个内核模块的源代码,通常我们先看模块加载时做哪些工作,这样就可以了解到模块具体的功能会在什么条件下执行。而模块的卸载通常就是将模块加载时注册的函数和申请的资源进行释放等工作。因此,这里简单的分析一下init函数的实现,代码在ip_queue.c中。
static int __init ip_queue_init(void)
{
int status = -ENOMEM;
struct proc_dir_entry *proc;
/*注册netlink的通知链*/
netlink_register_notifier(&ipq_nl_notifier);
/*内核态创建netlink的socket,并注册ipq_rcv_sk函数实现接收用户空间下发的配置数据*/
ipqnl = netlink_kernel_create(NETLINK_FIREWALL, 0, ipq_rcv_sk,
THIS_MODULE);
if (ipqnl == NULL) {
printk(KERN_ERR "ip_queue: failed to create netlink socket\n");
goto cleanup_netlink_notifier;
}
/*注册proc文件*/
proc = proc_net_create(IPQ_PROC_FS_NAME, 0, ipq_get_info);
if (proc)
proc->owner = THIS_MODULE;
else {
printk(KERN_ERR "ip_queue: failed to create proc entry\n");
goto cleanup_ipqnl;
}
/*注册网络设备的通知链*/
register_netdevice_notifier(&ipq_dev_notifier);
ipq_sysctl_header = register_sysctl_table(ipq_root_table, 0);
/*注册IP Queue机制的报文处理结构,主要包含一个报文的入队处理函数,下面具体分析*/
status = nf_register_queue_handler(PF_INET, &nfqh);
if (status < 0) {
printk(KERN_ERR "ip_queue: failed to register queue handler\n");
goto cleanup_sysctl;
}
return status;
cleanup_sysctl:
unregister_sysctl_table(ipq_sysctl_header);
unregister_netdevice_notifier(&ipq_dev_notifier);
proc_net_remove(IPQ_PROC_FS_NAME);
cleanup_ipqnl:
sock_release(ipqnl->sk_socket);
mutex_lock(&ipqnl_mutex);
mutex_unlock(&ipqnl_mutex);
cleanup_netlink_notifier:
netlink_unregister_notifier(&ipq_nl_notifier);
return status;
}
IP Queue模块的初始化函数最重要的两个工作:
(1)创建用于IP Queue的netlink socket,实现接收用户态的数据的函数ipq_rcv_sk();
(2)注册IP Queue报文的入队处理函数,并将数据包按照用户的配置将相关信息发向用户空间。当NF框架的hook函数对报文的处理返回NF_QUEUE时,该函数作为报文下一步被处理的入口函数。
模块初始化中还有一些其他诸如注册通知链的工作,这里我们不作具体分析,想了解相关细节的朋友可以查阅具体的资料。至于通知链的相关知识,也可以参考论坛上scutan兄的精华帖《内核通知链 学习笔记》。链接:
。ip_queue报文入队处理函数的注册
在上面分析的模块注册代码中,IP Queue的报文入队处理函数的注册是通过调用nf_register_queue_handler()来实现的。因此,有必要了解一下该函数的源码,源码位于nf_queue.c中:
/* return EBUSY when somebody else is registered, return EEXIST if the
* same handler is registered, return 0 in case of success. */
int nf_register_queue_handler(int pf, struct nf_queue_handler *qh)
{
int ret;
/*IP协议族的值必须在当前指定的范围内*/
if (pf >= NPROTO)
return -EINVAL;
write_lock_bh(&queue_handler_lock);
/*该queue handler已经被注册过了*/
if (queue_handler[pf] == qh)
ret = -EEXIST;
/*该协议族已经被注册了handler了*/
else if (queue_handler[pf])
ret = -EBUSY;
/*将该协议的queue hanler指向参数qh*/
else {
queue_handler[pf] = qh;
ret = 0;
}
write_unlock_bh(&queue_handler_lock);
return ret;
}
该函数的代码比较简单,先来了解一下函数的两个参数:
(1)pf:IP协议族的值,PF_INET和PF_INET6分别代表IPv4和IPv6。
(2)qh:NF中对报文进行Queue的结构体,定义如下(netfilter.h):
/* Packet queuing */
struct nf_queue_handler {
int (*outfn)(struct sk_buff *skb, struct nf_info *info,
unsigned int queuenum, void *data);
void *data;
char *name;
};
由此可见,该结构体主要包含一个函数指针,用于处理NF框架需要Queue的报文。data应该是用来保存一些私有数据,name则是该queue handler的名称。
代码中已经包含了该函数源码的简单注释,这里再对该函数进行一下简单的总结:
(1) 每个协议族只能注册一个queue handler;
(2) 随后报文的处理中,可以根据报文的协议族,就可以找到报文进行Queue时的处理函数queue_handler[pf]->outfn()。
在IP Queue模块中,queue handler的注册如下所示:
status = nf_register_queue_handler(PF_INET, &nfqh);
可见,注册的是IPv4协议族的报文处理函数,而nfqh结构体的定义如下:
static struct nf_queue_handler nfqh = {
.name = "ip_queue",
.outfn = &ipq_enqueue_packet,
};
这里,IP Queue处理报文的函数终于闪亮登场了,我们前面啰嗦了半天,主要就是想理顺一下思路,顺理成章的引出该函数。