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 <noreply@anthropic.com>
This commit is contained in:
user
2026-02-16 22:40:43 +01:00
parent e8d803abe6
commit 35acc744ac
+49 -39
View File
@@ -3,67 +3,77 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import json
import logging import logging
import re 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 create_connection
from derp.http import urlopen as _urlopen
from derp.plugin import command from derp.plugin import command
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
_MAX_BATCH = 5 _MAX_BATCH = 5
_SMTP_TIMEOUT = 15 _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,}$") _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" def _doh_query(name: str, qtype: str) -> list[dict]:
_DNS_PORT = 9053 """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]: def _resolve_mx(domain: str) -> list[str]:
"""Resolve MX records for a domain via Tor DNS. Returns list of MX hosts.""" """Resolve MX records via DNS-over-HTTPS. Returns list of MX hosts."""
query = build_query(domain, QTYPES["MX"]) answers = _doh_query(domain, "MX")
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) if not answers:
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:
return [] return []
# MX results are "priority host" format # DoH MX data is "priority host." format
mx_hosts = [] mx_entries = []
for r in results: for ans in answers:
parts = r.split(None, 1) val = ans.get("data", "")
parts = val.split(None, 1)
if len(parts) == 2: if len(parts) == 2:
mx_hosts.append(parts[1].rstrip(".")) mx_entries.append((int(parts[0]), parts[1].rstrip(".")))
else: else:
mx_hosts.append(r.rstrip(".")) mx_entries.append((0, val.rstrip(".")))
mx_entries.sort(key=lambda x: x[0])
# Sort by priority (already parsed as "prio host") return [host for _, host in mx_entries]
return mx_hosts
def _resolve_a(domain: str) -> str | None: def _resolve_a(domain: str) -> str | None:
"""Resolve A record for a domain as fallback. Returns first IP or None.""" """Resolve A record via DNS-over-HTTPS as fallback."""
query = build_query(domain, QTYPES["A"]) answers = _doh_query(domain, "A")
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) if answers:
sock.settimeout(5) return answers[0].get("data", "")
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]
return None return None