raw socket 使用 BPF 过滤报文

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/mrpre/article/details/89916940

raw socket 使用 BPF 过滤报文

抓包

使用 raw socket 进行网络底层抓包,想必大家都清楚(仔细想想tcpdump原理)。这里不赘述,网上许多资料。
注意,网卡需要开启混杂模式、其次程序运行需要root权限。

#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <net/if_arp.h>
#include <net/if.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <net/ethernet.h>
#include <netpacket/packet.h>

unsigned char buffer[102];
void main()
{
    int fd, n, i;
    
    fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
    if (fd < 0 ) {
        perror("socket fail\n");
        exit(1);
    }

    /*block*/
    n = recvfrom(fd, buffer, sizeof(buffer), 0, NULL, NULL);
    if (n < 0) {
        perror("recvfrom none\n");
        exit(1);
    }

    for (i = 0; i < n ;i++) {
        printf("%02x ", buffer[i]);
    }
}

希望能够过滤

法1:抓所有的包,在用户态recvfrom后,自己使用代码逻辑进行过滤。
法2:告诉底层 raw socket, 哪些包是自己想要的,直接在底层被过滤掉。

法1,行得通,但是性能肯定扛不住,所有的包需要从内核拷贝至用户态,这个消耗巨大。
法2,即使用bpf。

首先,获得 过滤规则 对应的的bpf code。比如,我想过滤端口为 9090 的tcp报文,使用tcpdump的-dd参数,就能获得。

$sudo  tcpdump -dd -i eth0 tcp port 9090 -s 0
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 6, 0x000086dd },
{ 0x30, 0, 0, 0x00000014 },
{ 0x15, 0, 15, 0x00000006 },
{ 0x28, 0, 0, 0x00000036 },
{ 0x15, 12, 0, 0x00002382 },
{ 0x28, 0, 0, 0x00000038 },
{ 0x15, 10, 11, 0x00002382 },
{ 0x15, 0, 10, 0x00000800 },
{ 0x30, 0, 0, 0x00000017 },
{ 0x15, 0, 8, 0x00000006 },
{ 0x28, 0, 0, 0x00000014 },
{ 0x45, 6, 0, 0x00001fff },
{ 0xb1, 0, 0, 0x0000000e },
{ 0x48, 0, 0, 0x0000000e },
{ 0x15, 2, 0, 0x00002382 },
{ 0x48, 0, 0, 0x00000010 },
{ 0x15, 0, 1, 0x00002382 },
{ 0x6, 0, 0, 0x00040000 },
{ 0x6, 0, 0, 0x00000000 },

使用bpf code

#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <net/if_arp.h>
#include <net/if.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <net/ethernet.h>
#include <netpacket/packet.h>

#include <linux/filter.h>

unsigned char buffer[102];
void main()
{
    int fd, n, i;
    struct sock_fprog filter;

    struct sock_filter code[] = {
    { 0x28, 0, 0, 0x0000000c },
    { 0x15, 0, 6, 0x000086dd },
    { 0x30, 0, 0, 0x00000014 },
    { 0x15, 0, 15, 0x00000006 },
    { 0x28, 0, 0, 0x00000036 },
    { 0x15, 12, 0, 0x00002382 },
    { 0x28, 0, 0, 0x00000038 },
    { 0x15, 10, 11, 0x00002382 },
    { 0x15, 0, 10, 0x00000800 },
    { 0x30, 0, 0, 0x00000017 },
    { 0x15, 0, 8, 0x00000006 },
    { 0x28, 0, 0, 0x00000014 },
    { 0x45, 6, 0, 0x00001fff },
    { 0xb1, 0, 0, 0x0000000e },
    { 0x48, 0, 0, 0x0000000e },
    { 0x15, 2, 0, 0x00002382 },
    { 0x48, 0, 0, 0x00000010 },
    { 0x15, 0, 1, 0x00002382 },
    { 0x6, 0, 0, 0x00040000 },
    { 0x6, 0, 0, 0x00000000 },
 
    };
   
    filter.len = sizeof(code)/sizeof(struct sock_filter); 
    filter.filter = code;
    
    fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
    if (fd < 0 ) {
        perror("socket fail\n");
        exit(1);
    }
    //设置 sk_filter
    if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) < 0) {
        perror("setsockopt fail\n"); 
        exit(1);   
    }

    /*block*/
    n = recvfrom(fd, buffer, sizeof(buffer), 0, NULL, NULL);
    if (n < 0) {
        perror("recvfrom none\n");
        exit(1);
    }

    for (i = 0; i < n ;i++) {
        printf("%02x ", buffer[i]);
    }
}

此时运行程序,只有9090端口的tcp报文到达本机,recvfrom才会返回。

原理

net/packet/af_packet.c

static int packet_rcv(struct sk_buff *skb, struct net_device *dev,
		      struct packet_type *pt, struct net_device *orig_dev)
{
    ........
	res = run_filter(skb, sk, snaplen);
	if (!res)
		goto drop_n_restore;

在包被raw socket处理时,会执行run_filter,而这过滤规则正是 上文 tcpdump输出的code,这些code由bpf解释程序运行。

实际上,可议使用tcpdump -d查看具体code的行为,如下,类似汇编语言,相对而言可读性高一点了(当然前提你需要熟悉二三层协议)。

$sudo  tcpdump -d -i eth0 tcp port 9090 -s 0 
(000) ldh      [12]														#加载12字节后的自己,即偏去6字节的smac 和 dmac
(001) jeq      #0x86dd          jt 2	jf 8       						# 如果 网络层类型不是0x86dd(IPV6),则跳转到 8
(002) ldb      [20]
(003) jeq      #0x6             jt 4	jf 19
(004) ldh      [54]
(005) jeq      #0x2382          jt 18	jf 6
(006) ldh      [56]
(007) jeq      #0x2382          jt 18	jf 19
(008) jeq      #0x800           jt 9	jf 19                     		#如果 网络层类型不是0x0800(IPV4),则跳转到 19,不处理
(009) ldb      [23]															#ip头中的protocol字段
(010) jeq      #0x6             jt 11	jf 19							#不是6表示不是tcp,不处理
(011) ldh      [20]
(012) jset     #0x1fff          jt 19	jf 13							#检测是否是ip分片,是的就不处理
(013) ldxb     4*([14]&0xf)												#ip头中的length字段,偏过这个长度,就是tcp头
(014) ldh      [x + 14]
(015) jeq      #0x2382          jt 18	jf 16							#0x2382  是9090.匹配sport是否是9090
(016) ldh      [x + 16]
(017) jeq      #0x2382          jt 18	jf 19							#0x2382  是9090.匹配dport是否是9090
(018) ret      #262144
(019) ret      #0
展开阅读全文

没有更多推荐了,返回首页