""" Port scanning utilities Lightweight wrapper around nmap for automated firewall testing. Supports: - Scanning individual TCP/UDP ports with detailed state detection - Parallel scanning of multiple ports using threads - Predefined set of common service ports for convenience """ import threading from typing import List, Dict, Union, Tuple class PortScanner: """Simple port scanner using netcat for firewall testing""" # Well-known ports for testing common services WELL_KNOWN_PORTS = [ (22, "tcp", "ssh"), (53, "udp", "dns"), (67, "udp", "dhcp"), (69, "udp", "tftp"), (80, "tcp", "http"), (443, "tcp", "https"), (5353, "udp", "mdns"), (7681, "tcp", "ttyd"), (8080, "tcp", "http-alt"), (8443, "tcp", "https-alt"), (7, "tcp", "echo"), (1234, "tcp", "test-mid"), (9999, "tcp", "test-high"), ] def __init__(self, netns=None): """ Initialize port scanner Args: netns: Network namespace object (IsolatedMacVlan) or netns name """ self.netns = netns def scan_port(self, host: str, port: int, protocol: str = "tcp", timeout: int = 3) -> Dict[str, Union[str, bool]]: """ Scan a single port using nmap for accurate firewall state detection Args: host: Target hostname or IP address port: Port number to scan protocol: 'tcp' or 'udp' timeout: Connection timeout in seconds Returns: Dict with keys: 'open' (bool), 'status' (str), 'response' (str) """ # Build optimized nmap command based on protocol if protocol.lower() == "tcp": proto = "-sT" elif protocol.lower() == "udp": proto = "-sU" else: raise ValueError(f"Unsupported protocol: {protocol}") in6 = "" if ":" in host: in6 = "-6" cmd = f"nmap -n {proto} {in6} -Pn -p {port} --host-timeout={timeout}" \ f" --min-rate=1000 --max-retries=1 --disable-arp-ping {host}" result = self.netns.runsh(cmd) # Parse nmap output to determine port state output = result.stdout # Look for the specific port line in nmap output # Format: "PORT STATE SERVICE" or "22/tcp open ssh" port_line = None for line in output.split('\n'): if f"{port}/" in line and protocol in line: port_line = line.strip() break if port_line: if "open|filtered" in port_line: # No ICMP port unreachable, likely firewall dropping silently status = "open|filtered" is_open = False elif "closed|filtered" in port_line: # No service running, or firewall dropping status = "closed|filtered" is_open = False elif "open" in port_line: status = "open" is_open = True elif "filtered" in port_line: # Blocked/Rejected by firewall status = "filtered" is_open = False elif "closed" in port_line: # Port reachable but service not running status = "closed" is_open = False else: # Unknown state status = "unknown" is_open = False else: # No port line found - likely filtered or error status = "filtered" is_open = False return { "open": is_open, "status": status, "response": output.strip() } def scan_ports(self, host: str, port_specs: List[Tuple[int, str, str]], timeout: int = 3) -> List[Tuple[int, str, Dict]]: """ Scan multiple ports in parallel using threads Args: host: Target hostname or IP address port_specs: List (port, protocol, name) e.g. [(80, "tcp", "http"), (53, "udp", "dns")] timeout: Connection timeout per port Returns: List of (port, name, result) scan results. """ results = [] threads = [] lock = threading.Lock() def scan_worker(port: int, protocol: str, name: str): try: result = self.scan_port(host, port, protocol, timeout) with lock: results.append((port, name, result)) except Exception as e: with lock: results.append((port, name, { "open": False, "status": "error", "response": str(e) })) for port, protocol, name in port_specs: thread = threading.Thread(target=scan_worker, args=(port, protocol, name)) threads.append(thread) thread.start() for thread in threads: thread.join() # Sort results by port number for consistent output results.sort(key=lambda x: x[0]) return results