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:
@@ -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})"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user