Files

161 lines
4.8 KiB
Python

#!/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 <ip> <port>
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()