1. 前言
在2.6.17内核中获取sk_buff中各协议头的参数不再是直接读取skb包中定义的各协议头部分指针,而是使用skb_header_pointer()函数来获取,为什么要进行这种变化值得玩味。
以下内核代码版本为2.6.17.11。
2. skb_header_pointer函数
该函数本身很简单,定义如下:
/* include/linux/skbuff.h */
static inline void *skb_header_pointer(const struct sk_buff *skb, int offset,
int len, void *buffer)
{
int hlen = skb_headlen(skb);
if (hlen - offset >= len)
return skb->data + offset;
if (skb_copy_bits(skb, offset, buffer, len) < 0)
return NULL;
return buffer;
}
其中参数为:
skb:数据包struct sk_buff的指针
offset:相对数据起始头(如IP头)的偏移量
len:数据长度
buffer:缓冲区,大小不小于len
其中skb_headlen()定义为:
/* include/linux/skbuff.h */
static inline unsigned int skb_headlen(const struct sk_buff *skb)
{
return skb->len - skb->data_len;
}
其中skb->len是数据包长度,在IPv4中就是单个完整IP包的总长,但这些数据并不一定都在当前内存页;skb->data_len表示在其他页的数据长度,因此skb->len - skb->data_len表示在当前页的数据大小。
如果skb->data_len不为0,表示该IP包的数据分属不同的页,该数据包也就被成为非线性化的,函数skb_is_nonlinear()就是通过该参数判断,一般刚进行完碎片重组的skb包就属于此类。更详细的关于sk_buff的那几个数据指针的意思可参考:
这样skb_header_pointer()函数就好理解了,先判断要处理的数据是否都在当前页面内,如果是,则返回可以直接对数据处理,返回所求数据指针,否则用skb_copy_bits()函数进行拷贝,这个函数也是个比较复杂的处理函数:
/* net/core/skbuff.c */
/* Copy some data bits from skb to kernel buffer. */
int skb_copy_bits(const struct sk_buff *skb, int offset, void *to, int len)
{
int i, copy;
int start = skb_headlen(skb);
if (offset > (int)skb->len - len)
goto fault;
/* Copy header. */
if ((copy = start - offset) > 0) {
// 拷贝在当前页面中的部分
if (copy > len)
copy = len;
memcpy(to, skb->data + offset, copy);
if ((len -= copy) == 0)
return 0;
offset += copy;
to += copy;
}
// 拷贝本skb中其他碎片中的部分
for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {
int end;
BUG_TRAP(start <= offset + len);
end = start + skb_shinfo(skb)->frags[i].size;
if ((copy = end - offset) > 0) {
u8 *vaddr;
if (copy > len)
copy = len;
vaddr = kmap_skb_frag(&skb_shinfo(skb)->frags[i]);
memcpy(to,
vaddr + skb_shinfo(skb)->frags[i].page_offset+
offset - start, copy);
kunmap_skb_frag(vaddr);
if ((len -= copy) == 0)
return 0;
offset += copy;
to += copy;
}
start = end;
}
// 拷贝其他碎片skb中的数据部分
if (skb_shinfo(skb)->frag_list) {
struct sk_buff *list = skb_shinfo(skb)->frag_list;
for (; list; list = list->next) {
int end;
BUG_TRAP(start <= offset + len);
end = start + list->len;
if ((copy = end - offset) > 0) {
if (copy > len)
copy = len;
if (skb_copy_bits(list, offset - start,
to, copy))
goto fault;
if ((len -= copy) == 0)
return 0;
offset += copy;
to += copy;
}
start = end;
}
}
if (!len)
return 0;
fault:
return -EFAULT;
}
3. 与2.4的区别
在2.4中是没有这一函数的,因为2.4的netfilter首先进行碎片包重组,随即进行skb的线性化检查,对非线性skb包进行线性化,因此合法skb包进入后续hook点操作时实际skb->data_len就都是0了,可以直接操作。
netfilter的碎片重组函数为ip_ct_gather_frags(),在2.4中碎片重组完还进行线性化,而2.6.17中重组完就直接返回了,并不进行线性化操作,因此以后在使用的时候必须检查要处理的数据是否在内存页面中。
4. 结论
由于2.6.17中的碎片重组操作后不进行skb数据包的线性化,因此数据可能存在于不同的内存页面中,对于不在同一页面中的情况不能直接进行数据操作,需要将数据拷贝到一个单独缓冲区后再进行处理。
不知道为什么要这么作,是为提高效率?是否因为现在网络上碎片包已经很少了,大部分情况下都是完整包,但提前线性化检查一下会降低多少效率呢?是为以后发出包的时候不需要再重新分片?