分类: LINUX
2008-12-03 19:13:55
序
笔者将会通过包括本文在内的三篇文章,对IP Queue机制从用户态的应用到内核态的模块程序设计进行分析。三篇文章的题目分别是:
Linux内核IP Queue机制的分析(一)——用户态接收数据包
Linux内核IP Queue机制的分析(二)——用户态处理并回传数据包
Linux内核IP Queue机制的分析(三)——内核态ip_queue代码分析
笔者希望通过这三篇文章,能够给不熟悉或者对IP Queue使用有问题的朋友一些帮助。文章中分析不妥或错误之处,希望各位朋友可以及时指出。
本文欢迎自由转载,但请标明出处,并保证本文的完整性。
作者:Godbach
日期:
本文的大纲如下:
一、基础知识
1. Netfilter
2. Netlink机制
二、IP Queue编程接口
三、一个实现接收内核态发送的IP Queue数据包的用户态例程
1. libipq.h
2. libipq.c
3. ipq_user.c
四、应用程序的测试
1. 测试环境的建立
2. 程序的测试
基础知识部分的很多部分内容都重点参考或者直接引用了《如何用IP Queue机制编写用户态防火墙》,原文的链接为:。在此,向该文的作者表示感谢。
Linux内核在Netfilter(下文简称NF)框架的基础上提供了IP Queue机制,使得基于用户态(User Mode)的防火墙开发成为可能。
内核中NF对网络报文的处理这里不做详细描述。假设读者已经熟悉NF的工作原理和工作流程。但这里还是要简单介绍一下NF中各个钩子(hook)函数对数据包处理的返回值,即该函数告诉内核对该数据包的处理意见。所有的返回值如下:
NF_DROP: 丢弃该报文,释放所有与该报文相关的资源;
NF_ACCEPT: 接受该报文,并继续处理;
NF_STOLEN: 该报文已经被HOOK函数接管,协议栈无须继续处理;
NF_QUEUE: 将该报文传递到用户态去做进一步的处理;
NF_REPEAT: 再次调用本HOOK函数。
当HOOK处理函数返回值为NF_QUEUE时,内核协议栈将通过IP Queue机制把当前报文传递到用户态,由用户态的应用程序进行处理。这样,只要能够在相应的HOOK点上返回NF_QUEUE值,就可以将符合要求的报文传送到用户态去做进一步对报文行处理。随后,用户态程序会将处理后的报文以及对报文的处理意见(ACCEPT,DROP等)传递给内核协议栈。内核协议栈就会按照用户态对报文的处理意见将报文做接受、丢弃等处理。整个处理的过程就相当于一个用户态的防火墙,所有数据包的实质性处理都放在用户态进行。这样,即使是不具有深入内核知识的开发人员,也可以开发出适应一定应用场合的用户态防火墙。
机制
前面讲到,所谓IP Queue机制,只是当NF上Hook函数对数据包处理的返回值为NF_QUEUE时,协议栈会将数据包交给内核中的ip_queue模块。而ip_queue又是怎么将数据包传递给用户态的呢?这里就涉及到在内核开发中常见的问题,如何将内核态的数据传递到用户态,实现内核空间和用户空间的通信。具体实现的方法有多种。本人的博客中也总结了若干种,并配有测试的例程:http://blog.chinaunix.net/u/33048/article.html.对于IP Queue,则是使用Netlink机制实现内核态和用户态的交互。
NetLink是Linux系统特有的、基于socket编程接口的通信机制。它是一个面向数据报文的服务,并提供NETLINK_ROUTE(更新和修改路由操作)、NETLINK_FIREWALL (接受和发送IPv4协议NF传输的包,基于内核的ip_queue模块),NETLINK_ARPD(用户态ARP表操作)等多种通信协议。在创建基于IP Queue的NetLink Socket时,将采用如下系统调用:
fd =
socket(PF_NETLINK, SOCK_RAW, NETLINK_FIREWALL);
这里,PF_NETLINK指明要创建NetLink Socket;SOCK_RAW指明采用原始套接字,也可以采用SOCK_DGRAM,因为NetLink机制的实现并不区分SOCK_RAW和SOCK_DGRAM;参数NETLINK_FIREWALL则指明通信协议采用IP Queue。
既然IP Queue是基于NetLink的,其消息格式自然也遵从NetLink的规范。NetLink消息由两部分组成:消息头(struct nlmsghdr)和数据负载(data payload)。
消息头的定义如下(include/linux/netlink.h):
[quote]
struct nlmsghdr
{
__u32 nlmsg_len;
/*消息长度*/
__u16
nlmsg_type;/*消息类型*/
__u16
nlmsg_flags;/*额外的标志*/
__u32
nlmsg_seq; /*序列号*/
__u32
nlmsg_pid; /*进程号*/
[/quote]
struct nlmsghdr
{
__u32 nlmsg_len;
/*消息长度*/
__u16
nlmsg_type;/*消息类型*/
__u16
nlmsg_flags;/*额外的标志*/
__u32
nlmsg_seq; /*序列号*/
__u32
nlmsg_pid; /*进程号*/
};
所有的IP Queue消息都将包含一个struct nlmsghdr消息头,具体的IP Queue消息则包含在NetLink消息的数据负载中。有关NetLink消息格式的详情可以参见手册页Netlink(7)。
IP
Queue编程接口
使用IP Queue机制的程序必须包含如下的头文件:
#include
在这个头文件中定义了所有IP Queue消息的格式。以下谈到关于IP Queue的若干个数据结构以及宏定义都包含在该文头件。
IP Queue消息可以分为两大类:由内核协议栈发给用户态进程的IP Queue消息和由用户态进程发给内核的IP Queue消息。
由内核协议栈发给用户态进程的IP Queue消息(nlmsghdr.nlmsg_type =
IPQM_PACKET),其数据结构为ipq_packet_msg_t,定义如下:
/* Messages
sent from kernel */
typedef struct
ipq_packet_msg {
unsigned long packet_id; /* 报文的ID号 */
unsigned long mark; /* NF标记值 */
long timestamp_sec; /*报文到达时间(秒) */
long timestamp_usec; /* 报文到达时间(毫秒) */
unsigned int hook; /* 报文所处的NF hook点 */
char indev_name[IFNAMSIZ]; /* 流入网口名称 */
char outdev_name[IFNAMSIZ]; /* 流出网口名称 */
unsigned short hw_protocol; /*硬件协议(网络顺序)*/
unsigned short hw_type; /* 硬件类型 */
unsigned char hw_addrlen; /*硬件地址长度*/
unsigned char hw_addr[8]; /* 硬件地址 */
size_t data_len; /* 报文数据的长度 */
unsigned char payload[0]; /* 报文本身的数据,可选 */
}
ipq_packet_msg_t;
这个数据结构也被称为“报文的元数据”。个人理解,所谓“报文的元数据”应该是关于报文的摘要信息,而不包括报文数据的本身。从上面的这个结构体中也可以看出来。内核除可以单独向用户进程传递“报文的元数据”以外,也可以同时传递报文本身。此时,报文本身的数据将存储在ipq_packet_msg_t数据成员payload开始的地方。
至于内核在什么情况下向用户传递报文的元数据,什么情况下向用户传递报文的元数据加报文本身的数据,那就要看用户所请求的模式了。下面将讲述用户态发到内核态消息的格式,也正好解答了我们这里提出的问题。
用户态发到内核态的消息,其数据结构如下所示:
typedef struct
ipq_peer_msg {
union {
ipq_verdict_msg_t verdict;
ipq_mode_msg_t mode;
} msg;
}
ipq_peer_msg_t;
通过该数据结构可知,这类消息又分为“模式设置消息(nlmsghdr.nlmsg_type = IPQM_MODE)”和“断言消息(nlmsghdr.nlmsg_type = IPQM_VERDICT)”两个子类。
“模式设置消息”的数据结构定义如下:
typedef struct ipq_mode_msg {
unsigned char value;/* 请求的模式 */
size_t range;/* 请求拷贝的报文长度*/
} ipq_mode_msg_t;
这里,请求模式value的值可以是IPQ_COPY_NONE、IPQ_COPY_META和IPQ_COPY_PACKET,具体解释如下:
(1)请求模式value为IPQ_COPY_NONE时,报文将被丢弃;
(2)请求模式value为IPQ_COPY_META时,内核将在其后的报文传递中只传递“报文的元数据”。
以上两种情形传递range的值将被内核忽略。内核中会将该值置为0。
(3)请求模式value为IPQ_COPY_PACKET时,内核将同时传递“报文的元数据”和报文本身,报文本身的传递长度由ipq_mode_msg_t的另一个数据成员range指定。 range的最大值不能超过IP报文的最大长度,也就是0xFFFF。否则,会被自动置为0xFFFF。如果请求的长度大于报文自身的长度,将会按照报文自身长度进行传递。
另一子类即“断言消息”,其数据类型定义如下:
typedef struct
ipq_verdict_msg {
unsigned int
value;
unsigned long
id;
size_t
data_len;
unsigned char
payload[0];
}
ipq_verdict_msg_t;
其中,value是用户态程序回传给内核的对当前报文的处理意见,可以是NF_ACCEPT或NF_DROP等值。id则是用以区分报文的标识号,即内核传来的ipq_packet_msg_t结构中的packet_id。当用户态程序修改了当前报文以后,需要将报文重新传递回内核,此时,新的报文内容必须存储在payload的开始处,并由data_len指明新报文的长度。
从上述内容可以看出,在整个IP Queue的报文传递过程中,用户态程序和内核协议栈之间的互动顺序是:
(1)用户态程序利用“模式设置消息”告诉内核协议栈所请求的报文传递模式;
(2)根据这个模式,内核组织好等待传递的消息,通过NetLink Socket发给用户态程序;
(3)用户态程序对接收到的数据包进行处理,得出该报文的处理意见(可能同时修改当前报文),并回传给内核。
IP
Queue数据包的用户态例程
由于IP Queue是使用Netlink机制进行内核态和用户态通信的。因此,用户态要接收内核态发送的IP Queue数据包,就需要设计相应的Netlink程序,也就是设计相应的基于Netlink的socket程序即可。这里,我不会详细介绍如何使用Netlink机制实现用户态和内核态进行通信。我假设阅读本文的朋友,已经熟悉了Netlink的使用。如果对Netlink的使用还不是很熟悉,那么可以参考独孤九贱大侠的文章——《Linux 用户态与内核态的交互——netlink 篇》,其链接为:
.
这篇文章提供了一个使用netlink的完整的例程,包括内核态和用户态。讲的非常清楚,我看完这篇文章,又跑了一下上面提供的例程,基本上熟悉了Netlink的使用方法。
当然,如果读者不想花时间再去了解netlink的话,也可以通过这篇文章熟悉Netlink的使用。因为我这里提供的是完整的用户态例程,我会将源码完全提供出来,对于急于通过执行程序观察结果来学习Netlink和IP Queue的朋友,也可以通过随后提供的方法编译并执行程序。
以下讲述用户态例程接收IP Queue数据包的程序设计。
其实,由于Netlink程序也是使用socket的方式进行通信。那么接收IP Queue报文的方式应该遵循socket的标准流程,具体流程如下:
(1)调用socket()创建一个地址类型为PF_NETLINK(AF_NETLINK)的套接字。该套接字使用SOCK_RAW方式传输数据,协议类型为NETLINK_FIREWALL,即使用IP Queue;
(2)调用bind()将本地地址(Netlink通信双方使用该协议特有的地址格式,见下面struct sockaddr_nl)绑定到已建立的套接字上;
struct sockaddr_nl {
sa_family_t nl_family;
/* AF_NETLINK */
unsigned short nl_pad;
/* Zero. */
pid_t nl_pid; /* Process ID. */
__u32 nl_groups; /* Multicast groups mask. */
};
(3)调用sendto()发送相关的配置信息,告诉内核应用程序准备接受的是数据包的元数据,还是同时包括数据包本身;
(4)调用recvfrom()接受内核态发送来的IP Queue报文;
(5)调用close()关闭套接字,结束通信。
看了以上流程,我相信很多熟悉socket编程的朋友已经可以写出接收IP Queue报文的用户态程序了。
本文中的示例代码的实现整体也是依照上面的步骤。但在细节的实现上,参考了iptables源码给给出的libipq库的实现代码。libipq库是iptables中封装的实现用户态接收和发送IP Queue报文操作的,也就相当于对上面总结的IP Queue报文接受流程进行封装。整个libipq库分别由libipq.c和libipq.h两个源文件。我这里将两个源文件移植(基于iptables-
以下将对三个源文件进行分析。
该头文件定义了一个关键的数据结构,并提供了所有进行Netlink通信的API.
数据结构的定义如下:
struct
ipq_handle
{
int fd;
struct sockaddr_nl local;
struct sockaddr_nl peer;
};
其中,fd是socket通信的描述符,local和peer分别是Netlink通信双方的地址。
除了定义数据结构,剩下的主要就是提供给用户调用的API,函数列表如下:
struct
ipq_handle *ipq_create_handle(u_int32_t flags, u_int32_t protocol);
int
ipq_destroy_handle(struct ipq_handle *h);
ssize_t
ipq_read(const struct ipq_handle *h, unsigned char *buf, size_t len);
int
ipq_set_mode(const struct ipq_handle *h, u_int8_t mode, size_t len);
ipq_packet_msg_t
*ipq_get_packet(const unsigned char *buf);
int
ipq_message_type(const unsigned char *buf);
int
ipq_get_msgerr(const unsigned char *buf);
int
ipq_set_verdict(const struct ipq_handle *h,
ipq_id_t id,
unsigned int verdict,
size_t data_len,
unsigned char *buf);
int
ipq_ctl(const struct ipq_handle *h, int request, ...);
char
*ipq_errstr(void);
void
ipq_perror(const char *s);
我将在下面libipq.c的讲解中对若干我们将要用到的一些函数进行分析。
该源文件实现了libipq.h中定义的所有函数,并定义了一些出错信息。
(1)ipq_create_handle()函数申请了一个struct ipq_handle *h结构体,用来存储随后创建的IPv4 socket通信的fd,以及通信双方的地址。本函数完成了通信双方地址的初始化,并将本地地址绑定到已生成的fd上。
ipq_create_handle()函数的源码如下
struct
ipq_handle *ipq_create_handle()
{
int status;
struct ipq_handle *h;
h = (struct ipq_handle *)malloc(sizeof(struct
ipq_handle));
if (h == NULL) {
ipq_errno = IPQ_ERR_HANDLE;
return NULL;
}
memset(h, 0, sizeof(struct ipq_handle));
if (protocol == PF_INET)
h->fd
= socket(PF_NETLINK, SOCK_RAW, NETLINK_FIREWALL);
else {
ipq_errno = IPQ_ERR_PROTOCOL;
free(h);
return NULL;
}
if (h->fd == -1) {
ipq_errno = IPQ_ERR_SOCKET;
close(h->fd);
free(h);
return NULL;
}
memset(&h->local, 0, sizeof(struct
sockaddr_nl));
h->local.nl_family = AF_NETLINK;
/*传递本地的pid*/
h->local.nl_pid = getpid();
h->local.nl_groups = 0;
status = bind(h->fd, (struct sockaddr
*)&h->local, sizeof(h->local));
if (status == -1) {
ipq_errno = IPQ_ERR_BIND;
close(h->fd);
free(h);
return NULL;
}
memset(&h->peer, 0, sizeof(struct
sockaddr_nl));
h->peer.nl_family = AF_NETLINK;
/*代表通信的另一方为内核*/
h->peer.nl_pid = 0;
h->peer.nl_groups = 0;
return h;
}
ipq_destroy_handle()函数关闭由ipq_create_handle()建立起来的fd,并释放申请的内存。源码如下:
int
ipq_destroy_handle(struct ipq_handle *h)
{
if (h) {
close(h->fd);
free(h);
}
return 0;
}
(2)向内核发送模式请求的函数
int
ipq_set_mode(const struct ipq_handle *h,
u_int8_t mode, size_t range)
{
/*构造一个向内核发送报文的结构体*/
struct {
struct nlmsghdr nlh;
ipq_peer_msg_t pm;
} req;
memset(&req, 0, sizeof(req));
req.nlh.nlmsg_len =
NLMSG_LENGTH(sizeof(req));
req.nlh.nlmsg_flags = NLM_F_REQUEST;
req.nlh.nlmsg_type = IPQM_MODE;
req.nlh.nlmsg_pid = h->local.nl_pid;
/*告诉协议栈所请求的报文传递模式*/
req.pm.msg.mode.value = mode;
/*请求内核返回报文的长度*/
req.pm.msg.mode.range = range;
return ipq_netlink_sendto(h, (void
*)&req, req.nlh.nlmsg_len);
}
在构造完向内核发送的结构体req并设置相关内容之后,调用ipq_netlink_sendto函数发送用户态的请求数据,该函数代码如下:
static ssize_t
ipq_netlink_sendto(const struct ipq_handle *h,
const void
*msg, size_t len)
{
int status = sendto(h->fd, msg, len, 0,
(struct sockaddr
*)&h->peer, sizeof(h->peer));
if (status < 0)
ipq_errno = IPQ_ERR_SEND;
return status;
}
ipq_netlink_sendto函数直接调用了sendto系统调用发送用户态的数据,返回的是发送出去的数据长度。当sendto调用失败时,对全局变量ipq_errno 赋值IPQ_ERR_SEND。这样方便以后用专门返回出错信息的函数引用。
(3)用户态发送了请求数据包之后,就处于等待接收内核返回数据包的状态。一旦内核NF得到包处理函数返回NF_QUEUE时,该包就会被ip_queue模块发送到用户态。用户态接收IP Queue数据包的函数为:
ssize_t
ipq_read(const struct ipq_handle *h, unsigned char *buf, size_t len)
该函数的代码如下。其中buf存储来自内核态的数据包,len为buf的长度。
ssize_t
ipq_read(const struct ipq_handle *h,
unsigned char *buf, size_t
len)
{
return ipq_netlink_recvfrom(h, buf, len);
}
该函数直接调用ipq_netlink_recvfrom()函数,其源码为:
static ssize_t
ipq_netlink_recvfrom(const struct ipq_handle *h,
unsigned char
*buf, size_t len)
{
unsigned int addrlen;
int status;
struct nlmsghdr *nlh;
/*buf长度的校验,不能小于Netlink Message的头部长度*/
if (len < sizeof(struct nlmsgerr)) {
ipq_errno = IPQ_ERR_RECVBUF;
return -1;
}
addrlen = sizeof(h->peer);
status = recvfrom(h->fd, buf, len, 0,
(struct sockaddr
*)&h->peer, &addrlen);
if (status < 0) {
ipq_errno = IPQ_ERR_RECV;
return status;
}
/*判断接收到的发送方的地址长度是否正确*/
if (addrlen != sizeof(h->peer)) {
ipq_errno = IPQ_ERR_RECV;
return -1;
}
/*内核态向用户态发送数据报文时,其pid=0*/
if (h->peer.nl_pid != 0) {
ipq_errno = IPQ_ERR_RECV;
return -1;
}
if (status == 0) {
ipq_errno = IPQ_ERR_NLEOF;
return -1;
}
nlh = (struct nlmsghdr *)buf;
/*判断是否发生数据报文被截断的情况*/
if (nlh->nlmsg_flags & MSG_TRUNC ||
nlh->nlmsg_len > status) {
ipq_errno = IPQ_ERR_RTRUNC;
return -1;
}
return status;
}
该函数返回读取到报文的实际长度。
至此,我们已经可以通过上面几个函数实现从内核态接收到既定模式的IP Queue报文。
(4)输出出错信息
char
*ipq_errstr(void)
{
return ipq_strerror(ipq_errno);
}
static char
*ipq_strerror(int errcode)
{
if (errcode < 0 || errcode >
IPQ_MAXERR)
errcode = IPQ_ERR_IMPL;
return ipq_errmap[errcode].message;
}
根据函数执行过程中记录的出错信息,打印对相关出错的具体提示。
这个函数就是具体的测试函数。功能比较简单,通过调用libipq.c中提供的API实现获取IP Queue数据包的简单信息,包括数据包在本地机上的入口以及报文的长度等,
整个源码如下:
/*
* ipq_usr.c
*
* Testing program for receiving IP Queue
packets from kernel
*
* Dec 1, 2008
* Godbach created.
*
* This program is free software; you can
redistribute it and/or modify
* it under the terms of the GNU General Public
License as published by
* the Free Software Foundation; either version
2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that
it will be useful,
* but WITHOUT ANY WARRANTY; without even the
implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE. See the
* GNU General Public License for more details.
*
*/
#include
#include
#include
#include
"libipq.h"
struct ipq_handle
*h = NULL;
static void
sig_int(int signo)
{
ipq_destroy_handle(h);
printf("Exit: %s\n",
ipq_errstr());
exit(0);
}
int main(void)
{
unsigned char buf[1024];
/* creat handle*/
h = ipq_create_handle(0, PF_INET);
if(h == NULL){
printf("%s\n", ipq_errstr());
return 0;
}
printf("ipq_creat_handle
success!\n");
/*set mode*/
unsigned char mode = IPQ_COPY_PACKET;
int range = sizeof(buf);
int ret = ipq_set_mode(h, mode, range);
printf("ipq_set_mode: send bytes =%d,
range=%d\n", ret, range);
/*register signal handler*/
signal(SIGINT, sig_int);
/*read packet from kernel*/
int status;
struct nlmsghdr *nlh;
ipq_packet_msg_t *ipq_packet;
while(1){
status = ipq_read(h, buf,
sizeof(buf));
if(status > sizeof(struct
nlmsghdr))
{
nlh = (struct nlmsghdr *)buf;
ipq_packet =
ipq_get_packet(buf);
printf("recv bytes =%d,
nlmsg_len=%d, indev=%s, datalen=%d, packet_id=%x\n", status,
nlh->nlmsg_len,
ipq_packet->indev_name, ipq_packet->data_len,
ipq_packet->packet_id);
}
}
return 0;
}
测试环境的建立
(1)内核态:要求已编译的内核支持Netlink机制, 并进入内核源码目录net/ipv4/netfilter下,检查是否生成ip_queue.ko。如果有相应的文件,则确保该模块是否加载,没有加载的话,modprobe ip_queue进行加载
(2)用户态:要求有已经安装iptables,并加上一条如下规则:
iptables -I
INPUT -p icmp -j QUEUE
这里我们在INPUT链上开始处添加了一条对所有ICMP报文进行IP Queue的规则。通过添加不同的iptables规则,可以对不同的报文进行IP Queue。
如果系统上没有安装iptables的话,那么可以用一个简单的内核模块来实现其功能。即在NF对应的Hook点上注册一个钩子函数,对于某种类型的数据包直接return NF_QUEUE即可。附件的源码中我提供了一个模块程序,在NF的PRE_ROUTING出注册了一个对所有ICMP报文return NF_QUEUE的模块。没有iptables的朋友可以使用这个小模块程序替代。
注意:我这里所使用的内核为
程序的测试
搭建好上面提示的环境之后,可以对应用程序的源码进行编译:
gcc libipq.c
ipq_user.c -o ipq_user
执行ipq_user:
[root@localhost
ipq_user]# ./ipq_user
ipq_creat_handle
success!
ipq_set_mode:
send bytes =44, range=1024
随后,程序处于等待接受内核数据包的状态。我们从另外一台主机发送ping包到本地主机,然后看到终端的输出为:
[root@localhost
ipq_user]# ./ipq_user
ipq_creat_handle
success!
ipq_set_mode:
send bytes =44, range=1024
recv bytes
=148, nlmsg_len=148, indev=eth0, datalen=60, packet_id=c
recv bytes
=148, nlmsg_len=148, indev=eth0, datalen=60, packet_id=cb
recv bytes
=148, nlmsg_len=148, indev=eth0, datalen=60, packet_id=c72aa920
recv bytes
=148, nlmsg_len=148, indev=eth0, datalen=60, packet_id=c
Exit: No error
从以上信息中可以看出:
(1)用户态发送的模式设置信息的包长度为44 bytes;
(2)接收到内核态发送的包长度为148bytes, 这和从收到的IP Queue包中保存的长度nlmsg_len一致。
(3)ping包进入本地主机的eth0网口,报文的长度为60 bytes。这个实际的ping包的长度一致。
好了,到现在为止,我们已经成功的通过程序接收到内核态发送的IP Queue数据包。我将在下一篇文章中讲解用户态对接收到报文的简单处理以及发送给内核的整个过程。
附件
附件中提供了此次测试进行的应用程序的源码,和一个简单的对icmp报文进行IP Queue的内核模块程序。
|
wangyuangood2009-01-03 21:09:00
喜欢你作品中的严谨的写作风格,通俗易懂的文笔以及求实的态度。期待你的Linux内核IP Queue机制的分析(二) Linux内核IP Queue机制的分析(三)
wangyuangood2009-01-03 21:08:48
喜欢你作品中的严谨的写作风格,通俗易懂的文笔以及求实的态度。期待你的Linux内核IP Queue机制的分析(二) Linux内核IP Queue机制的分析(三)
wangyuangood2009-01-03 21:05:10
大家可以鄙视我,因为我向来看帖不回帖!但是今天,我不得不败在你的才华裙下!!楼主,我服了U!! 我爱你篇有技术含量的文章!!