Files
derp/plugins/dns.py
user 26063a0e8f feat: add TCP DNS plugin with SOCKS5 proxy support
Extract shared DNS wire-format helpers into src/derp/dns.py so both
the UDP plugin (dns.py) and the new TCP plugin (tdns.py) share the
same encode/decode/build/parse logic.

The !tdns command routes queries through the SOCKS5 proxy via
derp.http.open_connection, using TCP framing (2-byte length prefix).
Default server: 1.1.1.1.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 16:09:35 +01:00

89 lines
2.6 KiB
Python

"""Plugin: DNS record lookup (raw UDP, pure stdlib)."""
from __future__ import annotations
import asyncio
import ipaddress
import socket
from derp.dns import (
QTYPES,
RCODES,
build_query,
get_resolver,
parse_response,
reverse_name,
)
from derp.plugin import command
async def _query(name: str, qtype: int, server: str,
timeout: float = 5.0) -> tuple[int, list[str]]:
"""Send a DNS query over UDP and return (rcode, [values])."""
query = build_query(name, qtype)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(timeout)
loop = asyncio.get_running_loop()
try:
await loop.run_in_executor(None, sock.sendto, query, (server, 53))
data = await asyncio.wait_for(
loop.run_in_executor(None, sock.recv, 4096),
timeout=timeout,
)
return parse_response(data)
except (TimeoutError, socket.timeout):
return -1, []
except OSError:
return -2, []
finally:
sock.close()
@command("dns", help="DNS lookup: !dns <target> [A|AAAA|MX|NS|TXT|CNAME|PTR|SOA]")
async def cmd_dns(bot, message):
"""Query DNS records for a domain or reverse-lookup an IP."""
parts = message.text.split(None, 3)
if len(parts) < 2:
await bot.reply(message, "Usage: !dns <domain|ip> [type]")
return
target = parts[1]
qtype_str = parts[2].upper() if len(parts) > 2 else None
# Auto-detect: IP -> PTR, domain -> A
if qtype_str is None:
try:
ipaddress.ip_address(target)
qtype_str = "PTR"
except ValueError:
qtype_str = "A"
qtype = QTYPES.get(qtype_str)
if qtype is None:
valid = ", ".join(sorted(QTYPES))
await bot.reply(message, f"Unknown type: {qtype_str} (valid: {valid})")
return
lookup = target
if qtype_str == "PTR":
try:
lookup = reverse_name(target)
except ValueError:
await bot.reply(message, f"Invalid IP for PTR: {target}")
return
server = get_resolver()
rcode, results = await _query(lookup, qtype, server)
if rcode == -1:
await bot.reply(message, f"{target} {qtype_str}: timeout")
elif rcode == -2:
await bot.reply(message, f"{target} {qtype_str}: network error")
elif rcode != 0:
err = RCODES.get(rcode, f"error {rcode}")
await bot.reply(message, f"{target} {qtype_str}: {err}")
elif not results:
await bot.reply(message, f"{target} {qtype_str}: no records")
else:
await bot.reply(message, f"{target} {qtype_str}: {', '.join(results)}")