| #!/usr/bin/env python3 | |
| # SPDX-License-Identifier: GPL-2.0 | |
| """ | |
| API level tests for RSS (mostly Netlink vs IOCTL). | |
| """ | |
| import errno | |
| import glob | |
| import random | |
| from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_is, ksft_ne, ksft_raises | |
| from lib.py import KsftSkipEx, KsftFailEx | |
| from lib.py import defer, ethtool, CmdExitFailure | |
| from lib.py import EthtoolFamily, NlError | |
| from lib.py import NetDrvEnv | |
| def _require_2qs(cfg): | |
| qcnt = len(glob.glob(f"/sys/class/net/{cfg.ifname}/queues/rx-*")) | |
| if qcnt < 2: | |
| raise KsftSkipEx(f"Local has only {qcnt} queues") | |
| return qcnt | |
| def _ethtool_create(cfg, act, opts): | |
| output = ethtool(f"{act} {cfg.ifname} {opts}").stdout | |
| # Output will be something like: "New RSS context is 1" or | |
| # "Added rule with ID 7", we want the integer from the end | |
| return int(output.split()[-1]) | |
| def _ethtool_get_cfg(cfg, fl_type, to_nl=False): | |
| descr = ethtool(f"-n {cfg.ifname} rx-flow-hash {fl_type}").stdout | |
| if to_nl: | |
| converter = { | |
| "IP SA": "ip-src", | |
| "IP DA": "ip-dst", | |
| "L4 bytes 0 & 1 [TCP/UDP src port]": "l4-b-0-1", | |
| "L4 bytes 2 & 3 [TCP/UDP dst port]": "l4-b-2-3", | |
| } | |
| ret = set() | |
| else: | |
| converter = { | |
| "IP SA": "s", | |
| "IP DA": "d", | |
| "L3 proto": "t", | |
| "L4 bytes 0 & 1 [TCP/UDP src port]": "f", | |
| "L4 bytes 2 & 3 [TCP/UDP dst port]": "n", | |
| } | |
| ret = "" | |
| for line in descr.split("\n")[1:-2]: | |
| # if this raises we probably need to add more keys to converter above | |
| if to_nl: | |
| ret.add(converter[line]) | |
| else: | |
| ret += converter[line] | |
| return ret | |
| def test_rxfh_nl_set_fail(cfg): | |
| """ | |
| Test error path of Netlink SET. | |
| """ | |
| _require_2qs(cfg) | |
| ethnl = EthtoolFamily() | |
| ethnl.ntf_subscribe("monitor") | |
| with ksft_raises(NlError): | |
| ethnl.rss_set({"header": {"dev-name": "lo"}, | |
| "indir": None}) | |
| with ksft_raises(NlError): | |
| ethnl.rss_set({"header": {"dev-index": cfg.ifindex}, | |
| "indir": [100000]}) | |
| ntf = next(ethnl.poll_ntf(duration=0.2), None) | |
| ksft_is(ntf, None) | |
| def test_rxfh_nl_set_indir(cfg): | |
| """ | |
| Test setting indirection table via Netlink. | |
| """ | |
| qcnt = _require_2qs(cfg) | |
| # Test some SETs with a value | |
| reset = defer(cfg.ethnl.rss_set, | |
| {"header": {"dev-index": cfg.ifindex}, "indir": None}) | |
| cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex}, | |
| "indir": [1]}) | |
| rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}}) | |
| ksft_eq(set(rss.get("indir", [-1])), {1}) | |
| cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex}, | |
| "indir": [0, 1]}) | |
| rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}}) | |
| ksft_eq(set(rss.get("indir", [-1])), {0, 1}) | |
| # Make sure we can't set the queue count below max queue used | |
| with ksft_raises(CmdExitFailure): | |
| ethtool(f"-L {cfg.ifname} combined 0 rx 1") | |
| with ksft_raises(CmdExitFailure): | |
| ethtool(f"-L {cfg.ifname} combined 1 rx 0") | |
| # Test reset back to default | |
| reset.exec() | |
| rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}}) | |
| ksft_eq(set(rss.get("indir", [-1])), set(range(qcnt))) | |
| def test_rxfh_nl_set_indir_ctx(cfg): | |
| """ | |
| Test setting indirection table for a custom context via Netlink. | |
| """ | |
| _require_2qs(cfg) | |
| # Get setting for ctx 0, we'll make sure they don't get clobbered | |
| dflt = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}}) | |
| # Create context | |
| ctx_id = _ethtool_create(cfg, "-X", "context new") | |
| defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete") | |
| cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex}, | |
| "context": ctx_id, "indir": [1]}) | |
| rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}, | |
| "context": ctx_id}) | |
| ksft_eq(set(rss.get("indir", [-1])), {1}) | |
| ctx0 = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}}) | |
| ksft_eq(ctx0, dflt) | |
| cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex}, | |
| "context": ctx_id, "indir": [0, 1]}) | |
| rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}, | |
| "context": ctx_id}) | |
| ksft_eq(set(rss.get("indir", [-1])), {0, 1}) | |
| ctx0 = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}}) | |
| ksft_eq(ctx0, dflt) | |
| # Make sure we can't set the queue count below max queue used | |
| with ksft_raises(CmdExitFailure): | |
| ethtool(f"-L {cfg.ifname} combined 0 rx 1") | |
| with ksft_raises(CmdExitFailure): | |
| ethtool(f"-L {cfg.ifname} combined 1 rx 0") | |
| def test_rxfh_indir_ntf(cfg): | |
| """ | |
| Check that Netlink notifications are generated when RSS indirection | |
| table was modified. | |
| """ | |
| _require_2qs(cfg) | |
| ethnl = EthtoolFamily() | |
| ethnl.ntf_subscribe("monitor") | |
| ethtool(f"--disable-netlink -X {cfg.ifname} weight 0 1") | |
| reset = defer(ethtool, f"-X {cfg.ifname} default") | |
| ntf = next(ethnl.poll_ntf(duration=0.2), None) | |
| if ntf is None: | |
| raise KsftFailEx("No notification received") | |
| ksft_eq(ntf["name"], "rss-ntf") | |
| ksft_eq(set(ntf["msg"]["indir"]), {1}) | |
| reset.exec() | |
| ntf = next(ethnl.poll_ntf(duration=0.2), None) | |
| if ntf is None: | |
| raise KsftFailEx("No notification received after reset") | |
| ksft_eq(ntf["name"], "rss-ntf") | |
| ksft_is(ntf["msg"].get("context"), None) | |
| ksft_ne(set(ntf["msg"]["indir"]), {1}) | |
| def test_rxfh_indir_ctx_ntf(cfg): | |
| """ | |
| Check that Netlink notifications are generated when RSS indirection | |
| table was modified on an additional RSS context. | |
| """ | |
| _require_2qs(cfg) | |
| ctx_id = _ethtool_create(cfg, "-X", "context new") | |
| defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete") | |
| ethnl = EthtoolFamily() | |
| ethnl.ntf_subscribe("monitor") | |
| ethtool(f"--disable-netlink -X {cfg.ifname} context {ctx_id} weight 0 1") | |
| ntf = next(ethnl.poll_ntf(duration=0.2), None) | |
| if ntf is None: | |
| raise KsftFailEx("No notification received") | |
| ksft_eq(ntf["name"], "rss-ntf") | |
| ksft_eq(ntf["msg"].get("context"), ctx_id) | |
| ksft_eq(set(ntf["msg"]["indir"]), {1}) | |
| def test_rxfh_nl_set_key(cfg): | |
| """ | |
| Test setting hashing key via Netlink. | |
| """ | |
| dflt = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}}) | |
| defer(cfg.ethnl.rss_set, | |
| {"header": {"dev-index": cfg.ifindex}, | |
| "hkey": dflt["hkey"], "indir": None}) | |
| # Empty key should error out | |
| with ksft_raises(NlError) as cm: | |
| cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex}, | |
| "hkey": None}) | |
| ksft_eq(cm.exception.nl_msg.extack['bad-attr'], '.hkey') | |
| # Set key to random | |
| mod = random.randbytes(len(dflt["hkey"])) | |
| cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex}, | |
| "hkey": mod}) | |
| rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}}) | |
| ksft_eq(rss.get("hkey", [-1]), mod) | |
| # Set key to random and indir tbl to something at once | |
| mod = random.randbytes(len(dflt["hkey"])) | |
| cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex}, | |
| "indir": [0, 1], "hkey": mod}) | |
| rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}}) | |
| ksft_eq(rss.get("hkey", [-1]), mod) | |
| ksft_eq(set(rss.get("indir", [-1])), {0, 1}) | |
| def test_rxfh_fields(cfg): | |
| """ | |
| Test reading Rx Flow Hash over Netlink. | |
| """ | |
| flow_types = ["tcp4", "tcp6", "udp4", "udp6"] | |
| ethnl = EthtoolFamily() | |
| cfg_nl = ethnl.rss_get({"header": {"dev-index": cfg.ifindex}}) | |
| for fl_type in flow_types: | |
| one = _ethtool_get_cfg(cfg, fl_type, to_nl=True) | |
| ksft_eq(one, cfg_nl["flow-hash"][fl_type], | |
| comment="Config for " + fl_type) | |
| def test_rxfh_fields_set(cfg): | |
| """ Test configuring Rx Flow Hash over Netlink. """ | |
| flow_types = ["tcp4", "tcp6", "udp4", "udp6"] | |
| # Collect current settings | |
| cfg_old = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}}) | |
| # symmetric hashing is config-order-sensitive make sure we leave | |
| # symmetric mode, or make the flow-hash sym-compatible first | |
| changes = [{"flow-hash": cfg_old["flow-hash"],}, | |
| {"input-xfrm": cfg_old.get("input-xfrm", {}),}] | |
| if cfg_old.get("input-xfrm"): | |
| changes = list(reversed(changes)) | |
| for old in changes: | |
| defer(cfg.ethnl.rss_set, {"header": {"dev-index": cfg.ifindex},} | old) | |
| # symmetric hashing prevents some of the configs below | |
| if cfg_old.get("input-xfrm"): | |
| cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex}, | |
| "input-xfrm": {}}) | |
| for fl_type in flow_types: | |
| cur = _ethtool_get_cfg(cfg, fl_type) | |
| if cur == "sdfn": | |
| change_nl = {"ip-src", "ip-dst"} | |
| change_ic = "sd" | |
| else: | |
| change_nl = {"l4-b-0-1", "l4-b-2-3", "ip-src", "ip-dst"} | |
| change_ic = "sdfn" | |
| cfg.ethnl.rss_set({ | |
| "header": {"dev-index": cfg.ifindex}, | |
| "flow-hash": {fl_type: change_nl} | |
| }) | |
| reset = defer(ethtool, f"--disable-netlink -N {cfg.ifname} " | |
| f"rx-flow-hash {fl_type} {cur}") | |
| cfg_nl = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}}) | |
| ksft_eq(change_nl, cfg_nl["flow-hash"][fl_type], | |
| comment=f"Config for {fl_type} over Netlink") | |
| cfg_ic = _ethtool_get_cfg(cfg, fl_type) | |
| ksft_eq(change_ic, cfg_ic, | |
| comment=f"Config for {fl_type} over IOCTL") | |
| reset.exec() | |
| cfg_nl = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}}) | |
| ksft_eq(cfg_old["flow-hash"][fl_type], cfg_nl["flow-hash"][fl_type], | |
| comment=f"Un-config for {fl_type} over Netlink") | |
| cfg_ic = _ethtool_get_cfg(cfg, fl_type) | |
| ksft_eq(cur, cfg_ic, comment=f"Un-config for {fl_type} over IOCTL") | |
| # Try to set multiple at once, the defer was already installed at the start | |
| change = {"ip-src"} | |
| if change == cfg_old["flow-hash"]["tcp4"]: | |
| change = {"ip-dst"} | |
| cfg.ethnl.rss_set({ | |
| "header": {"dev-index": cfg.ifindex}, | |
| "flow-hash": {x: change for x in flow_types} | |
| }) | |
| cfg_nl = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}}) | |
| for fl_type in flow_types: | |
| ksft_eq(change, cfg_nl["flow-hash"][fl_type], | |
| comment=f"multi-config for {fl_type} over Netlink") | |
| def test_rxfh_fields_set_xfrm(cfg): | |
| """ Test changing Rx Flow Hash vs xfrm_input at once. """ | |
| def set_rss(cfg, xfrm, fh): | |
| cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex}, | |
| "input-xfrm": xfrm, "flow-hash": fh}) | |
| # Install the reset handler | |
| cfg_old = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}}) | |
| # symmetric hashing is config-order-sensitive make sure we leave | |
| # symmetric mode, or make the flow-hash sym-compatible first | |
| changes = [{"flow-hash": cfg_old["flow-hash"],}, | |
| {"input-xfrm": cfg_old.get("input-xfrm", {}),}] | |
| if cfg_old.get("input-xfrm"): | |
| changes = list(reversed(changes)) | |
| for old in changes: | |
| defer(cfg.ethnl.rss_set, {"header": {"dev-index": cfg.ifindex},} | old) | |
| # Make sure we start with input-xfrm off, and tcp4 config non-sym | |
| set_rss(cfg, {}, {}) | |
| set_rss(cfg, {}, {"tcp4": {"ip-src"}}) | |
| # Setting sym and fixing tcp4 config not expected to pass right now | |
| with ksft_raises(NlError): | |
| set_rss(cfg, {"sym-xor"}, {"tcp4": {"ip-src", "ip-dst"}}) | |
| # One at a time should work, hopefully | |
| set_rss(cfg, 0, {"tcp4": {"ip-src", "ip-dst"}}) | |
| no_support = False | |
| try: | |
| set_rss(cfg, {"sym-xor"}, {}) | |
| except NlError: | |
| try: | |
| set_rss(cfg, {"sym-or-xor"}, {}) | |
| except NlError: | |
| no_support = True | |
| if no_support: | |
| raise KsftSkipEx("no input-xfrm supported") | |
| # Disabling two at once should not work either without kernel changes | |
| with ksft_raises(NlError): | |
| set_rss(cfg, {}, {"tcp4": {"ip-src"}}) | |
| def test_rxfh_fields_ntf(cfg): | |
| """ Test Rx Flow Hash notifications. """ | |
| cur = _ethtool_get_cfg(cfg, "tcp4") | |
| if cur == "sdfn": | |
| change = {"ip-src", "ip-dst"} | |
| else: | |
| change = {"l4-b-0-1", "l4-b-2-3", "ip-src", "ip-dst"} | |
| ethnl = EthtoolFamily() | |
| ethnl.ntf_subscribe("monitor") | |
| ethnl.rss_set({ | |
| "header": {"dev-index": cfg.ifindex}, | |
| "flow-hash": {"tcp4": change} | |
| }) | |
| reset = defer(ethtool, | |
| f"--disable-netlink -N {cfg.ifname} rx-flow-hash tcp4 {cur}") | |
| ntf = next(ethnl.poll_ntf(duration=0.2), None) | |
| if ntf is None: | |
| raise KsftFailEx("No notification received after IOCTL change") | |
| ksft_eq(ntf["name"], "rss-ntf") | |
| ksft_eq(ntf["msg"]["flow-hash"]["tcp4"], change) | |
| ksft_eq(next(ethnl.poll_ntf(duration=0.01), None), None) | |
| reset.exec() | |
| ntf = next(ethnl.poll_ntf(duration=0.2), None) | |
| if ntf is None: | |
| raise KsftFailEx("No notification received after Netlink change") | |
| ksft_eq(ntf["name"], "rss-ntf") | |
| ksft_ne(ntf["msg"]["flow-hash"]["tcp4"], change) | |
| ksft_eq(next(ethnl.poll_ntf(duration=0.01), None), None) | |
| def test_rss_ctx_add(cfg): | |
| """ Test creating an additional RSS context via Netlink """ | |
| _require_2qs(cfg) | |
| # Test basic creation | |
| ctx = cfg.ethnl.rss_create_act({"header": {"dev-index": cfg.ifindex}}) | |
| d = defer(ethtool, f"-X {cfg.ifname} context {ctx.get('context')} delete") | |
| ksft_ne(ctx.get("context", 0), 0) | |
| ksft_ne(set(ctx.get("indir", [0])), {0}, | |
| comment="Driver should init the indirection table") | |
| # Try requesting the ID we just got allocated | |
| with ksft_raises(NlError) as cm: | |
| ctx = cfg.ethnl.rss_create_act({ | |
| "header": {"dev-index": cfg.ifindex}, | |
| "context": ctx.get("context"), | |
| }) | |
| ethtool(f"-X {cfg.ifname} context {ctx.get('context')} delete") | |
| d.exec() | |
| ksft_eq(cm.exception.nl_msg.error, -errno.EBUSY) | |
| # Test creating with a specified RSS table, and context ID | |
| ctx_id = ctx.get("context") | |
| ctx = cfg.ethnl.rss_create_act({ | |
| "header": {"dev-index": cfg.ifindex}, | |
| "context": ctx_id, | |
| "indir": [1], | |
| }) | |
| ethtool(f"-X {cfg.ifname} context {ctx.get('context')} delete") | |
| ksft_eq(ctx.get("context"), ctx_id) | |
| ksft_eq(set(ctx.get("indir", [0])), {1}) | |
| def test_rss_ctx_ntf(cfg): | |
| """ Test notifications for creating additional RSS contexts """ | |
| ethnl = EthtoolFamily() | |
| ethnl.ntf_subscribe("monitor") | |
| # Create / delete via Netlink | |
| ctx = cfg.ethnl.rss_create_act({"header": {"dev-index": cfg.ifindex}}) | |
| cfg.ethnl.rss_delete_act({ | |
| "header": {"dev-index": cfg.ifindex}, | |
| "context": ctx["context"], | |
| }) | |
| ntf = next(ethnl.poll_ntf(duration=0.2), None) | |
| if ntf is None: | |
| raise KsftFailEx("[NL] No notification after context creation") | |
| ksft_eq(ntf["name"], "rss-create-ntf") | |
| ksft_eq(ctx, ntf["msg"]) | |
| ntf = next(ethnl.poll_ntf(duration=0.2), None) | |
| if ntf is None: | |
| raise KsftFailEx("[NL] No notification after context deletion") | |
| ksft_eq(ntf["name"], "rss-delete-ntf") | |
| # Create / deleve via IOCTL | |
| ctx_id = _ethtool_create(cfg, "--disable-netlink -X", "context new") | |
| ethtool(f"--disable-netlink -X {cfg.ifname} context {ctx_id} delete") | |
| ntf = next(ethnl.poll_ntf(duration=0.2), None) | |
| if ntf is None: | |
| raise KsftFailEx("[IOCTL] No notification after context creation") | |
| ksft_eq(ntf["name"], "rss-create-ntf") | |
| ntf = next(ethnl.poll_ntf(duration=0.2), None) | |
| if ntf is None: | |
| raise KsftFailEx("[IOCTL] No notification after context deletion") | |
| ksft_eq(ntf["name"], "rss-delete-ntf") | |
| def main() -> None: | |
| """ Ksft boiler plate main """ | |
| with NetDrvEnv(__file__, nsim_test=False) as cfg: | |
| cfg.ethnl = EthtoolFamily() | |
| ksft_run(globs=globals(), case_pfx={"test_"}, args=(cfg, )) | |
| ksft_exit() | |
| if __name__ == "__main__": | |
| main() |