#!/usr/bin/env python3 """MQTT Broker Scanner & Auth Tester. Scans VLANs for MQTT brokers (port 1883/8883) and tests connectivity by sending a raw MQTT v3.1.1 CONNECT packet and reading the CONNACK. Usage: python3 mqtt-broker-scan.py # Scan known homelab VLANs python3 mqtt-broker-scan.py 10.0.30.0/24 # Scan specific subnet python3 mqtt-broker-scan.py 10.0.30.10 1883 # Test specific broker """ import socket import struct import sys import concurrent.futures KNOWN_SUBNETS = [ "10.0.30.", # Server/HA VLAN "10.0.40.", # Guest/IoT VLAN "10.0.50.", # IoT VLAN ] def check_port(ip, port, timeout=0.3): """Quick TCP connect check.""" try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(timeout) result = s.connect_ex((ip, port)) s.close() return result == 0 except Exception: return False def encode_remaining_length(length): """MQTT variable-length encoding for remaining length field.""" bytes_rl = [] while True: byte = length % 128 length = length // 128 if length > 0: byte += 128 bytes_rl.append(byte) if length == 0: break return bytes(bytes_rl) def mqtt_connect_test(host, port=1883, client_id="hermes-scan", timeout=5): """Send minimal MQTT v3.1.1 CONNECT, read CONNACK, return status string.""" try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(timeout) s.connect((host, port)) # Variable header: Protocol Name + Level + Flags + Keep Alive var_header = ( b'\x00\x04MQTT' # Protocol name "MQTT" b'\x04' # Protocol level (3.1.1) b'\x02' # Flags: Clean Session b'\x00\x3c' # Keep alive: 60s ) # Payload: Client ID (length-prefixed) cid = client_id.encode('utf-8') payload = struct.pack('>H', len(cid)) + cid # Fixed header: CONNECT (0x10) + remaining length remaining = len(var_header) + len(payload) fixed_header = b'\x10' + encode_remaining_length(remaining) s.send(fixed_header + var_header + payload) # Read CONNACK: 4 bytes (0x20, 0x02, session_present, return_code) connack = s.recv(4) s.close() if len(connack) >= 4 and connack[0] == 0x20: rc = connack[3] codes = { 0: 'Accepted', 1: 'Bad protocol version', 2: 'Client ID rejected', 3: 'Server unavailable', 4: 'Bad username/password', 5: 'Not authorized', } return codes.get(rc, f'Unknown ({rc})') else: return f'Unexpected response: {connack.hex()}' except Exception as e: return f'Connection failed: {e}' def scan_subnet(prefix, ports=(1883, 8883)): """Scan a /24 subnet for open MQTT ports. Returns list of (ip, port) tuples.""" found = [] def scan(ip): hits = [] for p in ports: if check_port(ip, p): hits.append(p) return (ip, hits) with concurrent.futures.ThreadPoolExecutor(max_workers=128) as ex: futures = { ex.submit(scan, f"{prefix}{i}"): i for i in range(1, 255) } for f in concurrent.futures.as_completed(futures): ip, hits = f.result() if hits: found.append((ip, hits)) return sorted(found) def main(): args = sys.argv[1:] if len(args) >= 2: # Test specific broker: mqtt-broker-scan.py host, port = args[0], int(args[1]) print(f"Testing {host}:{port}...") result = mqtt_connect_test(host, port) print(f" Result: {result}") return if len(args) == 1 and '/' in args[0]: # Scan specific subnet: mqtt-broker-scan.py 10.0.30.0/24 prefix = args[0].rsplit('.', 1)[0] + '.' print(f"Scanning {prefix}0/24 for MQTT brokers...") results = scan_subnet(prefix) else: # Scan known homelab subnets print("Scanning known homelab VLANs for MQTT brokers...") results = [] for prefix in KNOWN_SUBNETS: print(f" Scanning {prefix}0/24...", end=' ', flush=True) found = scan_subnet(prefix) if found: print(f"found {len(found)}") results.extend(found) else: print("none") print(f"\n=== Results: {len(results)} broker(s) found ===") for ip, ports in results: for port in ports: proto = "MQTT" if port == 1883 else f"MQTT-TLS({port})" auth = mqtt_connect_test(ip, port) print(f" {proto} {ip}:{port} -> {auth}") if __name__ == '__main__': main()