From 7f9257a46b5e76df25e528b75d3c7a93d5368597 Mon Sep 17 00:00:00 2001 From: Aaron Campbell Date: Wed, 10 Jun 2026 14:46:04 -0300 Subject: [PATCH] netdev: add queue-get and queue-create with queue leasing Add the "netdev" generic netlink family with NetDevQueueGet (decodes the nested lease attribute) and NetDevQueueCreate (creates an rx queue on a virtual device and leases it to a real queue on a physical device). Queue leasing lets a virtual netdev proxy a physical NIC rx queue so io_uring zero-copy and AF_XDP can address it by (ifindex, queue-id). The lease nest is encoded with NLA_F_NESTED and may carry an optional netns-id. Mirrors include/uapi/linux/netdev.h. Tests skip unless the kernel supports the commands. Tests use netdevsim as a hardware-free lease source (it implements the kernel's queue management ops): TestNetDevQueueLeaseRoundTrip leases a netdevsim rx queue to a netkit peer and reads it back, asserting the decoded lease matches exactly. It requires root and is skipped when netdevsim or the queue commands are unavailable. Signed-off-by: Aaron Campbell --- netdev_linux.go | 275 +++++++++++++++++++++++++++++++++++++++++++ netdev_linux_test.go | 274 ++++++++++++++++++++++++++++++++++++++++++ nl/netdev_linux.go | 56 +++++++++ 3 files changed, 605 insertions(+) create mode 100644 netdev_linux.go create mode 100644 netdev_linux_test.go create mode 100644 nl/netdev_linux.go diff --git a/netdev_linux.go b/netdev_linux.go new file mode 100644 index 000000000..be22c1f2b --- /dev/null +++ b/netdev_linux.go @@ -0,0 +1,275 @@ +package netlink + +import ( + "errors" + "fmt" + "syscall" + + "github.com/vishvananda/netlink/nl" + "golang.org/x/sys/unix" +) + +// NetDevQueueType identifies the direction of a netdev queue. +type NetDevQueueType uint32 + +const ( + NetDevQueueTypeRx NetDevQueueType = nl.NETDEV_QUEUE_TYPE_RX + NetDevQueueTypeTx NetDevQueueType = nl.NETDEV_QUEUE_TYPE_TX +) + +// NetDevQueueID identifies a single queue on a netdevice by its id and type. +type NetDevQueueID struct { + ID uint32 + Type NetDevQueueType +} + +// NetDevQueueLease identifies the peer endpoint of a queue lease. Its direction +// depends on where it is used: in [NetDevQueueCreateRequest] it names the +// physical queue to lease from, while in a [NetDevQueue] returned by +// [NetDevQueueGet] it names the virtual queue leasing the queried physical +// queue. +type NetDevQueueLease struct { + // IfIndex identifies the peer netdevice. + IfIndex uint32 + // Queue identifies the peer queue on that netdevice. + Queue NetDevQueueID + // NetNSID is the network namespace id of the peer device, relative to the + // caller's namespace. NetNSIDSet reports whether it was provided. + NetNSID int32 + NetNSIDSet bool +} + +// NetDevQueue is a queue on a netdevice as reported by queue-get. +type NetDevQueue struct { + IfIndex uint32 + ID uint32 + Type NetDevQueueType + NapiID uint32 + // Lease is non-nil when this physical queue is leased by a virtual queue. + // It identifies that virtual queue, not the physical queue queried here. + Lease *NetDevQueueLease +} + +// netdevRequest builds and executes a request against the "netdev" generic +// netlink family, returning the attribute lists of each response message. +func (h *Handle) netdevRequest(command uint8, flags int, attrs []*nl.RtAttr) ([][]syscall.NetlinkRouteAttr, error) { + f, err := h.GenlFamilyGet(nl.NETDEV_FAMILY_NAME) + if err != nil { + return nil, err + } + req := h.newNetlinkRequest(int(f.ID), flags) + req.AddData(&nl.Genlmsg{ + Command: command, + Version: nl.NETDEV_FAMILY_VERSION, + }) + for _, a := range attrs { + req.AddData(a) + } + + msgs, executeErr := req.Execute(unix.NETLINK_GENERIC, 0) + if executeErr != nil && !errors.Is(executeErr, ErrDumpInterrupted) { + return nil, executeErr + } + out := make([][]syscall.NetlinkRouteAttr, 0, len(msgs)) + for _, m := range msgs { + parsed, err := nl.ParseRouteAttr(m[nl.SizeofGenlmsg:]) + if err != nil { + return nil, err + } + out = append(out, parsed) + } + return out, executeErr +} + +func readNetDevUint32(a syscall.NetlinkRouteAttr) (uint32, error) { + if len(a.Value) < 4 { + return 0, fmt.Errorf("netlink: attribute %d too short: got %d bytes, want 4", a.Attr.Type&nl.NLA_TYPE_MASK, len(a.Value)) + } + return native.Uint32(a.Value), nil +} + +// parseNetDevQueueID decodes a queue-id nested attribute (NETDEV_A_QUEUE_ID +// and NETDEV_A_QUEUE_TYPE) into a NetDevQueueID. +func parseNetDevQueueID(value []byte) (NetDevQueueID, error) { + var q NetDevQueueID + attrs, err := nl.ParseRouteAttr(value) + if err != nil { + return q, err + } + for _, a := range attrs { + switch a.Attr.Type & nl.NLA_TYPE_MASK { + case nl.NETDEV_A_QUEUE_ID: + v, err := readNetDevUint32(a) + if err != nil { + return q, err + } + q.ID = v + case nl.NETDEV_A_QUEUE_TYPE: + v, err := readNetDevUint32(a) + if err != nil { + return q, err + } + q.Type = NetDevQueueType(v) + } + } + return q, nil +} + +// parseNetDevQueueLease decodes a lease nested attribute (the peer ifindex, +// nested queue-id, and optional netns-id) into a NetDevQueueLease. +func parseNetDevQueueLease(value []byte) (*NetDevQueueLease, error) { + attrs, err := nl.ParseRouteAttr(value) + if err != nil { + return nil, err + } + lease := &NetDevQueueLease{} + for _, a := range attrs { + switch a.Attr.Type & nl.NLA_TYPE_MASK { + case nl.NETDEV_A_LEASE_IFINDEX: + v, err := readNetDevUint32(a) + if err != nil { + return nil, err + } + lease.IfIndex = v + case nl.NETDEV_A_LEASE_QUEUE: + q, err := parseNetDevQueueID(a.Value) + if err != nil { + return nil, err + } + lease.Queue = q + case nl.NETDEV_A_LEASE_NETNS_ID: + v, err := readNetDevUint32(a) + if err != nil { + return nil, err + } + lease.NetNSID = int32(v) + lease.NetNSIDSet = true + } + } + return lease, nil +} + +// parseNetDevQueue decodes the attributes of a queue-get response into a +// NetDevQueue, including its nested lease attribute when present. +func parseNetDevQueue(attrs []syscall.NetlinkRouteAttr) (*NetDevQueue, error) { + q := &NetDevQueue{} + for _, a := range attrs { + switch a.Attr.Type & nl.NLA_TYPE_MASK { + case nl.NETDEV_A_QUEUE_IFINDEX: + v, err := readNetDevUint32(a) + if err != nil { + return nil, err + } + q.IfIndex = v + case nl.NETDEV_A_QUEUE_ID: + v, err := readNetDevUint32(a) + if err != nil { + return nil, err + } + q.ID = v + case nl.NETDEV_A_QUEUE_TYPE: + v, err := readNetDevUint32(a) + if err != nil { + return nil, err + } + q.Type = NetDevQueueType(v) + case nl.NETDEV_A_QUEUE_NAPI_ID: + v, err := readNetDevUint32(a) + if err != nil { + return nil, err + } + q.NapiID = v + case nl.NETDEV_A_QUEUE_LEASE: + lease, err := parseNetDevQueueLease(a.Value) + if err != nil { + return nil, err + } + q.Lease = lease + } + } + return q, nil +} + +// NetDevQueueGet returns information about a single queue on the netdevice +// identified by ifIndex, including its lease binding if it has one. +// Equivalent to: `ynl --do queue-get --json '{"ifindex":.., "id":.., "type":..}'` +func NetDevQueueGet(ifIndex int, id uint32, qType NetDevQueueType) (*NetDevQueue, error) { + return pkgHandle.NetDevQueueGet(ifIndex, id, qType) +} + +// NetDevQueueGet returns information about a single queue. See [NetDevQueueGet]. +func (h *Handle) NetDevQueueGet(ifIndex int, id uint32, qType NetDevQueueType) (*NetDevQueue, error) { + attrs := []*nl.RtAttr{ + nl.NewRtAttr(nl.NETDEV_A_QUEUE_IFINDEX, nl.Uint32Attr(uint32(ifIndex))), + nl.NewRtAttr(nl.NETDEV_A_QUEUE_ID, nl.Uint32Attr(id)), + nl.NewRtAttr(nl.NETDEV_A_QUEUE_TYPE, nl.Uint32Attr(uint32(qType))), + } + msgs, err := h.netdevRequest(nl.NETDEV_CMD_QUEUE_GET, unix.NLM_F_ACK, attrs) + if err != nil { + return nil, err + } + if len(msgs) == 0 { + return nil, fmt.Errorf("netlink: no response for queue-get") + } + return parseNetDevQueue(msgs[0]) +} + +// NetDevQueueCreateRequest describes a queue-create operation that creates a +// new rx queue on a virtual netdevice and leases it to a real queue on a +// physical netdevice. +type NetDevQueueCreateRequest struct { + // IfIndex is the virtual netdevice on which to create the new queue. + IfIndex int + // Type is the queue type. Only rx queues may be leased today. + Type NetDevQueueType + // Lease names the physical device and real queue to lease. + Lease NetDevQueueLease +} + +// NetDevQueueCreate creates a new queue on a virtual netdevice and leases it to +// a real queue on a physical netdevice, returning the new queue's id. Requires +// CAP_NET_ADMIN. +// Equivalent to: `ynl --do queue-create --json +// '{"ifindex":.., "type":"rx", "lease":{"ifindex":.., "queue":{"id":.., "type":"rx"}}}'` +func NetDevQueueCreate(req NetDevQueueCreateRequest) (uint32, error) { + return pkgHandle.NetDevQueueCreate(req) +} + +// NetDevQueueCreate creates and leases a queue. See [NetDevQueueCreate]. +func (h *Handle) NetDevQueueCreate(req NetDevQueueCreateRequest) (uint32, error) { + if req.Lease.NetNSIDSet && req.Lease.NetNSID < 0 { + return 0, fmt.Errorf("netlink: lease netns id must be non-negative, got %d", req.Lease.NetNSID) + } + + // Build the nested lease attribute. Container attributes must carry the + // NLA_F_NESTED flag: the kernel's nla_parse_nested rejects them otherwise + // ("NLA_F_NESTED is missing"). + lease := nl.NewRtAttr(unix.NLA_F_NESTED|nl.NETDEV_A_QUEUE_LEASE, nil) + lease.AddRtAttr(nl.NETDEV_A_LEASE_IFINDEX, nl.Uint32Attr(req.Lease.IfIndex)) + if req.Lease.NetNSIDSet { + lease.AddRtAttr(nl.NETDEV_A_LEASE_NETNS_ID, nl.Uint32Attr(uint32(req.Lease.NetNSID))) + } + queue := lease.AddRtAttr(unix.NLA_F_NESTED|nl.NETDEV_A_LEASE_QUEUE, nil) + queue.AddRtAttr(nl.NETDEV_A_QUEUE_ID, nl.Uint32Attr(req.Lease.Queue.ID)) + queue.AddRtAttr(nl.NETDEV_A_QUEUE_TYPE, nl.Uint32Attr(uint32(req.Lease.Queue.Type))) + + attrs := []*nl.RtAttr{ + nl.NewRtAttr(nl.NETDEV_A_QUEUE_IFINDEX, nl.Uint32Attr(uint32(req.IfIndex))), + nl.NewRtAttr(nl.NETDEV_A_QUEUE_TYPE, nl.Uint32Attr(uint32(req.Type))), + lease, + } + + msgs, err := h.netdevRequest(nl.NETDEV_CMD_QUEUE_CREATE, unix.NLM_F_ACK, attrs) + if err != nil { + return 0, err + } + if len(msgs) == 0 { + return 0, fmt.Errorf("netlink: no response for queue-create") + } + for _, a := range msgs[0] { + if a.Attr.Type&nl.NLA_TYPE_MASK == nl.NETDEV_A_QUEUE_ID { + return readNetDevUint32(a) + } + } + return 0, fmt.Errorf("netlink: queue-create reply missing queue id") +} diff --git a/netdev_linux_test.go b/netdev_linux_test.go new file mode 100644 index 000000000..e8d50bf66 --- /dev/null +++ b/netdev_linux_test.go @@ -0,0 +1,274 @@ +package netlink + +import ( + "crypto/rand" + "errors" + "fmt" + "os" + "path/filepath" + "syscall" + "testing" + "time" + + "github.com/vishvananda/netlink/nl" +) + +// setupNetDevTest skips the test unless the running kernel exposes the netdev +// genl family and the required commands. +func setupNetDevTest(t *testing.T, reqCommands ...int) { + t.Helper() + skipUnlessRoot(t) + gFam, err := GenlFamilyGet(nl.NETDEV_FAMILY_NAME) + if err != nil { + if errors.Is(err, syscall.ENOENT) { + t.Skipf("netdev genl family not available: %v", err) + } + t.Fatalf("failed to query netdev genl family: %v", err) + } + for _, c := range reqCommands { + found := false + for _, op := range gFam.Ops { + if op.ID == uint32(c) { + found = true + break + } + } + if !found { + t.Skipf("host doesn't support netdev command %d", c) + } + } +} + +func addNetkitForTest(t *testing.T, link Link) { + t.Helper() + if err := LinkAdd(link); err != nil { + if errors.Is(err, syscall.EOPNOTSUPP) || errors.Is(err, syscall.ENOENT) { + t.Skipf("netkit is not supported: %v", err) + } + t.Fatalf("failed to create netkit device: %v", err) + } +} + +// netDevTestNetkitName returns a unique netkit device name for a test run, so +// that a stale interface left over from a previously interrupted run cannot +// collide with the one a test creates (which would otherwise cause a false +// skip at LinkAdd). The random suffix keeps the name well under IFNAMSIZ-1 +// (15) bytes. +func netDevTestNetkitName(t *testing.T) string { + t.Helper() + var b [3]byte + if _, err := rand.Read(b[:]); err != nil { + t.Fatalf("failed to generate random suffix: %v", err) + } + return fmt.Sprintf("nlt%02x%02x%02x", b[0], b[1], b[2]) +} + +// setupNetdevsim creates a netdevsim device with the requested number of rx/tx +// queues and returns its netdev plus a cleanup function. netdevsim implements +// the kernel's queue management ops, so it can act as a physical lease source +// without real hardware. The test is skipped if netdevsim is unavailable. +func setupNetdevsim(t *testing.T, queueCount int) (Link, func()) { + t.Helper() + skipUnlessKModuleLoaded(t, "netdevsim") + + const maxCreateAttempts = 10 + var id int + for attempt := 1; attempt <= maxCreateAttempts; attempt++ { + var idb [2]byte + if _, err := rand.Read(idb[:]); err != nil { + t.Fatalf("failed to generate netdevsim id: %v", err) + } + id = int(idb[0])<<8 | int(idb[1]) + + // Format: " ". + spec := fmt.Sprintf("%d 1 %d", id, queueCount) + err := os.WriteFile("/sys/bus/netdevsim/new_device", []byte(spec), 0o200) + if err == nil { + break + } + if errors.Is(err, syscall.ENOSPC) || errors.Is(err, syscall.EEXIST) { + if attempt < maxCreateAttempts { + continue + } + t.Fatalf("could not create netdevsim device after %d id collisions: %v", maxCreateAttempts, err) + } + if errors.Is(err, syscall.ENOENT) || errors.Is(err, syscall.EOPNOTSUPP) { + t.Skipf("netdevsim device creation is not available: %v", err) + } + t.Fatalf("failed to create netdevsim device: %v", err) + } + + busDev := fmt.Sprintf("netdevsim%d", id) + cleanup := func() { + _ = os.WriteFile("/sys/bus/netdevsim/del_device", []byte(fmt.Sprintf("%d", id)), 0o200) + } + + // The netdev is created asynchronously and udev renames it to the + // predictable "eninp1" form. Resolve it via netlink (LinkByName), + // which does not depend on the /sys/class/net view. Fall back to scanning + // the bus device's net/ directory in case naming differs. + wantName := fmt.Sprintf("eni%dnp1", id) + netDir := filepath.Join("/sys/bus/netdevsim/devices", busDev, "net") + var link Link + deadline := time.Now().Add(3 * time.Second) + for { + if l, err := LinkByName(wantName); err == nil { + link = l + break + } + if entries, err := os.ReadDir(netDir); err == nil && len(entries) > 0 { + if l, lerr := LinkByName(entries[0].Name()); lerr == nil { + link = l + break + } + } + if time.Now().After(deadline) { + cleanup() + t.Skipf("netdevsim netdev %q did not appear", wantName) + } + time.Sleep(20 * time.Millisecond) + } + + // queue-get only reports a queue once the device is up and its NAPI is + // attached, so bring the device up before returning it. + if err := LinkSetUp(link); err != nil { + cleanup() + t.Fatalf("failed to bring netdevsim %q up: %v", wantName, err) + } + return link, cleanup +} + +// TestNetDevQueueLeaseRoundTrip verifies the full encode and decode path: it +// creates an rx queue on a netkit peer device and leases it to a real queue on +// a netdevsim device, then reads the netdevsim queue back and asserts the +// decoded lease points to the netkit peer with the exact expected values. +func TestNetDevQueueLeaseRoundTrip(t *testing.T) { + setupNetDevTest(t, nl.NETDEV_CMD_QUEUE_CREATE, nl.NETDEV_CMD_QUEUE_GET) + + // netdevsim acts as the physical lease source; queue 1 is the real rx + // queue we lease (queueCount=2 gives rx queues 0 and 1). + const physQueueID = 1 + sim, simCleanup := setupNetdevsim(t, 2) + defer simCleanup() + physIdx := sim.Attrs().Index + + // netkit pair: only the non-primary (peer) device may lease, and it needs + // rx queue headroom (real_num_rx_queues < num_rx_queues), so give the peer + // the extra rx queues. + peerName := netDevTestNetkitName(t) + nk := &Netkit{ + LinkAttrs: LinkAttrs{Name: netDevTestNetkitName(t)}, + Mode: NETKIT_MODE_L3, + Policy: NETKIT_POLICY_FORWARD, + PeerPolicy: NETKIT_POLICY_FORWARD, + } + nk.SetPeerAttrs(&LinkAttrs{Name: peerName, NumRxQueues: 4}) + addNetkitForTest(t, nk) + defer LinkDel(nk) + + peer, err := LinkByName(peerName) + if err != nil { + t.Fatalf("failed to get netkit peer %s: %v", peerName, err) + } + peerIdx := peer.Attrs().Index + + // queue-create: make a new rx queue on the netkit peer and lease it to the + // netdevsim rx queue. + newID, err := NetDevQueueCreate(NetDevQueueCreateRequest{ + IfIndex: peerIdx, + Type: NetDevQueueTypeRx, + Lease: NetDevQueueLease{ + IfIndex: uint32(physIdx), + Queue: NetDevQueueID{ID: physQueueID, Type: NetDevQueueTypeRx}, + }, + }) + if err != nil { + t.Fatalf("queue-create failed: %v", err) + } + + // queue-get the physical (netdevsim) queue: the kernel reports the lease + // pointing back to the virtual netkit peer. This exercises the lease + // decoder, and the values must match exactly. + q, err := NetDevQueueGet(physIdx, physQueueID, NetDevQueueTypeRx) + if err != nil { + t.Fatalf("queue-get on physical queue failed: %v", err) + } + if q.Lease == nil { + t.Fatalf("queue-get on leased physical queue returned no lease info") + } + if q.Lease.IfIndex != uint32(peerIdx) { + t.Errorf("lease ifindex = %d, want netkit peer %d", q.Lease.IfIndex, peerIdx) + } + if q.Lease.Queue.ID != newID { + t.Errorf("lease queue id = %d, want created queue %d", q.Lease.Queue.ID, newID) + } + if q.Lease.Queue.Type != NetDevQueueTypeRx { + t.Errorf("lease queue type = %d, want rx", q.Lease.Queue.Type) + } +} + +// TestNetDevQueueCreateNetNSID verifies that the optional NETDEV_A_LEASE_NETNS_ID +// attribute is encoded and reaches the kernel. The kernel resolves netns-id +// (get_net_ns_by_id) only after fully parsing the nested lease structure and +// before it looks up the lease device, so a netns-id that resolves to no +// namespace fails with ENONET. A malformed message would be rejected earlier +// with EINVAL and could never reach ENONET, so this is a cut-and-dry, single +// errno assertion proving the nested encode (including netns-id) is correct. +// It needs no second namespace or special hardware. +func TestNetDevQueueCreateNetNSID(t *testing.T) { + setupNetDevTest(t, nl.NETDEV_CMD_QUEUE_CREATE) + + name := netDevTestNetkitName(t) + link := &Netkit{ + LinkAttrs: LinkAttrs{Name: name, NumRxQueues: 4}, + Mode: NETKIT_MODE_L3, + Policy: NETKIT_POLICY_FORWARD, + PeerPolicy: NETKIT_POLICY_FORWARD, + } + addNetkitForTest(t, link) + defer LinkDel(link) + + nk, err := LinkByName(name) + if err != nil { + t.Fatalf("failed to get %s: %v", name, err) + } + + // A non-negative netns-id that is very unlikely to map to any namespace + // relative to the caller. The kernel reads the attribute only when the id + // is >= 0, so this must not be negative. + const bogusNetNSID = 0x6f6f6f + + _, err = NetDevQueueCreate(NetDevQueueCreateRequest{ + IfIndex: nk.Attrs().Index, + Type: NetDevQueueTypeRx, + Lease: NetDevQueueLease{ + IfIndex: 1, // lo; irrelevant, netns resolution fails first + Queue: NetDevQueueID{ID: 0, Type: NetDevQueueTypeRx}, + NetNSID: bogusNetNSID, + NetNSIDSet: true, + }, + }) + if err == nil { + t.Fatal("queue-create with a bogus netns-id unexpectedly succeeded") + } + if !errors.Is(err, syscall.ENONET) { + t.Fatalf("expected ENONET (proving netns-id was parsed before device lookup), got: %v", err) + } + t.Logf("netns-id encoded and parsed by kernel; got expected ENONET: %v", err) +} + +func TestNetDevQueueCreateRejectsNegativeNetNSID(t *testing.T) { + _, err := (&Handle{}).NetDevQueueCreate(NetDevQueueCreateRequest{ + Lease: NetDevQueueLease{ + NetNSID: -1, + NetNSIDSet: true, + }, + }) + if err == nil { + t.Fatal("queue-create with a negative netns id unexpectedly succeeded") + } + const want = "netlink: lease netns id must be non-negative, got -1" + if err.Error() != want { + t.Fatalf("error = %q, want %q", err, want) + } +} diff --git a/nl/netdev_linux.go b/nl/netdev_linux.go new file mode 100644 index 000000000..23279b946 --- /dev/null +++ b/nl/netdev_linux.go @@ -0,0 +1,56 @@ +package nl + +// Constants for the "netdev" generic netlink family, mirroring +// include/uapi/linux/netdev.h. Only the subset required for queue +// management (queue-get, queue-create) and queue leasing is defined here. + +const ( + NETDEV_FAMILY_NAME = "netdev" + NETDEV_FAMILY_VERSION = 1 +) + +// netdev_queue_type +const ( + NETDEV_QUEUE_TYPE_RX = iota + NETDEV_QUEUE_TYPE_TX +) + +// Commands (enum netdev_cmd). Numbering starts at 1 to match the UAPI, where +// NETDEV_CMD_DEV_GET = 1. +const ( + NETDEV_CMD_DEV_GET = iota + 1 + NETDEV_CMD_DEV_ADD_NTF + NETDEV_CMD_DEV_DEL_NTF + NETDEV_CMD_DEV_CHANGE_NTF + NETDEV_CMD_PAGE_POOL_GET + NETDEV_CMD_PAGE_POOL_ADD_NTF + NETDEV_CMD_PAGE_POOL_DEL_NTF + NETDEV_CMD_PAGE_POOL_CHANGE_NTF + NETDEV_CMD_PAGE_POOL_STATS_GET + NETDEV_CMD_QUEUE_GET + NETDEV_CMD_NAPI_GET + NETDEV_CMD_QSTATS_GET + NETDEV_CMD_BIND_RX + NETDEV_CMD_NAPI_SET + NETDEV_CMD_BIND_TX + NETDEV_CMD_QUEUE_CREATE +) + +// Queue attribute set (enum starting at NETDEV_A_QUEUE_ID = 1). +const ( + NETDEV_A_QUEUE_ID = iota + 1 + NETDEV_A_QUEUE_IFINDEX + NETDEV_A_QUEUE_TYPE + NETDEV_A_QUEUE_NAPI_ID + NETDEV_A_QUEUE_DMABUF + NETDEV_A_QUEUE_IO_URING + NETDEV_A_QUEUE_XSK + NETDEV_A_QUEUE_LEASE +) + +// Lease nested attribute set (enum starting at NETDEV_A_LEASE_IFINDEX = 1). +const ( + NETDEV_A_LEASE_IFINDEX = iota + 1 + NETDEV_A_LEASE_QUEUE + NETDEV_A_LEASE_NETNS_ID +)