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 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, **kwargs): 341 self.netns = None 342 self._nk_host_ifname = None 343 self.nk_guest_ifname = None 344 self._tc_clsact_added = False 345 self._tc_attached = False 346 self._primary_rx_redirect_attached = False 347 self._primary_rx_redirect_clsact_added = False 348 self._bpf_prog_pref = None 349 self._bpf_prog_id = None 350 self._init_ns_attached = False 351 self._remote_route_added = False 352 self._old_fwd = None 353 self._old_accept_ra = None 354 355 super().__init__(src_path, **kwargs) 356 357 self.require_ipver("6") 358 local_prefix = self.env.get("LOCAL_PREFIX_V6") 359 if not local_prefix: 360 raise KsftSkipEx("LOCAL_PREFIX_V6 required") 361 362 net = ipaddress.IPv6Network(local_prefix, strict=False) 363 self.ipv6_prefix = str(net.network_address) 364 self.nk_host_ipv6 = f"{self.ipv6_prefix}2:1" 365 self.nk_guest_ipv6 = f"{self.ipv6_prefix}2:2" 366 367 local_v6 = ipaddress.IPv6Address(self.addr_v["6"]) 368 if local_v6 in net: 369 raise KsftSkipEx("LOCAL_V6 must not fall within LOCAL_PREFIX_V6") 370 371 rtnl = RtnlFamily() 372 rtnl.newlink( 373 { 374 "linkinfo": { 375 "kind": "netkit", 376 "data": { 377 "mode": "l2", 378 "policy": "forward", 379 "peer-policy": "forward", 380 }, 381 }, 382 "num-rx-queues": rxqueues, 383 }, 384 flags=[Netlink.NLM_F_CREATE, Netlink.NLM_F_EXCL], 385 ) 386 387 all_links = ip("-d link show", json=True) 388 netkit_links = [link for link in all_links 389 if link.get('linkinfo', {}).get('info_kind') == 'netkit' 390 and 'UP' not in link.get('flags', [])] 391 392 if len(netkit_links) != 2: 393 raise KsftSkipEx("Failed to create netkit pair") 394 395 netkit_links.sort(key=lambda x: x['ifindex']) 396 self._nk_host_ifname = netkit_links[1]['ifname'] 397 self.nk_guest_ifname = netkit_links[0]['ifname'] 398 self.nk_host_ifindex = netkit_links[1]['ifindex'] 399 self.nk_guest_ifindex = netkit_links[0]['ifindex'] 400 401 self._setup_ns() 402 self._attach_bpf() 403 if primary_rx_redirect: 404 self._attach_primary_rx_redirect_bpf() 405 406 def __del__(self): 407 if self._primary_rx_redirect_attached: 408 cmd(f"tc filter del dev {self._nk_host_ifname} ingress", fail=False) 409 self._primary_rx_redirect_attached = False 410 411 if self._primary_rx_redirect_clsact_added: 412 cmd(f"tc qdisc del dev {self._nk_host_ifname} clsact", fail=False) 413 self._primary_rx_redirect_clsact_added = False 414 415 if self._tc_attached: 416 cmd(f"tc filter del dev {self.ifname} ingress pref {self._bpf_prog_pref}") 417 self._tc_attached = False 418 419 if self._tc_clsact_added: 420 cmd(f"tc qdisc del dev {self.ifname} clsact") 421 self._tc_clsact_added = False 422 423 if self._remote_route_added: 424 cmd(f"ip -6 route del {self.nk_guest_ipv6}/128", 425 host=self.remote, fail=False) 426 self._remote_route_added = False 427 428 if self._nk_host_ifname: 429 cmd(f"ip link del dev {self._nk_host_ifname}") 430 self._nk_host_ifname = None 431 self.nk_guest_ifname = None 432 433 if self._init_ns_attached: 434 cmd("ip netns del init", fail=False) 435 self._init_ns_attached = False 436 437 if self.netns: 438 del self.netns 439 self.netns = None 440 441 if self._old_fwd is not None: 442 with open("/proc/sys/net/ipv6/conf/all/forwarding", "w", 443 encoding="utf-8") as f: 444 f.write(self._old_fwd) 445 self._old_fwd = None 446 if self._old_accept_ra is not None: 447 with open("/proc/sys/net/ipv6/conf/all/accept_ra", "w", 448 encoding="utf-8") as f: 449 f.write(self._old_accept_ra) 450 self._old_accept_ra = None 451 452 super().__del__() 453 454 def _setup_ns(self): 455 fwd_path = "/proc/sys/net/ipv6/conf/all/forwarding" 456 ra_path = "/proc/sys/net/ipv6/conf/all/accept_ra" 457 with open(fwd_path, encoding="utf-8") as f: 458 self._old_fwd = f.read().strip() 459 with open(ra_path, encoding="utf-8") as f: 460 self._old_accept_ra = f.read().strip() 461 with open(fwd_path, "w", encoding="utf-8") as f: 462 f.write("1") 463 with open(ra_path, "w", encoding="utf-8") as f: 464 f.write("2") 465 466 self.netns = NetNS() 467 cmd("ip netns attach init 1") 468 self._init_ns_attached = True 469 ip("netns set init 0", ns=self.netns) 470 ip(f"link set dev {self.nk_guest_ifname} netns {self.netns.name}") 471 ip(f"link set dev {self._nk_host_ifname} up") 472 ip(f"-6 addr add fe80::1/64 dev {self._nk_host_ifname} nodad") 473 ip(f"-6 route add {self.nk_guest_ipv6}/128 via fe80::2 dev {self._nk_host_ifname}") 474 475 ip("link set lo up", ns=self.netns) 476 ip(f"link set dev {self.nk_guest_ifname} up", ns=self.netns) 477 ip(f"-6 addr add fe80::2/64 dev {self.nk_guest_ifname}", ns=self.netns) 478 ip(f"-6 addr add {self.nk_guest_ipv6}/64 dev {self.nk_guest_ifname} nodad", ns=self.netns) 479 ip(f"-6 route add default via fe80::1 dev {self.nk_guest_ifname}", ns=self.netns) 480 481 def _tc_ensure_clsact(self, ifname=None): 482 """Ensure a clsact qdisc exists on @ifname. 483 484 Returns True if this call added the qdisc, otherwise returns False. 485 """ 486 if ifname is None: 487 ifname = self.ifname 488 qdisc = json.loads(cmd(f"tc -j qdisc show dev {ifname}").stdout) 489 for q in qdisc: 490 if q['kind'] == 'clsact': 491 return False 492 cmd(f"tc qdisc add dev {ifname} clsact") 493 return True 494 495 def _get_bpf_prog_ids(self): 496 filters = json.loads(cmd(f"tc -j filter show dev {self.ifname} ingress").stdout) 497 for bpf in filters: 498 if 'options' not in bpf: 499 continue 500 if bpf['options']['bpf_name'].startswith('nk_forward.bpf'): 501 return (bpf['pref'], bpf['options']['prog']['id']) 502 raise Exception("Failed to get BPF prog ID") 503 504 def _find_bss_map_id(self, prog_id): 505 """Find the .bss map ID for a loaded BPF program.""" 506 prog_info = bpftool(f"prog show id {prog_id}", json=True) 507 for map_id in prog_info.get("map_ids", []): 508 map_info = bpftool(f"map show id {map_id}", json=True) 509 if map_info.get("name", "").endswith("bss"): 510 return map_id 511 raise Exception(f"Failed to find .bss map for prog {prog_id}") 512 513 def _attach_bpf(self): 514 bpf_obj = self.test_dir / "nk_forward.bpf.o" 515 if not bpf_obj.exists(): 516 raise KsftSkipEx("BPF prog not found") 517 518 if self._tc_ensure_clsact(): 519 self._tc_clsact_added = True 520 cmd(f"tc filter add dev {self.ifname} ingress bpf obj {bpf_obj}" 521 " sec tc/ingress direct-action") 522 self._tc_attached = True 523 524 (self._bpf_prog_pref, self._bpf_prog_id) = self._get_bpf_prog_ids() 525 bss_map_id = self._find_bss_map_id(self._bpf_prog_id) 526 527 ipv6_addr = ipaddress.IPv6Address(self.ipv6_prefix) 528 ipv6_bytes = ipv6_addr.packed 529 ifindex_bytes = self.nk_host_ifindex.to_bytes(4, byteorder='little') 530 value = ipv6_bytes + ifindex_bytes 531 value_hex = ' '.join(f'{b:02x}' for b in value) 532 bpftool(f"map update id {bss_map_id} key hex 00 00 00 00 value hex {value_hex}") 533 534 def _attach_primary_rx_redirect_bpf(self): 535 """Attach BPF redirect program on the primary netkit ingress.""" 536 bpf_obj = self.test_dir / "nk_primary_rx_redirect.bpf.o" 537 if not bpf_obj.exists(): 538 raise KsftSkipEx("Primary RX redirect BPF prog not found") 539 540 if self._tc_ensure_clsact(self._nk_host_ifname): 541 self._primary_rx_redirect_clsact_added = True 542 cmd(f"tc filter add dev {self._nk_host_ifname} ingress" 543 f" bpf obj {bpf_obj} sec tc/ingress direct-action") 544 self._primary_rx_redirect_attached = True 545 546 ip(f"-6 route add {self.nk_guest_ipv6}/128 via {self.addr_v['6']}", 547 host=self.remote) 548 self._remote_route_added = True 549 550 filters = json.loads( 551 cmd(f"tc -j filter show dev {self._nk_host_ifname} ingress").stdout) 552 redirect_prog_id = None 553 for bpf in filters: 554 if 'options' not in bpf: 555 continue 556 if bpf['options']['bpf_name'].startswith('nk_primary_rx_redirect'): 557 redirect_prog_id = bpf['options']['prog']['id'] 558 break 559 if redirect_prog_id is None: 560 raise Exception("Failed to get primary RX redirect BPF prog ID") 561 562 bss_map_id = self._find_bss_map_id(redirect_prog_id) 563 phys_ifindex_bytes = self.ifindex.to_bytes(4, byteorder=sys.byteorder) 564 value_hex = ' '.join(f'{b:02x}' for b in phys_ifindex_bytes) 565 bpftool(f"map update id {bss_map_id} key hex 00 00 00 00 value hex {value_hex}") 566