From 35acc744ac4613b9ba06bc21ae0691a9f28e1cbc Mon Sep 17 00:00:00 2001 From: user Date: Mon, 16 Feb 2026 22:40:43 +0100 Subject: [PATCH] fix: use DNS-over-HTTPS with provider rotation for emailcheck Tor exit nodes poison plain DNS on port 53 and MITM some HTTPS connections. Replace raw TCP DNS with DoH (Google, Cloudflare, Quad9) and retry up to 5 times across providers to find a clean exit node. MX results are now sorted by priority. Co-Authored-By: Claude Opus 4.6 --- plugins/emailcheck.py | 88 ++++++++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 39 deletions(-) diff --git a/plugins/emailcheck.py b/plugins/emailcheck.py index ae0c23b..d007470 100644 --- a/plugins/emailcheck.py +++ b/plugins/emailcheck.py @@ -3,67 +3,77 @@ from __future__ import annotations import asyncio +import json import logging import re -import socket +import urllib.request -from derp.dns import QTYPES, build_query, parse_response from derp.http import create_connection +from derp.http import urlopen as _urlopen from derp.plugin import command log = logging.getLogger(__name__) _MAX_BATCH = 5 _SMTP_TIMEOUT = 15 +_DOH_PROVIDERS = [ + "https://dns.google/resolve", + "https://cloudflare-dns.com/dns-query", + "https://dns.quad9.net:5053/dns-query", +] +_DOH_ATTEMPTS = 5 _EMAIL_RE = re.compile(r"^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$") -# Tor DNS resolver for MX lookups -_DNS_ADDR = "10.200.1.13" -_DNS_PORT = 9053 + +def _doh_query(name: str, qtype: str) -> list[dict]: + """Query DNS-over-HTTPS via SOCKS5 proxy with provider rotation. + + Retries across multiple DoH providers to work around unreliable + Tor exit nodes that drop or MITM HTTPS connections. + """ + last_err = None + for attempt in range(_DOH_ATTEMPTS): + provider = _DOH_PROVIDERS[attempt % len(_DOH_PROVIDERS)] + url = f"{provider}?name={name}&type={qtype}" + req = urllib.request.Request(url, headers={ + "Accept": "application/dns-json", + "User-Agent": "derp-bot", + }) + try: + with _urlopen(req, timeout=10) as resp: + data = json.loads(resp.read()) + return data.get("Answer", []) + except Exception as exc: + last_err = exc + log.debug("emailcheck: DoH attempt %d/%d failed (%s): %s", + attempt + 1, _DOH_ATTEMPTS, provider, exc) + raise last_err # type: ignore[misc] def _resolve_mx(domain: str) -> list[str]: - """Resolve MX records for a domain via Tor DNS. Returns list of MX hosts.""" - query = build_query(domain, QTYPES["MX"]) - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.settimeout(5) - try: - sock.sendto(query, (_DNS_ADDR, _DNS_PORT)) - data, _ = sock.recvfrom(4096) - finally: - sock.close() - - rcode, results = parse_response(data) - if rcode != 0 or not results: + """Resolve MX records via DNS-over-HTTPS. Returns list of MX hosts.""" + answers = _doh_query(domain, "MX") + if not answers: return [] - # MX results are "priority host" format - mx_hosts = [] - for r in results: - parts = r.split(None, 1) + # DoH MX data is "priority host." format + mx_entries = [] + for ans in answers: + val = ans.get("data", "") + parts = val.split(None, 1) if len(parts) == 2: - mx_hosts.append(parts[1].rstrip(".")) + mx_entries.append((int(parts[0]), parts[1].rstrip("."))) else: - mx_hosts.append(r.rstrip(".")) - - # Sort by priority (already parsed as "prio host") - return mx_hosts + mx_entries.append((0, val.rstrip("."))) + mx_entries.sort(key=lambda x: x[0]) + return [host for _, host in mx_entries] def _resolve_a(domain: str) -> str | None: - """Resolve A record for a domain as fallback. Returns first IP or None.""" - query = build_query(domain, QTYPES["A"]) - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.settimeout(5) - try: - sock.sendto(query, (_DNS_ADDR, _DNS_PORT)) - data, _ = sock.recvfrom(4096) - finally: - sock.close() - - rcode, results = parse_response(data) - if rcode == 0 and results: - return results[0] + """Resolve A record via DNS-over-HTTPS as fallback.""" + answers = _doh_query(domain, "A") + if answers: + return answers[0].get("data", "") return None