Merge branch 'merge-rehearsal' into dev

# Conflicts:
#	decnet/templates/postgres/server.py
#	decnet/templates/rdp/Dockerfile
#	decnet/templates/redis/Dockerfile
#	decnet/templates/smtp/Dockerfile
#	decnet/templates/smtp/entrypoint.sh
#	decnet/templates/snmp/Dockerfile
#	decnet/templates/snmp/entrypoint.sh
#	decnet/templates/tftp/Dockerfile
#	decnet/templates/tftp/entrypoint.sh
#	decnet/templates/vnc/Dockerfile
#	decnet/templates/vnc/entrypoint.sh
#	templates/rdp/Dockerfile
#	templates/smb/Dockerfile
#	templates/smtp/Dockerfile
#	templates/smtp/entrypoint.sh
#	templates/snmp/Dockerfile
#	templates/snmp/entrypoint.sh
#	templates/tftp/Dockerfile
#	templates/tftp/entrypoint.sh
#	templates/vnc/Dockerfile
#	tests/services/test_smtp_relay.py
This commit is contained in:
2026-05-01 02:27:20 -04:00
299 changed files with 13964 additions and 1487 deletions

View File

@@ -303,11 +303,44 @@ def remove_bridge_network(client: docker.DockerClient, name: str) -> None:
# Host-side macvlan interface (hairpin fix)
# ---------------------------------------------------------------------------
def _require_root() -> None:
if os.geteuid() != 0:
raise PermissionError(
"MACVLAN host-side interface setup requires root. Run with sudo."
)
# Linux capability bit positions — see capabilities(7).
_CAP_NET_ADMIN = 12
def _has_cap_net_admin() -> bool:
"""True if the current process holds CAP_NET_ADMIN in its effective set.
Reads ``/proc/self/status`` rather than calling ``capget(2)`` so we
don't need a libcap dependency. ``CapEff`` is a 64-bit hex bitmask;
bit 12 is CAP_NET_ADMIN.
"""
try:
with open("/proc/self/status", "r") as fh:
for line in fh:
if line.startswith("CapEff:"):
bits = int(line.split()[1], 16)
return bool(bits & (1 << _CAP_NET_ADMIN))
except OSError:
pass
return False
def _require_net_admin() -> None:
"""Reject early if the process can't run ``ip link add ... macvlan``.
CAP_NET_ADMIN is what the kernel actually checks for netlink RTM_NEWLINK
of a macvlan/ipvlan slave; euid==0 is sufficient (it grants every cap)
but not necessary. Prefer the cap check so the systemd unit's
``AmbientCapabilities=CAP_NET_ADMIN`` is honoured without forcing the
whole API to run as root.
"""
if os.geteuid() == 0 or _has_cap_net_admin():
return
raise PermissionError(
"MACVLAN host-side interface setup needs CAP_NET_ADMIN. "
"Either run as root or grant the cap (systemd: "
"AmbientCapabilities=CAP_NET_ADMIN)."
)
def setup_host_macvlan(interface: str, host_macvlan_ip: str, decky_ip_range: str) -> None:
@@ -317,7 +350,9 @@ def setup_host_macvlan(interface: str, host_macvlan_ip: str, decky_ip_range: str
host-helper first: the two drivers can share a parent NIC on paper but
leaving the opposite helper in place is just cruft after a driver swap.
"""
_require_root()
_require_net_admin()
_run(["ip", "link", "del", HOST_IPVLAN_IFACE], check=False)
_run(["ip", "link", "del", HOST_IPVLAN_IFACE], check=False)
@@ -332,7 +367,7 @@ def setup_host_macvlan(interface: str, host_macvlan_ip: str, decky_ip_range: str
def teardown_host_macvlan(decky_ip_range: str) -> None:
_require_root()
_require_net_admin()
_run(["ip", "route", "del", decky_ip_range, "dev", HOST_MACVLAN_IFACE], check=False)
_run(["ip", "link", "del", HOST_MACVLAN_IFACE], check=False)
@@ -344,7 +379,9 @@ def setup_host_ipvlan(interface: str, host_ipvlan_ip: str, decky_ip_range: str)
host-helper first so a prior macvlan deploy doesn't leave its slave
dangling on the parent NIC after the driver swap.
"""
_require_root()
_require_net_admin()
_run(["ip", "link", "del", HOST_MACVLAN_IFACE], check=False)
_run(["ip", "link", "del", HOST_MACVLAN_IFACE], check=False)
@@ -358,7 +395,7 @@ def setup_host_ipvlan(interface: str, host_ipvlan_ip: str, decky_ip_range: str)
def teardown_host_ipvlan(decky_ip_range: str) -> None:
_require_root()
_require_net_admin()
_run(["ip", "route", "del", decky_ip_range, "dev", HOST_IPVLAN_IFACE], check=False)
_run(["ip", "link", "del", HOST_IPVLAN_IFACE], check=False)
@@ -378,3 +415,47 @@ def ips_to_range(ips: list[str]) -> str:
strict=False,
)
return str(network)
# ---------------------------------------------------------------------------
# Container veth resolution (for tc netem tarpit)
# ---------------------------------------------------------------------------
def get_container_pid(container_name: str) -> int:
"""Return the PID of a running container's init process."""
client = docker.from_env()
try:
container = client.containers.get(container_name)
except docker.errors.NotFound:
raise LookupError(f"container {container_name!r} not found")
pid = container.attrs["State"]["Pid"]
if not pid:
raise LookupError(f"container {container_name!r} is not running (PID=0)")
return pid
def get_container_veth(container_name: str) -> str:
"""Return the host veth interface name paired to container_name's eth0.
Reads /sys/class/net/eth0/iflink from inside the container to get the
peer interface index, then matches it against ``ip link show`` on the host.
Requires no nsenter and no elevated privileges beyond what Docker exec grants.
"""
result = _run(
["docker", "exec", container_name, "cat", "/sys/class/net/eth0/iflink"],
check=False,
)
if result.returncode != 0:
raise LookupError(
f"container {container_name!r} not reachable: {result.stderr.strip()}"
)
peer_index = result.stdout.strip()
links = _run(["ip", "link", "show"])
for line in links.stdout.splitlines():
if line.startswith(f"{peer_index}:"):
# Format: "42: veth3a4b5c@if41: <BROADCAST,...>"
iface = line.split(":")[1].strip().split("@")[0]
return iface
raise LookupError(
f"no host veth found for container {container_name!r} (peer ifindex {peer_index})"
)