mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-22 07:48:26 +00:00
Cross-compilation and macOS compatibility fixes (#1)
- 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 <origami74@gmail.com>
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -13,3 +13,5 @@
|
||||
|
||||
# Claude Code
|
||||
.claude/
|
||||
|
||||
deploy/
|
||||
@@ -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<u16, TransportError> {
|
||||
);
|
||||
}
|
||||
|
||||
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: {}",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Thin wrapper around subprocess for docker exec calls."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import logging
|
||||
|
||||
|
||||
@@ -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 <pid>`` 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 <pid>`` 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
|
||||
|
||||
Reference in New Issue
Block a user