vpn: require an active WireGuard handshake before downloading (refuse a dead tunnel)
This commit is contained in:
+47
-2
@@ -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"])
|
||||
|
||||
Reference in New Issue
Block a user