Repository: cilium/ebpf
Pull request #1257 (2023-12-06 15:49:50)

Add support for netkit device
Adds support for attaching bpf programs to [netkit devices](https://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf-next.git/commit/?id=22360fad5889cbefe1eca695b0cc0273ab280b56) using bpf links.

Generated code is currently based on `6.7-rc4`

TODO : 

- [x] Decide on a way to create `netkit` device in CI for testing.
- [x] Re-gen code once `6.7` is out and validate for any changes.

chent1996: Thanks for this great work!
borkmann: @hemanthmalla happy new year! Given 6.7 is officially released now, could you move forward with the PR? Thanks! :)
hemanthmalla: @lmb I remember reading somewhere, that `cilium/ebpf` prefers not to have libs like `vishvananda/netlink` as dependencies. Do you have thoughts on how we should go about creating a `netkit` device in CI for tests?
Is shelling out to do something like `ip link add nk0 type netkit` the best option here ?
ti-mo: > @lmb I remember reading somewhere, that `cilium/ebpf` prefers not to have libs like `vishvananda/netlink` as dependencies. Do you have thoughts on how we should go about creating a `netkit` device in CI for tests? Is shelling out to do something like `ip link add nk0 type netkit` the best option here ?

Interesting, I never realized we attached to ifindex 1 in the XDP tests, but that obviously won't work for netkit. I think newer versions of Go automatically prune testing-only module dependencies, so technically there wouldn't be any downside to importing e.g. `jsimonetti/rtnetlink`. (e.g. vendoring the lib doesn't automatically pull in quicktest either, afaik)

I think we can soften our stance on importing a netlink lib for testing if this is the case. Not sure if we can do the same for examples. I think shelling out to `ip` would be strictly worse, since we'll need to deal with the various failure cases then. Using a library would make it easier to degrade gracefully (e.g. skip a test if creating a netkit dev returns EINVAL etc.) without resorting to screen scraping `ip`. I'd like to avoid that.
hemanthmalla: Test failures seem flaky to me. Previously only `ci / Run tests on previous stable Go` failed, re-triggered the tests on same commit changes and now ` Run tests on pre-built kernel (6.7)` seems to fail. How do I re-trigger just the failed test ?

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 85e2c70..898f78a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,7 +7,7 @@ on:
 
 env:
   TMPDIR: /tmp
-  CI_MAX_KERNEL_VERSION: '6.6'
+  CI_MAX_KERNEL_VERSION: '6.7'
   CI_MIN_CLANG_VERSION: '11'
   go_version: '~1.22'
   prev_go_version: '~1.21'
@@ -33,7 +33,7 @@ jobs:
           go-version: '${{ env.go_version }}'
 
       - name: Run golangci-lint
-        uses: golangci/golangci-lint-action@v3.7.0
+        uses: golangci/golangci-lint-action@v4.0.0
         with:
           args: "--out-format colored-line-number"
           skip-pkg-cache: true
diff --git a/attachtype_string.go b/attachtype_string.go
index 7e3caed..bece896 100644
--- a/attachtype_string.go
+++ b/attachtype_string.go
@@ -58,11 +58,18 @@ func _() {
 	_ = x[AttachTCXIngress-46]
 	_ = x[AttachTCXEgress-47]
 	_ = x[AttachTraceUprobeMulti-48]
+	_ = x[AttachCgroupUnixConnect-49]
+	_ = x[AttachCgroupUnixSendmsg-50]
+	_ = x[AttachCgroupUnixRecvmsg-51]
+	_ = x[AttachCgroupUnixGetpeername-52]
+	_ = x[AttachCgroupUnixGetsockname-53]
+	_ = x[AttachNetkitPrimary-54]
+	_ = x[AttachNetkitPeer-55]
 }
 
-const _AttachType_name = "NoneCGroupInetEgressCGroupInetSockCreateCGroupSockOpsSkSKBStreamParserSkSKBStreamVerdictCGroupDeviceSkMsgVerdictCGroupInet4BindCGroupInet6BindCGroupInet4ConnectCGroupInet6ConnectCGroupInet4PostBindCGroupInet6PostBindCGroupUDP4SendmsgCGroupUDP6SendmsgLircMode2FlowDissectorCGroupSysctlCGroupUDP4RecvmsgCGroupUDP6RecvmsgCGroupGetsockoptCGroupSetsockoptTraceRawTpTraceFEntryTraceFExitModifyReturnLSMMacTraceIterCgroupInet4GetPeernameCgroupInet6GetPeernameCgroupInet4GetSocknameCgroupInet6GetSocknameXDPDevMapCgroupInetSockReleaseXDPCPUMapSkLookupXDPSkSKBVerdictSkReuseportSelectSkReuseportSelectOrMigratePerfEventTraceKprobeMultiLSMCgroupStructOpsNetfilterTCXIngressTCXEgressTraceUprobeMulti"
+const _AttachType_name = "NoneCGroupInetEgressCGroupInetSockCreateCGroupSockOpsSkSKBStreamParserSkSKBStreamVerdictCGroupDeviceSkMsgVerdictCGroupInet4BindCGroupInet6BindCGroupInet4ConnectCGroupInet6ConnectCGroupInet4PostBindCGroupInet6PostBindCGroupUDP4SendmsgCGroupUDP6SendmsgLircMode2FlowDissectorCGroupSysctlCGroupUDP4RecvmsgCGroupUDP6RecvmsgCGroupGetsockoptCGroupSetsockoptTraceRawTpTraceFEntryTraceFExitModifyReturnLSMMacTraceIterCgroupInet4GetPeernameCgroupInet6GetPeernameCgroupInet4GetSocknameCgroupInet6GetSocknameXDPDevMapCgroupInetSockReleaseXDPCPUMapSkLookupXDPSkSKBVerdictSkReuseportSelectSkReuseportSelectOrMigratePerfEventTraceKprobeMultiLSMCgroupStructOpsNetfilterTCXIngressTCXEgressTraceUprobeMultiCgroupUnixConnectCgroupUnixSendmsgCgroupUnixRecvmsgCgroupUnixGetpeernameCgroupUnixGetsocknameNetkitPrimaryNetkitPeer"
 
-var _AttachType_index = [...]uint16{0, 4, 20, 40, 53, 70, 88, 100, 112, 127, 142, 160, 178, 197, 216, 233, 250, 259, 272, 284, 301, 318, 334, 350, 360, 371, 381, 393, 399, 408, 430, 452, 474, 496, 505, 526, 535, 543, 546, 558, 575, 601, 610, 626, 635, 644, 653, 663, 672, 688}
+var _AttachType_index = [...]uint16{0, 4, 20, 40, 53, 70, 88, 100, 112, 127, 142, 160, 178, 197, 216, 233, 250, 259, 272, 284, 301, 318, 334, 350, 360, 371, 381, 393, 399, 408, 430, 452, 474, 496, 505, 526, 535, 543, 546, 558, 575, 601, 610, 626, 635, 644, 653, 663, 672, 688, 705, 722, 739, 760, 781, 794, 804}
 
 func (i AttachType) String() string {
 	if i >= AttachType(len(_AttachType_index)-1) {
diff --git a/docs/Pipfile.lock b/docs/Pipfile.lock
index 922429a..5609e77 100644
--- a/docs/Pipfile.lock
+++ b/docs/Pipfile.lock
@@ -311,12 +311,12 @@
         },
         "mkdocs-material": {
             "hashes": [
-                "sha256:14563314bbf97da4bfafc69053772341babfaeb3329cde01d3e63cec03997af8",
-                "sha256:2a429213e83f84eda7a588e2b186316d806aac602b7f93990042f7a1f3d3cf65"
+                "sha256:5f69cef6a8aaa4050b812f72b1094fda3d079b9a51cf27a247244c03ec455e97",
+                "sha256:d6f0c269f015e48c76291cdc79efb70f7b33bbbf42d649cfe475522ebee61b1f"
             ],
             "index": "pypi",
             "markers": "python_version >= '3.8'",
-            "version": "==9.5.8"
+            "version": "==9.5.12"
         },
         "mkdocs-material-extensions": {
             "hashes": [
@@ -375,11 +375,11 @@
         },
         "python-dateutil": {
             "hashes": [
-                "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
-                "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
+                "sha256:78e73e19c63f5b20ffa567001531680d939dc042bf7850431877645523c66709",
+                "sha256:cbf2f1da5e6083ac2fbfd4da39a25f34312230110440f424a14c7558bb85d82e"
             ],
             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==2.8.2"
+            "version": "==2.9.0"
         },
         "pytz": {
             "hashes": [
@@ -586,11 +586,11 @@
         },
         "urllib3": {
             "hashes": [
-                "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20",
-                "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224"
+                "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d",
+                "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"
             ],
             "markers": "python_version >= '3.8'",
-            "version": "==2.2.0"
+            "version": "==2.2.1"
         },
         "watchdog": {
             "hashes": [
diff --git a/internal/cmd/gentypes/main.go b/internal/cmd/gentypes/main.go
index 03dde11..3a7243c 100644
--- a/internal/cmd/gentypes/main.go
+++ b/internal/cmd/gentypes/main.go
@@ -476,6 +476,17 @@ import (
 				rename("cnt", "count"),
 			},
 		},
+		{
+			"LinkCreateNetkit", retFd, "link_create", "BPF_LINK_CREATE",
+			[]patch{
+				choose(1, "target_ifindex"),
+				choose(4, "netkit"),
+				replace(enumTypes["AttachType"], "attach_type"),
+				flattenAnon,
+				flattenAnon,
+				rename("relative_fd", "relative_fd_or_id"),
+			},
+		},
 		{
 			"LinkUpdate", retError, "link_update", "BPF_LINK_UPDATE",
 			nil,
@@ -575,6 +586,9 @@ import (
 			replace(enumTypes["AttachType"], "attach_type"),
 		}},
 		{"NetfilterLinkInfo", "netfilter", nil},
+		{"NetkitLinkInfo", "netkit", []patch{
+			replace(enumTypes["AttachType"], "attach_type"),
+		}},
 	}
 
 	sort.Slice(linkInfoExtraTypes, func(i, j int) bool {
@@ -604,6 +618,7 @@ import (
 		{"kprobe_multi", "kprobe_multi"},
 		{"perf_event", "perf_event"},
 		{"tcx", "tcx"},
+		{"netkit", "netkit"},
 	})
 	if err != nil {
 		return nil, fmt.Errorf("splitting linkInfo: %w", err)
diff --git a/internal/sys/types.go b/internal/sys/types.go
index db7baa0..67ed588 100644
--- a/internal/sys/types.go
+++ b/internal/sys/types.go
@@ -740,6 +740,25 @@ func LinkCreateNetfilter(attr *LinkCreateNetfilterAttr) (*FD, error) {
 	return NewFD(int(fd))
 }
 
+type LinkCreateNetkitAttr struct {
+	ProgFd           uint32
+	TargetIfindex    uint32
+	AttachType       AttachType
+	Flags            uint32
+	RelativeFdOrId   uint32
+	_                [4]byte
+	ExpectedRevision uint64
+	_                [32]byte
+}
+
+func LinkCreateNetkit(attr *LinkCreateNetkitAttr) (*FD, error) {
+	fd, err := BPF(BPF_LINK_CREATE, unsafe.Pointer(attr), unsafe.Sizeof(*attr))
+	if err != nil {
+		return nil, err
+	}
+	return NewFD(int(fd))
+}
+
 type LinkCreatePerfEventAttr struct {
 	ProgFd     uint32
 	TargetFd   uint32
@@ -1228,6 +1247,11 @@ type NetfilterLinkInfo struct {
 	Flags    uint32
 }
 
+type NetkitLinkInfo struct {
+	Ifindex    uint32
+	AttachType AttachType
+}
+
 type RawTracepointLinkInfo struct {
 	TpName    Pointer
 	TpNameLen uint32
diff --git a/link/link.go b/link/link.go
index 03368a2..bcee8b6 100644
--- a/link/link.go
+++ b/link/link.go
@@ -104,6 +104,8 @@ func wrapRawLink(raw *RawLink) (_ Link, err error) {
 		return &tcxLink{*raw}, nil
 	case NetfilterType:
 		return &netfilterLink{*raw}, nil
+	case NetkitType:
+		return &netkitLink{*raw}, nil
 	default:
 		return raw, nil
 	}
@@ -140,6 +142,7 @@ type NetNsInfo sys.NetNsLinkInfo
 type XDPInfo sys.XDPLinkInfo
 type TCXInfo sys.TcxLinkInfo
 type NetfilterInfo sys.NetfilterLinkInfo
+type NetkitInfo sys.NetkitLinkInfo
 
 // Tracing returns tracing type-specific link info.
 //
@@ -343,6 +346,8 @@ func (l *RawLink) Info() (*Info, error) {
 		extra = &TCXInfo{}
 	case NetfilterType:
 		extra = &NetfilterInfo{}
+	case NetkitType:
+		extra = &NetkitInfo{}
 	default:
 		return nil, fmt.Errorf("unknown link info type: %d", info.Type)
 	}
diff --git a/link/netkit.go b/link/netkit.go
new file mode 100644
index 0000000..36ed72a
--- /dev/null
+++ b/link/netkit.go
@@ -0,0 +1,71 @@
+package link
+
+import (
+	"fmt"
+	"runtime"
+
+	"github.com/cilium/ebpf"
+	"github.com/cilium/ebpf/internal/sys"
+)
+
+type NetkitOptions struct {
+	// Index of the interface to attach to.
+	Interface int
+	// Program to attach.
+	Program *ebpf.Program
+	// One of the AttachNetkit* constants.
+	Attach ebpf.AttachType
+	// Attach relative to an anchor. Optional.
+	Anchor Anchor
+	// Only attach if the expected revision matches.
+	ExpectedRevision uint64
+	// Flags control the attach behaviour. Specify an Anchor instead of
+	// F_LINK, F_ID, F_BEFORE, F_AFTER and R_REPLACE. Optional.
+	Flags uint32
+}
+
+func AttachNetkit(opts NetkitOptions) (Link, error) {
+	if opts.Interface < 0 {
+		return nil, fmt.Errorf("interface %d is out of bounds", opts.Interface)
+	}
+
+	if opts.Flags&anchorFlags != 0 {
+		return nil, fmt.Errorf("disallowed flags: use Anchor to specify attach target")
+	}
+
+	attr := sys.LinkCreateNetkitAttr{
+		ProgFd:           uint32(opts.Program.FD()),
+		AttachType:       sys.AttachType(opts.Attach),
+		TargetIfindex:    uint32(opts.Interface),
+		ExpectedRevision: opts.ExpectedRevision,
+		Flags:            opts.Flags,
+	}
+
+	if opts.Anchor != nil {
+		fdOrID, flags, err := opts.Anchor.anchor()
+		if err != nil {
+			return nil, fmt.Errorf("attach netkit link: %w", err)
+		}
+
+		attr.RelativeFdOrId = fdOrID
+		attr.Flags |= flags
+	}
+
+	fd, err := sys.LinkCreateNetkit(&attr)
+	runtime.KeepAlive(opts.Program)
+	runtime.KeepAlive(opts.Anchor)
+	if err != nil {
+		if haveFeatErr := haveNetkit(); haveFeatErr != nil {
+			return nil, haveFeatErr
+		}
+		return nil, fmt.Errorf("attach netkit link: %w", err)
+	}
+
+	return &netkitLink{RawLink{fd, ""}}, nil
+}
+
+type netkitLink struct {
+	RawLink
+}
+
+var _ Link = (*netkitLink)(nil)
diff --git a/link/syscalls.go b/link/syscalls.go
index 53476c9..d09b5ac 100644
--- a/link/syscalls.go
+++ b/link/syscalls.go
@@ -27,6 +27,7 @@ const (
 	TCXType           = sys.BPF_LINK_TYPE_TCX
 	UprobeMultiType   = sys.BPF_LINK_TYPE_UPROBE_MULTI
 	NetfilterType     = sys.BPF_LINK_TYPE_NETFILTER
+	NetkitType        = sys.BPF_LINK_TYPE_NETKIT
 )
 
 var haveProgAttach = internal.NewFeatureTest("BPF_PROG_ATTACH", "4.10", func() error {
@@ -162,3 +163,38 @@ var haveTCX = internal.NewFeatureTest("tcx", "6.6", func() error {
 	}
 	return errors.New("syscall succeeded unexpectedly")
 })
+
+var haveNetkit = internal.NewFeatureTest("netkit", "6.7", func() error {
+	prog, err := ebpf.NewProgram(&ebpf.ProgramSpec{
+		Type:    ebpf.SchedCLS,
+		License: "MIT",
+		Instructions: asm.Instructions{
+			asm.Mov.Imm(asm.R0, 0),
+			asm.Return(),
+		},
+	})
+
+	if err != nil {
+		return internal.ErrNotSupported
+	}
+
+	defer prog.Close()
+	attr := sys.LinkCreateNetkitAttr{
+		// We rely on this being checked during the syscall.
+		// With an otherwise correct payload we expect ENODEV here
+		// as an indication that the feature is present.
+		TargetIfindex: ^uint32(0),
+		ProgFd:        uint32(prog.FD()),
+		AttachType:    sys.AttachType(ebpf.AttachNetkitPrimary),
+	}
+
+	_, err = sys.LinkCreateNetkit(&attr)
+
+	if errors.Is(err, unix.ENODEV) {
+		return nil
+	}
+	if err != nil {
+		return ErrNotSupported
+	}
+	return errors.New("syscall succeeded unexpectedly")
+})
diff --git a/types.go b/types.go
index 5146721..542c239 100644
--- a/types.go
+++ b/types.go
@@ -220,6 +220,13 @@ const (
 	AttachTCXIngress                 = AttachType(sys.BPF_TCX_INGRESS)
 	AttachTCXEgress                  = AttachType(sys.BPF_TCX_EGRESS)
 	AttachTraceUprobeMulti           = AttachType(sys.BPF_TRACE_UPROBE_MULTI)
+	AttachCgroupUnixConnect          = AttachType(sys.BPF_CGROUP_UNIX_CONNECT)
+	AttachCgroupUnixSendmsg          = AttachType(sys.BPF_CGROUP_UNIX_SENDMSG)
+	AttachCgroupUnixRecvmsg          = AttachType(sys.BPF_CGROUP_UNIX_RECVMSG)
+	AttachCgroupUnixGetpeername      = AttachType(sys.BPF_CGROUP_UNIX_GETPEERNAME)
+	AttachCgroupUnixGetsockname      = AttachType(sys.BPF_CGROUP_UNIX_GETSOCKNAME)
+	AttachNetkitPrimary              = AttachType(sys.BPF_NETKIT_PRIMARY)
+	AttachNetkitPeer                 = AttachType(sys.BPF_NETKIT_PEER)
 )
 
 // AttachFlags of the eBPF program used in BPF_PROG_ATTACH command


Test patch:
diff --git a/link/syscalls_test.go b/link/syscalls_test.go
index 28dfd3b..475dd44 100644
--- a/link/syscalls_test.go
+++ b/link/syscalls_test.go
@@ -25,3 +25,7 @@ func TestHaveProgQuery(t *testing.T) {
 func TestHaveTCX(t *testing.T) {
 	testutils.CheckFeatureTest(t, haveTCX)
 }
+
+func TestHaveNetkit(t *testing.T) {
+	testutils.CheckFeatureTest(t, haveNetkit)
+}
diff --git a/prog_test.go b/prog_test.go
index e43adb7..0cb58e0 100644
--- a/prog_test.go
+++ b/prog_test.go
@@ -9,6 +9,7 @@ import (
 	"os"
 	"path/filepath"
 	"runtime"
+	"slices"
 	"strings"
 	"syscall"
 	"testing"
@@ -475,6 +476,8 @@ func TestProgramVerifierLogTruncated(t *testing.T) {
 	logSize := 128
 
 	check := func(t *testing.T, err error) {
+		t.Helper()
+
 		if err == nil {
 			t.Fatal("Expected an error")
 		}
@@ -490,26 +493,18 @@ func TestProgramVerifierLogTruncated(t *testing.T) {
 	// Generate a base program of sufficient size whose verifier log does not fit
 	// a 128-byte buffer. This should always result in ENOSPC, setting the
 	// VerifierError.Truncated flag.
-	base := func() (out asm.Instructions) {
-		for i := 0; i < 32; i++ {
-			out = append(out, asm.Mov.Reg(asm.R0, asm.R1))
-		}
-		return
-	}()
+	var base asm.Instructions
+	for i := 0; i < 32; i++ {
+		base = append(base, asm.Mov.Reg(asm.R0, asm.R1))
+	}
 
-	invalid := func() (out asm.Instructions) {
-		out = base
-		// Touch R10 (read-only frame pointer) to reliably force a verifier error.
-		out = append(out, asm.Mov.Reg(asm.R10, asm.R0))
-		out = append(out, asm.Return())
-		return
-	}()
+	// Touch R10 (read-only frame pointer) to reliably force a verifier error.
+	invalid := slices.Clone(base)
+	invalid = append(invalid, asm.Mov.Reg(asm.R10, asm.R0))
+	invalid = append(invalid, asm.Return())
 
-	valid := func() (out asm.Instructions) {
-		out = base
-		out = append(out, asm.Return())
-		return
-	}()
+	valid := slices.Clone(base)
+	valid = append(valid, asm.Return())
 
 	// Start out with testing against the invalid program.
 	spec := &ProgramSpec{