1# SPDX-License-Identifier: GPL-2.0 2 3import ipaddress 4import os 5import sys 6import time 7import json 8from pathlib import Path 9from lib.py import KsftSkipEx, KsftXfailEx 10from lib.py import ksft_setup, wait_file 11from lib.py import cmd, ethtool, ip, CmdExitFailure 12from lib.py import NetNS, NetdevSimDev, UserNetNS 13from .remote import Remote 14from . import bpftool, RtnlFamily, Netlink 15 16 17class NetDrvEnvBase: 18 """ 19 Base class for a NIC / host environments 20 21 Attributes: 22 test_dir: Path to the source directory of the test 23 net_lib_dir: Path to the net/lib directory 24 """ 25 def __init__(self, src_path): 26 self.src_path = Path(src_path) 27 self.test_dir = self.src_path.parent.resolve() 28 self.net_lib_dir = (Path(__file__).parent / "../../../../net/lib").resolve() 29 30 self.env = self._load_env_file() 31 32 # Following attrs must be set be inheriting classes 33 self.dev = None 34 35 def _load_env_file(self): 36 env = os.environ.copy() 37 38 src_dir = Path(self.src_path).parent.resolve() 39 if not (src_dir / "net.config").exists(): 40 return ksft_setup(env) 41 42 with open((src_dir / "net.config").as_posix(), 'r') as fp: 43 for line in fp.readlines(): 44 full_file = line 45 # Strip comments 46 pos = line.find("#") 47 if pos >= 0: 48 line = line[:pos] 49 line = line.strip() 50 if not line: 51 continue 52 pair = line.split('=', maxsplit=1) 53 if len(pair) != 2: 54 raise Exception("Can't parse configuration line:", full_file) 55 env[pair[0]] = pair[1] 56 return ksft_setup(env) 57 58 def __del__(self): 59 pass 60 61 def __enter__(self): 62 ip(f"link set dev {self.dev['ifname']} up") 63 wait_file(f"/sys/class/net/{self.dev['ifname']}/carrier", 64 lambda x: x.strip() == "1") 65 66 return self 67 68 def __exit__(self, ex_type, ex_value, ex_tb): 69 """ 70 __exit__ gets called at the end of a "with" block. 71 """ 72 self.__del__() 73 74 75class NetDrvEnv(NetDrvEnvBase): 76 """ 77 Class for a single NIC / host env, with no remote end 78 """ 79 def __init__(self, src_path, nsim_test=None, **kwargs): 80 super().__init__(src_path) 81 82 self._ns = None 83 84 if 'NETIF' in self.env: 85 if nsim_test is True: 86 raise KsftXfailEx("Test only works on netdevsim") 87 88 self.dev = ip("-d link show dev " + self.env['NETIF'], json=True)[0] 89 else: 90 if nsim_test is False: 91 raise KsftXfailEx("Test does not work on netdevsim") 92 93 self._ns = NetdevSimDev(**kwargs) 94 self.dev = self._ns.nsims[0].dev 95 self.ifname = self.dev['ifname'] 96 self.ifindex = self.dev['ifindex'] 97 98 def __del__(self): 99 if self._ns: 100 self._ns.remove() 101 self._ns = None 102 103 104class NetDrvEpEnv(NetDrvEnvBase): 105 """ 106 Class for an environment with a local device and "remote endpoint" 107 which can be used to send traffic in. 108 109 For local testing it creates two network namespaces and a pair 110 of netdevsim devices. 111 """ 112 113 # Network prefixes used for local tests 114 nsim_v4_pfx = "192.0.2." 115 nsim_v6_pfx = "2001:db8::" 116 117 def __init__(self, src_path, nsim_test=None): 118 super().__init__(src_path) 119 120 self._stats_settle_time = None 121 122 # Things we try to destroy 123 self.remote = None 124 # These are for local testing state 125 self._netns = None 126 self._ns = None 127 self._ns_peer = None 128 129 self.addr_v = { "4": None, "6": None } 130 self.remote_addr_v = { "4": None, "6": None } 131 132 if "NETIF" in self.env: 133 if nsim_test is True: 134 raise KsftXfailEx("Test only works on netdevsim") 135 self._check_env() 136 137 self.dev = ip("-d link show dev " + self.env['NETIF'], json=True)[0] 138 139 self.addr_v["4"] = self.env.get("LOCAL_V4") 140 self.addr_v["6"] = self.env.get("LOCAL_V6") 141 self.remote_addr_v["4"] = self.env.get("REMOTE_V4") 142 self.remote_addr_v["6"] = self.env.get("REMOTE_V6") 143 kind = self.env["REMOTE_TYPE"] 144 args = self.env["REMOTE_ARGS"] 145 else: 146 if nsim_test is False: 147 raise KsftXfailEx("Test does not work on netdevsim") 148 149 self.create_local() 150 151 self.dev = self._ns.nsims[0].dev 152 153 self.addr_v["4"] = self.nsim_v4_pfx + "1" 154 self.addr_v["6"] = self.nsim_v6_pfx + "1" 155 self.remote_addr_v["4"] = self.nsim_v4_pfx + "2" 156 self.remote_addr_v["6"] = self.nsim_v6_pfx + "2" 157 kind = "netns" 158 args = self._netns.name 159 160 self.remote = Remote(kind, args, src_path) 161 162 self.addr_ipver = "6" if self.addr_v["6"] else "4" 163 self.addr = self.addr_v[self.addr_ipver] 164 self.remote_addr = self.remote_addr_v[self.addr_ipver] 165 166 # Bracketed addresses, some commands need IPv6 to be inside [] 167 self.baddr = f"[{self.addr_v['6']}]" if self.addr_v["6"] else self.addr_v["4"] 168 self.remote_baddr = f"[{self.remote_addr_v['6']}]" if self.remote_addr_v["6"] else self.remote_addr_v["4"] 169 170 self.ifname = self.dev['ifname'] 171 self.ifindex = self.dev['ifindex'] 172 173 # resolve remote interface name 174 self.remote_ifname = self.resolve_remote_ifc() 175 self.remote_dev = ip("-d link show dev " + self.remote_ifname, 176 host=self.remote, json=True)[0] 177 self.remote_ifindex = self.remote_dev['ifindex'] 178 179 self._required_cmd = {} 180 181 def create_local(self): 182 self._netns = NetNS() 183 self._ns = NetdevSimDev() 184 self._ns_peer = NetdevSimDev(ns=self._netns) 185 186 with open("/proc/self/ns/net") as nsfd0, \ 187 open("/var/run/netns/" + self._netns.name) as nsfd1: 188 ifi0 = self._ns.nsims[0].ifindex 189 ifi1 = self._ns_peer.nsims[0].ifindex 190 NetdevSimDev.ctrl_write('link_device', 191 f'{nsfd0.fileno()}:{ifi0} {nsfd1.fileno()}:{ifi1}') 192 193 ip(f" addr add dev {self._ns.nsims[0].ifname} {self.nsim_v4_pfx}1/24") 194 ip(f"-6 addr add dev {self._ns.nsims[0].ifname} {self.nsim_v6_pfx}1/64 nodad") 195 ip(f" link set dev {self._ns.nsims[0].ifname} up") 196 197 ip(f" addr add dev {self._ns_peer.nsims[0].ifname} {self.nsim_v4_pfx}2/24", ns=self._netns) 198 ip(f"-6 addr add dev {self._ns_peer.nsims[0].ifname} {self.nsim_v6_pfx}2/64 nodad", ns=self._netns) 199 ip(f" link set dev {self._ns_peer.nsims[0].ifname} up", ns=self._netns) 200 201 def _check_env(self): 202 vars_needed = [ 203 ["LOCAL_V4", "LOCAL_V6"], 204 ["REMOTE_V4", "REMOTE_V6"], 205 ["REMOTE_TYPE"], 206 ["REMOTE_ARGS"] 207 ] 208 missing = [] 209 210 for choice in vars_needed: 211 for entry in choice: 212 if entry in self.env: 213 break 214 else: 215 missing.append(choice) 216 # Make sure v4 / v6 configs are symmetric 217 if ("LOCAL_V6" in self.env) != ("REMOTE_V6" in self.env): 218 missing.append(["LOCAL_V6", "REMOTE_V6"]) 219 if ("LOCAL_V4" in self.env) != ("REMOTE_V4" in self.env): 220 missing.append(["LOCAL_V4", "REMOTE_V4"]) 221 if missing: 222 raise Exception("Invalid environment, missing configuration:", missing, 223 "Please see tools/testing/selftests/drivers/net/README.rst") 224 225 def resolve_remote_ifc(self): 226 v4 = v6 = None 227 if self.remote_addr_v["4"]: 228 v4 = ip("addr show to " + self.remote_addr_v["4"], json=True, host=self.remote) 229 if self.remote_addr_v["6"]: 230 v6 = ip("addr show to " + self.remote_addr_v["6"], json=True, host=self.remote) 231 if v4 and v6 and v4[0]["ifname"] != v6[0]["ifname"]: 232 raise Exception("Can't resolve remote interface name, v4 and v6 don't match") 233 if (v4 and len(v4) > 1) or (v6 and len(v6) > 1): 234 raise Exception("Can't resolve remote interface name, multiple interfaces match") 235 return v6[0]["ifname"] if v6 else v4[0]["ifname"] 236 237 def __del__(self): 238 if self._ns: 239 self._ns.remove() 240 self._ns = None 241 if self._ns_peer: 242 self._ns_peer.remove() 243 self._ns_peer = None 244 if self._netns: 245 del self._netns 246 self._netns = None 247 if self.remote: 248 del self.remote 249 self.remote = None 250 251 def require_ipver(self, ipver): 252 if not self.addr_v[ipver] or not self.remote_addr_v[ipver]: 253 raise KsftSkipEx(f"Test requires IPv{ipver} connectivity") 254 255 def require_nsim(self, nsim_test=True): 256 """Require or exclude netdevsim for this test""" 257 if nsim_test and self._ns is None: 258 raise KsftXfailEx("Test only works on netdevsim") 259 if nsim_test is False and self._ns is not None: 260 raise KsftXfailEx("Test does not work on netdevsim") 261 262 def get_local_nsim_dev(self): 263 """Returns the local netdevsim device or None. 264 Using this method is discouraged, as it makes tests nsim-specific. 265 Standard interfaces available on all HW should ideally be used. 266 This method is intended for the few cases where nsim-specific 267 assertions need to be verified which cannot be verified otherwise. 268 """ 269 return self._ns 270 271 def _require_cmd(self, comm, key, host=None): 272 cached = self._required_cmd.get(comm, {}) 273 if cached.get(key) is None: 274 cached[key] = cmd("command -v -- " + comm, fail=False, 275 shell=True, host=host).ret == 0 276 self._required_cmd[comm] = cached 277 return cached[key] 278 279 def require_cmd(self, comm, local=True, remote=False): 280 if local: 281 if not self._require_cmd(comm, "local"): 282 raise KsftSkipEx("Test requires command: " + comm) 283 if remote: 284 if not self._require_cmd(comm, "remote", host=self.remote): 285 raise KsftSkipEx("Test requires (remote) command: " + comm) 286 287 def wait_hw_stats_settle(self): 288 """ 289 Wait for HW stats to become consistent, some devices DMA HW stats 290 periodically so events won't be reflected until next sync. 291 Good drivers will tell us via ethtool what their sync period is. 292 """ 293 if self._stats_settle_time is None: 294 data = {} 295 try: 296 data = ethtool("-c " + self.ifname, json=True)[0] 297 except CmdExitFailure as e: 298 if "Operation not supported" not in e.cmd.stderr: 299 raise 300 301 self._stats_settle_time = \ 302 1.25 * data.get('stats-block-usecs', 20000) / 1000 / 1000 303 304 time.sleep(self._stats_settle_time) 305 306 307class NetDrvContEnv(NetDrvEpEnv): 308 """ 309 Class for an environment with a netkit pair setup for forwarding traffic 310 between the physical interface and a network namespace. 311 NETIF = "eth0" 312 LOCAL_V6 = "2001:db8:1::1" 313 REMOTE_V6 = "2001:db8:1::2" 314 LOCAL_PREFIX_V6 = "2001:db8:2::0/64" 315 316 +-----------------------------+ +------------------------------+ 317 dst | INIT NS | | TEST NS | 318 2001: | +---------------+ | | | 319 db8:2::2| | NETIF | | bpf | | 320 +---|>| 2001:db8:1::1 | |redirect| +-------------------------+ | 321 | | | |-----------|--------|>| Netkit | | 322 | | +---------------+ | _peer | | nk_guest | | 323 | | +-------------+ Netkit pair | | | fe80::2/64 | | 324 | | | Netkit |.............|........|>| 2001:db8:2::2/64 | | 325 | | | nk_host | | | +-------------------------+ | 326 | | | fe80::1/64 | | | | 327 | | +-------------+ | | route: | 328 | | | | default | 329 | | route: | | via fe80::1 dev nk_guest | 330 | | 2001:db8:2::2/128 | +------------------------------+ 331 | | via fe80::2 dev nk_host | 332 | +-----------------------------+ 333 | 334 | +---------------+ 335 | | REMOTE | 336 +---| 2001:db8:1::2 | 337 +---------------+ 338 """ 339 340 def __init__(self, src_path, rxqueues=1, primary_rx_redirect=False, 341 userns=False, **kwargs): 342 self.netns = None 343 self._userns = userns 344 self._nk_host_ifname = None 345 self.nk_guest_ifname = None 346 self._tc_clsact_added = False 347 self._tc_attached = False 348 self._primary_rx_redirect_attached = False 349 self._primary_rx_redirect_clsact_added = False 350 self._bpf_prog_pref = None 351 self._bpf_prog_id = None 352 self._init_ns_attached = False 353 self._remote_route_added = False 354 self._old_fwd = None 355 self._old_accept_ra = None 356 357 super().__init__(src_path, **kwargs) 358 359 self.require_ipver("6") 360 local_prefix = self.env.get("LOCAL_PREFIX_V6") 361 if not local_prefix: 362 raise KsftSkipEx("LOCAL_PREFIX_V6 required") 363 364 net = ipaddress.IPv6Network(local_prefix, strict=False) 365 self.ipv6_prefix = str(net.network_address) 366 self.nk_host_ipv6 = f"{self.ipv6_prefix}2:1" 367 self.nk_guest_ipv6 = f"{self.ipv6_prefix}2:2" 368 369 local_v6 = ipaddress.IPv6Address(self.addr_v["6"]) 370 if local_v6 in net: 371 raise KsftSkipEx("LOCAL_V6 must not fall within LOCAL_PREFIX_V6") 372 373 rtnl = RtnlFamily() 374 rtnl.newlink( 375 { 376 "linkinfo": { 377 "kind": "netkit", 378 "data": { 379 "mode": "l2", 380 "policy": "forward", 381 "peer-policy": "forward", 382 }, 383 }, 384 "num-rx-queues": rxqueues, 385 }, 386 flags=[Netlink.NLM_F_CREATE, Netlink.NLM_F_EXCL], 387 ) 388 389 all_links = ip("-d link show", json=True) 390 netkit_links = [link for link in all_links 391 if link.get('linkinfo', {}).get('info_kind') == 'netkit' 392 and 'UP' not in link.get('flags', [])] 393 394 if len(netkit_links) != 2: 395 raise KsftSkipEx("Failed to create netkit pair") 396 397 netkit_links.sort(key=lambda x: x['ifindex']) 398 self._nk_host_ifname = netkit_links[1]['ifname'] 399 self.nk_guest_ifname = netkit_links[0]['ifname'] 400 self.nk_host_ifindex = netkit_links[1]['ifindex'] 401 self.nk_guest_ifindex = netkit_links[0]['ifindex'] 402 403 self._setup_ns() 404 self._attach_bpf() 405 if primary_rx_redirect: 406 self._attach_primary_rx_redirect_bpf() 407 408 def __del__(self): 409 if self._primary_rx_redirect_attached: 410 cmd(f"tc filter del dev {self._nk_host_ifname} ingress", fail=False) 411 self._primary_rx_redirect_attached = False 412 413 if self._primary_rx_redirect_clsact_added: 414 cmd(f"tc qdisc del dev {self._nk_host_ifname} clsact", fail=False) 415 self._primary_rx_redirect_clsact_added = False 416 417 if self._tc_attached: 418 cmd(f"tc filter del dev {self.ifname} ingress pref {self._bpf_prog_pref}") 419 self._tc_attached = False 420 421 if self._tc_clsact_added: 422 cmd(f"tc qdisc del dev {self.ifname} clsact") 423 self._tc_clsact_added = False 424 425 if self._remote_route_added: 426 cmd(f"ip -6 route del {self.nk_guest_ipv6}/128", 427 host=self.remote, fail=False) 428 self._remote_route_added = False 429 430 if self._nk_host_ifname: 431 cmd(f"ip link del dev {self._nk_host_ifname}") 432 self._nk_host_ifname = None 433 self.nk_guest_ifname = None 434 435 if self._init_ns_attached: 436 cmd("ip netns del init", fail=False) 437 self._init_ns_attached = False 438 439 if self.netns: 440 del self.netns 441 self.netns = None 442 443 if self._old_fwd is not None: 444 with open("/proc/sys/net/ipv6/conf/all/forwarding", "w", 445 encoding="utf-8") as f: 446 f.write(self._old_fwd) 447 self._old_fwd = None 448 if self._old_accept_ra is not None: 449 with open("/proc/sys/net/ipv6/conf/all/accept_ra", "w", 450 encoding="utf-8") as f: 451 f.write(self._old_accept_ra) 452 self._old_accept_ra = None 453 454 super().__del__() 455 456 def _setup_ns(self): 457 fwd_path = "/proc/sys/net/ipv6/conf/all/forwarding" 458 ra_path = "/proc/sys/net/ipv6/conf/all/accept_ra" 459 with open(fwd_path, encoding="utf-8") as f: 460 self._old_fwd = f.read().strip() 461 with open(ra_path, encoding="utf-8") as f: 462 self._old_accept_ra = f.read().strip() 463 with open(fwd_path, "w", encoding="utf-8") as f: 464 f.write("1") 465 with open(ra_path, "w", encoding="utf-8") as f: 466 f.write("2") 467 468 self.netns = UserNetNS() if self._userns else NetNS() 469 cmd("ip netns attach init 1") 470 self._init_ns_attached = True 471 ip("netns set init 0", ns=self.netns) 472 ip(f"link set dev {self.nk_guest_ifname} netns {self.netns.name}") 473 ip(f"link set dev {self._nk_host_ifname} up") 474 ip(f"-6 addr add fe80::1/64 dev {self._nk_host_ifname} nodad") 475 ip(f"-6 route add {self.nk_guest_ipv6}/128 via fe80::2 dev {self._nk_host_ifname}") 476 477 ip("link set lo up", ns=self.netns) 478 ip(f"link set dev {self.nk_guest_ifname} up", ns=self.netns) 479 ip(f"-6 addr add fe80::2/64 dev {self.nk_guest_ifname}", ns=self.netns) 480 ip(f"-6 addr add {self.nk_guest_ipv6}/64 dev {self.nk_guest_ifname} nodad", ns=self.netns) 481 ip(f"-6 route add default via fe80::1 dev {self.nk_guest_ifname}", ns=self.netns) 482 483 def _tc_ensure_clsact(self, ifname=None): 484 """Ensure a clsact qdisc exists on @ifname. 485 486 Returns True if this call added the qdisc, otherwise returns False. 487 """ 488 if ifname is None: 489 ifname = self.ifname 490 qdisc = json.loads(cmd(f"tc -j qdisc show dev {ifname}").stdout) 491 for q in qdisc: 492 if q['kind'] == 'clsact': 493 return False 494 cmd(f"tc qdisc add dev {ifname} clsact") 495 return True 496 497 def _get_bpf_prog_ids(self): 498 filters = json.loads(cmd(f"tc -j filter show dev {self.ifname} ingress").stdout) 499 for bpf in filters: 500 if 'options' not in bpf: 501 continue 502 if bpf['options']['bpf_name'].startswith('nk_forward.bpf'): 503 return (bpf['pref'], bpf['options']['prog']['id']) 504 raise Exception("Failed to get BPF prog ID") 505 506 def _find_bss_map_id(self, prog_id): 507 """Find the .bss map ID for a loaded BPF program.""" 508 prog_info = bpftool(f"prog show id {prog_id}", json=True) 509 for map_id in prog_info.get("map_ids", []): 510 map_info = bpftool(f"map show id {map_id}", json=True) 511 if map_info.get("name", "").endswith("bss"): 512 return map_id 513 raise Exception(f"Failed to find .bss map for prog {prog_id}") 514 515 def _attach_bpf(self): 516 bpf_obj = self.test_dir / "nk_forward.bpf.o" 517 if not bpf_obj.exists(): 518 raise KsftSkipEx("BPF prog not found") 519 520 if self._tc_ensure_clsact(): 521 self._tc_clsact_added = True 522 cmd(f"tc filter add dev {self.ifname} ingress bpf obj {bpf_obj}" 523 " sec tc/ingress direct-action") 524 self._tc_attached = True 525 526 (self._bpf_prog_pref, self._bpf_prog_id) = self._get_bpf_prog_ids() 527 bss_map_id = self._find_bss_map_id(self._bpf_prog_id) 528 529 ipv6_addr = ipaddress.IPv6Address(self.ipv6_prefix) 530 ipv6_bytes = ipv6_addr.packed 531 ifindex_bytes = self.nk_host_ifindex.to_bytes(4, byteorder='little') 532 value = ipv6_bytes + ifindex_bytes 533 value_hex = ' '.join(f'{b:02x}' for b in value) 534 bpftool(f"map update id {bss_map_id} key hex 00 00 00 00 value hex {value_hex}") 535 536 def _attach_primary_rx_redirect_bpf(self): 537 """Attach BPF redirect program on the primary netkit ingress.""" 538 bpf_obj = self.test_dir / "nk_primary_rx_redirect.bpf.o" 539 if not bpf_obj.exists(): 540 raise KsftSkipEx("Primary RX redirect BPF prog not found") 541 542 if self._tc_ensure_clsact(self._nk_host_ifname): 543 self._primary_rx_redirect_clsact_added = True 544 cmd(f"tc filter add dev {self._nk_host_ifname} ingress" 545 f" bpf obj {bpf_obj} sec tc/ingress direct-action") 546 self._primary_rx_redirect_attached = True 547 548 ip(f"-6 route add {self.nk_guest_ipv6}/128 via {self.addr_v['6']}", 549 host=self.remote) 550 self._remote_route_added = True 551 552 filters = json.loads( 553 cmd(f"tc -j filter show dev {self._nk_host_ifname} ingress").stdout) 554 redirect_prog_id = None 555 for bpf in filters: 556 if 'options' not in bpf: 557 continue 558 if bpf['options']['bpf_name'].startswith('nk_primary_rx_redirect'): 559 redirect_prog_id = bpf['options']['prog']['id'] 560 break 561 if redirect_prog_id is None: 562 raise Exception("Failed to get primary RX redirect BPF prog ID") 563 564 bss_map_id = self._find_bss_map_id(redirect_prog_id) 565 phys_ifindex_bytes = self.ifindex.to_bytes(4, byteorder=sys.byteorder) 566 value_hex = ' '.join(f'{b:02x}' for b in phys_ifindex_bytes) 567 bpftool(f"map update id {bss_map_id} key hex 00 00 00 00 value hex {value_hex}") 568