【Linux 驱动】Netfilter/Iptables (七) 内核协议栈skb封装分析(续六)
2015-12-25 15:24
681 查看
上文介绍了netfilter机制下,如何重造并发送一个skb,涉及到内核协议栈编程,而不是我们平时所说的用户层socket网络编程。
我们先来介绍下上面skb重构程序涉及到的几个函数:
首先,有必要说下,也是后面每段程序中都有说道的,就是开发源码树版本是3.13的,这个版本的skb_buff和我们常见的2.4、2.6有很大的不同。
一、主要看一下四个字段:
其表示含义如下图所示《深入理解linux网络技术内幕》
head和end指向缓冲区的头部和尾部,而data和tail指向实际数据的头部和尾部,每一层会在head和data之间填充协议头,或者在tail和end之间添加新的协议数据。
所以根据上面的分析,head和data之间是协议头和有效负载,比如传输层、网络层、以太网帧头。
重点:sk_buff 中的数据部分包含协议层头部和有效负载!
下面就是sk_buff中各个头部的长度字段:
二、alloc_skb 和 skb_reserve
还有,对于alloc_skb(),我们只需要指定其数据部分的大小即可(应用层数据+协议层头部),该函数内部会额外把skb_buff所占的那部分空间也给分配出来,这无需我们操心。
skb_reserve(),是一个定位函数,就是前面alloc一个skb之后,用这个函数调整下data和tail数据指针
skb_reserve() 这个函数就是在缓冲区中预留skb协议头部以及有效数据部分的空间,空间可大可小。
经过前面的alloc_skb和skb_reserve,现在那两个指针(data和tail)发生了变化:
alloc_skb():
skb_reserve(skb, len)之后,data和tail的指针发生了变化,换句话说在head与data之间预留了len大小的空间用于填充协议头和有效负载。
上面预留了整体空间,下面我们还要细分空间,上面的整体空间包括了各个协议层头部和有效负载部分,我们还得按照各层协议头部的防止顺序合理划分空间:
三、skb_push 和 skb_put
怎么分,我们前面用的是 skb_push()
如果前面你看懂了,知道sk_buff 的数据空间布局的话,这个函数一看便知。额,sk_buff 中有好几个len,分别表征有效负载长度,各个协议头部长度,以及整个长度。
再来看看 skb_put()
所以调用skb_put() 的话,前面的skb_reserve()就只需要预留以太网帧头的空间,后面则调用skb_put(),依次往后追加细分网络层头部、协议层头部和有效负载部分了。
两个函数都是用于细分空间,返回细分空间后的首部地址指针,便于后续的协议头部以及有效负载数据填充。
空间细分之后就是单纯的协议层字段填充和有效负载填充了。
可以看出来创建skb几个重要的函数接口就是:
alloc_skb、skb_reserver、skb_push、skb_put
后面三个函数其实都是简单的修改指针操作。
上篇及本篇介绍的都是自己创建一个skb,实际上我们还可以拷贝(skb_copy())接收到的skb,然后针对性的修改发送出去。或者直接修改接收到的skb。由于接收到的skb,空间已经细分好,我们则不需要以上后面三个函数,直接调用以下几个函数定位到各个协议层头部:
然后针对性的修改即可,其余原理是差不多的,这里就不额外介绍了。
参考资料:
《深入理解Linux网络技术内幕》
http://www.2cto.com/os/201502/376226.html
我们先来介绍下上面skb重构程序涉及到的几个函数:
首先,有必要说下,也是后面每段程序中都有说道的,就是开发源码树版本是3.13的,这个版本的skb_buff和我们常见的2.4、2.6有很大的不同。
一、主要看一下四个字段:
//typedef unsigned int sk_buff_data_t; sk_buff_data_t tail; sk_buff_data_t end; unsigned char *head, *data;
其表示含义如下图所示《深入理解linux网络技术内幕》
head和end指向缓冲区的头部和尾部,而data和tail指向实际数据的头部和尾部,每一层会在head和data之间填充协议头,或者在tail和end之间添加新的协议数据。
所以根据上面的分析,head和data之间是协议头和有效负载,比如传输层、网络层、以太网帧头。
重点:sk_buff 中的数据部分包含协议层头部和有效负载!
下面就是sk_buff中各个头部的长度字段:
//局部 __u16 transport_header; __u16 network_header; __u16 mac_header; //sk_buff取消了在该数据结构内放置联合体union字段, //所以编程的时候不能直接调用sk_buff中的联合体成员定位到对应的传输层头部、网络层头部等。 //但是却提供给我们更为便捷的方式: //举网络层为例 static inline struct iphdr *ip_hdr(const struct sk_buff *skb) { return (struct iphdr *)skb_network_header(skb); } static inline unsigned char *skb_network_header(const struct sk_buff *skb) { return skb->head + skb->network_header; } //和我们在linux网路协议栈分析的如出一辙,就是通过先定位整个换缓冲区的头端, //再根据协议层的偏移量定位该协议层,至于协议头的顺序..都知道
二、alloc_skb 和 skb_reserve
还有,对于alloc_skb(),我们只需要指定其数据部分的大小即可(应用层数据+协议层头部),该函数内部会额外把skb_buff所占的那部分空间也给分配出来,这无需我们操心。
skb_reserve(),是一个定位函数,就是前面alloc一个skb之后,用这个函数调整下data和tail数据指针
static inline void skb_reserve(struct sk_buff *skb, int len) { skb->data += len; skb->tail += len; } //len取多大,取决于你后面怎么填充数据了
skb_reserve() 这个函数就是在缓冲区中预留skb协议头部以及有效数据部分的空间,空间可大可小。
经过前面的alloc_skb和skb_reserve,现在那两个指针(data和tail)发生了变化:
alloc_skb():
//tail以及tail之前的字段初始化为0 memset(skb, 0, offsetof(struct sk_buff, tail)); /* Account for allocated memory : skb + skb->head */ ... skb->head = data; skb->data = data; ... skb->end = skb->tail + size; //head和data均指向分配的skb数据部分(协议头+有效负载)的首地址位置, //end则指向尾端位置,tail值就是0
skb_reserve(skb, len)之后,data和tail的指针发生了变化,换句话说在head与data之间预留了len大小的空间用于填充协议头和有效负载。
上面预留了整体空间,下面我们还要细分空间,上面的整体空间包括了各个协议层头部和有效负载部分,我们还得按照各层协议头部的防止顺序合理划分空间:
三、skb_push 和 skb_put
怎么分,我们前面用的是 skb_push()
unsigned char *skb_push(struct sk_buff *skb, unsigned int len) { skb->data -= len; skb->len += len; if (unlikely(skb->data<skb->head)) skb_under_panic(skb, len, __builtin_return_address(0)); return skb->data; } //看到起实现,我们就知道,调用这个函数来细分空间,必须严格按照数据帧的格式来划分 //以太网帧头 | 网络层首部 | 传输层首部 | 应用层数据 //所以先得细分应用层数据,最后是以太网帧头 //eg, pdata = skb_push(skb, pkt_len); udph = (struct udphdr*)skb_push(skb, sizeof(struct udphdr)); iph = (struct iphdr*)skb_push(skb, sizeof(struct iphdr)); ethdr = (struct ethhdr*)skb_push(skb, sizeof(struct ethhdr));
如果前面你看懂了,知道sk_buff 的数据空间布局的话,这个函数一看便知。额,sk_buff 中有好几个len,分别表征有效负载长度,各个协议头部长度,以及整个长度。
再来看看 skb_put()
unsigned char *skb_put(struct sk_buff *skb, unsigned int len) { unsigned char *tmp = skb_tail_pointer(skb); SKB_LINEAR_ASSERT(skb); skb->tail += len; skb->len += len; if (unlikely(skb->tail > skb->end)) skb_over_panic(skb, len, __builtin_return_address(0)); return tmp; } //看代码,这里改动的是tail(数据的尾部指针),所以这里就是在数据区的尾部追加数据, //和skb_push()恰恰是反过来的
所以调用skb_put() 的话,前面的skb_reserve()就只需要预留以太网帧头的空间,后面则调用skb_put(),依次往后追加细分网络层头部、协议层头部和有效负载部分了。
两个函数都是用于细分空间,返回细分空间后的首部地址指针,便于后续的协议头部以及有效负载数据填充。
空间细分之后就是单纯的协议层字段填充和有效负载填充了。
可以看出来创建skb几个重要的函数接口就是:
alloc_skb、skb_reserver、skb_push、skb_put
后面三个函数其实都是简单的修改指针操作。
上篇及本篇介绍的都是自己创建一个skb,实际上我们还可以拷贝(skb_copy())接收到的skb,然后针对性的修改发送出去。或者直接修改接收到的skb。由于接收到的skb,空间已经细分好,我们则不需要以上后面三个函数,直接调用以下几个函数定位到各个协议层头部:
static inline struct ethhdr *eth_hdr(const struct sk_buff *skb); static inline struct iphdr *ip_hdr(const struct sk_buff *skb); static inline struct tcphdr *tcp_hdr(const struct sk_buff *skb); static inline struct udphdr *udp_hdr(const struct sk_buff *skb); ...
然后针对性的修改即可,其余原理是差不多的,这里就不额外介绍了。
参考资料:
《深入理解Linux网络技术内幕》
http://www.2cto.com/os/201502/376226.html
相关文章推荐
- linux 启动过程以及 /etc/rc.d/init.d/目录的一点理解
- linux下查看最消耗CPU、内存的进程
- 【Linux】C语言实现文件夹拷贝
- ubuntu14.04(linux)字符界面下openfire安装配置
- Linux中在主机上实现对备机上目录及文件的操作的C代码实现
- /bin、/sbin、/usr/bin、/usr/sbin 目录的区别
- linux sed&awk简述
- CentOS6.5安装VMwareTools
- Linux C编程--main函数参数解析
- Linux搭建FFMPEG环境实现MP4格式转m3u8格式
- HZ jiffies tick linux
- 制作NFS文件系统
- Ubuntu和CentOS下安装配置samba服务详细过程(超级简单的smb.conf)
- linux下的C语言编程(总结篇)
- Linux/Unix 桌面趣事:让桌面下雪
- Linux 服务器安全加固
- linux curl模拟登录网页
- 安装centos出错
- linux常用与用户管理操作命令
- Linux套接字通信常用函数-accept