简介
如果读取数据包时eBPF程序想要读取超过数据包边界的内容,eBPF程序将会被停止执行。
硬件架构
寄存器
* R0 - return value from in-kernel function, and exit value for eBPF program
* R1 - R5 - arguments from eBPF program to in-kernel function
* R6 - R9 - callee saved registers that in-kernel function will preserve
* R10 - read-only frame pointer to access stack
R1-R5是函数调用的参数寄存器,每次调用了之后这几个寄存器的值可能被改变,所以需要每次调用其他函数后都要重新填充。这5个寄存器被映射到对应平台的实际的参数寄存器,在x86-64下,寄存器的映射方法如下:
R0 - rax
R1 - rdi
R2 - rsi
R3 - rdx
R4 - rcx
R5 - r8
R6 - rbx
R7 - r13
R8 - r14
R9 - r15
R10 – rbp
因为x86-64规定rdi, rsi,rdx, rcx, r8, r9用来做参数传递,rbx, r12 - r15用来做调用者保留。
R6-R9会在eBPF调用其他函数前后保持一致,所以可以用来放eBPF变量。
函数调用
一个eBPF函数被调用的时候会自动带一个参数ctx传递给eBPF程序,放在R1里,(在__bpf_prog_run()函数中实现),这个ctx对于用作filter的eBPF程序来说是skb,对于用作seccomp来说是seccomp_data。所以可以看出,一个使用xt_bpf模块的eBPF过滤程序的原理是在约定的hook点,eBPF被调用,skb被作为第一个参数传进eBPF程序,执行完毕,返回值R0作为判断这个包处理结果的返回值(是否丢弃等)。
指令编码
指令类型类型
在指令编码上,使用8位进行编码,针对不同的指令,这8位的使用情况是不同的,但LSB的最后3位都是用来存储指令类型的。指令主要有如下几种类型:
BPF_LD 0x00
BPF_LDX 0x01
BPF_ST 0x02
BPF_STX 0x03
BPF_ALU 0x04
BPF_JMP 0x05
BPF_ALU64 0x07
以上是表示指令编码的最后3位。BPF_JMP是跳转类型的指令,目前有10个。BPF_ALU和BPF_ALU64是运算类指令,目前有14个。而剩下的则是加载与存储类型的指令。也就是说eBPF一共有3大类指令:跳转、运算、加载与存储。
跳转:BPF_JMP
当最后3位是BPF_ALU或BPF_JMP时,8位的指令编码如上,中间一位有两种取值,表示这个指令使用的源寄存器:
可以看出这一位如果是BPF_X(0),使用src_reg作为源寄存器,如果是BPF_K(1),则使用32位的立即数作为源寄存器。
最后3位是BPF_JMP时,操作符包括:
BPF_JA 0x00
BPF_JEQ 0x10
BPF_JGT 0x20
BPF_JGE 0x30
BPF_JSET 0x40
BPF_JNE 0x50 /* eBPF only: jump != */
BPF_JSGT 0x60 /* eBPF only: signed ‘>‘ */
BPF_JSGE 0x70 /* eBPF only: signed ‘>=‘ */
BPF_CALL 0x80 /* eBPF only: function call */
BPF_EXIT 0x90 /* eBPF only: function return */
运算:BPF_ALU和BPF_ALU64
当最后3位是BPF_ALU或者是BPF_ALU64时:
BPF_ADD 0x00
BPF_SUB 0x10
BPF_MUL 0x20
BPF_DIV 0x30
BPF_OR 0x40
BPF_AND 0x50
BPF_LSH 0x60
BPF_RSH 0x70
BPF_NEG 0x80
BPF_MOD 0x90
BPF_XOR 0xa0
BPF_MOV 0xb0 /* eBPF only: mov reg to reg */
BPF_ARSH 0xc0 /* eBPF only: sign extending shift right */
BPF_END 0xd0 /* eBPF only: endianness conversion */
举例
BPF_XOR | BPF_K| BPF_ALU 意味着:src_reg = src_reg ^ imm32
BPF_MOV | BPF_X| BPF_ALU :将src_reg的值移动到dst_reg
BPF_ADD | BPF_X| BPF_ALU64 :dst_reg = dst_reg + src_reg
加载与存储
加载与存储指令也有多个,每个可以操作的数据的大小是不一样的,这个大小的区别在中间2个位:
BPF_W 0x00:4个字节
BPF_H 0x08 :2个字节
BPF_B 0x10 :1个字节
BPF_DW 0x18 :8个字节
前3个位的mode包括:
BPF_IMM 0x00 /* used for 32-bit mov inclassic BPF and 64-bit in eBPF */
BPF_ABS 0x20
BPF_IND 0x40
BPF_MEM 0x60
BPF_LEN 0x80 /* classic BPF only, reserved in eBPF */
BPF_MSH 0xa0 /* classic BPF only, reserved in eBPF */
BPF_XADD 0xc0 /* eBPF only,exclusive add */
其中BPF_ABS和BPF_IND只能用在数据包处理上,在这时,R6里面是输入数据包sk_buff,R0是输出数据包。
bpf_mov R6, R1 /* save ctx */
bpf_mov R2, 2
bpf_mov R3, 3
bpf_mov R4, 4
bpf_mov R5, 5
bpf_call foo
bpf_mov R7, R0 /* save foo() return value */
bpf_mov R1, R6 /* restore ctx for next call */
bpf_mov R2, 6
bpf_mov R3, 7
bpf_mov R4, 8
bpf_mov R5, 9
bpf_call bar
bpf_add R0, R7
bpf_exit
翻译成x86-64是:
push %rbp
mov %rsp,%rbp
sub $0x228,%rsp
mov %rbx,-0x228(%rbp)
mov%r13,-0x220(%rbp)
mov %rdi,%rbx
mov $0x2,%esi
mov $0x3,%edx
mov $0x4,%ecx
mov $0x5,%r8d
callq foo
mov %rax,%r13
mov %rbx,%rdi
mov $0x2,%esi
mov $0x3,%edx
mov $0x4,%ecx
mov $0x5,%r8d
callq bar
add %r13,%rax
mov -0x228(%rbp),%rbx
mov -0x220(%rbp),%r13
leaveq
retq
在c就是:
u64 bpf_filter(u64 ctx)
{
return foo(ctx, 2, 3, 4, 5) + bar(ctx,6, 7, 8, 9);
}
可行的bpf开发方法:
使用tcpdump、使用iptables
iptables -A INPUT \
-p udp --dport 53 \
-m bpf --bytecode "14,0 0 0 20,177 0 0 0,12 0 0 0,7 0 0 0,64 0 00,21 0 7 124090465,64 0 0 4,21 0 5 1836084325,64 0 0 8,21 0 3 56848237,80 0 012,21 0 1 0,6 0 0 1,6 0 0 0," \
-j DROP
llvm、内核提供的编译器、iovisitor的uBPF编译器
BPF CompilerCollection (BCC)这个工具集包含很多用来观测内核性能的工具,全部使用eBPF,并且提供了python的外部编程能力。其也是使用llvm用作底层编译器,并且整合了llvm中对bpf支持的最新进展。但是要求内核支持版本是4.1。
使用llvm编译并插入eBPF
使用llvm,使用llvm可以使用如下命令编译:
clang-3.7 -O2 -target bpf -c sockex1_kern.c-o soc1.o -I/lib/modules/3.19.0-15-generic/build/include-I/lib/modules/3.19.0-15-generic/build/arch/x86/include/uapi/
这样可以编译一个包含了map和eBPF代码的elf文件。然而这个文件并不是用来直接插入内核的eBPf程序代码,它只是一个包含了需要插入内核的各种信息的一个集合体,内核代码在sample/bpf/里面有提供解析这个文件的代码逻辑,可以自动实现解析和插入内核。
但是这种有个缺点,这种都是使用的bpf系统调用进行插入的eBPF代码,然而这个系统调用只支持插入到特定的内核位置:
BPF_PROG_TYPE_SOCKET_FILTER, //附在某一个socket上,只对某一个socket产生影响。
BPF_PROG_TYPE_KPROBE, //附在kprobe上
BPF_PROG_TYPE_SCHED_CLS, //附在cls_bpf分类模块上
BPF_PROG_TYPE_SCHED_ACT, //附在act_bpf模块上
可以看到,无法插入到我们希望的hook点上(因为hook点是netfilter的基础设施,netfilter使用xt_bpf来支持bpf,所以就没有在系统调用层级支持)。而类型BPF_PROG_TYPE_SOCKET_FILTER的bpf,即使是使用raw,得到的数据也只是一份拷贝。这就注定了这个机制只能用来分析,不能用来做过滤。所以,目前这条路不通,过滤只能采用xt_bpf。
使用xt_bpf
这是iptables的扩展,可以使用-m参数传递bpf代码进内核。我们可以使用内核提供的bpf编译程序编译代码,或者是tcpdump –ddd,或者是iptables的nfbpf_compile.c程序。但是这三个编译得到的都是cBPF代码。iptables的用户端程序也只支持cBPF代码。所以使用xt_bpf模块的合理选择应该是使用cBPF编程。
如果要使用eBPF编程,可行的方法是在自己实现钩子函数中执行eBPF,或者是复用在xt_bpf的代码。