Skip to content

Commit

Permalink
pin: new package for loading bpf pins and walking bpffs directories
Browse files Browse the repository at this point in the history
This commit adds a new package pin with two main APIs: Load() and WalkDir().
It's split off from the root package since ebpf cannot import link.

Signed-off-by: Timo Beckers <timo@isovalent.com>
  • Loading branch information
ti-mo committed Dec 16, 2024
1 parent 981518b commit 0a77f25
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 0 deletions.
3 changes: 3 additions & 0 deletions pin/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package pin provides utility functions for working with pinned objects on bpffs.

package pin
40 changes: 40 additions & 0 deletions pin/load.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package pin

import (
"fmt"

"github.com/cilium/ebpf"
"github.com/cilium/ebpf/internal/sys"
"github.com/cilium/ebpf/link"
)

// Pinner is an interface implemented by all eBPF objects that support pinning
// to a bpf virtual filesystem.
type Pinner interface {
Pin(string) error
}

// Load retrieves a pinned object from a bpf virtual filesystem. It returns one
// of [ebpf.Map], [ebpf.Program], or [link.Link].
//
// Trying to open anything other than a bpf object is an error.
func Load(path string, opts *ebpf.LoadPinOptions) (Pinner, error) {
fd, typ, err := sys.ObjGetTyped(&sys.ObjGetAttr{
Pathname: sys.NewStringPointer(path),
FileFlags: opts.Marshal(),
})
if err != nil {
return nil, fmt.Errorf("opening pin %s: %w", path, err)
}

switch typ {
case sys.BPF_TYPE_MAP:
return ebpf.NewMapFromFD(fd.Disown())
case sys.BPF_TYPE_PROG:
return ebpf.NewProgramFromFD(fd.Disown())
case sys.BPF_TYPE_LINK:
return link.NewFromFD(fd.Disown())
}

return nil, fmt.Errorf("unknown object type %d", typ)
}
85 changes: 85 additions & 0 deletions pin/load_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package pin

import (
"path/filepath"
"testing"

"github.com/go-quicktest/qt"

"github.com/cilium/ebpf"
"github.com/cilium/ebpf/asm"
"github.com/cilium/ebpf/internal/testutils"
)

func mustPinnedProgram(t *testing.T, path string) *ebpf.Program {
t.Helper()

spec := &ebpf.ProgramSpec{
Name: "test",
Type: ebpf.SocketFilter,
Instructions: asm.Instructions{
asm.LoadImm(asm.R0, 2, asm.DWord),
asm.Return(),
},
License: "MIT",
}

p, err := ebpf.NewProgram(spec)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { p.Close() })

if err := p.Pin(path); err != nil {
t.Fatal(err)
}

return p
}

func mustPinnedMap(t *testing.T, path string) *ebpf.Map {
t.Helper()

spec := &ebpf.MapSpec{
Name: "test",
Type: ebpf.Array,
KeySize: 4,
ValueSize: 4,
MaxEntries: 1,
}

m, err := ebpf.NewMap(spec)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { m.Close() })

if err := m.Pin(path); err != nil {
t.Fatal(err)
}

return m
}

func TestLoad(t *testing.T) {
testutils.SkipOnOldKernel(t, "4.10", "reading program fdinfo")

tmp := testutils.TempBPFFS(t)

mpath := filepath.Join(tmp, "map")
ppath := filepath.Join(tmp, "prog")

mustPinnedMap(t, mpath)
mustPinnedProgram(t, ppath)

_, err := Load(tmp, nil)
qt.Assert(t, qt.IsNotNil(err))

m, err := Load(mpath, nil)
qt.Assert(t, qt.IsNil(err))
qt.Assert(t, qt.Satisfies(m, testutils.Contains[*ebpf.Map]))

p, err := Load(ppath, nil)
qt.Assert(t, qt.IsNil(err))
qt.Assert(t, qt.Satisfies(p, testutils.Contains[*ebpf.Program]))
}
49 changes: 49 additions & 0 deletions pin/walk.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package pin

import (
"fmt"
"io/fs"
"os"
"path/filepath"

"github.com/cilium/ebpf/internal/linux"
"github.com/cilium/ebpf/internal/unix"
)

// WalkDirFunc is the type of the function called for each object visited by
// [WalkDir]. It's identical to [fs.WalkDirFunc], but with an extra [Pinner]
// argument. If the visited node is a directory, obj is nil.
//
// err contains any errors encountered during bpffs traversal or object loading.
type WalkDirFunc func(path string, d fs.DirEntry, obj Pinner, err error) error

// WalkDir walks the file tree rooted at path, calling bpffn for each node in
// the tree, including directories. Running WalkDir on a non-bpf filesystem is
// an error. Otherwise identical in behavior to [fs.WalkDir].
//
// See the [WalkDirFunc] for more information.
func WalkDir(root string, bpffn WalkDirFunc) error {
fsType, err := linux.FSType(root)
if err != nil {
return err
}
if fsType != unix.BPF_FS_MAGIC {
return fmt.Errorf("%s is not on a bpf filesystem", root)
}

fn := func(path string, d fs.DirEntry, err error) error {
if err != nil {
return bpffn(path, nil, nil, err)
}

if d.IsDir() {
return bpffn(path, d, nil, err)
}

obj, err := Load(filepath.Join(root, path), nil)

return bpffn(path, d, obj, err)
}

return fs.WalkDir(os.DirFS(root), ".", fn)
}
54 changes: 54 additions & 0 deletions pin/walk_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package pin

import (
"io/fs"
"os"
"path/filepath"
"testing"

"github.com/go-quicktest/qt"

"github.com/cilium/ebpf"
"github.com/cilium/ebpf/internal/testutils"
)

func TestWalkDir(t *testing.T) {
testutils.SkipOnOldKernel(t, "4.10", "reading program fdinfo")

tmp := testutils.TempBPFFS(t)
dir := filepath.Join(tmp, "dir")
qt.Assert(t, qt.IsNil(os.Mkdir(dir, 0755)))

mustPinnedProgram(t, filepath.Join(tmp, "pinned_prog"))
mustPinnedMap(t, filepath.Join(dir, "pinned_map"))

entries := make(map[string]string)

bpffn := func(path string, d fs.DirEntry, obj Pinner, err error) error {
qt.Assert(t, qt.IsNil(err))

if path == "." {
return nil
}

switch obj.(type) {
case *ebpf.Program:
entries[path] = "prog"
case *ebpf.Map:
entries[path] = "map"
default:
entries[path] = ""
}

return nil
}
qt.Assert(t, qt.IsNil(WalkDir(tmp, bpffn)))

qt.Assert(t, qt.DeepEquals(entries, map[string]string{
"pinned_prog": "prog",
"dir": "",
"dir/pinned_map": "map",
}))

qt.Assert(t, qt.IsNotNil(WalkDir("/", nil)))
}

0 comments on commit 0a77f25

Please sign in to comment.