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>
89 lines
2.6 KiB
Python
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)}")
|