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 nk_guest_dev = ip(f"link show dev {self.nk_guest_ifname}", 474 json=True, ns=self.netns)[0] 475 self.nk_guest_ifindex = nk_guest_dev['ifindex'] 476 ip(f"link set dev {self.nk_host_ifname} up") 477 ip(f"-6 addr add fe80::1/64 dev {self.nk_host_ifname} nodad") 478 ip(f"-6 route add {self.nk_guest_ipv6}/128 via fe80::2 dev {self.nk_host_ifname}") 479 480 ip("link set lo up", ns=self.netns) 481 ip(f"link set dev {self.nk_guest_ifname} up", ns=self.netns) 482 ip(f"-6 addr add fe80::2/64 dev {self.nk_guest_ifname}", ns=self.netns) 483 ip(f"-6 addr add {self.nk_guest_ipv6}/64 dev {self.nk_guest_ifname} nodad", ns=self.netns) 484 ip(f"-6 route add default via fe80::1 dev {self.nk_guest_ifname}", ns=self.netns) 485 486 def _tc_ensure_clsact(self, ifname=None): 487 """Ensure a clsact qdisc exists on @ifname. 488 489 Returns True if this call added the qdisc, otherwise returns False. 490 """ 491 if ifname is None: 492 ifname = self.ifname 493 qdisc = json.loads(cmd(f"tc -j qdisc show dev {ifname}").stdout) 494 for q in qdisc: 495 if q['kind'] == 'clsact': 496 return False 497 cmd(f"tc qdisc add dev {ifname} clsact") 498 return True 499 500 def _get_bpf_prog_ids(self): 501 filters = json.loads(cmd(f"tc -j filter show dev {self.ifname} ingress").stdout) 502 for bpf in filters: 503 if 'options' not in bpf: 504 continue 505 if bpf['options']['bpf_name'].startswith('nk_forward.bpf'): 506 return (bpf['pref'], bpf['options']['prog']['id']) 507 raise Exception("Failed to get BPF prog ID") 508 509 def _find_bss_map_id(self, prog_id): 510 """Find the .bss map ID for a loaded BPF program.""" 511 prog_info = bpftool(f"prog show id {prog_id}", json=True) 512 for map_id in prog_info.get("map_ids", []): 513 map_info = bpftool(f"map show id {map_id}", json=True) 514 if map_info.get("name", "").endswith("bss"): 515 return map_id 516 raise Exception(f"Failed to find .bss map for prog {prog_id}") 517 518 def _find_bpf_obj(self, name): 519 bpf_obj = self.test_dir / name 520 if bpf_obj.exists(): 521 return bpf_obj 522 bpf_obj = self.test_dir / "hw" / name 523 if bpf_obj.exists(): 524 return bpf_obj 525 return None 526 527 def detach_bpf(self): 528 if self._tc_attached: 529 cmd(f"tc filter del dev {self.ifname} ingress pref " 530 f"{self._bpf_prog_pref}", fail=False) 531 self._tc_attached = False 532 533 def attach_bpf(self): 534 bpf_obj = self._find_bpf_obj("nk_forward.bpf.o") 535 if not bpf_obj: 536 raise KsftSkipEx("BPF prog nk_forward.bpf.o not found") 537 538 if self._tc_ensure_clsact(): 539 self._tc_clsact_added = True 540 cmd(f"tc filter add dev {self.ifname} ingress bpf obj {bpf_obj}" 541 " sec tc/ingress direct-action") 542 self._tc_attached = True 543 544 (self._bpf_prog_pref, self._bpf_prog_id) = self._get_bpf_prog_ids() 545 bss_map_id = self._find_bss_map_id(self._bpf_prog_id) 546 547 ipv6_addr = ipaddress.IPv6Address(self.ipv6_prefix) 548 ipv6_bytes = ipv6_addr.packed 549 ifindex_bytes = self.nk_host_ifindex.to_bytes(4, byteorder='little') 550 value = ipv6_bytes + ifindex_bytes 551 value_hex = ' '.join(f'{b:02x}' for b in value) 552 bpftool(f"map update id {bss_map_id} key hex 00 00 00 00 value hex {value_hex}") 553 554 def _attach_primary_rx_redirect_bpf(self): 555 """Attach BPF redirect program on the primary netkit ingress.""" 556 bpf_obj = self._find_bpf_obj("nk_primary_rx_redirect.bpf.o") 557 if not bpf_obj: 558 raise KsftSkipEx("nk_primary_rx_redirect.bpf.o not found") 559 560 if self._tc_ensure_clsact(self.nk_host_ifname): 561 self._primary_rx_redirect_clsact_added = True 562 cmd(f"tc filter add dev {self.nk_host_ifname} ingress" 563 f" bpf obj {bpf_obj} sec tc/ingress direct-action") 564 self._primary_rx_redirect_attached = True 565 566 ip(f"-6 route add {self.nk_guest_ipv6}/128 via {self.addr_v['6']}", 567 host=self.remote) 568 self._remote_route_added = True 569 570 filters = json.loads( 571 cmd(f"tc -j filter show dev {self.nk_host_ifname} ingress").stdout) 572 redirect_prog_id = None 573 for bpf in filters: 574 if 'options' not in bpf: 575 continue 576 if bpf['options']['bpf_name'].startswith('nk_primary_rx_redirect'): 577 redirect_prog_id = bpf['options']['prog']['id'] 578 break 579 if redirect_prog_id is None: 580 raise Exception("Failed to get primary RX redirect BPF prog ID") 581 582 bss_map_id = self._find_bss_map_id(redirect_prog_id) 583 phys_ifindex_bytes = self.ifindex.to_bytes(4, byteorder=sys.byteorder) 584 value_hex = ' '.join(f'{b:02x}' for b in phys_ifindex_bytes) 585 bpftool(f"map update id {bss_map_id} key hex 00 00 00 00 value hex {value_hex}") 586