"""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, )