Adds VPN-aware leak handling, chain testing UX improvements, hardened Firefox launch/profile management, privacy/device hardening modules, and tray/status upgrades so the app is production-ready as the new baseline. Co-authored-by: Cursor <cursoragent@cursor.com>
113 lines
3.4 KiB
Python
113 lines
3.4 KiB
Python
"""DNS leak checks and cache flush."""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import subprocess
|
|
from dataclasses import dataclass
|
|
|
|
import httpx
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
_DOH_GOOGLE = "https://dns.google/resolve?name=whoami.dnsleaktest.com&type=A"
|
|
|
|
|
|
@dataclass
|
|
class DnsLeakResult:
|
|
ok: bool
|
|
system_resolvers: list[str]
|
|
message: str
|
|
doh_ip: str | None = None
|
|
|
|
|
|
def get_system_dns_servers() -> list[str]:
|
|
try:
|
|
r = subprocess.run(
|
|
["powershell", "-NoProfile", "-Command",
|
|
"(Get-DnsClientServerAddress -AddressFamily IPv4 | "
|
|
"Where-Object { $_.ServerAddresses } | "
|
|
"Select-Object -ExpandProperty ServerAddresses) -join ','"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=12,
|
|
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
|
)
|
|
raw = (r.stdout or "").strip()
|
|
if not raw:
|
|
return []
|
|
return [x.strip() for x in raw.replace(";", ",").split(",") if x.strip()]
|
|
except Exception:
|
|
return []
|
|
|
|
|
|
def flush_dns_cache() -> tuple[bool, str]:
|
|
try:
|
|
r = subprocess.run(
|
|
["ipconfig", "/flushdns"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=15,
|
|
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
|
)
|
|
msg = (r.stdout or r.stderr or "").strip().splitlines()[-1] if r.returncode == 0 else "flush failed"
|
|
return r.returncode == 0, msg
|
|
except Exception as e:
|
|
return False, str(e)
|
|
|
|
|
|
def check_dns_leak_hint(local_proxy: str | None = None) -> DnsLeakResult:
|
|
"""Heuristic: list configured DNS servers; note if any are public ISP resolvers.
|
|
|
|
Full DNS leak testing needs OS-level routing; this flags obvious misconfig.
|
|
"""
|
|
resolvers = get_system_dns_servers()
|
|
private_prefixes = ("127.", "10.", "192.168.", "172.16.", "172.17.", "172.18.",
|
|
"172.19.", "172.2", "172.30.", "172.31.", "0.0.0.0")
|
|
public = [r for r in resolvers if not any(r.startswith(p) for p in private_prefixes)]
|
|
|
|
doh_ip: str | None = None
|
|
try:
|
|
with httpx.Client(timeout=8.0, verify=True) as c:
|
|
r = c.get(_DOH_GOOGLE)
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
answers = data.get("Answer") or []
|
|
if answers:
|
|
doh_ip = str(answers[0].get("data", ""))
|
|
except Exception:
|
|
pass
|
|
|
|
if not resolvers:
|
|
return DnsLeakResult(
|
|
ok=True,
|
|
system_resolvers=[],
|
|
message="No IPv4 DNS servers reported (DHCP may assign later).",
|
|
doh_ip=doh_ip,
|
|
)
|
|
|
|
if public and local_proxy:
|
|
return DnsLeakResult(
|
|
ok=False,
|
|
system_resolvers=resolvers,
|
|
message=(
|
|
f"DNS may bypass proxy chain: public resolvers {', '.join(public)}. "
|
|
"Use kill-switch + VPN, or set DNS to localhost when hardened."
|
|
),
|
|
doh_ip=doh_ip,
|
|
)
|
|
|
|
if public and not local_proxy:
|
|
return DnsLeakResult(
|
|
ok=False,
|
|
system_resolvers=resolvers,
|
|
message=f"Public DNS resolvers active: {', '.join(public)}",
|
|
doh_ip=doh_ip,
|
|
)
|
|
|
|
return DnsLeakResult(
|
|
ok=True,
|
|
system_resolvers=resolvers,
|
|
message=f"DNS servers: {', '.join(resolvers)}",
|
|
doh_ip=doh_ip,
|
|
)
|