无需libpacp库,BPF指令高效捕获指定数据包

【环境】无libpacp库的Linux服务器
【要求】高效率读取数据包,并过滤指定端口和ip

目前遇到两个问题

  • 一是手写BPF,难以兼容,有些无法正常过滤
  • 二是性能消耗问题,尽可能控制到1%

大方向:过滤数据包要在内核层处理,不能放到应用层

Ⅰ 基本常识普及

以太网帧

正常一个从网卡读取到的数据包,基本格式如下

Preamble: AA AA AA AA AA AA AA
SFD: AB
Destination MAC: 00 11 22 33 44 55 
Source MAC: 66 77 88 99 AA BB
EtherType: 08 00  // 表示 IPv4
Data: ...  // 这里是 IPv4 数据包
FCS: 12 34 56 78

参数详细说明
在这里插入图片描述
常见以太网协议类型

case 0x0800:
    fmt.Println(" (IPv4)")
case 0x0806:
    fmt.Println(" (ARP)")
case 0x86DD:
    fmt.Println(" (IPv6)")
case 0x8100:
    fmt.Println(" (VLAN标签)")

ipv4数据包

在这里插入图片描述

Version: 4
IHL: 5
DSCP: 00
Total Length: 00 3C  // 表示 60 字节
Identification: 12 34
Flags: 00
Fragment Offset: 00 00
TTL: 40
Protocol: 06  // 表示 TCP
Header Checksum: 56 78
Source IP: C0 A8 01 01  // 192.168.1.1
Destination IP: C0 A8 01 02  // 192.168.1.2
Options: None
Data: ...  // 这里是 TCP 段

在这里插入图片描述

TCP包

Source Port: 12 34
Destination Port: 56 78
Sequence Number: 00 00 00 01
Acknowledgment Number: 00 00 00 00
Data Offset: 5
Reserved: 00
Control Bits: 02  // 表示 SYN
Window Size: 12 34
Checksum: 56 78
Urgent Pointer: 00 00
Options: None
Data: ...  // 这里是应用层数据

数据包过滤案例说明

第一步:捕获原始数据包frame

1 128 194 0 0 0 130 162 63 131 202 77 0 38 66 66 3 0 0 0 0 0 112 0 60 178 51 77 202 131 0 0 0 0 112 0 60 178 51 77 202 131 128 2 0 0 20 0 2 0 0 0 0 0 0 0 0 0 0 0

取前14个字节
目标mac地址:提取前六个字节
源mac地址:提取后六个字节
以太网类型EtherType:最后两个字节,索引12、13,共两个字节

  • ipv4:0x0800
  • ipv6:0x86DD
  • ARP:0x0806

第二步:IP包判断

以太网头(14) + IP协议字段偏移(9)

tcp/udp判断:第24个字节,索引23,共一个字节
Tcp:6

  • 源端口:索引24、25,最方便的是,判断完后,去掉9字节,留下最后的数据
  • 目标端口:索引26、27

UDP:17
ICMP:1

Ⅱ 如何写出高效有用的BPF指令

第一种:手写BPF指令
【限制】不适合写太过复杂的规则
【工具】使用bpf库

第二种:将tcpdump生成的c语言规则数组,转换成BPF指令
【限制】tcp和udp只能选择一个
【工具】tcpdump + Bpf库

第三种:调用第三方包装好的库libpacp
【限制】需要安装libpacp环境

实际上libpacp实现bpf指令转换主要有两种方式

  • 第一种:AST树拆分tcp指令,生成bpf指令
  • 第二种:转换tcpdump生成的C语言规则数组

Ⅲ BPF过滤案例说明

第一种:手写BPF指令

LoadAbsolute加载数据包:从数据包的指定偏移位置加载一定大小的数据

type LoadAbsolute struct {
        Off  uint32  //定位到数据包中想要加载数据的具体位置
        Size int // 1, 2 or 4,要加载的数据大小,限制加载大小
}

JumpIf 跳转

type JumpIf struct {
    Cond      JumpTest  //用于指定跳转的测试条件
    Val       uint32   // 表示用于比较的值。在进行条件判断时,会将某个寄存器或者数据包中的值与这个 Val 进行比较。
    SkipTrue  uint8  // 当条件判断为真时,要跳过的指令数量。
    SkipFalse uint8  // 当条件判断为假时,要跳过的指令数量
}

RetConstant 退出BPF程序,返回一个常量值

type RetConstant struct {
        Val uint32    //返回值,0表示忽视,4096返回数据包大小
}

