Repository: cilium/ebpf
Pull request #867 (2022-11-25 10:36:59)

link: add QueryPrograms API
This PR adds a wrapper around `BPF_PROG_QUERY` and with this API it's possible to query cgroups, network namespaces or LIRC2 devices for attached programs.

Signed-off-by: Robin Gögge <r.goegge@isovalent.com>


Patch:
diff --git a/internal/cmd/gentypes/main.go b/internal/cmd/gentypes/main.go
index f6ba841..20cba13 100644
--- a/internal/cmd/gentypes/main.go
+++ b/internal/cmd/gentypes/main.go
@@ -419,6 +419,14 @@ import (
 			"IterCreate", retFd, "iter_create", "BPF_ITER_CREATE",
 			nil,
 		},
+		{
+			"ProgQuery", retError, "prog_query", "BPF_PROG_QUERY",
+			[]patch{
+				replace(enumTypes["AttachType"], "attach_type"),
+				replace(pointer, "prog_ids"),
+				rename("prog_cnt", "prog_count"),
+			},
+		},
 	}
 
 	sort.Slice(attrs, func(i, j int) bool {
diff --git a/internal/sys/types.go b/internal/sys/types.go
index c823248..bb77879 100644
--- a/internal/sys/types.go
+++ b/internal/sys/types.go
@@ -1003,6 +1003,21 @@ func ProgLoad(attr *ProgLoadAttr) (*FD, error) {
 	return NewFD(int(fd))
 }
 
+type ProgQueryAttr struct {
+	TargetFd    uint32
+	AttachType  AttachType
+	QueryFlags  uint32
+	AttachFlags uint32
+	ProgIds     Pointer
+	ProgCount   uint32
+	_           [4]byte
+}
+
+func ProgQuery(attr *ProgQueryAttr) error {
+	_, err := BPF(BPF_PROG_QUERY, unsafe.Pointer(attr), unsafe.Sizeof(*attr))
+	return err
+}
+
 type ProgRunAttr struct {
 	ProgFd      uint32
 	Retval      uint32
diff --git a/link/query.go b/link/query.go
new file mode 100644
index 0000000..8c88241
--- /dev/null
+++ b/link/query.go
@@ -0,0 +1,63 @@
+package link
+
+import (
+	"fmt"
+	"os"
+	"unsafe"
+
+	"github.com/cilium/ebpf"
+	"github.com/cilium/ebpf/internal/sys"
+)
+
+// 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
+	// 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.
+//
+// It only returns IDs of programs that were attached using PROG_ATTACH and not bpf_link.
+// 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)
+	}
+
+	f, err := os.Open(opts.Path)
+	if err != nil {
+		return nil, fmt.Errorf("can't open file: %s", err)
+	}
+	defer f.Close()
+
+	// query the number of programs to allocate correct slice size
+	attr := sys.ProgQueryAttr{
+		TargetFd:   uint32(f.Fd()),
+		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)
+	}
+
+	// return nil if no progs are attached
+	if attr.ProgCount == 0 {
+		return nil, nil
+	}
+
+	// we have at least one prog, so we query again
+	progIds := make([]ebpf.ProgramID, attr.ProgCount)
+	attr.ProgIds = sys.NewPointer(unsafe.Pointer(&progIds[0]))
+	attr.ProgCount = uint32(len(progIds))
+	if err := sys.ProgQuery(&attr); err != nil {
+		return nil, fmt.Errorf("can't query program IDs: %w", err)
+	}
+
+	return progIds, nil
+
+}
diff --git a/link/syscalls.go b/link/syscalls.go
index a9fed90..38f7ae9 100644
--- a/link/syscalls.go
+++ b/link/syscalls.go
@@ -102,3 +102,22 @@ var haveBPFLink = internal.NewFeatureTest("bpf_link", "5.7", func() error {
 	}
 	return err
 })
+
+var haveProgQuery = internal.NewFeatureTest("BPF_PROG_QUERY", "4.15", func() error {
+	attr := sys.ProgQueryAttr{
+		// We rely on this being checked during the syscall.
+		// With an otherwise correct payload we expect EBADF here
+		// as an indication that the feature is present.
+		TargetFd:   ^uint32(0),
+		AttachType: sys.AttachType(ebpf.AttachCGroupInetIngress),
+	}
+
+	err := sys.ProgQuery(&attr)
+	if errors.Is(err, unix.EINVAL) {
+		return internal.ErrNotSupported
+	}
+	if errors.Is(err, unix.EBADF) {
+		return nil
+	}
+	return err
+})


Test patch:
diff --git a/link/query_test.go b/link/query_test.go
new file mode 100644
index 0000000..c52a4de
--- /dev/null
+++ b/link/query_test.go
@@ -0,0 +1,81 @@
+package link
+
+import (
+	"testing"
+
+	"github.com/cilium/ebpf"
+	"github.com/cilium/ebpf/internal/testutils"
+)
+
+func TestQueryPrograms(t *testing.T) {
+	for name, fn := range map[string]func(*testing.T) (*ebpf.Program, QueryOptions){
+		"cgroup": queryCgroupFixtures,
+		"netns":  queryNetNSFixtures,
+	} {
+		t.Run(name, func(t *testing.T) {
+			prog, opts := fn(t)
+			ids, err := QueryPrograms(opts)
+			testutils.SkipIfNotSupported(t, err)
+			if err != nil {
+				t.Fatal("Can't query programs:", err)
+			}
+
+			progInfo, err := prog.Info()
+			if err != nil {
+				t.Fatal("Can't get program info:", err)
+			}
+
+			progId, _ := progInfo.ID()
+
+			for _, id := range ids {
+				if id == progId {
+					return
+				}
+			}
+			t.Fatalf("Can't find program ID %d in query result: %v", progId, ids)
+		})
+	}
+}
+
+func queryCgroupFixtures(t *testing.T) (*ebpf.Program, QueryOptions) {
+	cgroup, prog := mustCgroupFixtures(t)
+
+	link, err := newProgAttachCgroup(cgroup, ebpf.AttachCGroupInetEgress, prog, 0)
+	if err != nil {
+		t.Fatal("Can't create link:", err)
+	}
+	t.Cleanup(func() {
+		link.Close()
+	})
+
+	return prog, QueryOptions{Path: cgroup.Name(), Attach: ebpf.AttachCGroupInetEgress}
+}
+
+func queryNetNSFixtures(t *testing.T) (*ebpf.Program, QueryOptions) {
+	testutils.SkipOnOldKernel(t, "4.20", "flow_dissector program")
+
+	prog := mustLoadProgram(t, ebpf.FlowDissector, ebpf.AttachFlowDissector, "")
+
+	// RawAttachProgramOptions.Target needs to be 0, as PROG_ATTACH with namespaces
+	// only works with the threads current netns. Any other fd will be rejected.
+	if err := RawAttachProgram(RawAttachProgramOptions{
+		Target:  0,
+		Program: prog,
+		Attach:  ebpf.AttachFlowDissector,
+	}); err != nil {
+		t.Fatal(err)
+	}
+
+	t.Cleanup(func() {
+		err := RawDetachProgram(RawDetachProgramOptions{
+			Target:  0,
+			Program: prog,
+			Attach:  ebpf.AttachFlowDissector,
+		})
+		if err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	return prog, QueryOptions{Path: "/proc/self/ns/net", Attach: ebpf.AttachFlowDissector}
+}