from __future__ import annotations import io import logging import shutil import subprocess import zipfile from pathlib import Path import httpx from .paths import gost_exe_path log = logging.getLogger(__name__) GOST_RELEASE_ZIP = ( "https://github.com/go-gost/gost/releases/download/v3.2.6/" "gost_3.2.6_windows_amd64.zip" ) def _add_defender_exclusion(path: Path) -> None: """Add Windows Defender exclusion so GOST is not quarantined.""" try: subprocess.run( ["powershell", "-NoProfile", "-NonInteractive", "-Command", f"Add-MpPreference -ExclusionPath '{path.parent}' -ExclusionProcess 'gost.exe' -ErrorAction SilentlyContinue"], capture_output=True, timeout=30, creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), ) log.info("Defender exclusion added for %s", path.parent) except Exception: pass # non-fatal def ensure_gost(target: Path | None = None) -> Path: exe = target or gost_exe_path() if exe.is_file() and exe.stat().st_size > 10_000: return exe exe.parent.mkdir(parents=True, exist_ok=True) # Add exclusion BEFORE downloading so Defender doesn't nuke it on write _add_defender_exclusion(exe) log.info("Downloading GOST %s", GOST_RELEASE_ZIP) with httpx.Client(timeout=120.0, follow_redirects=True) as c: r = c.get(GOST_RELEASE_ZIP) r.raise_for_status() data = r.content if len(data) < 64 or data[:2] != b"PK": raise RuntimeError("Downloaded GOST zip looks invalid (not a zip).") with zipfile.ZipFile(io.BytesIO(data), "r") as z: names = [n for n in z.namelist() if n.lower().endswith("gost.exe")] if not names: raise RuntimeError("gost.exe not found in release zip") with z.open(names[0]) as src, open(exe, "wb") as dst: shutil.copyfileobj(src, dst) # Add exclusion again after write in case Defender scanned during extraction _add_defender_exclusion(exe) log.info("GOST installed at %s", exe) return exe def build_gost_cmd(gost: Path, listen_http: str, forwards: list[str]) -> list[str]: args = [str(gost), "-L", f"http://{listen_http}"] for f in forwards: args.extend(["-F", f]) return args def popen_no_window(args: list[str]) -> subprocess.Popen: # Use DEVNULL for both pipes — GOST writes logs to stderr, leaving pipes # unread would fill the OS buffer and deadlock the child process. cr = getattr(subprocess, "CREATE_NO_WINDOW", 0) return subprocess.Popen( args, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, creationflags=cr, ) def terminate_process(proc: subprocess.Popen | None) -> None: if proc is None: return if proc.poll() is not None: return try: proc.terminate() proc.wait(timeout=5) except Exception: try: proc.kill() except Exception: pass