From c1589c02af2d40de0ca1877a4a60e29a74cbc388 Mon Sep 17 00:00:00 2001 From: Arjen <18398758+Origami74@users.noreply.github.com> Date: Thu, 26 Feb 2026 08:48:51 -0300 Subject: [PATCH] Cross-compilation and macOS compatibility fixes (#1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ethernet ioctl: cfg-gate the ioctl request parameter type — c_int on musl, c_ulong on gnu — so the same code compiles on both x86_64-unknown-linux-gnu and x86_64-unknown-linux-musl targets - Chaos harness macOS support: replace direct host 'ip link' invocations with a privileged Docker container helper (--net=host --pid=host) that shares the Docker VM's namespaces, making veth pair setup work on both Linux and macOS (where host 'ip' is unavailable and container PIDs live inside the Docker Desktop VM) - Python 3.9 compat: add 'from __future__ import annotations' to docker_exec.py for X|Y union syntax support - Add deploy/ to .gitignore Co-authored-by: origami74 --- .gitignore | 2 + src/transport/ethernet/socket.rs | 12 +++- testing/chaos/sim/docker_exec.py | 2 + testing/chaos/sim/veth.py | 106 ++++++++++++++++++++++++++----- 4 files changed, 103 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index 9833871..03c7977 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ # Claude Code .claude/ + +deploy/ \ No newline at end of file diff --git a/src/transport/ethernet/socket.rs b/src/transport/ethernet/socket.rs index c346382..ecc243a 100644 --- a/src/transport/ethernet/socket.rs +++ b/src/transport/ethernet/socket.rs @@ -329,7 +329,11 @@ fn get_mac_addr(fd: RawFd, if_index: i32) -> Result<[u8; 6], TransportError> { ); } - let ret = unsafe { libc::ioctl(fd, libc::SIOCGIFHWADDR as libc::c_ulong, &ifr) }; + #[cfg(target_env = "musl")] + let ioctl_req = libc::SIOCGIFHWADDR as libc::c_int; + #[cfg(not(target_env = "musl"))] + let ioctl_req = libc::SIOCGIFHWADDR as libc::c_ulong; + let ret = unsafe { libc::ioctl(fd, ioctl_req, &ifr) }; if ret < 0 { return Err(TransportError::StartFailed(format!( "ioctl(SIOCGIFHWADDR) failed: {}", @@ -375,7 +379,11 @@ fn get_if_mtu(fd: RawFd, if_index: i32) -> Result { ); } - let ret = unsafe { libc::ioctl(fd, libc::SIOCGIFMTU as libc::c_ulong, &ifr) }; + #[cfg(target_env = "musl")] + let ioctl_req = libc::SIOCGIFMTU as libc::c_int; + #[cfg(not(target_env = "musl"))] + let ioctl_req = libc::SIOCGIFMTU as libc::c_ulong; + let ret = unsafe { libc::ioctl(fd, ioctl_req, &ifr) }; if ret < 0 { return Err(TransportError::StartFailed(format!( "ioctl(SIOCGIFMTU) failed: {}", diff --git a/testing/chaos/sim/docker_exec.py b/testing/chaos/sim/docker_exec.py index 558dd8f..2d8781b 100644 --- a/testing/chaos/sim/docker_exec.py +++ b/testing/chaos/sim/docker_exec.py @@ -1,5 +1,7 @@ """Thin wrapper around subprocess for docker exec calls.""" +from __future__ import annotations + import subprocess import logging diff --git a/testing/chaos/sim/veth.py b/testing/chaos/sim/veth.py index ed34ee4..95d1e96 100644 --- a/testing/chaos/sim/veth.py +++ b/testing/chaos/sim/veth.py @@ -9,6 +9,24 @@ each container's network namespace. Naming: After creation, the container-side MAC addresses are queried and stored in SimNode.ethernet_macs for use in config generation. + +Implementation note +------------------- +All ``ip link`` operations that manipulate the host network stack are +executed inside a short-lived privileged Docker container that shares +the host network and PID namespaces (``--net=host --pid=host``). This +works on both Linux and macOS: + +* **Linux** – the helper container shares the real host network/PID + namespaces, so ``ip link set ... netns `` behaves identically to + running ``ip`` directly on the host. +* **macOS** – Docker containers run inside a Linux VM; the helper + container shares *that* VM's namespaces, which is exactly where the + simulation containers live. Running ``ip`` on the macOS host would + never work because the container PIDs are in the VM, not macOS. + +The helper image is resolved from the running simulation containers +(which already have ``iproute2`` from the chaos Dockerfile). """ from __future__ import annotations @@ -21,7 +39,6 @@ from .topology import SimTopology, veth_interface_name log = logging.getLogger(__name__) - class VethManager: """Manages veth pairs for Ethernet-transport edges.""" @@ -30,6 +47,34 @@ class VethManager: # Track created host-side temp names for cleanup self._host_pairs: list[tuple[str, str, str, str]] = [] # (node_a, node_b, host_name_a, host_name_b) + self._ip_image: str | None = None + + def _get_image(self) -> str: + """Resolve the Docker image to use for ip(8) helper containers. + + Uses the image of the first simulation container (which already has + iproute2 installed via the chaos Dockerfile). Must be called after + containers are started. + """ + if self._ip_image is not None: + return self._ip_image + + first_node = next(iter(sorted(self.topology.nodes))) + container = self.topology.container_name(first_node) + result = subprocess.run( + ["docker", "inspect", "-f", "{{.Config.Image}}", container], + capture_output=True, + text=True, + timeout=10, + ) + if result.returncode != 0 or not result.stdout.strip(): + raise RuntimeError( + f"Cannot determine Docker image for ip(8) helper " + f"(docker inspect {container} failed): {result.stderr.strip()}" + ) + self._ip_image = result.stdout.strip() + log.debug("Using sim image %s for ip(8) helper", self._ip_image) + return self._ip_image def setup_all(self): """Create veth pairs for all Ethernet edges. @@ -45,10 +90,11 @@ class VethManager: if not eth_edges: return - log.info("Setting up %d Ethernet veth pairs...", len(eth_edges)) + image = self._get_image() + log.info("Setting up %d Ethernet veth pairs (helper image: %s)...", len(eth_edges), image) for a, b in eth_edges: - self._create_veth_pair(a, b) + self._create_veth_pair(a, b, image) log.info( "Veth setup complete: %d pairs", @@ -62,6 +108,7 @@ class VethManager: destroyed. We re-create the veth pairs for all Ethernet edges involving this node. """ + image = self._get_image() for a, b in self.topology.ethernet_edges(): if a != node_id and b != node_id: continue @@ -69,17 +116,18 @@ class VethManager: nn_a = a.replace("n", "") nn_b = b.replace("n", "") host_a = f"vh{nn_a}{nn_b}a" - _run_host(["ip", "link", "delete", host_a], check=False) + _run_host(["ip", "link", "delete", host_a], image, check=False) # Re-create - self._create_veth_pair(a, b) + self._create_veth_pair(a, b, image) def teardown_all(self): """Clean up all veth pairs.""" + image = self._get_image() for _, _, host_a, _ in self._host_pairs: - _run_host(["ip", "link", "delete", host_a], check=False) + _run_host(["ip", "link", "delete", host_a], image, check=False) self._host_pairs.clear() - def _create_veth_pair(self, node_a: str, node_b: str): + def _create_veth_pair(self, node_a: str, node_b: str, image: str): """Create a single veth pair between two containers.""" container_a = self.topology.container_name(node_a) container_b = self.topology.container_name(node_b) @@ -102,19 +150,19 @@ class VethManager: final_b = veth_interface_name(node_b, node_a) # Clean up any stale pair - _run_host(["ip", "link", "delete", host_a], check=False) + _run_host(["ip", "link", "delete", host_a], image, check=False) # Create veth pair on host ok = _run_host([ "ip", "link", "add", host_a, "type", "veth", "peer", "name", host_b, - ]) + ], image) if not ok: log.warning("Failed to create veth pair %s/%s", host_a, host_b) return # Move into container namespaces - _run_host(["ip", "link", "set", host_a, "netns", str(pid_a)]) - _run_host(["ip", "link", "set", host_b, "netns", str(pid_b)]) + _run_host(["ip", "link", "set", host_a, "netns", str(pid_a)], image) + _run_host(["ip", "link", "set", host_b, "netns", str(pid_b)], image) # Rename and bring up inside containers docker_exec_quiet( @@ -175,19 +223,43 @@ def _get_mac_in_container(container: str, iface: str) -> str | None: return None -def _run_host(cmd: list[str], check: bool = True) -> bool: - """Run a command on the host. Returns True on success.""" +def _run_host(cmd: list[str], image: str, check: bool = True) -> bool: + """Run an ``ip`` command via a privileged Docker container. + + Uses ``--net=host --pid=host --privileged`` so the container shares + the Docker host's (or Docker Desktop VM's) network and PID + namespaces. This makes ``ip link set ... netns `` work + correctly on both Linux and macOS. + + ``image`` should be a Docker image that has ``iproute2`` installed + (e.g. the simulation's own image built from the chaos Dockerfile). + + ``--entrypoint ip`` overrides the image's default entrypoint so the + simulation entrypoint script is not executed. + """ + docker_cmd = [ + "docker", "run", "--rm", + "--privileged", + "--net=host", + "--pid=host", + "--entrypoint", "ip", + image, + ] + cmd[1:] # cmd[0] is "ip", skip it since it's now the entrypoint try: result = subprocess.run( - cmd, + docker_cmd, capture_output=True, text=True, - timeout=10, + timeout=30, ) if check and result.returncode != 0: - log.debug("Host cmd failed: %s -> %s", " ".join(cmd), result.stderr.strip()) + log.debug( + "ip cmd failed: %s -> %s", + " ".join(cmd), + result.stderr.strip(), + ) return False return result.returncode == 0 except subprocess.TimeoutExpired: - log.warning("Host cmd timed out: %s", " ".join(cmd)) + log.warning("ip cmd timed out: %s", " ".join(cmd)) return False