1#!/usr/bin/env python3 2# SPDX-License-Identifier: GPL-2.0 3 4""" 5GRO (Generic Receive Offload) conformance tests. 6 7Validates that GRO coalescing works correctly by running the gro 8binary in different configurations and checking for correct packet 9coalescing behavior. 10 11Test cases: 12 - data: Data packets with same size/headers and correct seq numbers coalesce 13 - ack: Pure ACK packets do not coalesce 14 - flags: Packets with PSH, SYN, URG, RST flags do not coalesce 15 - tcp: Packets with incorrect checksum, non-consecutive seqno don't coalesce 16 - ip: Packets with different ECN, TTL, TOS, or IP options don't coalesce 17 - large: Packets larger than GRO_MAX_SIZE don't coalesce 18""" 19 20import os 21from lib.py import ksft_run, ksft_exit, ksft_pr 22from lib.py import NetDrvEpEnv, KsftXfailEx 23from lib.py import bkg, cmd, defer, ethtool, ip 24from lib.py import ksft_variants 25 26 27def _resolve_dmac(cfg, ipver): 28 """ 29 Find the destination MAC address remote host should use to send packets 30 towards the local host. It may be a router / gateway address. 31 """ 32 33 attr = "dmac" + ipver 34 # Cache the response across test cases 35 if hasattr(cfg, attr): 36 return getattr(cfg, attr) 37 38 route = ip(f"-{ipver} route get {cfg.addr_v[ipver]}", 39 json=True, host=cfg.remote)[0] 40 gw = route.get("gateway") 41 # Local L2 segment, address directly 42 if not gw: 43 setattr(cfg, attr, cfg.dev['address']) 44 return getattr(cfg, attr) 45 46 # ping to make sure neighbor is resolved, 47 # bind to an interface, for v6 the GW is likely link local 48 cmd(f"ping -c1 -W0 -I{cfg.remote_ifname} {gw}", host=cfg.remote) 49 50 neigh = ip(f"neigh get {gw} dev {cfg.remote_ifname}", 51 json=True, host=cfg.remote)[0] 52 setattr(cfg, attr, neigh['lladdr']) 53 return getattr(cfg, attr) 54 55 56def _write_defer_restore(cfg, path, val, defer_undo=False): 57 with open(path, "r", encoding="utf-8") as fp: 58 orig_val = fp.read().strip() 59 if str(val) == orig_val: 60 return 61 with open(path, "w", encoding="utf-8") as fp: 62 fp.write(val) 63 if defer_undo: 64 defer(_write_defer_restore, cfg, path, orig_val) 65 66 67def _set_mtu_restore(dev, mtu, host): 68 if dev['mtu'] < mtu: 69 ip(f"link set dev {dev['ifname']} mtu {mtu}", host=host) 70 defer(ip, f"link set dev {dev['ifname']} mtu {dev['mtu']}", host=host) 71 72 73def _set_ethtool_feat(dev, current, feats, host=None): 74 s2n = {True: "on", False: "off"} 75 76 new = ["-K", dev] 77 old = ["-K", dev] 78 no_change = True 79 for name, state in feats.items(): 80 new += [name, s2n[state]] 81 old += [name, s2n[current[name]["active"]]] 82 83 if current[name]["active"] != state: 84 no_change = False 85 if current[name]["fixed"]: 86 raise KsftXfailEx(f"Device does not support {name}") 87 if no_change: 88 return 89 90 eth_cmd = ethtool(" ".join(new), host=host) 91 defer(ethtool, " ".join(old), host=host) 92 93 # If ethtool printed something kernel must have modified some features 94 if eth_cmd.stdout: 95 ksft_pr(eth_cmd) 96 97 98def _setup(cfg, mode, test_name): 99 """ Setup hardware loopback mode for GRO testing. """ 100 101 if not hasattr(cfg, "bin_remote"): 102 cfg.bin_local = cfg.test_dir / "gro" 103 cfg.bin_remote = cfg.remote.deploy(cfg.bin_local) 104 105 if not hasattr(cfg, "feat"): 106 cfg.feat = ethtool(f"-k {cfg.ifname}", json=True)[0] 107 cfg.remote_feat = ethtool(f"-k {cfg.remote_ifname}", 108 host=cfg.remote, json=True)[0] 109 110 # "large" test needs at least 4k MTU 111 if test_name == "large": 112 _set_mtu_restore(cfg.dev, 4096, None) 113 _set_mtu_restore(cfg.remote_dev, 4096, cfg.remote) 114 115 if mode == "sw": 116 flush_path = f"/sys/class/net/{cfg.ifname}/gro_flush_timeout" 117 irq_path = f"/sys/class/net/{cfg.ifname}/napi_defer_hard_irqs" 118 119 _write_defer_restore(cfg, flush_path, "200000", defer_undo=True) 120 _write_defer_restore(cfg, irq_path, "10", defer_undo=True) 121 122 _set_ethtool_feat(cfg.ifname, cfg.feat, 123 {"generic-receive-offload": True, 124 "rx-gro-hw": False, 125 "large-receive-offload": False}) 126 elif mode == "hw": 127 _set_ethtool_feat(cfg.ifname, cfg.feat, 128 {"generic-receive-offload": False, 129 "rx-gro-hw": True, 130 "large-receive-offload": False}) 131 132 # Some NICs treat HW GRO as a GRO sub-feature so disabling GRO 133 # will also clear HW GRO. Use a hack of installing XDP generic 134 # to skip SW GRO, even when enabled. 135 feat = ethtool(f"-k {cfg.ifname}", json=True)[0] 136 if not feat["rx-gro-hw"]["active"]: 137 ksft_pr("Driver clears HW GRO and SW GRO is cleared, using generic XDP workaround") 138 prog = cfg.net_lib_dir / "xdp_dummy.bpf.o" 139 ip(f"link set dev {cfg.ifname} xdpgeneric obj {prog} sec xdp") 140 defer(ip, f"link set dev {cfg.ifname} xdpgeneric off") 141 142 # Attaching XDP may change features, fetch the latest state 143 feat = ethtool(f"-k {cfg.ifname}", json=True)[0] 144 145 _set_ethtool_feat(cfg.ifname, feat, 146 {"generic-receive-offload": True, 147 "rx-gro-hw": True, 148 "large-receive-offload": False}) 149 elif mode == "lro": 150 # netdevsim advertises LRO for feature inheritance testing with 151 # bonding/team tests but it doesn't actually perform the offload 152 cfg.require_nsim(nsim_test=False) 153 154 _set_ethtool_feat(cfg.ifname, cfg.feat, 155 {"generic-receive-offload": False, 156 "rx-gro-hw": False, 157 "large-receive-offload": True}) 158 159 try: 160 # Disable TSO for local tests 161 cfg.require_nsim() # will raise KsftXfailEx if not running on nsim 162 163 _set_ethtool_feat(cfg.remote_ifname, cfg.remote_feat, 164 {"tcp-segmentation-offload": False}, 165 host=cfg.remote) 166 except KsftXfailEx: 167 pass 168 169 170def _gro_variants(): 171 """Generator that yields all combinations of protocol and test types.""" 172 173 for mode in ["sw", "hw", "lro"]: 174 for protocol in ["ipv4", "ipv6", "ipip"]: 175 for test_name in ["data", "ack", "flags", "tcp", "ip", "large"]: 176 yield mode, protocol, test_name 177 178 179@ksft_variants(_gro_variants()) 180def test(cfg, mode, protocol, test_name): 181 """Run a single GRO test with retries.""" 182 183 ipver = "6" if protocol[-1] == "6" else "4" 184 cfg.require_ipver(ipver) 185 186 _setup(cfg, mode, test_name) 187 188 base_cmd_args = [ 189 f"--{protocol}", 190 f"--dmac {_resolve_dmac(cfg, ipver)}", 191 f"--smac {cfg.remote_dev['address']}", 192 f"--daddr {cfg.addr_v[ipver]}", 193 f"--saddr {cfg.remote_addr_v[ipver]}", 194 f"--test {test_name}", 195 "--verbose" 196 ] 197 base_args = " ".join(base_cmd_args) 198 199 # Each test is run 6 times to deflake, because given the receive timing, 200 # not all packets that should coalesce will be considered in the same flow 201 # on every try. 202 max_retries = 6 203 for attempt in range(max_retries): 204 rx_cmd = f"{cfg.bin_local} {base_args} --rx --iface {cfg.ifname}" 205 tx_cmd = f"{cfg.bin_remote} {base_args} --iface {cfg.remote_ifname}" 206 207 fail_now = attempt >= max_retries - 1 208 209 with bkg(rx_cmd, ksft_ready=True, exit_wait=True, 210 fail=fail_now) as rx_proc: 211 cmd(tx_cmd, host=cfg.remote) 212 213 if rx_proc.ret == 0: 214 return 215 216 ksft_pr(rx_proc) 217 218 if test_name == "large" and os.environ.get("KSFT_MACHINE_SLOW"): 219 ksft_pr(f"Ignoring {protocol}/{test_name} failure due to slow environment") 220 return 221 222 ksft_pr(f"Attempt {attempt + 1}/{max_retries} failed, retrying...") 223 224 225def main() -> None: 226 """ Ksft boiler plate main """ 227 228 with NetDrvEpEnv(__file__) as cfg: 229 ksft_run(cases=[test], args=(cfg,)) 230 ksft_exit() 231 232 233if __name__ == "__main__": 234 main() 235