Linux kernel mirror (for testing)
git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel
os
linux
1#!/usr/bin/env python3
2# SPDX-License-Identifier: GPL-2.0
3
4"""
5API level tests for RSS (mostly Netlink vs IOCTL).
6"""
7
8import errno
9import glob
10import random
11from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_is, ksft_ne, ksft_raises
12from lib.py import KsftSkipEx, KsftFailEx
13from lib.py import defer, ethtool, CmdExitFailure
14from lib.py import EthtoolFamily, NlError
15from lib.py import NetDrvEnv
16
17
18def _require_2qs(cfg):
19 qcnt = len(glob.glob(f"/sys/class/net/{cfg.ifname}/queues/rx-*"))
20 if qcnt < 2:
21 raise KsftSkipEx(f"Local has only {qcnt} queues")
22 return qcnt
23
24
25def _ethtool_create(cfg, act, opts):
26 output = ethtool(f"{act} {cfg.ifname} {opts}").stdout
27 # Output will be something like: "New RSS context is 1" or
28 # "Added rule with ID 7", we want the integer from the end
29 return int(output.split()[-1])
30
31
32def _ethtool_get_cfg(cfg, fl_type, to_nl=False):
33 descr = ethtool(f"-n {cfg.ifname} rx-flow-hash {fl_type}").stdout
34
35 if to_nl:
36 converter = {
37 "IP SA": "ip-src",
38 "IP DA": "ip-dst",
39 "L4 bytes 0 & 1 [TCP/UDP src port]": "l4-b-0-1",
40 "L4 bytes 2 & 3 [TCP/UDP dst port]": "l4-b-2-3",
41 }
42
43 ret = set()
44 else:
45 converter = {
46 "IP SA": "s",
47 "IP DA": "d",
48 "L3 proto": "t",
49 "L4 bytes 0 & 1 [TCP/UDP src port]": "f",
50 "L4 bytes 2 & 3 [TCP/UDP dst port]": "n",
51 }
52
53 ret = ""
54
55 for line in descr.split("\n")[1:-2]:
56 # if this raises we probably need to add more keys to converter above
57 if to_nl:
58 ret.add(converter[line])
59 else:
60 ret += converter[line]
61 return ret
62
63
64def test_rxfh_nl_set_fail(cfg):
65 """
66 Test error path of Netlink SET.
67 """
68 _require_2qs(cfg)
69
70 ethnl = EthtoolFamily()
71 ethnl.ntf_subscribe("monitor")
72
73 with ksft_raises(NlError):
74 ethnl.rss_set({"header": {"dev-name": "lo"},
75 "indir": None})
76
77 with ksft_raises(NlError):
78 ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
79 "indir": [100000]})
80 ntf = next(ethnl.poll_ntf(duration=0.2), None)
81 ksft_is(ntf, None)
82
83
84def test_rxfh_nl_set_indir(cfg):
85 """
86 Test setting indirection table via Netlink.
87 """
88 qcnt = _require_2qs(cfg)
89
90 # Test some SETs with a value
91 reset = defer(cfg.ethnl.rss_set,
92 {"header": {"dev-index": cfg.ifindex}, "indir": None})
93 cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
94 "indir": [1]})
95 rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
96 ksft_eq(set(rss.get("indir", [-1])), {1})
97
98 cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
99 "indir": [0, 1]})
100 rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
101 ksft_eq(set(rss.get("indir", [-1])), {0, 1})
102
103 # Make sure we can't set the queue count below max queue used
104 with ksft_raises(CmdExitFailure):
105 ethtool(f"-L {cfg.ifname} combined 0 rx 1")
106 with ksft_raises(CmdExitFailure):
107 ethtool(f"-L {cfg.ifname} combined 1 rx 0")
108
109 # Test reset back to default
110 reset.exec()
111 rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
112 ksft_eq(set(rss.get("indir", [-1])), set(range(qcnt)))
113
114
115def test_rxfh_nl_set_indir_ctx(cfg):
116 """
117 Test setting indirection table for a custom context via Netlink.
118 """
119 _require_2qs(cfg)
120
121 # Get setting for ctx 0, we'll make sure they don't get clobbered
122 dflt = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
123
124 # Create context
125 ctx_id = _ethtool_create(cfg, "-X", "context new")
126 defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
127
128 cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
129 "context": ctx_id, "indir": [1]})
130 rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex},
131 "context": ctx_id})
132 ksft_eq(set(rss.get("indir", [-1])), {1})
133
134 ctx0 = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
135 ksft_eq(ctx0, dflt)
136
137 cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
138 "context": ctx_id, "indir": [0, 1]})
139 rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex},
140 "context": ctx_id})
141 ksft_eq(set(rss.get("indir", [-1])), {0, 1})
142
143 ctx0 = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
144 ksft_eq(ctx0, dflt)
145
146 # Make sure we can't set the queue count below max queue used
147 with ksft_raises(CmdExitFailure):
148 ethtool(f"-L {cfg.ifname} combined 0 rx 1")
149 with ksft_raises(CmdExitFailure):
150 ethtool(f"-L {cfg.ifname} combined 1 rx 0")
151
152
153def test_rxfh_indir_ntf(cfg):
154 """
155 Check that Netlink notifications are generated when RSS indirection
156 table was modified.
157 """
158 _require_2qs(cfg)
159
160 ethnl = EthtoolFamily()
161 ethnl.ntf_subscribe("monitor")
162
163 ethtool(f"--disable-netlink -X {cfg.ifname} weight 0 1")
164 reset = defer(ethtool, f"-X {cfg.ifname} default")
165
166 ntf = next(ethnl.poll_ntf(duration=0.2), None)
167 if ntf is None:
168 raise KsftFailEx("No notification received")
169 ksft_eq(ntf["name"], "rss-ntf")
170 ksft_eq(set(ntf["msg"]["indir"]), {1})
171
172 reset.exec()
173 ntf = next(ethnl.poll_ntf(duration=0.2), None)
174 if ntf is None:
175 raise KsftFailEx("No notification received after reset")
176 ksft_eq(ntf["name"], "rss-ntf")
177 ksft_is(ntf["msg"].get("context"), None)
178 ksft_ne(set(ntf["msg"]["indir"]), {1})
179
180
181def test_rxfh_indir_ctx_ntf(cfg):
182 """
183 Check that Netlink notifications are generated when RSS indirection
184 table was modified on an additional RSS context.
185 """
186 _require_2qs(cfg)
187
188 ctx_id = _ethtool_create(cfg, "-X", "context new")
189 defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
190
191 ethnl = EthtoolFamily()
192 ethnl.ntf_subscribe("monitor")
193
194 ethtool(f"--disable-netlink -X {cfg.ifname} context {ctx_id} weight 0 1")
195
196 ntf = next(ethnl.poll_ntf(duration=0.2), None)
197 if ntf is None:
198 raise KsftFailEx("No notification received")
199 ksft_eq(ntf["name"], "rss-ntf")
200 ksft_eq(ntf["msg"].get("context"), ctx_id)
201 ksft_eq(set(ntf["msg"]["indir"]), {1})
202
203
204def test_rxfh_nl_set_key(cfg):
205 """
206 Test setting hashing key via Netlink.
207 """
208
209 dflt = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
210 defer(cfg.ethnl.rss_set,
211 {"header": {"dev-index": cfg.ifindex},
212 "hkey": dflt["hkey"], "indir": None})
213
214 # Empty key should error out
215 with ksft_raises(NlError) as cm:
216 cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
217 "hkey": None})
218 ksft_eq(cm.exception.nl_msg.extack['bad-attr'], '.hkey')
219
220 # Set key to random
221 mod = random.randbytes(len(dflt["hkey"]))
222 cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
223 "hkey": mod})
224 rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
225 ksft_eq(rss.get("hkey", [-1]), mod)
226
227 # Set key to random and indir tbl to something at once
228 mod = random.randbytes(len(dflt["hkey"]))
229 cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
230 "indir": [0, 1], "hkey": mod})
231 rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
232 ksft_eq(rss.get("hkey", [-1]), mod)
233 ksft_eq(set(rss.get("indir", [-1])), {0, 1})
234
235
236def test_rxfh_fields(cfg):
237 """
238 Test reading Rx Flow Hash over Netlink.
239 """
240
241 flow_types = ["tcp4", "tcp6", "udp4", "udp6"]
242 ethnl = EthtoolFamily()
243
244 cfg_nl = ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
245 for fl_type in flow_types:
246 one = _ethtool_get_cfg(cfg, fl_type, to_nl=True)
247 ksft_eq(one, cfg_nl["flow-hash"][fl_type],
248 comment="Config for " + fl_type)
249
250
251def test_rxfh_fields_set(cfg):
252 """ Test configuring Rx Flow Hash over Netlink. """
253
254 flow_types = ["tcp4", "tcp6", "udp4", "udp6"]
255
256 # Collect current settings
257 cfg_old = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
258 # symmetric hashing is config-order-sensitive make sure we leave
259 # symmetric mode, or make the flow-hash sym-compatible first
260 changes = [{"flow-hash": cfg_old["flow-hash"],},
261 {"input-xfrm": cfg_old.get("input-xfrm", {}),}]
262 if cfg_old.get("input-xfrm"):
263 changes = list(reversed(changes))
264 for old in changes:
265 defer(cfg.ethnl.rss_set, {"header": {"dev-index": cfg.ifindex},} | old)
266
267 # symmetric hashing prevents some of the configs below
268 if cfg_old.get("input-xfrm"):
269 cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
270 "input-xfrm": {}})
271
272 for fl_type in flow_types:
273 cur = _ethtool_get_cfg(cfg, fl_type)
274 if cur == "sdfn":
275 change_nl = {"ip-src", "ip-dst"}
276 change_ic = "sd"
277 else:
278 change_nl = {"l4-b-0-1", "l4-b-2-3", "ip-src", "ip-dst"}
279 change_ic = "sdfn"
280
281 cfg.ethnl.rss_set({
282 "header": {"dev-index": cfg.ifindex},
283 "flow-hash": {fl_type: change_nl}
284 })
285 reset = defer(ethtool, f"--disable-netlink -N {cfg.ifname} "
286 f"rx-flow-hash {fl_type} {cur}")
287
288 cfg_nl = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
289 ksft_eq(change_nl, cfg_nl["flow-hash"][fl_type],
290 comment=f"Config for {fl_type} over Netlink")
291 cfg_ic = _ethtool_get_cfg(cfg, fl_type)
292 ksft_eq(change_ic, cfg_ic,
293 comment=f"Config for {fl_type} over IOCTL")
294
295 reset.exec()
296 cfg_nl = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
297 ksft_eq(cfg_old["flow-hash"][fl_type], cfg_nl["flow-hash"][fl_type],
298 comment=f"Un-config for {fl_type} over Netlink")
299 cfg_ic = _ethtool_get_cfg(cfg, fl_type)
300 ksft_eq(cur, cfg_ic, comment=f"Un-config for {fl_type} over IOCTL")
301
302 # Try to set multiple at once, the defer was already installed at the start
303 change = {"ip-src"}
304 if change == cfg_old["flow-hash"]["tcp4"]:
305 change = {"ip-dst"}
306 cfg.ethnl.rss_set({
307 "header": {"dev-index": cfg.ifindex},
308 "flow-hash": {x: change for x in flow_types}
309 })
310
311 cfg_nl = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
312 for fl_type in flow_types:
313 ksft_eq(change, cfg_nl["flow-hash"][fl_type],
314 comment=f"multi-config for {fl_type} over Netlink")
315
316
317def test_rxfh_fields_set_xfrm(cfg):
318 """ Test changing Rx Flow Hash vs xfrm_input at once. """
319
320 def set_rss(cfg, xfrm, fh):
321 cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
322 "input-xfrm": xfrm, "flow-hash": fh})
323
324 # Install the reset handler
325 cfg_old = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
326 # symmetric hashing is config-order-sensitive make sure we leave
327 # symmetric mode, or make the flow-hash sym-compatible first
328 changes = [{"flow-hash": cfg_old["flow-hash"],},
329 {"input-xfrm": cfg_old.get("input-xfrm", {}),}]
330 if cfg_old.get("input-xfrm"):
331 changes = list(reversed(changes))
332 for old in changes:
333 defer(cfg.ethnl.rss_set, {"header": {"dev-index": cfg.ifindex},} | old)
334
335 # Make sure we start with input-xfrm off, and tcp4 config non-sym
336 set_rss(cfg, {}, {})
337 set_rss(cfg, {}, {"tcp4": {"ip-src"}})
338
339 # Setting sym and fixing tcp4 config not expected to pass right now
340 with ksft_raises(NlError):
341 set_rss(cfg, {"sym-xor"}, {"tcp4": {"ip-src", "ip-dst"}})
342 # One at a time should work, hopefully
343 set_rss(cfg, 0, {"tcp4": {"ip-src", "ip-dst"}})
344 no_support = False
345 try:
346 set_rss(cfg, {"sym-xor"}, {})
347 except NlError:
348 try:
349 set_rss(cfg, {"sym-or-xor"}, {})
350 except NlError:
351 no_support = True
352 if no_support:
353 raise KsftSkipEx("no input-xfrm supported")
354 # Disabling two at once should not work either without kernel changes
355 with ksft_raises(NlError):
356 set_rss(cfg, {}, {"tcp4": {"ip-src"}})
357
358
359def test_rxfh_fields_ntf(cfg):
360 """ Test Rx Flow Hash notifications. """
361
362 cur = _ethtool_get_cfg(cfg, "tcp4")
363 if cur == "sdfn":
364 change = {"ip-src", "ip-dst"}
365 else:
366 change = {"l4-b-0-1", "l4-b-2-3", "ip-src", "ip-dst"}
367
368 ethnl = EthtoolFamily()
369 ethnl.ntf_subscribe("monitor")
370
371 ethnl.rss_set({
372 "header": {"dev-index": cfg.ifindex},
373 "flow-hash": {"tcp4": change}
374 })
375 reset = defer(ethtool,
376 f"--disable-netlink -N {cfg.ifname} rx-flow-hash tcp4 {cur}")
377
378 ntf = next(ethnl.poll_ntf(duration=0.2), None)
379 if ntf is None:
380 raise KsftFailEx("No notification received after IOCTL change")
381 ksft_eq(ntf["name"], "rss-ntf")
382 ksft_eq(ntf["msg"]["flow-hash"]["tcp4"], change)
383 ksft_eq(next(ethnl.poll_ntf(duration=0.01), None), None)
384
385 reset.exec()
386 ntf = next(ethnl.poll_ntf(duration=0.2), None)
387 if ntf is None:
388 raise KsftFailEx("No notification received after Netlink change")
389 ksft_eq(ntf["name"], "rss-ntf")
390 ksft_ne(ntf["msg"]["flow-hash"]["tcp4"], change)
391 ksft_eq(next(ethnl.poll_ntf(duration=0.01), None), None)
392
393
394def test_rss_ctx_add(cfg):
395 """ Test creating an additional RSS context via Netlink """
396
397 _require_2qs(cfg)
398
399 # Test basic creation
400 ctx = cfg.ethnl.rss_create_act({"header": {"dev-index": cfg.ifindex}})
401 d = defer(ethtool, f"-X {cfg.ifname} context {ctx.get('context')} delete")
402 ksft_ne(ctx.get("context", 0), 0)
403 ksft_ne(set(ctx.get("indir", [0])), {0},
404 comment="Driver should init the indirection table")
405
406 # Try requesting the ID we just got allocated
407 with ksft_raises(NlError) as cm:
408 ctx = cfg.ethnl.rss_create_act({
409 "header": {"dev-index": cfg.ifindex},
410 "context": ctx.get("context"),
411 })
412 ethtool(f"-X {cfg.ifname} context {ctx.get('context')} delete")
413 d.exec()
414 ksft_eq(cm.exception.nl_msg.error, -errno.EBUSY)
415
416 # Test creating with a specified RSS table, and context ID
417 ctx_id = ctx.get("context")
418 ctx = cfg.ethnl.rss_create_act({
419 "header": {"dev-index": cfg.ifindex},
420 "context": ctx_id,
421 "indir": [1],
422 })
423 ethtool(f"-X {cfg.ifname} context {ctx.get('context')} delete")
424 ksft_eq(ctx.get("context"), ctx_id)
425 ksft_eq(set(ctx.get("indir", [0])), {1})
426
427
428def test_rss_ctx_ntf(cfg):
429 """ Test notifications for creating additional RSS contexts """
430
431 ethnl = EthtoolFamily()
432 ethnl.ntf_subscribe("monitor")
433
434 # Create / delete via Netlink
435 ctx = cfg.ethnl.rss_create_act({"header": {"dev-index": cfg.ifindex}})
436 cfg.ethnl.rss_delete_act({
437 "header": {"dev-index": cfg.ifindex},
438 "context": ctx["context"],
439 })
440
441 ntf = next(ethnl.poll_ntf(duration=0.2), None)
442 if ntf is None:
443 raise KsftFailEx("[NL] No notification after context creation")
444 ksft_eq(ntf["name"], "rss-create-ntf")
445 ksft_eq(ctx, ntf["msg"])
446
447 ntf = next(ethnl.poll_ntf(duration=0.2), None)
448 if ntf is None:
449 raise KsftFailEx("[NL] No notification after context deletion")
450 ksft_eq(ntf["name"], "rss-delete-ntf")
451
452 # Create / deleve via IOCTL
453 ctx_id = _ethtool_create(cfg, "--disable-netlink -X", "context new")
454 ethtool(f"--disable-netlink -X {cfg.ifname} context {ctx_id} delete")
455 ntf = next(ethnl.poll_ntf(duration=0.2), None)
456 if ntf is None:
457 raise KsftFailEx("[IOCTL] No notification after context creation")
458 ksft_eq(ntf["name"], "rss-create-ntf")
459
460 ntf = next(ethnl.poll_ntf(duration=0.2), None)
461 if ntf is None:
462 raise KsftFailEx("[IOCTL] No notification after context deletion")
463 ksft_eq(ntf["name"], "rss-delete-ntf")
464
465
466def main() -> None:
467 """ Ksft boiler plate main """
468
469 with NetDrvEnv(__file__, nsim_test=False) as cfg:
470 cfg.ethnl = EthtoolFamily()
471 ksft_run(globs=globals(), case_pfx={"test_"}, args=(cfg, ))
472 ksft_exit()
473
474
475if __name__ == "__main__":
476 main()