From 660cb5415987d03f0c4a6ffcc6ecf909b0cfd93d Mon Sep 17 00:00:00 2001 From: Pavlos Tzianos Date: Tue, 18 Jul 2023 01:49:22 +0100 Subject: [PATCH] Add automatic loading of hook in XDP frags mode If an XDP entrypoint is loaded in XDP frags mode, the xdpcap hook can't be attached in linear buffer mode, and vice versa. This commit adds a check that allows the code to retry loading the actions in XDP frags mode if the first attempt fails. If the second attempt fails too, the program exits and shows the original error message. A new test is included checking that the attachment retry works as expected. Signed-off-by: Pavlos Tzianos --- cmd/xdpcap/filter.go | 27 ++++++++++++++++++-- cmd/xdpcap/filter_test.go | 53 +++++++++++++++++++++++++++++++++++++++ cmd/xdpcap/program.go | 22 +++++++++------- 3 files changed, 91 insertions(+), 11 deletions(-) diff --git a/cmd/xdpcap/filter.go b/cmd/xdpcap/filter.go index 6fa9e89..9b9f2bc 100644 --- a/cmd/xdpcap/filter.go +++ b/cmd/xdpcap/filter.go @@ -1,12 +1,16 @@ package main import ( + "fmt" + "os" + "github.com/cloudflare/xdpcap/internal" "github.com/cilium/ebpf" "github.com/cilium/ebpf/perf" "github.com/pkg/errors" "golang.org/x/net/bpf" + "golang.org/x/sys/unix" ) type packet struct { @@ -85,13 +89,32 @@ func newFilterWithMap(hookMap *ebpf.Map, opts filterOpts) (*filter, error) { actions: opts.actions, } - for _, action := range opts.actions { - program, err := newProgram(opts.filter, action, perfMap) + xdpFragsMode := false + for i, action := range opts.actions { + program, err := newProgram(opts.filter, action, perfMap, xdpFragsMode) if err != nil { return nil, errors.Wrapf(err, "loading filter program for %v", action) } err = attachProg(hookMap, program.program.FD(), action) + if errors.Is(err, unix.EINVAL) && i == 0 { + // attempt to load first action in XDP frags mode and retry attaching + // if this doesn't work then there is underlying issue that is not + // related to the hook being attached in XDP frags mode and we make + // sure to return the original attachment error. + xdpFragsMode = true + + var programErr error + if program, programErr = newProgram(opts.filter, action, perfMap, xdpFragsMode); programErr != nil { + return nil, errors.Wrapf(programErr, "loading filter program in XDP frags mode for %v", action) + } + + if attachProg(hookMap, program.program.FD(), action) == nil { + fmt.Fprintf(os.Stderr, "attaching filter actions in XDP frags mode\n") + err = nil + } + } + if err != nil { // close and detach any previously successfully attached programs, but not this one filter.close() diff --git a/cmd/xdpcap/filter_test.go b/cmd/xdpcap/filter_test.go index 11d52c5..0e93285 100644 --- a/cmd/xdpcap/filter_test.go +++ b/cmd/xdpcap/filter_test.go @@ -8,7 +8,9 @@ import ( "github.com/cloudflare/xdpcap/internal" "github.com/cilium/ebpf" + "github.com/cilium/ebpf/asm" "golang.org/x/net/bpf" + "golang.org/x/sys/unix" ) func testOpts(filter ...bpf.Instruction) filterOpts { @@ -44,6 +46,57 @@ func TestMissingFilter(t *testing.T) { } } +func TestFilterProgramForAllModes(t *testing.T) { + testFunc := func(xdpFragsEnabled bool) func(t *testing.T) { + return func(t *testing.T) { + hookMapSpec := internal.HookMapSpec.Copy() + hookMapSpec.Name = "test_mode_hook_map" + hookMap, err := ebpf.NewMap(hookMapSpec) + if err != nil { + t.Fatal(err) + } + + entrrypointSpec := &ebpf.ProgramSpec{ + Type: ebpf.XDP, + Instructions: asm.Instructions{ + // Context is already in R1. + asm.LoadMapPtr(asm.R2, hookMap.FD()), + asm.Mov.Imm(asm.R3, int32(xdpPass)), + asm.FnTailCall.Call(), + asm.Mov.Imm(asm.R0, int32(xdpPass)), + asm.Return(), + }, + } + + if xdpFragsEnabled { + entrrypointSpec.Flags = unix.BPF_F_XDP_HAS_FRAGS + } + + entrypoint, err := ebpf.NewProgram(entrrypointSpec) + if err != nil { + t.Fatal(err) + } + + _, err = entrypoint.Run(&ebpf.RunOptions{}) + if err != nil { + t.Fatal(err) + } + defer entrypoint.Close() + + opts := testOpts(bpf.RetConstant{Val: 0}) + opts.actions = []xdpAction{xdpPass} + filter, err := newFilterWithMap(hookMap, opts) + if err != nil { + t.Fatal(err) + } + filter.close() + } + } + + t.Run("linear_buffer", testFunc(false)) + t.Run("xdp_frags", testFunc(true)) +} + func TestUnknownAction(t *testing.T) { // progs with actions from 0-9. Only 0-3 are used currently. opts := testOpts(bpf.RetConstant{0}) diff --git a/cmd/xdpcap/program.go b/cmd/xdpcap/program.go index 7c92dc4..187d851 100644 --- a/cmd/xdpcap/program.go +++ b/cmd/xdpcap/program.go @@ -6,6 +6,7 @@ import ( "github.com/cloudflare/cbpfc" "github.com/pkg/errors" "golang.org/x/net/bpf" + "golang.org/x/sys/unix" ) const BPF_F_CURRENT_CPU int64 = 0xFFFFFFFF @@ -42,7 +43,7 @@ type program struct { } // newProgram builds an eBPF program that copies packets matching a cBPF program to userspace via perf -func newProgram(filter []bpf.Instruction, action xdpAction, perfMap *ebpf.Map) (*program, error) { +func newProgram(filter []bpf.Instruction, action xdpAction, perfMap *ebpf.Map, xdpFragsMode bool) (*program, error) { metricsMap, err := ebpf.NewMap(&metricsSpec) if err != nil { return nil, errors.Wrap(err, "creating metrics map") @@ -161,14 +162,17 @@ func newProgram(filter []bpf.Instruction, action xdpAction, perfMap *ebpf.Map) ( asm.Return(), ) - prog, err := ebpf.NewProgram( - &ebpf.ProgramSpec{ - Name: "xdpcap_filter", - Type: ebpf.XDP, - Instructions: insns, - License: "GPL", - }, - ) + progSpec := &ebpf.ProgramSpec{ + Name: "xdpcap_filter", + Type: ebpf.XDP, + Instructions: insns, + License: "GPL", + } + if xdpFragsMode { + progSpec.Flags = progSpec.Flags | unix.BPF_F_XDP_HAS_FRAGS + } + + prog, err := ebpf.NewProgram(progSpec) if err != nil { return nil, errors.Wrap(err, "loading filter") }