import subprocess from conftest import LIBEXEC SCRIPT_SANDBOX_HELPER = LIBEXEC / "left4me-script-sandbox" def test_script_sandbox_helper_present(): assert SCRIPT_SANDBOX_HELPER.is_file() assert SCRIPT_SANDBOX_HELPER.read_text().startswith("#!/bin/bash") mode = SCRIPT_SANDBOX_HELPER.stat().st_mode & 0o777 assert mode == 0o755, f"expected 0755, got {oct(mode)}" def test_script_sandbox_helper_passes_shell_syntax_check(): subprocess.run(["bash", "-n", str(SCRIPT_SANDBOX_HELPER)], check=True) def test_script_sandbox_helper_invokes_systemd_run_with_hardening(): text = SCRIPT_SANDBOX_HELPER.read_text() # systemd-run service mode (no --scope), with synchronous I/O to caller. assert "systemd-run" in text assert "--scope" not in text, "v2 uses transient service units, not scopes" assert "--pipe" in text assert "--wait" in text assert "--collect" in text assert "--unit=" in text # No bwrap. assert "bwrap" not in text assert "bubblewrap" not in text # UID drop via systemd directives. assert "User=left4me" in text assert "Group=left4me" in text # Cgroup limits unchanged from v1. assert "MemoryMax=4G" in text assert "MemorySwapMax=0" in text assert "TasksMax=512" in text assert "CPUQuota=200%" in text assert "RuntimeMaxSec=3600" in text # Hardening directives that v1 (scope mode) couldn't carry. assert "NoNewPrivileges=yes" in text assert "ProtectSystem=strict" in text assert "ProtectHome=yes" in text assert "PrivateTmp=yes" in text assert "PrivateDevices=yes" in text assert "PrivateIPC=yes" in text assert "ProtectKernelTunables=yes" in text assert "ProtectKernelModules=yes" in text assert "ProtectKernelLogs=yes" in text assert "ProtectControlGroups=yes" in text assert "RestrictNamespaces=yes" in text assert "RestrictSUIDSGID=yes" in text assert "LockPersonality=yes" in text assert "MemoryDenyWriteExecute=yes" in text assert "SystemCallFilter=" in text assert "@system-service" in text assert "@network-io" in text assert "CapabilityBoundingSet=" in text assert "AmbientCapabilities=" in text assert 'RestrictAddressFamilies="AF_INET AF_INET6 AF_UNIX"' in text # Network namespace stays shared with host. assert "PrivateNetwork=" not in text # Mount setup: /etc and /var/lib masked with tmpfs; selective binds back. assert 'TemporaryFileSystem="/etc /var/lib"' in text assert "BindReadOnlyPaths=" in text # The resolv.conf bind points at the sandbox-only file (not the host's # /etc/resolv.conf, which typically references a private-IP DNS server # that IPAddressDeny= blocks). assert "/etc/left4me/sandbox-resolv.conf:/etc/resolv.conf" in text assert "/etc/ssl" in text assert "/etc/ca-certificates" in text assert "/etc/nsswitch.conf" in text assert "/etc/alternatives" in text assert "${SCRIPT}:/script.sh" in text assert 'BindPaths="${OVERLAY_DIR}:/overlay"' in text # IP egress filter: allow public, deny localhost / RFC1918 / link-local / # multicast / CGNAT / ULA. systemd's "more specific rule wins" semantics # mean public IPs hit the allow and listed ranges hit the deny. # IPAddressDeny alone — no IPAddressAllow=any. Empirically, having both # set causes the allow to win on this systemd/kernel combo regardless of # the documented "more specific rule wins" behaviour. With only Deny, # the kernel's default "allow all" applies to non-listed addresses. assert "IPAddressDeny=" in text assert "IPAddressAllow=any" not in text # Explicit CIDRs — systemd-run's -p parser doesn't accept the # `localhost` / `link-local` / `multicast` shorthand keywords that # work in unit files (only the full strings parse). for token in ( "127.0.0.0/8", "::1/128", "169.254.0.0/16", "fe80::/10", "224.0.0.0/4", "ff00::/8", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "100.64.0.0/10", "fc00::/7", ): assert token in text, f"missing {token!r} in IPAddressDeny set" def test_script_sandbox_in_build_slice_with_oom_adjust(): text = SCRIPT_SANDBOX_HELPER.read_text() # Put the transient unit in the low-weight build slice so it yields to # game-server instances under CPU/IO contention. assert "--slice=l4d2-build.slice" in text # Sandbox dies first if the host hits memory pressure; servers # (OOMScoreAdjust=-200) survive. assert "-p OOMScoreAdjust=500" in text def test_script_sandbox_helper_validates_overlay_id(): text = SCRIPT_SANDBOX_HELPER.read_text() # Numeric-only overlay id assert '[[ "$OVERLAY_ID" =~ ^[0-9]+$ ]]' in text # Overlay dir must exist assert "/var/lib/left4me/overlays/" in text assert "[[ -d $OVERLAY_DIR ]]" in text # Script path must exist assert "[[ -f $SCRIPT ]]" in text def test_script_sandbox_helper_dry_run_mode(tmp_path): overlay_root = tmp_path / "var/lib/left4me/overlays/42" overlay_root.mkdir(parents=True) fake_script = tmp_path / "fake.sh" fake_script.write_text("echo hi") helper_text = SCRIPT_SANDBOX_HELPER.read_text() # We can't actually exec this without root; just verify the dry-run # guard short-circuits before systemd-run runs. assert 'LEFT4ME_SCRIPT_SANDBOX_DRY_RUN' in helper_text assert 'exit 0' in helper_text