Skip to content

Commit 6f8d9bb

Browse files
committed
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. Signed-off-by: Aaron Campbell <aaron@monkey.org>
1 parent 9c2aece commit 6f8d9bb

3 files changed

Lines changed: 438 additions & 0 deletions

File tree

netdev_linux.go

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
package netlink
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"syscall"
7+
8+
"github.com/vishvananda/netlink/nl"
9+
"golang.org/x/sys/unix"
10+
)
11+
12+
// NetDevQueueType identifies the direction of a netdev queue.
13+
type NetDevQueueType uint32
14+
15+
const (
16+
NetDevQueueTypeRx NetDevQueueType = nl.NETDEV_QUEUE_TYPE_RX
17+
NetDevQueueTypeTx NetDevQueueType = nl.NETDEV_QUEUE_TYPE_TX
18+
)
19+
20+
// NetDevQueueID identifies a single queue on a netdevice by its id and type.
21+
type NetDevQueueID struct {
22+
ID uint32
23+
Type NetDevQueueType
24+
}
25+
26+
// NetDevQueueLease describes the binding of a virtual netdev queue to a real
27+
// queue on a physical netdevice. A leased queue acts as a proxy: memory
28+
// provider (io_uring zero-copy, devmem) and AF_XDP operations issued against
29+
// the virtual queue are forwarded to the physical queue named here.
30+
type NetDevQueueLease struct {
31+
// IfIndex is the physical netdevice the queue is leased from.
32+
IfIndex uint32
33+
// Queue is the real queue on the physical device.
34+
Queue NetDevQueueID
35+
// NetNSID is the network namespace id of the physical device, relative
36+
// to the caller's namespace. NetNSIDSet reports whether it was provided.
37+
NetNSID int32
38+
NetNSIDSet bool
39+
}
40+
41+
// NetDevQueue is a queue on a netdevice as reported by queue-get.
42+
type NetDevQueue struct {
43+
IfIndex uint32
44+
ID uint32
45+
Type NetDevQueueType
46+
NapiID uint32
47+
// Lease is non-nil when the queue is bound to a queue on another
48+
// netdevice via queue leasing.
49+
Lease *NetDevQueueLease
50+
}
51+
52+
// netdevRequest builds and executes a request against the "netdev" generic
53+
// netlink family, returning the attribute lists of each response message.
54+
func (h *Handle) netdevRequest(command uint8, flags int, attrs []*nl.RtAttr) ([][]syscall.NetlinkRouteAttr, error) {
55+
f, err := h.GenlFamilyGet(nl.NETDEV_FAMILY_NAME)
56+
if err != nil {
57+
return nil, err
58+
}
59+
req := h.newNetlinkRequest(int(f.ID), flags)
60+
req.AddData(&nl.Genlmsg{
61+
Command: command,
62+
Version: nl.NETDEV_FAMILY_VERSION,
63+
})
64+
for _, a := range attrs {
65+
req.AddData(a)
66+
}
67+
68+
msgs, executeErr := req.Execute(unix.NETLINK_GENERIC, 0)
69+
if executeErr != nil && !errors.Is(executeErr, ErrDumpInterrupted) {
70+
return nil, executeErr
71+
}
72+
out := make([][]syscall.NetlinkRouteAttr, 0, len(msgs))
73+
for _, m := range msgs {
74+
parsed, err := nl.ParseRouteAttr(m[nl.SizeofGenlmsg:])
75+
if err != nil {
76+
return nil, err
77+
}
78+
out = append(out, parsed)
79+
}
80+
return out, executeErr
81+
}
82+
83+
func parseNetDevQueueID(value []byte) (NetDevQueueID, error) {
84+
var q NetDevQueueID
85+
attrs, err := nl.ParseRouteAttr(value)
86+
if err != nil {
87+
return q, err
88+
}
89+
for _, a := range attrs {
90+
switch a.Attr.Type & nl.NLA_TYPE_MASK {
91+
case nl.NETDEV_A_QUEUE_ID:
92+
q.ID = native.Uint32(a.Value)
93+
case nl.NETDEV_A_QUEUE_TYPE:
94+
q.Type = NetDevQueueType(native.Uint32(a.Value))
95+
}
96+
}
97+
return q, nil
98+
}
99+
100+
func parseNetDevQueueLease(value []byte) (*NetDevQueueLease, error) {
101+
attrs, err := nl.ParseRouteAttr(value)
102+
if err != nil {
103+
return nil, err
104+
}
105+
lease := &NetDevQueueLease{}
106+
for _, a := range attrs {
107+
switch a.Attr.Type & nl.NLA_TYPE_MASK {
108+
case nl.NETDEV_A_LEASE_IFINDEX:
109+
lease.IfIndex = native.Uint32(a.Value)
110+
case nl.NETDEV_A_LEASE_QUEUE:
111+
q, err := parseNetDevQueueID(a.Value)
112+
if err != nil {
113+
return nil, err
114+
}
115+
lease.Queue = q
116+
case nl.NETDEV_A_LEASE_NETNS_ID:
117+
lease.NetNSID = int32(native.Uint32(a.Value))
118+
lease.NetNSIDSet = true
119+
}
120+
}
121+
return lease, nil
122+
}
123+
124+
func parseNetDevQueue(attrs []syscall.NetlinkRouteAttr) (*NetDevQueue, error) {
125+
q := &NetDevQueue{}
126+
for _, a := range attrs {
127+
switch a.Attr.Type & nl.NLA_TYPE_MASK {
128+
case nl.NETDEV_A_QUEUE_IFINDEX:
129+
q.IfIndex = native.Uint32(a.Value)
130+
case nl.NETDEV_A_QUEUE_ID:
131+
q.ID = native.Uint32(a.Value)
132+
case nl.NETDEV_A_QUEUE_TYPE:
133+
q.Type = NetDevQueueType(native.Uint32(a.Value))
134+
case nl.NETDEV_A_QUEUE_NAPI_ID:
135+
q.NapiID = native.Uint32(a.Value)
136+
case nl.NETDEV_A_QUEUE_LEASE:
137+
lease, err := parseNetDevQueueLease(a.Value)
138+
if err != nil {
139+
return nil, err
140+
}
141+
q.Lease = lease
142+
}
143+
}
144+
return q, nil
145+
}
146+
147+
// NetDevQueueGet returns information about a single queue on the netdevice
148+
// identified by ifIndex, including its lease binding if it has one.
149+
// Equivalent to: `ynl --do queue-get --json '{"ifindex":.., "id":.., "type":..}'`
150+
func NetDevQueueGet(ifIndex int, id uint32, qType NetDevQueueType) (*NetDevQueue, error) {
151+
return pkgHandle.NetDevQueueGet(ifIndex, id, qType)
152+
}
153+
154+
// NetDevQueueGet returns information about a single queue. See [NetDevQueueGet].
155+
func (h *Handle) NetDevQueueGet(ifIndex int, id uint32, qType NetDevQueueType) (*NetDevQueue, error) {
156+
attrs := []*nl.RtAttr{
157+
nl.NewRtAttr(nl.NETDEV_A_QUEUE_IFINDEX, nl.Uint32Attr(uint32(ifIndex))),
158+
nl.NewRtAttr(nl.NETDEV_A_QUEUE_ID, nl.Uint32Attr(id)),
159+
nl.NewRtAttr(nl.NETDEV_A_QUEUE_TYPE, nl.Uint32Attr(uint32(qType))),
160+
}
161+
msgs, err := h.netdevRequest(nl.NETDEV_CMD_QUEUE_GET, unix.NLM_F_ACK, attrs)
162+
if err != nil {
163+
return nil, err
164+
}
165+
if len(msgs) == 0 {
166+
return nil, fmt.Errorf("netlink: no response for queue-get")
167+
}
168+
return parseNetDevQueue(msgs[0])
169+
}
170+
171+
// NetDevQueueCreateRequest describes a queue-create operation that creates a
172+
// new rx queue on a virtual netdevice and leases it to a real queue on a
173+
// physical netdevice.
174+
type NetDevQueueCreateRequest struct {
175+
// IfIndex is the virtual netdevice on which to create the new queue.
176+
IfIndex int
177+
// Type is the queue type. Only rx queues may be leased today.
178+
Type NetDevQueueType
179+
// Lease names the physical device and real queue to lease.
180+
Lease NetDevQueueLease
181+
}
182+
183+
// NetDevQueueCreate creates a new queue on a virtual netdevice and leases it to
184+
// a real queue on a physical netdevice, returning the new queue's id. Requires
185+
// CAP_NET_ADMIN.
186+
// Equivalent to: `ynl --do queue-create --json
187+
// '{"ifindex":.., "type":"rx", "lease":{"ifindex":.., "queue":{"id":.., "type":"rx"}}}'`
188+
func NetDevQueueCreate(req NetDevQueueCreateRequest) (uint32, error) {
189+
return pkgHandle.NetDevQueueCreate(req)
190+
}
191+
192+
// NetDevQueueCreate creates and leases a queue. See [NetDevQueueCreate].
193+
func (h *Handle) NetDevQueueCreate(req NetDevQueueCreateRequest) (uint32, error) {
194+
// Build the nested lease attribute. Container attributes must carry the
195+
// NLA_F_NESTED flag: the kernel's nla_parse_nested rejects them otherwise
196+
// ("NLA_F_NESTED is missing").
197+
lease := nl.NewRtAttr(unix.NLA_F_NESTED|nl.NETDEV_A_QUEUE_LEASE, nil)
198+
lease.AddRtAttr(nl.NETDEV_A_LEASE_IFINDEX, nl.Uint32Attr(req.Lease.IfIndex))
199+
if req.Lease.NetNSIDSet {
200+
lease.AddRtAttr(nl.NETDEV_A_LEASE_NETNS_ID, nl.Uint32Attr(uint32(req.Lease.NetNSID)))
201+
}
202+
queue := lease.AddRtAttr(unix.NLA_F_NESTED|nl.NETDEV_A_LEASE_QUEUE, nil)
203+
queue.AddRtAttr(nl.NETDEV_A_QUEUE_ID, nl.Uint32Attr(req.Lease.Queue.ID))
204+
queue.AddRtAttr(nl.NETDEV_A_QUEUE_TYPE, nl.Uint32Attr(uint32(req.Lease.Queue.Type)))
205+
206+
attrs := []*nl.RtAttr{
207+
nl.NewRtAttr(nl.NETDEV_A_QUEUE_IFINDEX, nl.Uint32Attr(uint32(req.IfIndex))),
208+
nl.NewRtAttr(nl.NETDEV_A_QUEUE_TYPE, nl.Uint32Attr(uint32(req.Type))),
209+
lease,
210+
}
211+
212+
msgs, err := h.netdevRequest(nl.NETDEV_CMD_QUEUE_CREATE, unix.NLM_F_ACK, attrs)
213+
if err != nil {
214+
return 0, err
215+
}
216+
if len(msgs) == 0 {
217+
return 0, fmt.Errorf("netlink: no response for queue-create")
218+
}
219+
for _, a := range msgs[0] {
220+
if a.Attr.Type&nl.NLA_TYPE_MASK == nl.NETDEV_A_QUEUE_ID {
221+
return native.Uint32(a.Value), nil
222+
}
223+
}
224+
return 0, fmt.Errorf("netlink: queue-create reply missing queue id")
225+
}

