From ebdb77144ad8070fa31bfbefd92359a0d7ec58c4 Mon Sep 17 00:00:00 2001 From: sascha Date: Sat, 5 Sep 2026 13:02:45 +0200 Subject: [PATCH] fix: validate wg candidate safely (app.py) --- app.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/app.py b/app.py index 22aa55d..613089b 100644 --- a/app.py +++ b/app.py @@ -12,7 +12,7 @@ from contextlib import asynccontextmanager from contextvars import ContextVar log = logging.getLogger("butler") -VERSION = "2.3.7" +VERSION = "2.3.8" API_DIR = os.environ.get("API_KEY_DIR", "/data/api") VAULT_CACHE_DIR = os.environ.get("VAULT_CACHE_DIR", "/data/vault-cache") @@ -2409,7 +2409,7 @@ def _media_tunnel_install_command(role: Literal["vps", "emby"], peer_public_key: +lines.extend(["", "[Peer]", "PublicKey = {peer_public_key}", "AllowedIPs = {settings['peer']}"]) +if {settings['endpoint']!r}: lines.append("Endpoint = " + {settings['endpoint']!r}) +if {settings['keepalive']!r}: lines.append("PersistentKeepalive = " + str({settings['keepalive']!r})) -+candidate = root / "wg-media.conf.candidate" ++candidate = root / "wg-media-candidate.conf" +candidate.write_text("\\n".join(lines) + "\\n") +os.chmod(candidate, 0o600) +check = subprocess.run(["wg-quick", "strip", str(candidate)], text=True, capture_output=True) @@ -2485,6 +2485,22 @@ def _media_tunnel_rollback_command(remove_keys: bool) -> str: return f'sudo -n python3 -c "import base64;exec(base64.b64decode(\'{encoded}\'))"' +def _media_tunnel_key_cleanup_command() -> str: + script = '''import json ++from pathlib import Path ++root = Path("/app-config/wireguard-media") ++if not (root / "wg-media.conf").exists(): ++ (root / "private.key").unlink(missing_ok=True) ++ (root / "public.key").unlink(missing_ok=True) ++ status = "new_keys_removed" ++else: ++ status = "kept_for_existing_config" ++print(json.dumps({"status": status})) ++'''.replace("\n+", "\n") + encoded = base64.b64encode(script.encode()).decode() + return f'sudo -n python3 -c "import base64;exec(base64.b64decode(\'{encoded}\'))"' + + def _media_host_target(name: str) -> str: inventory = _find_inventory_host(name) if not inventory: @@ -2531,6 +2547,11 @@ def _deploy_sascha_media_tunnel() -> dict: for target, created in reversed(configured): try: _ssh_json(target, _media_tunnel_rollback_command(created), 60) except Exception: pass + installed_targets = {target for target, _created in configured} + for target, key in ((vps, vps_key), (emby, emby_key)): + if target not in installed_targets and key.get("created"): + try: _ssh_json(target, _media_tunnel_key_cleanup_command(), 30) + except Exception: pass raise