fix: route blacklist and subdomain DNS through Tor resolver

Both plugins duplicated wire-format helpers and queried the system
resolver on port 53. Switch to shared derp.dns helpers and point
queries at the local Tor DNS resolver (127.0.0.1:9053) so lookups
go through Tor like all other outbound traffic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
user
2026-02-15 16:16:57 +01:00
parent 7520bba192
commit d5866a9867
3 changed files with 30 additions and 136 deletions

View File

@@ -1,13 +1,12 @@
"""Plugin: DNSBL/RBL IP reputation check (pure stdlib)."""
"""Plugin: DNSBL/RBL IP reputation check via Tor DNS resolver."""
from __future__ import annotations
import asyncio
import ipaddress
import os
import socket
import struct
from derp.dns import TOR_DNS_ADDR, TOR_DNS_PORT, build_query, parse_response
from derp.plugin import command
_DNSBLS = [
@@ -25,54 +24,16 @@ _DNSBLS = [
_TIMEOUT = 5.0
def _get_resolver() -> str:
"""Read first IPv4 nameserver from /etc/resolv.conf."""
try:
with open("/etc/resolv.conf") as f:
for line in f:
line = line.strip()
if line.startswith("nameserver"):
addr = line.split()[1]
try:
ipaddress.IPv4Address(addr)
return addr
except ValueError:
continue
except (OSError, IndexError):
pass
return "8.8.8.8"
def _build_a_query(name: str) -> bytes:
"""Build a minimal DNS A query."""
tid = os.urandom(2)
flags = struct.pack("!H", 0x0100)
counts = struct.pack("!HHHH", 1, 0, 0, 0)
encoded = b""
for label in name.rstrip(".").split("."):
encoded += bytes([len(label)]) + label.encode("ascii")
encoded += b"\x00"
return tid + flags + counts + encoded + struct.pack("!HH", 1, 1)
def _check_response(data: bytes) -> bool:
"""Check if DNS response has answer records (listed)."""
if len(data) < 12:
return False
_, flags, _, ancount = struct.unpack_from("!HHHH", data, 0)
rcode = flags & 0x0F
return rcode == 0 and ancount > 0
def _query_dnsbl(name: str, server: str) -> bool:
"""Blocking DNS A lookup, returns True if listed."""
query = _build_a_query(name)
def _query_dnsbl(name: str) -> bool:
"""Blocking DNS A lookup via Tor resolver, returns True if listed."""
query = build_query(name, 1)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(_TIMEOUT)
try:
sock.sendto(query, (server, 53))
sock.sendto(query, (TOR_DNS_ADDR, TOR_DNS_PORT))
data = sock.recv(512)
return _check_response(data)
rcode, results = parse_response(data)
return rcode == 0 and len(results) > 0
except (socket.timeout, OSError):
return False
finally:
@@ -84,12 +45,12 @@ def _reversed_ip(addr: str) -> str:
return ".".join(reversed(addr.split(".")))
async def _check_one(ip_rev: str, zone: str, label: str,
server: str) -> tuple[str, bool]:
async def _check_one(ip_rev: str, zone: str,
label: str) -> tuple[str, bool]:
"""Check one DNSBL asynchronously."""
name = f"{ip_rev}.{zone}"
loop = asyncio.get_running_loop()
listed = await loop.run_in_executor(None, _query_dnsbl, name, server)
listed = await loop.run_in_executor(None, _query_dnsbl, name)
return label, listed
@@ -117,9 +78,8 @@ async def cmd_blacklist(bot, message):
return
ip_rev = _reversed_ip(str(ip))
server = _get_resolver()
tasks = [_check_one(ip_rev, zone, label, server) for zone, label in _DNSBLS]
tasks = [_check_one(ip_rev, zone, label) for zone, label in _DNSBLS]
results = await asyncio.gather(*tasks)
listed = [label for label, hit in results if hit]