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:
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user