Skip to content

Commit

Permalink
link: support querying bpf_mprog based links
Browse files Browse the repository at this point in the history
bpf_mprog based attachment allows specifying an expected revision
when creating a link. The idea is that user space can first query
the list of attached programs and the current revision. It then
decides where to insert in the list and passes the current revision
as the expected revision to the kernel.

Return the current revision from QueryPrograms, and at the same time
take the Target as an int instead of a path. This matches what we do
for attachment and detachment.

Breaking change: QueryPrograms now returns a QueryResult. Users can
simply access QueryResult.Programs to retrieve the same information
as before.

Signed-off-by: Lorenz Bauer <lmb@isovalent.com>
  • Loading branch information
lmb committed Nov 17, 2023
1 parent 417f8a2 commit d208a19
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 51 deletions.
106 changes: 77 additions & 29 deletions link/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package link

import (
"fmt"
"os"
"unsafe"

"github.com/cilium/ebpf"
Expand All @@ -11,53 +10,102 @@ import (

// QueryOptions defines additional parameters when querying for programs.
type QueryOptions struct {
// Path can be a path to a cgroup, netns or LIRC2 device
Path string
// Target to query. This is usually a file descriptor but may refer to
// something else based on the attach type.
Target int
// Attach specifies the AttachType of the programs queried for
Attach ebpf.AttachType
// QueryFlags are flags for BPF_PROG_QUERY, e.g. BPF_F_QUERY_EFFECTIVE
QueryFlags uint32
}

// QueryPrograms retrieves ProgramIDs associated with the AttachType.
//
// Returns (nil, nil) if there are no programs attached to the queried kernel
// resource. Calling QueryPrograms on a kernel missing PROG_QUERY will result in
// ErrNotSupported.
func QueryPrograms(opts QueryOptions) ([]ebpf.ProgramID, error) {
if haveProgQuery() != nil {
return nil, fmt.Errorf("can't query program IDs: %w", ErrNotSupported)
}
// QueryResult describes which programs and links are active.
type QueryResult struct {
// List of attached programs.
Programs []AttachedProgram

f, err := os.Open(opts.Path)
if err != nil {
return nil, fmt.Errorf("can't open file: %s", err)
}
defer f.Close()
// Incremented by one every time the set of attached programs changes.
// May be zero if not supported by the [ebpf.AttachType].
Revision uint64
}

// HaveLinkInfo returns true if the kernel supports querying link information
// for a particular [ebpf.AttachType].
func (qr *QueryResult) HaveLinkInfo() bool {
return qr.Revision > 0
}

type AttachedProgram struct {
ID ebpf.ProgramID
linkID ID
}

// LinkID returns the ID associated with the program.
//
// Returns 0, false if the kernel doesn't support retrieving the ID or if the
// program wasn't attached via a link. See [QueryResult.HaveLinkInfo] if you
// need to tell the two apart.
func (ap *AttachedProgram) LinkID() (ID, bool) {
return ap.linkID, ap.linkID != 0
}

// QueryPrograms retrieves a list of programs for the given AttachType.
//
// Returns a slice of attached programs, which may be empty.
// revision counts how many times the set of attached programs has changed and
// may be zero if not supported by the [ebpf.AttachType].
// Returns ErrNotSupportd on a kernel without BPF_PROG_QUERY
func QueryPrograms(opts QueryOptions) (*QueryResult, error) {
// query the number of programs to allocate correct slice size
attr := sys.ProgQueryAttr{
TargetFdOrIfindex: uint32(f.Fd()),
TargetFdOrIfindex: uint32(opts.Target),
AttachType: sys.AttachType(opts.Attach),
QueryFlags: opts.QueryFlags,
}
if err := sys.ProgQuery(&attr); err != nil {
return nil, fmt.Errorf("can't query program count: %w", err)
err := sys.ProgQuery(&attr)
if err != nil {
if haveFeatErr := haveProgQuery(); haveFeatErr != nil {
return nil, fmt.Errorf("query programs: %w", haveFeatErr)
}
return nil, fmt.Errorf("query programs: %w", err)
}

// return nil if no progs are attached
if attr.Count == 0 {
return nil, nil
return &QueryResult{Revision: attr.Revision}, nil
}

// The minimum bpf_mprog revision is 1, so we can use the field to detect
// whether the attach type supports link ids.
haveLinkIDs := attr.Revision != 0

count := attr.Count
progIds := make([]ebpf.ProgramID, count)
attr = sys.ProgQueryAttr{
TargetFdOrIfindex: uint32(opts.Target),
AttachType: sys.AttachType(opts.Attach),
QueryFlags: opts.QueryFlags,
Count: count,
ProgIds: sys.NewPointer(unsafe.Pointer(&progIds[0])),
}

var linkIds []ID
if haveLinkIDs {
linkIds = make([]ID, count)
attr.LinkIds = sys.NewPointer(unsafe.Pointer(&linkIds[0]))
}

// we have at least one prog, so we query again
progIds := make([]ebpf.ProgramID, attr.Count)
attr.ProgIds = sys.NewPointer(unsafe.Pointer(&progIds[0]))
attr.TargetFdOrIfindex = uint32(len(progIds))
if err := sys.ProgQuery(&attr); err != nil {
return nil, fmt.Errorf("can't query program IDs: %w", err)
return nil, fmt.Errorf("query programs: %w", err)
}

return progIds, nil
// NB: attr.Count might have changed between the two syscalls.
var programs []AttachedProgram
for i, id := range progIds[:attr.Count] {
ap := AttachedProgram{ID: id}
if haveLinkIDs {
ap.linkID = linkIds[i]
}
programs = append(programs, ap)
}

return &QueryResult{programs, attr.Revision}, nil
}
98 changes: 76 additions & 22 deletions link/query_test.go
Original file line number Diff line number Diff line change
@@ -1,57 +1,91 @@
package link

import (
"os"
"testing"

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

qt "github.com/frankban/quicktest"
"golang.org/x/exp/slices"
)

func TestQueryPrograms(t *testing.T) {
for name, fn := range map[string]func(*testing.T) (*ebpf.Program, QueryOptions){
"cgroup": queryCgroupFixtures,
"netns": queryNetNSFixtures,
for name, fn := range map[string]func(*testing.T) (*ebpf.Program, Link, QueryOptions){
"cgroup": queryCgroupProgAttachFixtures,
"cgroup link": queryCgroupLinkFixtures,
"netns": queryNetNSFixtures,
"tcx": queryTCXFixtures,
} {
t.Run(name, func(t *testing.T) {
prog, opts := fn(t)
ids, err := QueryPrograms(opts)
prog, link, opts := fn(t)
result, err := QueryPrograms(opts)
testutils.SkipIfNotSupported(t, err)
if err != nil {
t.Fatal("Can't query programs:", err)
}
qt.Assert(t, err, qt.IsNil)

progInfo, err := prog.Info()
if err != nil {
t.Fatal("Can't get program info:", err)
qt.Assert(t, err, qt.IsNil)
progID, _ := progInfo.ID()

i := slices.IndexFunc(result.Programs, func(ap AttachedProgram) bool {
return ap.ID == progID
})
qt.Assert(t, i, qt.Not(qt.Equals), -1)

if name == "tcx" {
qt.Assert(t, result.Revision, qt.Not(qt.Equals), uint64(0))
}

progId, _ := progInfo.ID()
if result.HaveLinkInfo() {
ap := result.Programs[i]
linkInfo, err := link.Info()
qt.Assert(t, err, qt.IsNil)

for _, id := range ids {
if id == progId {
return
}
linkID, ok := ap.LinkID()
qt.Assert(t, ok, qt.IsTrue)
qt.Assert(t, linkID, qt.Equals, linkInfo.ID)
}
t.Fatalf("Can't find program ID %d in query result: %v", progId, ids)
})
}
}

func queryCgroupFixtures(t *testing.T) (*ebpf.Program, QueryOptions) {
func queryCgroupProgAttachFixtures(t *testing.T) (*ebpf.Program, Link, QueryOptions) {
cgroup, prog := mustCgroupFixtures(t)

link, err := newProgAttachCgroup(cgroup, ebpf.AttachCGroupInetEgress, prog, flagAllowOverride)
if err != nil {
t.Fatal("Can't create link:", err)
}
t.Cleanup(func() {
qt.Assert(t, link.Close(), qt.IsNil)
})

return prog, nil, QueryOptions{
Target: int(cgroup.Fd()),
Attach: ebpf.AttachCGroupInetEgress,
}
}

func queryCgroupLinkFixtures(t *testing.T) (*ebpf.Program, Link, QueryOptions) {
cgroup, prog := mustCgroupFixtures(t)

link, err := newProgAttachCgroup(cgroup, ebpf.AttachCGroupInetEgress, prog, 0)
link, err := newLinkCgroup(cgroup, ebpf.AttachCGroupInetEgress, prog)
testutils.SkipIfNotSupported(t, err)
if err != nil {
t.Fatal("Can't create link:", err)
}
t.Cleanup(func() {
link.Close()
qt.Assert(t, link.Close(), qt.IsNil)
})

return prog, QueryOptions{Path: cgroup.Name(), Attach: ebpf.AttachCGroupInetEgress}
return prog, nil, QueryOptions{
Target: int(cgroup.Fd()),
Attach: ebpf.AttachCGroupInetEgress,
}
}

func queryNetNSFixtures(t *testing.T) (*ebpf.Program, QueryOptions) {
func queryNetNSFixtures(t *testing.T) (*ebpf.Program, Link, QueryOptions) {
testutils.SkipOnOldKernel(t, "4.20", "flow_dissector program")

prog := mustLoadProgram(t, ebpf.FlowDissector, ebpf.AttachFlowDissector, "")
Expand All @@ -77,5 +111,25 @@ func queryNetNSFixtures(t *testing.T) (*ebpf.Program, QueryOptions) {
}
})

return prog, QueryOptions{Path: "/proc/self/ns/net", Attach: ebpf.AttachFlowDissector}
netns, err := os.Open("/proc/self/ns/net")
qt.Assert(t, err, qt.IsNil)
t.Cleanup(func() { netns.Close() })

return prog, nil, QueryOptions{
Target: int(netns.Fd()),
Attach: ebpf.AttachFlowDissector,
}
}

func queryTCXFixtures(t *testing.T) (*ebpf.Program, Link, QueryOptions) {
testutils.SkipOnOldKernel(t, "6.6", "TCX link")

prog := mustLoadProgram(t, ebpf.SchedCLS, ebpf.AttachTCXIngress, "")

link, iface := mustAttachTCX(t, prog, ebpf.AttachTCXIngress)

return prog, link, QueryOptions{
Target: iface,
Attach: ebpf.AttachTCXIngress,
}
}
4 changes: 4 additions & 0 deletions link/syscalls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@ func TestHaveProgAttachReplace(t *testing.T) {
func TestHaveBPFLink(t *testing.T) {
testutils.CheckFeatureTest(t, haveBPFLink)
}

func TestHaveProgQuery(t *testing.T) {
testutils.CheckFeatureTest(t, haveProgQuery)
}

0 comments on commit d208a19

Please sign in to comment.