Files
links/netscan/checks/dns.py
T
2026-03-21 16:41:14 +11:00

87 lines
2.9 KiB
Python

import socket
import struct
import logging
from .base import Finding
logger = logging.getLogger(__name__)
CHECK_NAME = 'dns_resolver'
TIMEOUT = 5
def _build_dns_query(domain: str) -> bytes:
"""Build a minimal DNS A query packet."""
header = struct.pack('>HHHHHH', 0xAAAA, 0x0100, 1, 0, 0, 0)
parts = domain.encode().split(b'.')
question = b''.join(bytes([len(p)]) + p for p in parts) + b'\x00'
question += struct.pack('>HH', 1, 1) # type A, class IN
return header + question
def _parse_dns_response(data: bytes) -> dict:
"""Return basic info from a DNS response header."""
if len(data) < 12:
return {'error': 'response too short'}
txid, flags, qdcount, ancount, nscount, arcount = struct.unpack('>HHHHHH', data[:12])
rcode = flags & 0x000F
return {
'txid': txid,
'flags': flags,
'rcode': rcode,
'ancount': ancount,
}
def run(profile) -> list:
host = profile.public_ip
raw = {'public_ip': host}
query = _build_dns_query('google.com')
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(TIMEOUT)
sock.sendto(query, (host, 53))
data, _ = sock.recvfrom(512)
sock.close()
parsed = _parse_dns_response(data)
raw['response'] = parsed
if parsed.get('rcode') == 0 and parsed.get('ancount', 0) > 0:
return [Finding(
check_name=CHECK_NAME,
severity='critical',
title=f'Open DNS resolver detected on {host}:53',
detail=(
'Your public IP responds to recursive DNS queries from external hosts. '
'This can be abused for DNS amplification attacks. '
'Note: NAT hairpin may cause false positive — verify from off-LAN.'
),
raw=raw,
)]
else:
return [Finding(
check_name=CHECK_NAME,
severity='ok',
title='DNS port 53 does not appear to be an open resolver',
detail=f'DNS query to {host}:53 returned rcode={parsed.get("rcode")} with {parsed.get("ancount", 0)} answers.',
raw=raw,
)]
except socket.timeout:
return [Finding(
check_name=CHECK_NAME,
severity='ok',
title='DNS port 53 timed out (not an open resolver)',
detail=f'No response from {host}:53 within {TIMEOUT}s. Port is likely closed or filtered.',
raw={**raw, 'error': 'timeout'},
)]
except Exception as e:
logger.warning(f'DNS check error: {e}')
return [Finding(
check_name=CHECK_NAME,
severity='info',
title='DNS resolver check failed',
detail=f'Could not complete DNS probe to {host}:53 — {e}',
raw={**raw, 'error': str(e)},
)]