-
Notifications
You must be signed in to change notification settings - Fork 839
Expand file tree
/
Copy pathnetdev_linux_test.go
More file actions
274 lines (251 loc) · 8.96 KB
/
Copy pathnetdev_linux_test.go
File metadata and controls
274 lines (251 loc) · 8.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
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: "<id> <port_count> <queue_count>".
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 "eni<id>np1" 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)
}
}