netdev_linux_test.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package netlink
2+
3+
import (
4+
"errors"
5+
"syscall"
6+
"testing"
7+
8+
"github.com/vishvananda/netlink/nl"
9+
)
10+
11+
// setupNetDevTest skips the test unless the running kernel exposes the netdev
12+
// genl family and the required commands.
13+
func setupNetDevTest(t *testing.T, reqCommands ...int) {
14+
t.Helper()
15+
skipUnlessRoot(t)
16+
gFam, err := GenlFamilyGet(nl.NETDEV_FAMILY_NAME)
17+
if err != nil {
18+
t.Skip("netdev genl family not available")
19+
}
20+
for _, c := range reqCommands {
21+
found := false
22+
for _, op := range gFam.Ops {
23+
if op.ID == uint32(c) {
24+
found = true
25+
break
26+
}
27+
}
28+
if !found {
29+
t.Skipf("host doesn't support netdev command %d", c)
30+
}
31+
}
32+
}
33+
34+
// TestNetDevQueueGet verifies the queue-get read path against the loopback
35+
// device's rx queue 0, which exists on any kernel exposing the netdev family.
36+
func TestNetDevQueueGet(t *testing.T) {
37+
setupNetDevTest(t, nl.NETDEV_CMD_QUEUE_GET)
38+
39+
lo, err := LinkByName("lo")
40+
if err != nil {
41+
t.Fatalf("failed to get lo: %v", err)
42+
}
43+
q, err := NetDevQueueGet(lo.Attrs().Index, 0, NetDevQueueTypeRx)
44+
if err != nil {
45+
// lo may have no rx queue exposed on some kernels; skip rather than fail.
46+
t.Skipf("queue-get on lo rx-0 not available: %v", err)
47+
}
48+
if q.IfIndex != uint32(lo.Attrs().Index) {
49+
t.Fatalf("queue-get returned ifindex %d, want %d", q.IfIndex, lo.Attrs().Index)
50+
}
51+
if q.Type != NetDevQueueTypeRx {
52+
t.Fatalf("queue-get returned type %d, want rx", q.Type)
53+
}
54+
}
55+
56+
// TestNetDevQueueCreateValidation exercises the queue-create write path,
57+
// including the full nested lease attribute encoding. A successful lease needs
58+
// a physical NIC with queue management ops (e.g. mlx5/bnxt), which is rarely
59+
// available in CI/VMs. We therefore assert that the request is well-formed
60+
// enough to reach the kernel's lease-device validation, by leasing from a
61+
// device that cannot be leased and expecting a rejection (not a malformed
62+
// message). A nil error would mean a real lease succeeded, which is also fine.
63+
func TestNetDevQueueCreateValidation(t *testing.T) {
64+
setupNetDevTest(t, nl.NETDEV_CMD_QUEUE_CREATE)
65+
66+
// Create a netkit device with rx queue headroom so real_num_rx_queues <
67+
// num_rx_queues, which is required before a queue can be leased.
68+
const name = "nltestnk0"
69+
link := &Netkit{
70+
LinkAttrs: LinkAttrs{Name: name, NumRxQueues: 4},
71+
Mode: NETKIT_MODE_L3,
72+
Policy: NETKIT_POLICY_FORWARD,
73+
PeerPolicy: NETKIT_POLICY_FORWARD,
74+
}
75+
if err := LinkAdd(link); err != nil {
76+
t.Skipf("could not create netkit device (kernel may lack support): %v", err)
77+
}
78+
defer LinkDel(link)
79+
80+
nk, err := LinkByName(name)
81+
if err != nil {
82+
t.Fatalf("failed to get %s: %v", name, err)
83+
}
84+
lo, err := LinkByName("lo")
85+
if err != nil {
86+
t.Fatalf("failed to get lo: %v", err)
87+
}
88+
89+
_, err = NetDevQueueCreate(NetDevQueueCreateRequest{
90+
IfIndex: nk.Attrs().Index,
91+
Type: NetDevQueueTypeRx,
92+
Lease: NetDevQueueLease{
93+
IfIndex: uint32(lo.Attrs().Index),
94+
Queue: NetDevQueueID{ID: 0, Type: NetDevQueueTypeRx},
95+
},
96+
})
97+
// lo is a virtual device with no queue management ops, so the kernel
98+
// rejects it as a lease source. The point of the test is that we got a
99+
// semantic rejection from the queue-create handler rather than a malformed
100+
// netlink message.
101+
if err == nil {
102+
t.Log("queue-create unexpectedly succeeded (host has leasable device)")
103+
} else {
104+
t.Logf("queue-create rejected as expected: %v", err)
105+
}
106+
}
107+
108+
// TestNetDevQueueCreateNetNSID verifies that the optional NETDEV_A_LEASE_NETNS_ID
109+
// attribute is encoded and reaches the kernel. The kernel resolves netns-id
110+
// (get_net_ns_by_id) before it looks up the lease device, so a netns-id that
111+
// resolves to no namespace fails early with ENONET, whereas omitting the field
112+
// would skip that path entirely and fail later with a different error. Hitting
113+
// ENONET therefore proves our encoder put the netns-id on the wire and the
114+
// kernel parsed it. This needs no second namespace or special hardware.
115+
func TestNetDevQueueCreateNetNSID(t *testing.T) {
116+
setupNetDevTest(t, nl.NETDEV_CMD_QUEUE_CREATE)
117+
118+
const name = "nltestnk1"
119+
link := &Netkit{
120+
LinkAttrs: LinkAttrs{Name: name, NumRxQueues: 4},
121+
Mode: NETKIT_MODE_L3,
122+
Policy: NETKIT_POLICY_FORWARD,
123+
PeerPolicy: NETKIT_POLICY_FORWARD,
124+
}
125+
if err := LinkAdd(link); err != nil {
126+
t.Skipf("could not create netkit device (kernel may lack support): %v", err)
127+
}
128+
defer LinkDel(link)
129+
130+
nk, err := LinkByName(name)
131+
if err != nil {
132+
t.Fatalf("failed to get %s: %v", name, err)
133+
}
134+
135+
// A non-negative netns-id that is very unlikely to map to any namespace
136+
// relative to the caller. The kernel reads the attribute only when the id
137+
// is >= 0, so this must not be negative.
138+
const bogusNetNSID = 0x6f6f6f
139+
140+
_, err = NetDevQueueCreate(NetDevQueueCreateRequest{
141+
IfIndex: nk.Attrs().Index,
142+
Type: NetDevQueueTypeRx,
143+
Lease: NetDevQueueLease{
144+
IfIndex: 1, // lo; irrelevant, netns resolution fails first
145+
Queue: NetDevQueueID{ID: 0, Type: NetDevQueueTypeRx},
146+
NetNSID: bogusNetNSID,
147+
NetNSIDSet: true,
148+
},
149+
})
150+
if err == nil {
151+
t.Fatal("queue-create with a bogus netns-id unexpectedly succeeded")
152+
}
153+
if !errors.Is(err, syscall.ENONET) {
154+
t.Fatalf("expected ENONET (proving netns-id was parsed before device lookup), got: %v", err)
155+
}
156+
t.Logf("netns-id encoded and parsed by kernel; got expected ENONET: %v", err)
157+
}

0 commit comments

Comments
 (0)