mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
Implement raw Ethernet transport using AF_PACKET SOCK_DGRAM on Linux with EtherType 0x88B5 (IEEE experimental range) and 1-byte frame type prefix (0x00=data, 0x01=beacon). Transport implementation: - EthernetConfig with interface, ethertype, MTU, buffer sizes, and four independent discovery knobs (discovery, announce, auto_connect, accept_connections) - PacketSocket/AsyncPacketSocket wrappers with ioctl helpers for interface index, MAC address, and MTU queries - EthernetTransport with Transport trait impl, async start/stop/send, receive loop dispatching data frames and discovery beacons - Discovery beacons (34 bytes: type + version + x-only pubkey) with DiscoveryBuffer for peer accumulation and dedup - Atomic statistics counters (frames, bytes, errors, beacons) - Platform-gated with #[cfg(target_os = "linux")] Transport-layer discovery integration: - Promote auto_connect() and accept_connections() to Transport trait with default implementations and TransportHandle dispatch - Extract initiate_connection() so both static peer config and discovery auto-connect share the same handshake initiation path - Add poll_transport_discovery() to the tick handler to drain discovery buffers and auto-connect to discovered peers - Enforce accept_connections() in handle_msg1() — transports with accept_connections=false silently drop inbound handshakes Node integration: - create_transports() handles Ethernet named instances - resolve_ethernet_addr() parses "interface/mac" address format - transport_mtu() generalized for multi-transport operation Test harness: - VethPair RAII struct for veth pair lifecycle management - Three #[ignore] integration tests requiring root/CAP_NET_RAW: two-node handshake, data exchange, mixed transport coexistence - Chaos harness: transport-aware topology model, VethManager for veth pairs between Docker containers, Ethernet-aware config gen, netem split (HTB+u32 for UDP, root netem for veth), transport-aware link flaps and node churn with veth re-setup - Container entrypoint waits for configured Ethernet interfaces before starting FIPS (handles veth creation timing) - New scenarios: ethernet-only (4-node ring), ethernet-mesh (6-node mixed UDP+Ethernet with netem and link flaps) Documentation: - fips-transport-layer.md: Ethernet section, beacon discovery, WiFi compatibility, updated discovery state, trait surface additions, implementation status table - fips-configuration.md: Ethernet parameter table, named instances, peer address format, mixed UDP+Ethernet example, complete reference - fips-wire-formats.md: Ethernet frame type prefix note
194 lines
6.3 KiB
Python
194 lines
6.3 KiB
Python
"""Veth pair management for Ethernet transport edges.
|
|
|
|
Creates veth pairs between Docker containers for Ethernet-transport
|
|
edges. Each Ethernet edge gets a veth pair with one end moved into
|
|
each container's network namespace. Naming:
|
|
|
|
Host (temporary): vh{NN}{MM}a / vh{NN}{MM}b
|
|
Container: ve-{local}-{peer} (via veth_interface_name())
|
|
|
|
After creation, the container-side MAC addresses are queried and
|
|
stored in SimNode.ethernet_macs for use in config generation.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import subprocess
|
|
|
|
from .docker_exec import docker_exec_quiet
|
|
from .topology import SimTopology, veth_interface_name
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class VethManager:
|
|
"""Manages veth pairs for Ethernet-transport edges."""
|
|
|
|
def __init__(self, topology: SimTopology):
|
|
self.topology = topology
|
|
# 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)
|
|
|
|
def setup_all(self):
|
|
"""Create veth pairs for all Ethernet edges.
|
|
|
|
For each Ethernet edge:
|
|
1. Get container PIDs
|
|
2. Create veth pair on host with temp names
|
|
3. Move ends into container network namespaces
|
|
4. Rename to final names and bring up
|
|
5. Query MACs and store in SimNode.ethernet_macs
|
|
"""
|
|
eth_edges = self.topology.ethernet_edges()
|
|
if not eth_edges:
|
|
return
|
|
|
|
log.info("Setting up %d Ethernet veth pairs...", len(eth_edges))
|
|
|
|
for a, b in eth_edges:
|
|
self._create_veth_pair(a, b)
|
|
|
|
log.info(
|
|
"Veth setup complete: %d pairs",
|
|
len(self._host_pairs),
|
|
)
|
|
|
|
def setup_node(self, node_id: str):
|
|
"""Re-create veth endpoints for a single node after container restart.
|
|
|
|
When a container restarts (node churn), its network namespace is
|
|
destroyed. We re-create the veth pairs for all Ethernet edges
|
|
involving this node.
|
|
"""
|
|
for a, b in self.topology.ethernet_edges():
|
|
if a != node_id and b != node_id:
|
|
continue
|
|
# Remove existing pair if any (host-side might still exist)
|
|
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)
|
|
# Re-create
|
|
self._create_veth_pair(a, b)
|
|
|
|
def teardown_all(self):
|
|
"""Clean up all veth pairs."""
|
|
for _, _, host_a, _ in self._host_pairs:
|
|
_run_host(["ip", "link", "delete", host_a], check=False)
|
|
self._host_pairs.clear()
|
|
|
|
def _create_veth_pair(self, node_a: str, node_b: 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)
|
|
|
|
# Get container PIDs
|
|
pid_a = _get_container_pid(container_a)
|
|
pid_b = _get_container_pid(container_b)
|
|
if pid_a is None or pid_b is None:
|
|
log.warning(
|
|
"Cannot create veth %s--%s: container PID not found", node_a, node_b
|
|
)
|
|
return
|
|
|
|
# Generate names
|
|
nn_a = node_a.replace("n", "")
|
|
nn_b = node_b.replace("n", "")
|
|
host_a = f"vh{nn_a}{nn_b}a"
|
|
host_b = f"vh{nn_a}{nn_b}b"
|
|
final_a = veth_interface_name(node_a, node_b)
|
|
final_b = veth_interface_name(node_b, node_a)
|
|
|
|
# Clean up any stale pair
|
|
_run_host(["ip", "link", "delete", host_a], check=False)
|
|
|
|
# Create veth pair on host
|
|
ok = _run_host([
|
|
"ip", "link", "add", host_a, "type", "veth", "peer", "name", host_b,
|
|
])
|
|
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)])
|
|
|
|
# Rename and bring up inside containers
|
|
docker_exec_quiet(
|
|
container_a,
|
|
f"ip link set {host_a} name {final_a} && ip link set {final_a} up",
|
|
timeout=10,
|
|
)
|
|
docker_exec_quiet(
|
|
container_b,
|
|
f"ip link set {host_b} name {final_b} && ip link set {final_b} up",
|
|
timeout=10,
|
|
)
|
|
|
|
# Query MAC addresses
|
|
mac_a = _get_mac_in_container(container_a, final_a)
|
|
mac_b = _get_mac_in_container(container_b, final_b)
|
|
|
|
if mac_a:
|
|
self.topology.nodes[node_a].ethernet_macs[node_b] = mac_a
|
|
if mac_b:
|
|
self.topology.nodes[node_b].ethernet_macs[node_a] = mac_b
|
|
|
|
self._host_pairs.append((node_a, node_b, host_a, host_b))
|
|
|
|
log.info(
|
|
"Veth %s(%s) -- %s(%s) MAC: %s / %s",
|
|
node_a, final_a, node_b, final_b,
|
|
mac_a or "?", mac_b or "?",
|
|
)
|
|
|
|
|
|
def _get_container_pid(container: str) -> int | None:
|
|
"""Get the PID of a running Docker container."""
|
|
try:
|
|
result = subprocess.run(
|
|
["docker", "inspect", "-f", "{{.State.Pid}}", container],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
)
|
|
if result.returncode == 0:
|
|
pid = int(result.stdout.strip())
|
|
return pid if pid > 0 else None
|
|
except (subprocess.TimeoutExpired, ValueError):
|
|
pass
|
|
return None
|
|
|
|
|
|
def _get_mac_in_container(container: str, iface: str) -> str | None:
|
|
"""Query the MAC address of an interface inside a container."""
|
|
result = docker_exec_quiet(
|
|
container,
|
|
f"cat /sys/class/net/{iface}/address",
|
|
timeout=5,
|
|
)
|
|
if result is not None:
|
|
return result.strip()
|
|
return None
|
|
|
|
|
|
def _run_host(cmd: list[str], check: bool = True) -> bool:
|
|
"""Run a command on the host. Returns True on success."""
|
|
try:
|
|
result = subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
)
|
|
if check and result.returncode != 0:
|
|
log.debug("Host 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))
|
|
return False
|