Harden resolver and expand traffic runtime controls

This commit is contained in:
beckline
2026-02-24 00:17:46 +03:00
parent 89eaaf3f23
commit 50518a641d
18 changed files with 2048 additions and 181 deletions

View File

@@ -105,6 +105,74 @@ def infer_app_key(cmdline: str) -> str:
return canonicalize_app_key("", cmdline)
def browser_harden_enabled() -> bool:
raw = str(os.environ.get("SVPN_BROWSER_HARDEN", "1") or "1").strip().lower()
return raw not in ("0", "false", "no", "off")
def is_chromium_like_cmd(tokens: list[str]) -> bool:
toks = [str(x or "").strip() for x in (tokens or []) if str(x or "").strip()]
if not toks:
return False
exe = os.path.basename(toks[0]).lower()
known = {
"google-chrome",
"google-chrome-stable",
"chromium",
"chromium-browser",
"microsoft-edge",
"microsoft-edge-stable",
"brave",
"brave-browser",
"opera",
"opera-beta",
"opera-developer",
"vivaldi",
"vivaldi-stable",
}
if exe in known:
return True
if any(x in exe for x in ("chrome", "chromium", "edge", "brave", "opera", "vivaldi")):
return True
if exe == "flatpak":
for i, t in enumerate(toks):
if t == "run":
for cand in toks[i + 1:]:
c = cand.strip().lower()
if not c or c.startswith("-") or c == "--":
continue
return any(x in c for x in ("chrome", "chromium", "edge", "brave", "opera", "vivaldi"))
break
return False
def maybe_harden_browser_cmdline(cmdline: str) -> str:
raw = (cmdline or "").strip()
if not raw or not browser_harden_enabled():
return raw
try:
toks = shlex.split(raw)
except Exception:
return raw
if not is_chromium_like_cmd(toks):
return raw
flags = [
"--disable-quic",
"--force-webrtc-ip-handling-policy=disable_non_proxied_udp",
]
low = [t.lower() for t in toks]
changed = False
for fl in flags:
fl_low = fl.lower()
if any(t == fl_low or t.startswith(fl_low + "=") for t in low):
continue
toks.append(fl)
changed = True
if not changed:
return raw
return " ".join(shlex.quote(t) for t in toks)
def canonicalize_app_key(app_key: str, cmdline: str) -> str:
key = (app_key or "").strip()
cmd = (cmdline or "").strip()
@@ -181,6 +249,19 @@ def systemctl_user(args: list[str], *, timeout: float = 4.0) -> tuple[int, str]:
out = ((p.stdout or "") + (p.stderr or "")).strip()
return int(p.returncode or 0), out
def stop_user_unit_best_effort(unit: str) -> tuple[bool, str]:
u = (unit or "").strip()
if not u:
return False, "empty unit"
code, out = systemctl_user(["stop", u], timeout=4.0)
if code == 0:
return True, out
code2, out2 = systemctl_user(["kill", u], timeout=4.0)
if code2 == 0:
return True, out2
msg = (out2 or out or f"stop/kill failed for {u}").strip()
return False, msg
def cgroup_path_from_pid(pid: int) -> str:
p = int(pid or 0)
@@ -246,7 +327,13 @@ def run_systemd_unit(cmdline: str, *, unit: str) -> str:
if p.returncode != 0:
raise RuntimeError(f"systemd-run failed: rc={p.returncode}\n{out}".strip())
cg = effective_cgroup_for_unit(unit, timeout_sec=3.0)
try:
cg = effective_cgroup_for_unit(unit, timeout_sec=3.0)
except Exception as e:
stopped, stop_msg = stop_user_unit_best_effort(unit)
if stopped:
raise RuntimeError(f"{e}\n\nUnit was stopped (fail-closed): {unit}") from e
raise RuntimeError(f"{e}\n\nWARNING: failed to stop unit {unit}: {stop_msg}") from e
return cg
@@ -307,7 +394,8 @@ def apply_mark(*, target: str, cgroup: str, unit: str, command: str, app_key: st
res = api_request("POST", "/api/v1/traffic/appmarks", json_body=payload, timeout=4.0)
if not bool(res.get("ok", False)):
raise RuntimeError(f"appmark failed: {res.get('message')}")
log(f"mark added: target={target} app={app_key} unit={unit} cgroup_id={res.get('cgroup_id')} ttl={res.get('timeout_sec')}")
ttl_txt = "persistent" if int(res.get("timeout_sec", 0) or 0) <= 0 else f"{int(res.get('timeout_sec', 0) or 0)}s"
log(f"mark added: target={target} app={app_key} unit={unit} cgroup_id={res.get('cgroup_id')} ttl={ttl_txt}")
def main(argv: list[str]) -> int:
@@ -322,27 +410,36 @@ def main(argv: list[str]) -> int:
cmd = str(prof.get("command") or "").strip()
if not cmd:
raise RuntimeError("profile command is empty")
run_cmd = maybe_harden_browser_cmdline(cmd)
if run_cmd != cmd:
log("browser hardening: added anti-leak flags")
target = str(prof.get("target") or "vpn").strip().lower()
if target not in ("vpn", "direct"):
target = "vpn"
app_key_raw = str(prof.get("app_key") or "").strip()
app_key = canonicalize_app_key(app_key_raw, cmd) or canonicalize_app_key("", cmd)
app_key = canonicalize_app_key(app_key_raw, run_cmd) or canonicalize_app_key("", run_cmd)
ttl = int(prof.get("ttl_sec", 0) or 0)
if ttl <= 0:
ttl = 24 * 60 * 60
if ttl < 0:
ttl = 0
# Try refresh first if already running.
if refresh_if_running(target=target, app_key=app_key, command=cmd, ttl_sec=ttl):
if refresh_if_running(target=target, app_key=app_key, command=run_cmd, ttl_sec=ttl):
if args.json:
print(json.dumps({"ok": True, "op": "refresh", "id": pid, "target": target, "app_key": app_key}))
return 0
unit = f"svpn-{target}-{int(time.time())}.service"
log(f"launching profile id={pid} target={target} app={app_key} unit={unit}")
cg = run_systemd_unit(cmd, unit=unit)
cg = run_systemd_unit(run_cmd, unit=unit)
log(f"ControlGroup: {cg}")
apply_mark(target=target, cgroup=cg, unit=unit, command=cmd, app_key=app_key, ttl_sec=ttl)
try:
apply_mark(target=target, cgroup=cg, unit=unit, command=run_cmd, app_key=app_key, ttl_sec=ttl)
except Exception as e:
stopped, stop_msg = stop_user_unit_best_effort(unit)
if stopped:
raise RuntimeError(f"{e}\n\nUnit was stopped (fail-closed): {unit}") from e
raise RuntimeError(f"{e}\n\nWARNING: failed to stop unit {unit}: {stop_msg}") from e
if args.json:
print(json.dumps({"ok": True, "op": "run", "id": pid, "target": target, "app_key": app_key, "unit": unit}))
return 0