vpn: require an active WireGuard handshake before downloading (refuse a dead tunnel)

This commit is contained in:
Konstantin Passig PC
2026-08-25 17:06:27 +02:00
parent b5d56d4d34
commit 85ae1fb184
4 changed files with 170 additions and 3 deletions
+47 -2
View File
@@ -137,6 +137,31 @@ def is_up(cfg) -> bool:
return rc == 0 and "no such device" not in out.lower()
def handshake_age(cfg) -> int | None:
"""Seconds since the last WireGuard handshake, or None if none/unknown."""
ns, iface = netns_name(cfg), iface_name(cfg)
rc, out = _sudo_out(["ip", "netns", "exec", ns, "wg", "show", iface])
if rc != 0:
return None
for line in out.splitlines():
m = re.search(r"latest handshake:\s*(.+)", line)
if not m:
continue
text = m.group(1).strip().lower()
if text == "no handshake":
return None
m2 = re.match(r"(\d+)\s+seconds? ago", text)
if m2:
return int(m2.group(1))
return None
def has_handshake(cfg, max_age: int = 180) -> bool:
"""True when the tunnel has a recent handshake (i.e. is actually usable)."""
age = handshake_age(cfg)
return age is not None and age <= max_age
def up(cfg) -> None:
ns, iface = netns_name(cfg), iface_name(cfg)
host_link = _host_link(iface)
@@ -195,12 +220,29 @@ def down(cfg) -> None:
_sudo(["rm", "-rf", f"/etc/netns/{ns}"])
def wait_for_handshake(cfg, timeout: float = 15.0) -> bool:
import time
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if has_handshake(cfg):
return True
time.sleep(0.5)
return has_handshake(cfg)
def ensure_up(cfg) -> None:
if is_up(cfg):
if has_handshake(cfg):
return
log.info("Mullvad tunnel is down — bringing it up")
log.info("Mullvad tunnel is down or not handshaken — bringing it up")
down(cfg) # converge from any stale half-configured state
up(cfg)
if not wait_for_handshake(cfg):
down(cfg)
raise RuntimeError(
"WireGuard tunnel came up but no handshake was established — "
"check the endpoint/reachability and that the tunnel is actually "
"active. Refusing to download over a dead tunnel."
)
def status_text(cfg) -> str:
@@ -210,6 +252,9 @@ def status_text(cfg) -> str:
return "tunnel: DOWN (namespace or WireGuard interface missing)"
lines = ["tunnel: UP", f"namespace : {ns}"]
lines.extend(line for line in out.splitlines())
age = handshake_age(cfg)
lines.append("handshake : " + ("usable" if has_handshake(cfg)
else ("none" if age is None else f"{age}s ago (stale)")))
_, route = _sudo_out(["ip", "netns", "exec", ns, "ip", "route", "show", "default"])
lines.append("default : " + (route or "(none)"))
_, dns = _sudo_out(["cat", f"/etc/netns/{ns}/resolv.conf"])