#0122 xdp & go¶
cgo¶
__uint128_t/__int128_t 类型¶
C 中的 __uint128_t/__int128_t 这两个类型在 go 中对应 [16]byte
。
import "fmt"
/*
__uint128_t u128;
*/
import "C"
func main() {
C.u128 = [16]byte{15: 255}
fmt.Println(C.u128)
}
packed struct¶
cgo 不支持 __packed__,以下代码在编译的时候会报 unknown field
错误。
package main
import "fmt"
/*
#include <stdint.h>
typedef struct {
uint32_t ipv4;
__uint128_t ipv6;
uint16_t proto;
uint16_t port;
} __attribute__((__packed__)) packed;
*/
import "C"
func main() {
fmt.Println(C.packed{ipv6: [16]byte{}})
// fmt.Printf("%#v\n", C.packed{})
}
报错信息如下:
./t.go:20:23: unknown field 'ipv6' in struct literal of type _Ctype_struct___0
注释掉引用 ipv6 字段的行,换成下面一行打印 struct 内容,可以看到结构体中缺少了 ipv6 这个字段。
main._Ctype_struct___0{ipv4:0x0, _:[16]uint8{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, proto:0x0, port:0x0}
https://github.com/golang/go/wiki/cgo#struct-alignment-issues
这个按官方文档的建议是将 struct 当成一个 []byte
来处理,具体可以参考:https://medium.com/@liamkelly17/working-with-packed-c-structs-in-cgo-224a0a3b708b
另外一个 dirty hack 就是似乎最后一个字段和前面字段有 padding 的话,就没问题了?,如下将结构体改成这样就没问题了。
typedef struct {
uint16_t proto;
uint16_t port;
uint32_t ipv4;
__uint128_t ipv6;
} __attribute__((__packed__)) packed;
如何获取 slice 底层数组的地址¶
s := []byte{0, 1, 2}
// 获取地址
fmt.Printf("%v\n", &s[0])
// 打印出 slice 结构体的内容
fmt.Printf("%#v\n", (*reflect.SliceHeader)(unsafe.Pointer(&s)))
运行结果:
&reflect.SliceHeader{Data:0xc00002c008, Len:3, Cap:3}
0xc00002c008
likeyly/unlikeyly 宏¶
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
likely, unlikely 这两个宏给编译器提供分支预测信息,告诉编译器哪个分支更容易执行到,让编译器在遇到分支的时候将 likely 中的代码安排在不需要 jump 的路径上,不 jump 就不会 flush processor pipeline,性能更优。
比如下面的代码:
if (unlikely (a == 2))
a++;
else
a--;
在编译为汇编后,编译器会如下安排指令:
80483ca: 83 f8 02 cmp $0x2,%eax
80483cd: 74 12 je 80483e1 <main+0x31>
// likely 的代码直接放在 jump 指令之后
80483cf: 48 dec %eax
cilium/ebpf map 接口中的 interface{} 参数¶
cilium/ebpf 库中不少 map 操作接口接收一个 interface{}
参数,这一类参数默认是使用 binary.Read/Write 按照主机字节序来将数据编码成 []byte
,如果需要自定义编码方法,可以使用自定义类型并实现 encoding.BinaryMarshaler 和 encoding.BinaryUnmarshaler 这两个接口。
https://godoc.org/github.com/cilium/ebpf#Map
当然也可以使用 unsafe.Pointer
绕过编码过程。
key := [5]byte{'h', 'e', 'l', 'l', 'o'}
value := uint32(23)
map.Put(unsafe.Pointer(&key), unsafe.Pointer(&value))
https://godoc.org/github.com/cilium/ebpf#example-Map–ZeroCopy
使用 go 加载 tc bpf¶
link, err := netlink.LinkByName("eth0")
if err != nil {
log.Fatal(err)
}
// tc qdisc add dev eth0 clsact
attrs := netlink.QdiscAttrs{
LinkIndex: link.Attrs().Index,
Handle: netlink.MakeHandle(0xffff, 0),
Parent: netlink.HANDLE_CLSACT,
}
qdisc := &netlink.GenericQdisc{
QdiscAttrs: attrs,
QdiscType: "clsact",
}
if err = netlink.QdiscReplace(qdisc); err != nil {
log.Fatal("Replacing qdisc failed:", err)
}
// tc filter add dev eth0 ingress bpf da obj foo.o sec mycls
filterattrs := netlink.FilterAttrs{
LinkIndex: link.Attrs().Index,
Parent: netlink.HANDLE_MIN_INGRESS,
Handle: netlink.MakeHandle(0, 1),
Protocol: unix.ETH_P_ALL,
Priority: 1,
}
filter := netlink.BpfFilter{
FilterAttrs: filterattrs,
Fd: prog.FD(),
Name: "mycls",
DirectAction: true,
}
if err := netlink.FilterReplace(&filter); err != nil {
log.Fatal("tc bpf filter create or replace failed: ", err)
}
xdpcap 使用方法¶
代码集成参见 这里 。
# xdpcap 命令安装
go get -u github.com/cloudflare/xdpcap/cmd/xdpcap
# 使用方法示例
xdpcap /path/to/pinned/map file.pcap
xdpcap /path/to/pinned/map - | tcpdump -r -
xdpcap /path/to/pinned/map - "tcp and port 80" | tcpdump -r -
负载均衡 xdpcap 捕获的包都是转发出去的 GUE 包,如果需要对内层 IP 包进行过滤,需要使用 xdpcap 项目提供的另一个工具 bpfoff
。该工具会重写 bpf 指令中加载数据的指令,修改指令的偏移量参数,让其跳过外层封包的 header。
# 安装 bpfoff 命令
go get -u github.com/cloudflare/xdpcap/cmd/bpfoff
# 过滤 GUE 封包的内层 IP 包。(ethernet 14 字节 + GUE 封包 40字节)
xdpcap /path/to/pinned/map file.pcap "$(bpfoff 54 "ip and tcp port 53")"
内核配置需求¶
内核需要打开了以下特性开关:
CONFIG_BPF=y
CONFIG_BPF_SYSCALL=y
# [optional, for tc filters]
CONFIG_NET_CLS_BPF=m
# [optional, for tc actions]
CONFIG_NET_ACT_BPF=m
CONFIG_BPF_JIT=y
# [for Linux kernel versions 4.1 through 4.6]
CONFIG_HAVE_BPF_JIT=y
# [for Linux kernel versions 4.7 and later]
CONFIG_HAVE_EBPF_JIT=y
# [optional, for kprobes]
CONFIG_BPF_EVENTS=y
https://github.com/iovisor/bcc/blob/master/INSTALL.md#kernel-configuration
另外 bpf 解包和第二跳转发程序需要以下提交中的新接口:
>= 5.2-rc1
>= v5.8-rc1
所以如果解包程序完全使用 bpf,需要内核版本 >= 5.8,目前符合这个需求的 Longterm release kernel 只有 5.10。
bpf verifier 提权漏洞¶
bpf 默认普通用户可用,因为 bpf verifier 的 bug,导致某些情况下无法检查出 bpf 指令中的内存越界访问,如:
所以线上环境最好使用 sysctl 将普通用户执行 bpf 的权限关闭。
kernel.unprivileged_bpf_disabled=1