struct sk_buff

struct sk_buff结构被网络的各个层所使用,链路层、网络层和传输层都用到了该数据结构。为了增进处理网络数据包的效率,Linux没有使用在各层之间拷贝数据的方式来传输数据,而采用了增加头部信息的方式来处理。在一块缓冲区的前面增加一块空间,只要将指针的值改变一下就好了,内核使用skb_reserve()来实现操作.

当数据向下传递,直至由网络设备把包发送出去之前,每层协议在处理packet时的第一件事情就是调用skb_reserve()。

但是,当数据是向上传递(网络设备接收到了新的数据)时,之前的协议层的信息就没有什么用了,这时只会将指针指向新的协议层,这样是为了节省CPU时间,提高效率。

管理struct sk_buff结构的几个函数:

static inline void skb_reserve(struct sk_buff *skb, unsigned int len);
static inline unsigned char *skb_put(struct sk_buff *skb, unsigned int len);
static inline unsigned char *skb_pull(struct sk_buff *skb, unsigned int len);
static inline unsigned char *skb_push(struct sk_buff *skb, unsigned int len);

skb_reserve()对一个空缓冲区,通过更改skb->tail和skb->data指针,为headroom预留空间。

skb_put()通过改变skb->tail的指针,减小tail域空间,往skb_buff中增加数据,增加到尾部;

skb_push()通过改变skb->data的指针,增加数据到skb_buff的头部。

skb_pull()改变skb->data指针,把缓冲区里面的数据从buffer的开头去除掉。

Advertisements
struct sk_buff

SKB(struct sk_buff)数据结构的部分分析

我参考的原文地址是http://vger.kernel.org/~davem/skb.html,记录在这里主要是为了帮助自己好理解内核中网络部分的代码。

