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