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() 477