之前写了三篇关于内核抓包功能的实现,包括抓包原理、实现以及抓包服务器的实现。基本的功能都已经有了,但是还有些小问题有待解决。今天有空就解决一下。
开发版本基于内核3.4.39。
先介绍一下问题背景,抓包框架是基于netfilter的,通过在Pre-Routing和Post-Routing链上分别挂一个钩子函数来对报文处理。抓包基本上需要报文所有的原始数据,包括mac地址、ip地址以及数据层。对于后两者数据本来都在,但是对于mac地址,就不一定啦。所有进来的报文都是带有mac地址的,但是对于本机出去的报文来说,mac地址也是有的,只不过在IP层还看不到,因为我们挂载post-routing链上的钩子仍属于IP层,系统报文在IP层只负责IP层头域的修改,至于mac层,还需要继续往下走。直到查找邻居缓存,找到后就填写mac地址发送出去,找不到的话就发送arp广播同时缓存该报文,一定时间内没收到arp响应或者缓存的报文数量过多,报文就会被丢弃。如果我们要在IP层去填充mac地址的话,就必须手动去查找系统邻居缓存了。至于怎么查,可以直接参考系统TCP/IP协议栈的处理方式。
我们首先看一下系统是怎么处理mac地址信息的:
首先,ip_output接收skb后,会先让挂在netfilter post-routing链上的钩子函数处理,我们的抓包钩子也在这里,处理完成后,如果报文还在的话,就传给ip_finish_output处理,这个函数主要就是根据 MTU 判断报文长度是否太长,太长的话就调用ip_fragment函数处理,不管报文是否分片,最终都会调用ip_finish_output2函数。
而这个ip_finish_output2函数就去获取邻居信息,通过调用dst_get_neighbour_noref得到邻居信息,之后调用neigh_output函数。neigh_output函数就去判断是否存在缓存项,存在的话就调用neigh_hh_output填充mac地址,填完之后就调用发送函数,至此报文发送完成,但是如果不存在缓存项的话,就会先缓存报文并发送arp请求,直到有响应或者超时或者缓存报文过多就把它丢弃。我们来看一下这几个函数:
static inline int ip_finish_output2(struct sk_buff *skb)
{
struct dst_entry *dst = skb_dst(skb); /* 协议无关的目的缓存相关的数据结构 */
struct rtable *rt = (struct rtable *)dst; /* 路由缓存相关结构体 */
struct net_device *dev = dst->dev;
unsigned int hh_len = LL_RESERVED_SPACE(dev);
struct neighbour *neigh;
//
if (rt->rt_type == RTN_MULTICAST) {
IP_UPD_PO_STATS(dev_net(dev), IPSTATS_MIB_OUTMCAST, skb->len);
} else if (rt->rt_type == RTN_BROADCAST)
IP_UPD_PO_STATS(dev_net(dev), IPSTATS_MIB_OUTBCAST, skb->len);
/* Be paranoid, rather than too clever. */
// 如果首部空间不够大的话,则重新分配
if (unlikely(skb_headroom(skb) < hh_len && dev->header_ops)) {
struct sk_buff *skb2;
skb2 = skb_realloc_headroom(skb, LL_RESERVED_SPACE(dev));
if (skb2 == NULL) {
kfree_skb(skb);
return -ENOMEM;
}
if (skb->sk)
skb_set_owner_w(skb2, skb->sk);
kfree_skb(skb);
skb = skb2;
}
rcu_read_lock();
//获取邻居信息,获取失败就丢弃报文
neigh = dst_get_neighbour_noref(dst);
if (neigh) {
//调用邻居层发送接口
int res = neigh_output(neigh, skb);
rcu_read_unlock();
return res;
}
rcu_read_unlock();
if (net_ratelimit())
printk(KERN_DEBUG "ip_finish_output2: No header cache and no neighbour!\n");
kfree_skb(skb);
return -EINVAL;
}
static inline int neigh_output(struct neighbour *n, struct sk_buff *skb)
{
struct hh_cache *hh = &n->hh;
if ((n->nud_state & NUD_CONNECTED) && hh->hh_len)
return neigh_hh_output(hh, skb); //存在缓存则填充并发送
else
return n->output(n, skb); //不存在则送至缓存队列
}
static inline int neigh_hh_output(struct hh_cache *hh, struct sk_buff *skb)
{
unsigned seq;
int hh_len;
//填充mac地址
do {
int hh_alen;
seq = read_seqbegin(&hh->hh_lock);
hh_len = hh->hh_len;
hh_alen = HH_DATA_ALIGN(hh_len);
memcpy(skb->data - hh_alen, hh->hh_data, hh_alen);
} while (read_seqretry(&hh->hh_lock, seq));
//将mac头域压入数据栈中
skb_push(skb, hh_len);
//发送函数,报文处理完成
return dev_queue_xmit(skb);
}
以上就是内核对网络报文mac地址的处理,可以看到在Netfilter 的Post-Routing链上报文是还没有mac地址的,因此我们这里可以手动去设置,方式就是参考内核的实现,只不过提前处理了。