Skip to content

Commit

Permalink
Add automatic loading of hook in XDP frags mode
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
Pavlos Tzianos authored and arthurfabre committed Oct 12, 2023
1 parent 2a7e833 commit 660cb54
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 11 deletions.
27 changes: 25 additions & 2 deletions cmd/xdpcap/filter.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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()
Expand Down
53 changes: 53 additions & 0 deletions cmd/xdpcap/filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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})
Expand Down
22 changes: 13 additions & 9 deletions cmd/xdpcap/program.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
}
Expand Down

0 comments on commit 660cb54

Please sign in to comment.