ci(workflows/bpf-test): add BPF test to cover route logic (#671)

Co-authored-by: mzz <2017@duck.com>
This commit is contained in:
./gray 2024-10-14 11:21:45 +08:00 committed by GitHub
parent 3b77ddfd67
commit 438c05cbd6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 1401 additions and 0 deletions

28
.github/workflows/bpf-test.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: BPF Test
on:
pull_request:
paths:
- "**/*.c"
- "**/*.h"
- "go.mod"
- "go.sum"
- ".github/workflows/bpf-test.yml"
permissions: read-all
jobs:
bpf_tests:
name: BPF Unit Test
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
with:
persist-credentials: false
fetch-depth: 0
- name: Run BPF tests
run: |
git submodule update --init
sudo make ebpf-test || (echo "Run 'make ebpf-test' locally to investigate failures"; exit 1)

View File

@ -79,6 +79,8 @@ clean-ebpf:
rm -f control/bpf_bpf*.o
@rm -f trace/bpf_bpf*.go && \
rm -f trace/bpf_bpf*.o
@rm -f control/kern/tests/bpftest_bpf*.go && \
rm -f control/kern/tests/bpftest_bpf*.o
fmt:
go fmt ./...
@ -99,4 +101,18 @@ ebpf: submodule clean-ebpf
ebpf-lint:
./scripts/checkpatch.pl --no-tree --strict --no-summary --show-types --color=always control/kern/tproxy.c --ignore COMMIT_COMMENT_SYMBOL,NOT_UNIFIED_DIFF,COMMIT_LOG_LONG_LINE,LONG_LINE_COMMENT,VOLATILE,ASSIGN_IN_IF,PREFER_DEFINED_ATTRIBUTE_MACRO,CAMELCASE,LEADING_SPACE,OPEN_ENDED_LINE,SPACING,BLOCK_COMMENT_STYLE
ebpf-test: export BPF_CLANG := $(CLANG)
ebpf-test: export BPF_STRIP_FLAG := $(STRIP_FLAG)
ebpf-test: export BPF_CFLAGS := $(CFLAGS)
ebpf-test: export BPF_TARGET := $(TARGET)
ebpf-test: export BPF_TRACE_TARGET := $(GOARCH)
ebpf-test: submodule clean-ebpf
@unset GOOS && \
unset GOARCH && \
unset GOARM && \
echo $(STRIP_FLAG) && \
go generate ./control/kern/tests/bpf_test.go && \
go clean -testcache && \
go test -v ./control/kern/tests/...
## End Ebpf

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,167 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2022-2024, daeuniverse Organization <dae@v2raya.org>
*/
package tests
import (
"errors"
"fmt"
"os"
"reflect"
"strings"
"testing"
"github.com/cilium/ebpf"
"github.com/vishvananda/netlink/nl"
)
//go:generate go run -mod=mod github.com/cilium/ebpf/cmd/bpf2go -cc "$BPF_CLANG" "$BPF_STRIP_FLAG" -cflags "$BPF_CFLAGS" -target "$BPF_TARGET" bpftest ./bpf_test.c -- -I../headers -I.
type programSet struct {
id string
pktgen *ebpf.Program
setup *ebpf.Program
check *ebpf.Program
}
func runBpfProgram(prog *ebpf.Program, data, ctx []byte) (statusCode uint32, dataOut, ctxOut []byte, err error) {
dataOut = make([]byte, len(data))
if len(dataOut) > 0 {
// See comments at https://github.com/cilium/ebpf/blob/20c4d8896bdde990ce6b80d59a4262aa3ccb891d/prog.go#L563-L567
dataOut = make([]byte, len(data)+256+2)
}
ctxOut = make([]byte, len(ctx))
opts := &ebpf.RunOptions{
Data: data,
DataOut: dataOut,
Context: ctx,
ContextOut: ctxOut,
Repeat: 1,
}
ret, err := prog.Run(opts)
return ret, opts.DataOut, ctxOut, err
}
func collectPrograms(t *testing.T) (progset []programSet, err error) {
obj := &bpftestObjects{}
pinPath := "/sys/fs/bpf/dae"
if err = os.MkdirAll(pinPath, 0755); err != nil && !os.IsExist(err) {
return
}
if err = loadBpftestObjects(obj,
&ebpf.CollectionOptions{
Maps: ebpf.MapOptions{
PinPath: pinPath,
},
Programs: ebpf.ProgramOptions{
LogSize: ebpf.DefaultVerifierLogSize * 10,
},
},
); err != nil {
var (
ve *ebpf.VerifierError
verifierLog string
)
if errors.As(err, &ve) {
verifierLog = fmt.Sprintf("Verifier error: %+v\n", ve)
}
t.Fatalf("Failed to load objects: %s\n%+v", verifierLog, err)
return nil, err
}
if err = obj.LpmArrayMap.Update(uint32(0), obj.UnusedLpmType, ebpf.UpdateAny); err != nil {
t.Fatalf("Failed to update LpmArrayMap: %s", err)
return
}
v := reflect.ValueOf(obj.bpftestPrograms)
typeOfV := v.Type()
for i := 0; i < v.NumField(); i++ {
progname := typeOfV.Field(i).Name
if strings.HasPrefix(progname, "Testsetup") {
progid := strings.TrimPrefix(progname, "Testsetup")
progset = append(progset, programSet{
id: progid,
pktgen: v.FieldByName("Testpktgen" + progid).Interface().(*ebpf.Program),
setup: v.FieldByName("Testsetup" + progid).Interface().(*ebpf.Program),
check: v.FieldByName("Testcheck" + progid).Interface().(*ebpf.Program),
})
}
}
return
}
func consumeBpfDebugLog(t *testing.T) {
readBpfDebugLog(t)
}
func printBpfDebugLog(t *testing.T) {
fmt.Print(readBpfDebugLog(t))
}
func readBpfDebugLog(t *testing.T) string {
file, err := os.Open("/sys/kernel/tracing/trace_pipe")
if err != nil {
t.Fatalf("Failed to open trace_pipe: %v", err)
}
defer file.Close()
buffer := make([]byte, 1024*64)
n, err := file.Read(buffer)
if err != nil {
t.Fatalf("Failed to read from trace_pipe: %v", err)
}
return string(buffer[:n])
}
func Test(t *testing.T) {
progsets, err := collectPrograms(t)
if err != nil {
t.Fatalf("error while collecting programs: %s", err)
}
for _, progset := range progsets {
t.Logf("Running test: %s\n", progset.id)
// create ctx with the max allowed size(4k - head room - tailroom)
data := make([]byte, 4096-256-320)
// sizeof(struct __sk_buff) < 256, let's make it 256
ctx := make([]byte, 256)
statusCode, data, ctx, err := runBpfProgram(progset.pktgen, data, ctx)
if err != nil {
t.Fatalf("error while running pktgen prog: %s", err)
}
if statusCode != 0 {
printBpfDebugLog(t)
t.Fatalf("error while running pktgen program: unexpected status code: %d", statusCode)
}
statusCode, data, ctx, err = runBpfProgram(progset.setup, data, ctx)
if err != nil {
printBpfDebugLog(t)
t.Fatalf("error while running setup prog: %s", err)
}
status := make([]byte, 4)
nl.NativeEndian().PutUint32(status, statusCode)
data = append(status, data...)
statusCode, data, ctx, err = runBpfProgram(progset.check, data, ctx)
if err != nil {
t.Fatalf("error while running check program: %+v", err)
}
if statusCode != 0 {
printBpfDebugLog(t)
t.Fatalf("error while running check program: unexpected status code: %d", statusCode)
}
consumeBpfDebugLog(t)
}
}

View File

@ -0,0 +1,184 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright (c) 2022-2024, daeuniverse Organization <dae@v2raya.org>
//go:build exclude
#define IP4_HLEN sizeof(struct iphdr)
#define IP6_HLEN sizeof(struct ipv6hdr)
#define TCP_HLEN sizeof(struct tcphdr)
#define OUTBOUND_USER_DEFINED_MIN 2
#define IPV4(a, b, c, d) (((a) << 24) | ((b) << 16) | ((c) << 8) | (d))
static const __u32 two_key = 2;
static const __u32 three_key = 3;
static const __u32 four_key = 4;
static __always_inline int
set_ipv4_tcp(struct __sk_buff *skb,
__u32 saddr, __u32 daddr,
__u16 sport, __u16 dport)
{
bpf_skb_change_tail(skb, ETH_HLEN + IP4_HLEN + TCP_HLEN, 0);
void *data = (void *)(long)skb->data;
void *data_end = (void *)(long)skb->data_end;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end) {
bpf_printk("data + sizeof(*eth) > data_end\n");
return TC_ACT_SHOT;
}
eth->h_dest[0] = 0x0;
eth->h_dest[1] = 0x1;
eth->h_dest[2] = 0x2;
eth->h_dest[3] = 0x3;
eth->h_dest[4] = 0x4;
eth->h_dest[5] = 0x5;
eth->h_source[0] = 0x6;
eth->h_source[1] = 0x7;
eth->h_source[2] = 0x8;
eth->h_source[3] = 0x9;
eth->h_source[4] = 0xa;
eth->h_source[5] = 0xb;
eth->h_proto = bpf_htons(ETH_P_IP);
struct iphdr *ip = data + ETH_HLEN;
if ((void *)(ip + 1) > data_end) {
bpf_printk("data + sizeof(*ip) > data_end\n");
return TC_ACT_SHOT;
}
ip->ihl = 5;
ip->version = 4;
ip->protocol = IPPROTO_TCP;
ip->saddr = bpf_htonl(saddr);
ip->daddr = bpf_htonl(daddr);
ip->tos = 4 << 2;
struct tcphdr *tcp = data + ETH_HLEN + IP4_HLEN;
if ((void *)(tcp + 1) > data_end) {
bpf_printk("data + sizeof(*tcp) > data_end\n");
return TC_ACT_SHOT;
}
tcp->source = bpf_htons(sport);
tcp->dest = bpf_htons(dport);
tcp->syn = 1;
return TC_ACT_OK;
}
static __always_inline int
check_routing_ipv4_tcp(struct __sk_buff *skb,
__u32 expected_status_code,
__u32 saddr, __u32 daddr,
__u16 sport, __u16 dport)
{
__u32 *status_code;
void *data = (void *)(long)skb->data;
void *data_end = (void *)(long)skb->data_end;
if (data + sizeof(*status_code) > data_end) {
bpf_printk("data + sizeof(*status_code) > data_end\n");
return TC_ACT_SHOT;
}
status_code = data;
if (*status_code != expected_status_code) {
bpf_printk("status_code(%d) != %d\n", *status_code, expected_status_code);
return TC_ACT_SHOT;
}
if (expected_status_code == TC_ACT_REDIRECT) {
if (skb->cb[0] != TPROXY_MARK) {
bpf_printk("skb->cb[0] != TPROXY_MARK\n");
return TC_ACT_SHOT;
}
if (skb->cb[1] != IPPROTO_TCP) {
bpf_printk("skb->cb[1] != IPPROTO_TCP\n");
return TC_ACT_SHOT;
}
}
struct ethhdr *eth = data + sizeof(*status_code);
if ((void *)(eth + 1) > data_end) {
bpf_printk("data + sizeof(*eth) > data_end\n");
return TC_ACT_SHOT;
}
if (eth->h_proto != bpf_htons(ETH_P_IP)) {
bpf_printk("eth->h_proto != ETH_P_IP\n");
return TC_ACT_SHOT;
}
struct iphdr *ip = (void *)eth + ETH_HLEN;
if ((void *)(ip + 1) > data_end) {
bpf_printk("data + sizeof(*ip) > data_end\n");
return TC_ACT_SHOT;
}
if (ip->protocol != IPPROTO_TCP) {
bpf_printk("ip->protocol != IPPROTO_TCP\n");
return TC_ACT_SHOT;
}
if (ip->saddr != bpf_htonl(saddr)) {
bpf_printk("ip->saddr != %pI4\n", &saddr);
return TC_ACT_SHOT;
}
if (ip->daddr != bpf_htonl(daddr)) {
bpf_printk("ip->daddr != %pI4\n", &daddr);
return TC_ACT_SHOT;
}
struct tcphdr *tcp = (void *)ip + IP4_HLEN;
if ((void *)(tcp + 1) > data_end) {
bpf_printk("data + sizeof(*tcp) > data_end\n");
return TC_ACT_SHOT;
}
if (tcp->source != bpf_htons(sport)) {
bpf_printk("tcp->source != %d\n", sport);
return TC_ACT_SHOT;
}
if (tcp->dest != bpf_htons(dport)) {
bpf_printk("tcp->dest != %d\n", dport);
return TC_ACT_SHOT;
}
if (expected_status_code == TC_ACT_REDIRECT) {
struct tuples tuples = {};
tuples.five.sip.u6_addr32[2] = bpf_htonl(0xffff);
tuples.five.sip.u6_addr32[3] = ip->saddr;
tuples.five.dip.u6_addr32[2] = bpf_htonl(0xffff);
tuples.five.dip.u6_addr32[3] = ip->daddr;
tuples.five.sport = tcp->source;
tuples.five.dport = tcp->dest;
tuples.five.l4proto = ip->protocol;
struct routing_result *routing_result;
routing_result = bpf_map_lookup_elem(&routing_tuples_map, &tuples.five);
if (!routing_result) {
bpf_printk("routing_result == NULL\n");
return TC_ACT_SHOT;
}
if (routing_result->outbound != OUTBOUND_USER_DEFINED_MIN) {
bpf_printk("routing_result->outbound != OUTBOUND_USER_DEFINED_MIN\n");
return TC_ACT_SHOT;
}
}
return TC_ACT_OK;
}
static __always_inline void
set_routing_fallback(__u8 outbound, bool must)
{
struct match_set ms = {};
ms.not = false;
ms.type = MatchType_Fallback;
ms.outbound = outbound;
ms.must = must;
ms.mark = 0;
bpf_map_update_elem(&routing_map, &one_key, &ms, BPF_ANY);
}