在Linux内核中,socket buffer(SKB)是Linux内核网络协议代码中最重要的基础。每一个发送或者接收的packet的处理都需要用到该数据结构。
下面结合其数据结构的各成员变量来说明其组成元素和在网络处理过程中发挥的作用:

     struct sk_buff {

/* These two members must be first. */

struct sk_buff *next;

struct sk_buff *prev;

struct sk_buff_head *list;

next 和prev是用来实现list链表的功能。packets在许多类型的链表list和队列queues中出现,比如一个TCP socket的发送队列。list参数来指明当前这个packet是在哪个队列之上。

     struct sock *sk;     

sk用来记录与SKB相关的socket.当一个packet到来或者要被发送出去的时候,与它相关的内存必须传给该socket来进行合适的内存计数。

     struct timeval stamp;     

stamp用来记录其时间戳,或者是到来时候的时间或者是发送出去的时间。但是计算时间的耗费是很大的,因此能避免就避免,除非有必要。net_enable_timestamp()和net_disable_timestamp()是与它相关的两个函数。
stamp在截包进行分析时很有用,还有可以用来实现一些特定的socket选项,还有一些net filter模块也会用到stamp。

     struct net_device *dev;

struct net_device *input_dev;

struct net_device *real_dev;

这三个变量用来帮助记录与该包相关的设备。之所以有这三个不同的网络设备来记录,是因为通过一个虚拟设备的时候,skb->dev可能会被改变。
因此,如果从一个设备A接收到packet,而这个设备A是一个绑定设备实例的一部分,(这个地方没理解作者原文的意思-_-!)。开始的时候skb->dev指向这个设备A,然后会把skb->dev赋给skb->real_dev,而skb->dev会更新为真正被帮定的设备。
同样的,当物理设备收到一个包的时候,它会将自己记录为skb->input_dev。这样,不管虚拟设备存在多少layers,skb->input_dev都能够被找到并确认为从网络上接收数据的真正设备。

     union {

struct tcphdr *th;

struct udphdr *uh;

struct icmphdr *icmph;

struct igmphdr *igmph;

struct iphdr *ipiph;

struct ipv6hdr *ipv6h;

unsigned char *raw;

} h;

union {

struct iphdr *iph;

struct ipv6hdr *ipv6h;

struct arphdr *arph;

unsigned char *raw;

} nh;

union {

unsigned char *raw;

} mac;     

这里保存的是各个协议层的头指针,在构造发送的包和解析接收到的包的时候极其有用。比如,skb->mac.raw被eth_type_trans()设置,当一个ethernet包被接收到的时候。然后我们就可以用skb->mac.raw来确定起mac地址的头。

     struct dst_entry *dst;     

dst是用来记录该包相关的路由。它告诉我们怎样将一个包送到最终目的地;它记录的不仅仅是输入所需的路由,也包括输出的。该数据结构同样复杂,暂不详述。

     struct sec_path *sp;     

不理解,不解释。

     char cb[40];     

它是SKB的控制块。它属于不透明的存储块(private_data?), 经常被协议和一些设备驱动程序用来存储与每个包相关的私有数据。比如,TCP利用它来存储序列号和帧的重传状态。

     unsigned int len,

data_len,

mac_len,

csum;     

len是指整个packet的大小。data_len是指当SKB是由多个页面缓冲组成的时候,在页面缓冲区域的字节总数(现在还没有研究它的具体意思,因此理解的不清楚,可能是错误的)。
mac_len保存的是MAC头的长度。一般来说它是没有必要维护的,只有在一些特殊的处理时才需要,比如IPSEC。
csum记录着该包的checksum。当我们构建一个往外发的packet的时候,先把数据从userpaace得到数据,然后计算checksum,它被累计在skb->csum中。这能够帮助我们计算最终的checksum。有时候可以忽略,当硬件设备来完成包的checksum时。
在处理输入的情况时,csum可以用来存储由设备计算出来的checksum值。如果设备提示 skb->ip_summed = CHECKSUM_HW,意味着csum是记录从skb->data开始的数据区域的checksum值。

     unsigned char local_df,

cloned:1,

nohdr:1,

pkt_type,

ip_summed;     

cloned用来得到对SKB数据的快速的索引,Linux建立起了cloned的概念。
pkt_type存储了该packet的类型信息,如PACKET_*,它的定义在linux/if_packet.h中。包括,PACKET_HOST, PACKET_BOARDCAST, PACKET_MULTICAST等等。
ip_summed记录进行checksum的方式.

     __u32 priority;     

它被用来实现QoS.

     unsigned short protocol,     security;     

protocol域是由像eth_type_trans()这样的方法来初始化的,它被赋予一个类诉ETH_P_*的值,在头文件linux/if_ether.h中有描述。

     void (*destructor)(struct sk_buff *skb);

//...

unsigned int truesize;     

这两个域用来做socket buffer的计数。

     atomic_t users;     

我们使用users域来记录SKB对象的引用情况。相关的函数有skb_get();kfree_skb();


unsigned char *head,

*data,

*tail,

*end;     

这4个指针是用来管理一个SKB包的线性存储空间的核心.它们表示了SKB缓冲区和数据部分的边界。

对应区域如下图所示:

+——–+ <—– head
|headroom|
|        |
+——–+ <—– data
|  data  |
|        |
|        |
|        |
+——–+ <—– tail
|tailroom|
|        |
+——–+ <—– end

SKB(struct sk_buff)数据结构的部分分析

vmware workstation 6.0 on linux 2.6.22 fix…

笔记本上装的是CentOS5.0,最近注意到在关机的时候,最后硬盘总是嘎吱一声响;主要是声音太响了,和windows下关机的声音不同。于是google了一下中文,发现有好多人在问,还给出了在ubuntu和fedora7上的workaround;我在CentOS5.0上看了一下阿,涉及到的sysfs项目根本不存在。没办法,又google了一下英文,终于找到了问题的原由。

这是广泛存在于2.6.22以前版本内核的bug,通常它会出现于使用SATA PATA硬盘和采用udev的linux版本上;别的系统不清楚,反正我笔记本上的CentOS是满足了-_-!。

Bug #63937 in linux-source-2.6.17 (Ubuntu) and

Kernel Bug Tracker Bug 7838

在2.6.22内核版本发布的说明中提到了该bug被修复。所以我就把CentOS5.0的kernel升级到了2.6.22-手动编译的,没发现现成的rpm包。

安装完成后,硬盘的问题没有了。哈哈,如释重负。不过,又发现了一个新的问题:系统在启动过程中提示vmware的服务没法启动,需要重新运行/usr/bin/vmware-config.pl来修复此问题。

按照说明一步一步来,在编译vmnet的内核模块驱动时出错了。出错的原因是linux2.6.22的面向网络的内核api和一些数据结构发生了改动,vmnet在编译过程中无法找到相对应的数据结构。

然后就求助google了,google到一篇blog

vmware workstation 6.0 on Linux 2.6.22

它提供了一个patch,或者用它提供的vmnet.tar把vmware对应安装目录下的vmnet.tar给替换掉就可以了。

替换掉之后,重新运行vmware-config.pl,编译安装就成功了。

enjoy it!

vmware workstation 6.0 on linux 2.6.22 fix…

studying network stack under kernel 2.6 (03)

4. 根据概率丢弃送往特定ip的packets

涉及到的api:

void kfree_skb (struct sk_buff * skb);

Drop a reference to the buffer and free it if the usage count has hit zero.

与前面的处理方式不太一样的地方在于,kfree_skb()的工作放在了内核源代码的原来的地方,而不是放在kernel module里面。如果在module里面将skb给释放掉,然后返回到kernel原来的程序处继续执行的话,可能会出现下面的代码继续存取该skb包的情况,因此引起严重的错误。

所以这次在kernel module中只是判断丢包的条件成立与否,真正的丢包工作就放在了原来的内核代码处。

涉及到的内核代码片段:dev_queue_xmit(skb);

int dev_queue_xmit(struct sk_buff *skb)
{
struct net_device *dev = skb->dev;
struct Qdisc *q;
int rc = -ENOMEM;

//...
}

在原先第一个实例的地方,可以添加一个HOOK:

if(ee_drop_func &amp;&amp; (*ee_drop_func)(skb))Ã  {

kfree_skb(skb);

return 0;

}

内核模块的内容:

在内核模块中,主要完成的工作是,实现这样一个函数:

int drop_packet_func(struct sk_buff *skb);

判断skb包的目的地址是否是需要丢包的地址,如果是的话,判断符合不符合丢包的条件,来设置返回值为1否,为1则表示需要丢弃。

代码略。

studying network stack under kernel 2.6 (03)

studying network stack under kernel 2.6 (02)

3.利用Module来获得收到的SYN封包数

基本思路:设置一个全局变量count来作计数器。当kernel收到一个SYN包时,计数加一;这样就可以得到具体的数值了。

kernel接收packets流程:netif_rx()->ip_rcv()->tcp_v4_rcv()->tcp_v4_do_rcv();其中,tcp_v4_rcv()函数是TCP用来建立SYN封包时调用的函数。所以,我们需要做的就是在该函数中插入一个HOOK。

位置:Linux 2.6.20 net/ipv4/tcp_ipv4.c:1611 line

int tcp_v4_rcv(struct sk_buff *skb);

在此函数之前,声明全局变量:

int ee_syn_count = 0;

EXPORT_SYMBOL(ee_syn_count);

int(*eefunc_syn)(struct sk_buff *skb) = 0;

EXPORT_SYMBOL(eefunc_syn);

需要将eefunc_syn函数的实现放在内核模块中,将对它的调用放在tcp_v4_rcv()函数中的bh_lock_sock(sk);…bh_unlock_sock(sk)之间。

此外,还要提供一个procfs接口来读取ee_syn_count所记录的数值。不过LDD3现在建议使用sysfs来导出信息,不再建议使用/proc文件系统。

内核模块的内容如下:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/ip.h>
#include <net/ip.h>
#include <linux/tcp.h>
#include <linux/skbuff.h>
#include <linux/proc_fs.h>
extern int ee_syn_count;
extern int (*eefunc_syn)(struct sk_buff *skb);
static int ee_packet_monitor(struct sk_buff *skb)
{
struct iphdr *iph;
struct tcphdr *th;
iph = skb->nh.iph;
if(iph->protocol == IPPROTO_TCP) {//useless?
th = skb->h.th;
if(th->syn) { //judge SYN packet
ee_syn_count ++;
if(ee_syn_count >= (1&lt;&lt;16 -1))
ee_syn_count = 0;
}
}
return 0;
}
static int ee_read_syn_count(char *buf, char **start, off_t offset,
int count, int *eof, void *data)
{
int len = sprintf(buf, "syn count = %d\\n", ee_syn_count);
return len;
}
static int __init ee_init(void)
{
eefunc_syn = ee_packet_monitor;
//create one /proc node
create_proc_read_entry("ee_packet_monitor", 0 /*default mode*/,
NULL/*parent dir*/, ee_read_syn_count,
NULL/*client data*/);
printk("ee_syn_module inserted\\n");
return 0;
}
static void __exit ee_exit(void)
{
eefunc_syn = 0;
remove_proc_entry("ee_packet_monitor", NULL/*parent dir*/);
printk("ee_syn_module removed\\n");
}
module_init(ee_init);
module_exit(ee_exit);
MODULE_LICENSE("GPL");

SYN的packet只是在TCP建立连接的时候才会有,因此在client或者server端只能在创建tcp连接的时候才能看到一个。因为ftp是使用tcp协议的,所以我就用ftp连接了一下别的电脑,然后关闭ftp连接;这样会使得ee_syn_count加1。可以使用如下的命令来看一下/proc文件系统的输出:

[root@localhost:lkm]cat /proc/ee_packet_monitor

会得到结果: syn count = 1;

studying network stack under kernel 2.6 (02)

studying network stack under kernel 2.6 (01)

在阅读《利用Module修改Linux TCP/IP Kernel》的时候的笔记。
1. 基本的修改方法:

预先在需要修改的内核代码部分插入简单的代码(HOOK),比如在源文件中加入一个全局的函数指针变量,在代码中加入判断语句,如果该函数指针不为空,则执行之,否则忽略。

然后重新编译,安装内核。

真正的实现部分放在了kernel module的编写上。需要实现一个函数,并把它的地址赋给前面所声明的全局的函数指针变量。

2. 实例1

修改kernel,使得kernel可以在每发出一个packet后,都能在/var/log/messages中产生一条log记录;

涉及到的源文件:net/core/dev.c中的dev_queue_xmit(struct sk_buff *skb);

我们可以在该函数的最开始阶段就加入HOOK代码,

if(hook_func_pointer){ hook_func_pointer;}

然后,需要编写一个kernel module来实现这个hook_func_pointer的功能,这个很简单了.

3. 实例2

利用IP层的Module来修改输出packet的IP header的内容;

IP header的字段可以从网上查到。因为它的绝大多数字段都有实际的用途,如果修改不当的话,可能会造成网络功能的不正常。这里要对TOS字段下手,type of service它可以使得packet具有更高的优先权;修改它应该不会有什么大问题。现在默认的包的TOS为BE,我们想改成EF或者AF。EF>AF>BE. 这个要看路由器是否支持TOS,所以影响不大。

涉及到的文件和位置:ip packet在发送出去的时候所涉及到的函数是ip_finish_output(struct sk_buff *skb).它在net/ipv4/ip_output.c中。照例插入HOOK

实现kernel module: 代码如下

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

#include <linux/skbuff.h>
#include <linux/ip.h>
#include <net/ip.h>

extern int (*eefunc02)(struct sk_buff*);

int change_TOS(struct sk_buff *skb)
{
struct iphdr *iph = skb->nh.iph;

if(iph) {
iph->tos = 0xef;
ip_send_check(skb->nh.iph);
}
return 0;
}

static int __init eeFunc_init(void)
{
eefunc02 = change_TOS;
printk("\\n---module init--\\n");
return 0;
}

static void __exit eeFunc_exit(void)
{
eefunc02 = 0;
printk("\\n--module exit--\\n");
}

module_init(eeFunc_init);
module_exit(eeFunc_exit);

MODULE_LICENSE("GPL");

接着的改进:只修改TCP的ACK包的tos值,书上将其修改为0xb8,说是对应EF.

int change_ack_TOS(struct sk_buff *skb)
{
struct iphdr *iph = skb->nh.iph;
struct tcphdr *th;

if(iph->protocol == IPPROTO_TCP) {
skb->h.raw = (unsigned char *)(skb->nh.raw + iph->ihl*4);
th = skb->h.th;
}

if((tcp_flag_word(th) &amp; TCP_FLAG_ACK)) {
skb->nh.iph->tos = 0xB8;
ip_send_check(skb->nh.iph);
}
return 0;
}
studying network stack under kernel 2.6 (01)