"""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 [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 [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)}")