mirror of
https://github.com/daeuniverse/dae.git
synced 2024-12-22 16:24:40 +07:00
ci(workflows/bpf-test): add BPF test to cover route logic (#671)
Co-authored-by: mzz <2017@duck.com>
This commit is contained in:
parent
3b77ddfd67
commit
438c05cbd6
28
.github/workflows/bpf-test.yml
vendored
Normal file
28
.github/workflows/bpf-test.yml
vendored
Normal 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)
|
||||
|
16
Makefile
16
Makefile
@ -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
|
||||
|
1006
control/kern/tests/bpf_test.c
Normal file
1006
control/kern/tests/bpf_test.c
Normal file
File diff suppressed because it is too large
Load Diff
167
control/kern/tests/bpf_test.go
Normal file
167
control/kern/tests/bpf_test.go
Normal 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)
|
||||
}
|
||||
}
|
184
control/kern/tests/bpf_test.h
Normal file
184
control/kern/tests/bpf_test.h
Normal 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);
|
||||
}
|
Loading…
Reference in New Issue
Block a user