VPN-aware leak detection, GOST stderr logging
- _is_same_network: /16 subnet comparison catches NordVPN IP rotation (exit_ip != real_ip exact-match missed same-VPN exits on rotated IPs) - Leak now blacklists the entire dead chain, not just rotate - GOST stderr/stdout -> gost.log (file, never deadlocks vs pipe) with 512KB rotation; last 8 lines shown in UI when GOST dies - read_gost_log_tail helper for live debugging - Health check uses same subnet check for consistency - 5 new subnet tests Made-with: Cursor
This commit is contained in:
@@ -6,10 +6,11 @@ import shutil
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from .paths import gost_exe_path
|
||||
from .paths import app_data_dir, gost_exe_path
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -71,19 +72,48 @@ def build_gost_cmd(gost: Path, listen_http: str, forwards: list[str]) -> list[st
|
||||
return args
|
||||
|
||||
|
||||
def gost_log_path() -> Path:
|
||||
return app_data_dir() / "gost.log"
|
||||
|
||||
|
||||
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.
|
||||
# stderr → rolling log file (not a pipe — pipes deadlock if unread; files never block).
|
||||
# stdout is also captured to the same file since GOST v3 mixes output channels.
|
||||
cr = getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
||||
log_path = gost_log_path()
|
||||
try:
|
||||
_rotate_gost_log(log_path)
|
||||
stderr_dest: Any = open(log_path, "ab") # noqa: WPS515 — intentional long-lived file handle
|
||||
except OSError:
|
||||
stderr_dest = subprocess.DEVNULL
|
||||
return subprocess.Popen(
|
||||
args,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
stdout=stderr_dest,
|
||||
stderr=stderr_dest,
|
||||
creationflags=cr,
|
||||
)
|
||||
|
||||
|
||||
def _rotate_gost_log(path: Path, max_bytes: int = 512 * 1024) -> None:
|
||||
"""Keep gost.log under max_bytes by truncating the oldest half when it grows too large."""
|
||||
try:
|
||||
if path.is_file() and path.stat().st_size > max_bytes:
|
||||
data = path.read_bytes()
|
||||
path.write_bytes(data[len(data) // 2:])
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def read_gost_log_tail(lines: int = 40) -> str:
|
||||
"""Return the last N lines of gost.log for display in the UI."""
|
||||
try:
|
||||
text = gost_log_path().read_text(encoding="utf-8", errors="replace")
|
||||
return "\n".join(text.splitlines()[-lines:])
|
||||
except OSError:
|
||||
return "(gost.log not found)"
|
||||
|
||||
|
||||
def terminate_process(proc: subprocess.Popen | None) -> None:
|
||||
if proc is None:
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user