LoadIndirect 从内存里以间接方式加载数据的操作

type LoadIndirect struct {
        Off  uint32   //定位到要加载数据的起始位置
        Size int // 1, 2 or 4  //要加载的数据的大小
}

BPF实战操作

调用第三方库https://pkg.go.dev/golang.org/x/net@v0.38.0/bpf

解析过程

bpf.Assemble([]bpf.Instruction{
        // Load "EtherType" field from the ethernet header.
        bpf.LoadAbsolute{Off: 12, Size: 2},
        // Skip over the next instruction if EtherType is not ARP.
        bpf.JumpIf{Cond: bpf.JumpNotEqual, Val: 0x0806, SkipTrue: 1},
        // Verdict is "send up to 4k of the packet to userspace."
        bpf.RetConstant{Val: 4096},
        // Verdict is "ignore packet."
        bpf.RetConstant{Val: 0},
})

BPF作用:捕获ARP数据包

bpf.LoadAbsolute{Off: 12, Size: 2}

从数据包偏移量为12字节位置,加载数据,也就是以太网头部加载协议字段

bpf.JumpIf{Cond: bpf.JumpNotEqual, Val: 0x0806, SkipTrue: 1}

条件判断,如果EtherType 字段不等于0x0806(ARP),则跳过下一条指令
EtherType 为ARP,则继续执行
EtherType 不为ARP,则跳过下一条指令

bpf.RetConstant{Val: 4096}

如果前面判断,EtherType 字段是0x0806,则返回常量值4096,到用户空间上

bpf.RetConstant{Val: 0}

如果前面的EtherType 不是ARP协议,则进入这一条指令,忽视该数据包

第二种:转换C语言规则数组

利用工具:tcpdump
执行指令

 tcpdump -i any -dd 'udp and (dst port 24359) and ip'

生成C语言的规则数组

{ 0x28, 0, 0, 0x00000000 },
{ 0x15, 9, 0, 0x000086dd },
{ 0x15, 0, 8, 0x00000800 },
{ 0x30, 0, 0, 0x0000001d },
{ 0x15, 0, 6, 0x00000011 },
{ 0x28, 0, 0, 0x0000001a },
{ 0x45, 4, 0, 0x00001fff },
{ 0xb1, 0, 0, 0x00000014 },
{ 0x48, 0, 0, 0x00000016 },
{ 0x15, 0, 1, 0x00005f27 },
{ 0x6, 0, 0, 0x00040000 },
{ 0x6, 0, 0, 0x00000000 },

go代码实现

package main

import (
    "fmt"
    "golang.org/x/net/bpf"
)

func main() {
    // 定义 BPF 指令
    instructions := []bpf.Instruction{
        // 加载字节:读取协议类型
        bpf.LoadAbsolute{Off: 0, Size: 1},
        // 比较:如果是 IPv6,跳转到第 9 条指令
        bpf.JumpIf{Cond: bpf.JumpEqual, Val: 0x86dd, SkipTrue: 9},
        // 比较:如果是 IPv4,继续执行
        bpf.JumpIf{Cond: bpf.JumpEqual, Val: 0x0800, SkipFalse: 8},
        // 加载半字:读取 IP 协议字段
        bpf.LoadAbsolute{Off: 29, Size: 2},
        // 比较:如果是 UDP,继续执行
        bpf.JumpIf{Cond: bpf.JumpEqual, Val: 0x11, SkipFalse: 6},
        // 加载字节:读取目标端口
        bpf.LoadAbsolute{Off: 26, Size: 2},
        // 比较:如果目标端口 >= 8191,跳转到第 4 条指令
        bpf.JumpIf{Cond: bpf.JumpGreaterOrEqual, Val: 8191, SkipTrue: 4},
        // 加载字节:读取源地址
        bpf.LoadAbsolute{Off: 20, Size: 4},
        // 比较:检查是否设置了特定标志位
        bpf.JumpIf{Cond: bpf.JumpBitsSet, Val: 0x0016, SkipFalse: 1},
        // 比较:如果目标端口 == 24359,继续执行
        bpf.JumpIf{Cond: bpf.JumpEqual, Val: 24359, SkipFalse: 1},
        // 返回:接受数据包
        bpf.RetConstant{Val: 0x00040000},
        // 返回:丢弃数据包
        bpf.RetConstant{Val: 0x00000000},
    }

    // 打印生成的指令
    for i, inst := range instructions {
        fmt.Printf("Instruction %d: %s\n", i, inst)
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值