Chinaunix首页 | 论坛 | 博客
  • 博客访问: 2326489
  • 博文数量: 145
  • 博客积分: 8668
  • 博客等级: 中将
  • 技术积分: 3922
  • 用 户 组: 普通用户
  • 注册时间: 2007-03-09 21:21
个人简介

work hard

文章分类

全部博文(145)

文章存档

2016年(1)

2015年(1)

2014年(1)

2013年(12)

2012年(3)

2011年(9)

2010年(34)

2009年(55)

2008年(20)

2007年(9)

分类: LINUX

2010-01-04 22:21:01

本文分析ip_queue的内核态源码。文中如有任何疏漏和差错,欢迎各位朋友指正。

由于本文内容较多,本人将其分为上、中、下三篇。其中中篇和下篇的链接如下:

中篇:http://blog.chinaunix.net/u/33048/showart_2139494.html

下篇:http://blog.chinaunix.net/u/33048/showart_2139500.html


 

      本文欢迎自由转载,但请标明出处,并保证本文的完整性。

      作者:Godbach

        Blog:http://Godbach.cublog.cn

      日期: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


本文大纲如下:


IP Queue的生效

数据包能够进入ip_queue模块,需要两个动作:

1)模块的加载:modprobe ip_queue

2NF上对数据包执行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_INNF_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 handlerIPv4协议中注册的queue handleripq_enqueue_packet(),即我们要分写ip_queue模块的代码。

至此,需要QUEUE的报文已经走入了我们要分析ip_queue,我们下面就开始走入正题,分析ip_queue的代码。

ip_queue代码分析

       ip_queue模块的代码较为简单,包含ip_queue.hip_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);

       /*内核态创建netlinksocket,并注册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 Queuenetlink 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;

}

该函数的代码比较简单,先来了解一下函数的两个参数:

1pfIP协议族的值,PF_INETPF_INET6分别代表IPv4IPv6

2qhNF中对报文进行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处理报文的函数终于闪亮登场了,我们前面啰嗦了半天,主要就是想理顺一下思路,顺理成章的引出该函数。

--未完待续

阅读(7251) | 评论(3) | 转发(11) |
给主人留下些什么吧!~~

GFree_Wind2012-12-13 10:11:21

godbach: to GFree_Wind:
你的设备是转发设备吗。这个报文如果没法出去,能知道去哪里了吗。还有,你是仅仅修改了 IP,其他任何都没改吗,否则的话 IP 头部校验和需要重新.....
转发肯定设置了。

校验和我没有改,因为我只想先验证一下是否可以这样做。发送端会检查校验和吗?

godbach2012-12-04 13:49:33

to GFree_Wind:
你的设备是转发设备吗。这个报文如果没法出去,能知道去哪里了吗。还有,你是仅仅修改了 IP,其他任何都没改吗,否则的话 IP 头部校验和需要重新计算的。如果校验和错误,是没法发出去的。

GFree_Wind2012-10-23 23:28:03

我现在有一个问题:我的想法是,利用ipq将某些数据包交到用户空间处理,然后用户空间修改这个包。再由kernel发回去。

我在mangle表的PREROUTING链中将某些数据包加入到QUEUE中,并由用户空间的进程修改。比如我交换了源地址和目的地址,并利用ipq_set_verdict再次将包还给kernel。这时包应该再次进入PREROUTING链中,地址被修改了,为什么包不会被发出去呢?