#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

https://kernelnewbies.org/FAQ/LikelyUnlikely

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

https://lwn.net/Articles/742170/