From d208a199d559902a1dcafc4038a7394d439377a9 Mon Sep 17 00:00:00 2001 From: Lorenz Bauer Date: Fri, 17 Nov 2023 09:59:54 +0000 Subject: [PATCH] link: support querying bpf_mprog based links 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 --- link/query.go | 106 ++++++++++++++++++++++++++++++------------ link/query_test.go | 98 +++++++++++++++++++++++++++++--------- link/syscalls_test.go | 4 ++ 3 files changed, 157 insertions(+), 51 deletions(-) diff --git a/link/query.go b/link/query.go index 08ca0e667..fe534f8ef 100644 --- a/link/query.go +++ b/link/query.go @@ -2,7 +2,6 @@ package link import ( "fmt" - "os" "unsafe" "github.com/cilium/ebpf" @@ -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 } diff --git a/link/query_test.go b/link/query_test.go index c52a4de6c..c02a21cb4 100644 --- a/link/query_test.go +++ b/link/query_test.go @@ -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, "") @@ -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, + } } diff --git a/link/syscalls_test.go b/link/syscalls_test.go index 361dc47a2..70fd98398 100644 --- a/link/syscalls_test.go +++ b/link/syscalls_test.go @@ -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) +}