1#!/usr/bin/env python3 2# SPDX-License-Identifier: GPL-2.0 3"""Test ethtool NFC (ntuple) flow steering rules.""" 4 5import random 6from enum import Enum, auto 7from lib.py import ksft_run, ksft_exit 8from lib.py import ksft_eq, ksft_ge 9from lib.py import ksft_variants, KsftNamedVariant 10from lib.py import EthtoolFamily, NetDrvEpEnv, NetdevFamily 11from lib.py import KsftSkipEx 12from lib.py import cmd, ethtool, defer, rand_ports, bkg, wait_port_listen 13 14 15class NtupleField(Enum): 16 SRC_IP = auto() 17 DST_IP = auto() 18 SRC_PORT = auto() 19 DST_PORT = auto() 20 21 22def _require_ntuple(cfg): 23 features = ethtool(f"-k {cfg.ifname}", json=True)[0] 24 if not features["ntuple-filters"]["active"]: 25 if features["ntuple-filters"]["fixed"]: 26 raise KsftSkipEx("Device does not support ntuple-filters") 27 ethtool(f"-K {cfg.ifname} ntuple-filters on") 28 defer(ethtool, f"-K {cfg.ifname} ntuple-filters off") 29 30 31def _get_rx_cnts(cfg, prev=None): 32 """Get Rx packet counts for all queues, as a simple list of integers 33 if @prev is specified the prev counts will be subtracted""" 34 cfg.wait_hw_stats_settle() 35 data = cfg.netdevnl.qstats_get({"ifindex": cfg.ifindex, "scope": ["queue"]}, dump=True) 36 data = [x for x in data if x['queue-type'] == "rx"] 37 max_q = max([x["queue-id"] for x in data]) 38 queue_stats = [0] * (max_q + 1) 39 for q in data: 40 queue_stats[q["queue-id"]] = q["rx-packets"] 41 if prev and q["queue-id"] < len(prev): 42 queue_stats[q["queue-id"]] -= prev[q["queue-id"]] 43 return queue_stats 44 45 46def _ntuple_rule_add(cfg, flow_spec): 47 """Install an NFC rule via ethtool.""" 48 49 output = ethtool(f"-N {cfg.ifname} {flow_spec}").stdout 50 rule_id = int(output.split()[-1]) 51 defer(ethtool, f"-N {cfg.ifname} delete {rule_id}") 52 53 54def _setup_isolated_queue(cfg): 55 """Default all traffic to queue 0, and pick a random queue to 56 steer NFC traffic to.""" 57 58 channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}}) 59 ch_max = channels['combined-max'] 60 qcnt = channels['combined-count'] 61 62 if ch_max < 2: 63 raise KsftSkipEx(f"Need at least 2 combined channels, max is {ch_max}") 64 65 desired_queues = min(ch_max, 4) 66 if qcnt >= desired_queues: 67 desired_queues = qcnt 68 else: 69 ethtool(f"-L {cfg.ifname} combined {desired_queues}") 70 defer(ethtool, f"-L {cfg.ifname} combined {qcnt}") 71 72 ethtool(f"-X {cfg.ifname} equal 1") 73 defer(ethtool, f"-X {cfg.ifname} default") 74 75 return random.randint(1, desired_queues - 1) 76 77 78def _send_traffic(cfg, ipver, proto, dst_port, src_port, pkt_cnt=40): 79 """Generate traffic with the desired flow signature.""" 80 81 cfg.require_cmd("socat", remote=True) 82 83 socat_proto = proto.upper() 84 dst_addr = f"[{cfg.addr_v['6']}]" if ipver == '6' else cfg.addr_v['4'] 85 86 extra_opts = ",nodelay" if proto == "tcp" else ",shut-null" 87 88 listen_cmd = (f"socat -{ipver} -t 2 -u " 89 f"{socat_proto}-LISTEN:{dst_port},reuseport /dev/null") 90 with bkg(listen_cmd, exit_wait=True): 91 wait_port_listen(dst_port, proto=proto) 92 send_cmd = f""" 93 bash -c 'for i in $(seq {pkt_cnt}); do echo msg; sleep 0.02; done' | 94 socat -{ipver} -u - \ 95 {socat_proto}:{dst_addr}:{dst_port},sourceport={src_port},reuseaddr{extra_opts} 96 """ 97 cmd(send_cmd, shell=True, host=cfg.remote) 98 99 100def _add_ntuple_rule_and_send_traffic(cfg, ipver, proto, fields, test_queue): 101 ports = rand_ports(2) 102 src_port = ports[0] 103 dst_port = ports[1] 104 flow_parts = [f"flow-type {proto}{ipver}"] 105 106 for field in fields: 107 if field == NtupleField.SRC_IP: 108 flow_parts.append(f"src-ip {cfg.remote_addr_v[ipver]}") 109 elif field == NtupleField.DST_IP: 110 flow_parts.append(f"dst-ip {cfg.addr_v[ipver]}") 111 elif field == NtupleField.SRC_PORT: 112 flow_parts.append(f"src-port {src_port}") 113 elif field == NtupleField.DST_PORT: 114 flow_parts.append(f"dst-port {dst_port}") 115 116 flow_parts.append(f"action {test_queue}") 117 _ntuple_rule_add(cfg, " ".join(flow_parts)) 118 _send_traffic(cfg, ipver, proto, dst_port=dst_port, src_port=src_port) 119 120 121def _ntuple_variants(): 122 for ipver in ["4", "6"]: 123 for proto in ["tcp", "udp"]: 124 for fields in [[NtupleField.SRC_IP], 125 [NtupleField.DST_IP], 126 [NtupleField.SRC_PORT], 127 [NtupleField.DST_PORT], 128 [NtupleField.SRC_IP, NtupleField.DST_IP], 129 [NtupleField.SRC_IP, NtupleField.DST_IP, 130 NtupleField.SRC_PORT, NtupleField.DST_PORT]]: 131 name = ".".join(f.name.lower() for f in fields) 132 yield KsftNamedVariant(f"{proto}{ipver}.{name}", 133 ipver, proto, fields) 134 135 136@ksft_variants(_ntuple_variants()) 137def queue(cfg, ipver, proto, fields): 138 """Test that an NFC rule steers traffic to the correct queue.""" 139 140 cfg.require_ipver(ipver) 141 _require_ntuple(cfg) 142 143 test_queue = _setup_isolated_queue(cfg) 144 145 cnts = _get_rx_cnts(cfg) 146 _add_ntuple_rule_and_send_traffic(cfg, ipver, proto, fields, test_queue) 147 cnts = _get_rx_cnts(cfg, prev=cnts) 148 149 ksft_ge(cnts[test_queue], 40, f"Traffic on test queue {test_queue}: {cnts}") 150 sum_idle = sum(cnts) - cnts[0] - cnts[test_queue] 151 ksft_eq(sum_idle, 0, f"Traffic on idle queues: {cnts}") 152 153 154def main() -> None: 155 """Ksft boilerplate main.""" 156 157 with NetDrvEpEnv(__file__, nsim_test=False) as cfg: 158 cfg.ethnl = EthtoolFamily() 159 cfg.netdevnl = NetdevFamily() 160 ksft_run([queue], args=(cfg,)) 161 ksft_exit() 162 163 164if __name__ == "__main__": 165 main() 166