【环境】无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)
}
}