Home > Coding > Python Network Scanner: Discover Hosts, MACs, and Open Ports in Your Lab

Python Network Scanner: Discover Hosts, MACs, and Open Ports in Your Lab

What’s up, network nerds? If you’ve ever stared at a /24 subnet in your home lab and thought, “What’s actually alive in here? And what’s that Cisco router hiding on port 80?”, you’re in the right place. Building on my earlier simple port scanner, I’ve leveled up to a full-blown network scanner. This bad boy sweeps subnets for live hosts (via ping), grabs hostnames and MACs for vendor clues, and even auto-port-scans the survivors to reveal open services.

No bloat, no installs—just Python’s stdlib doing the heavy lifting. Ideal for auditing your Cisco gear, spotting rogue IoT, or mapping that messy VLAN. Pro Tip: Ethical use only—scan your own turf!

Why Build This?

  • One-Stop Shop: Combines host discovery + port scanning, saving you from juggling tools.
  • Lab-Friendly: Tailored for internal nets like 192.168.1.0/24; catches Cisco defaults (SSH 22, HTTP 80/443, SNMP 161).
  • Smarts Included: MACs hint at hardware (e.g., Cisco OUIs like 00-40-96), hostnames via DNS, and threaded speed.
  • Zero Deps: Runs anywhere with Python 3.6+—no Nmap envy required.

I cooked this up after a subnet scan turned up 15 mystery hosts (shoutout to my 192.168.1.x crew). Now it’s battle-tested for your lab chaos.

The Code

Grab the full script below. Save as network_scanner.py and dive in.

import ipaddress
import subprocess
import sys
import socket
from concurrent.futures import ThreadPoolExecutor, as_completed
import re

def select_os():
    """
    Prompt user to select OS for ping/ARP commands.
    Returns 'windows' or 'unix' (for macOS/Linux).
    """
    print("Select your operating system:")
    print("1. macOS or Linux")
    print("2. Windows")
    
    while True:
        choice = input("Enter 1 or 2: ").strip()
        if choice == '1':
            return 'unix'
        elif choice == '2':
            return 'windows'
        else:
            print("Invalid choice. Please enter 1 or 2.")

def ping_host(ip, timeout=1, os_type='unix'):
    """
    Ping a host to check if it's alive.
    Uses system 'ping' command with 1 packet and timeout.
    Returns True if alive, False otherwise.
    """
    if os_type == 'windows':
        param = '-n'
        timeout_flag = '-w'  # Windows uses lowercase -w for timeout in ms
    else:
        param = '-c'
        timeout_flag = '-W'  # Unix/macOS uses -W for timeout in seconds
    
    timeout_ms = str(int(timeout * 1000))  # Convert to ms for consistency
    command = ['ping', param, '1', timeout_flag, timeout_ms, str(ip)]
    
    try:
        result = subprocess.run(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        return result.returncode == 0
    except Exception:
        return False

def get_hostname(ip):
    """
    Resolve hostname for the given IP.
    Returns hostname if resolvable, 'Unknown' otherwise.
    """
    try:
        hostname = socket.gethostbyaddr(str(ip))[0]
        return hostname
    except socket.herror:
        return 'Unknown'

def get_mac(ip, os_type='unix'):
    """
    Get MAC address via ARP (local network only).
    Returns MAC if found, 'N/A' otherwise.
    Parses system ARP table.
    """
    try:
        if os_type == 'windows':
            command = ['arp', '-a', str(ip)]
        else:
            command = ['arp', '-n', str(ip)]
        
        output = subprocess.check_output(command).decode('utf-8', errors='ignore')
        # Parse MAC (basic regex for common formats)
        mac_match = re.search(r'([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})', output)
        if mac_match:
            return mac_match.group(0).upper().replace(':', '-')
        return 'N/A'
    except Exception:
        return 'N/A'

def scan_host(ip, include_mac=False, os_type='unix'):
    """
    Scan a single host: ping, resolve hostname, and optional MAC.
    Returns a dict with results if alive, None if not.
    """
    if ping_host(ip, os_type=os_type):
        hostname = get_hostname(ip)
        mac = get_mac(ip, os_type) if include_mac else 'N/A'
        return {'ip': str(ip), 'hostname': hostname, 'mac': mac}
    return None

def scan_port(target, port):
    """
    Scan a single port to check if it's open.
    """
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(1)
        result = sock.connect_ex((target, port))
        if result == 0:
            try:
                service = socket.getservbyport(port)
            except OSError:
                service = 'Unknown'
            return f"{port} ({service})"
        sock.close()
    except Exception:
        pass
    return None

def port_scan(target, start_port=1, end_port=1024, max_workers=100):
    """
    Perform a port scan and return list of open ports.
    """
    print(f"  Port scanning {target} (ports {start_port}-{end_port})...")
    open_ports = []
    
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        future_to_port = {
            executor.submit(scan_port, target, port): port for port in range(start_port, end_port + 1)
        }
        
        for future in as_completed(future_to_port):
            result = future.result()
            if result:
                open_ports.append(result)
    
    return open_ports

def subnet_scan(cidr, max_workers=50, include_mac=False, os_type='unix'):
    """
    Perform a host discovery scan on the given CIDR subnet.
    """
    try:
        network = ipaddress.ip_network(cidr, strict=False)
        hosts = [host for host in network.hosts()]
    except ValueError as e:
        print(f"Invalid CIDR format: {e}")
        return []
    
    print(f"Scanning subnet {cidr} ({len(hosts)} hosts)...")
    alive_hosts = []
    
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        future_to_ip = {executor.submit(scan_host, ip, include_mac, os_type): ip for ip in hosts}
        
        for future in as_completed(future_to_ip):
            result = future.result()
            if result:
                alive_hosts.append(result)
                mac_str = f" [MAC: {result['mac']}]" if result['mac'] != 'N/A' else ""
                print(f"Alive: {result['ip']} ({result['hostname']}){mac_str}")
    
    print(f"Scan completed. Found {len(alive_hosts)} alive hosts.")
    return alive_hosts

def display_results(alive_hosts, open_ports_per_host=None):
    """
    Display scan results in a table.
    """
    if not alive_hosts:
        print("No alive hosts found.")
        return
    
    print("\n--- Host Discovery Results ---")
    headers = ['IP Address', 'Hostname', 'MAC Address']
    if open_ports_per_host:
        headers.append('Open Ports (Top 3)')
    print(f"{' | '.join(f'{h:<15}' for h in headers)}")
    print("-" * (15*4 + 3*3))  # Rough separator
    
    for host in sorted(alive_hosts, key=lambda x: x['ip']):
        row = [host['ip'][:15], host['hostname'][:15], host['mac'][:15]]
        if open_ports_per_host:
            ports = open_ports_per_host.get(host['ip'], [])
            row.append(', '.join(ports[:3])[:15])  # Top 3
        print(' | '.join(row))

if __name__ == "__main__":
    # Prompt for OS selection first
    os_type = select_os()
    print(f"Selected: {os_type.capitalize()}")
    
    cidr = input("Enter the subnet in CIDR notation (e.g., 10.42.52.0/24): ").strip()
    if not cidr:
        print("No CIDR provided. Exiting.")
        sys.exit(1)
    
    try:
        ipaddress.ip_network(cidr, strict=False)
    except ValueError:
        print("Invalid CIDR format.")
        sys.exit(1)
    
    include_mac = input("Include MAC lookup? (y/n, local only): ").lower() == 'y'
    alive_hosts = subnet_scan(cidr, include_mac=include_mac, os_type=os_type)
    display_results(alive_hosts)
    
    if alive_hosts:
        if input("\nPort scan all alive hosts? (y/n): ").lower() == 'y':
            open_ports_per_host = {}
            for host in alive_hosts:
                print(f"\n--- Scanning {host['ip']} ---")
                ports = port_scan(host['ip'])
                if ports:
                    open_ports_per_host[host['ip']] = ports
                    print(f"  Open: {', '.join(ports[:5])}...")  # Show top 5
                else:
                    print("  No open ports found.")
            
            display_results(alive_hosts, open_ports_per_host)
        else:
            # Selective scan
            target_ip = input("Enter IP to port scan (or 'all'): ").strip()
            if target_ip.lower() == 'all':
                # Reuse full scan logic here if needed
                pass
            elif any(h['ip'] == target_ip for h in alive_hosts):
                ports = port_scan(target_ip)
                print(f"\nOpen ports on {target_ip}: {', '.join(ports) if ports else 'None'}")

How to Use It

  1. Fire It Up: python network_scanner.py in your terminal.
  2. Input Subnet: Drop in your CIDR, like 192.168.1.0/24.
  3. Options: Toggle MAC lookup (local LAN magic) and port scans.
  4. Watch & Learn: Real-time pings, then a tidy table of IPs, names, MACs, and top ports.

Sample Output (from my recent 15-host sweep):

Scanning subnet 192.168.1.0/24 (254 hosts)...
Alive: 192.168.1.1 (Unknown) [MAC: 00-01-63-AB-CD-EF]
Alive: 192.168.1.51 (Unknown) [MAC: AA-BB-CC-DD-EE-FF]
...

--- Scanning 192.168.1.1 ---
  Port scanning 192.168.1.1 (ports 1-1024)...
  Open: 22 (ssh), 80 (http), 443 (https), 161 (snmp)...

IP Address     | Hostname      | MAC Address   | Open Ports (Top 3)
192.168.1.1   | Unknown       | 00-01-63-AB-CD-EF | 22 (ssh), 80 (http), 443 (https)
192.168.1.51  | Unknown       | AA-BB-CC-DD-EE-FF | 445 (microsoft-ds), 139 (netbios-ssn)
...

Boom—your router’s Cisco MAC and management ports exposed. Tweak timeouts or ranges for your setup.

Caveats and Next Steps

  • Gotchas: ICMP blocks? Swap ping for TCP probes. MACs are local-only; hostnames need DNS love.
  • Limits: TCP ports only, no UDP fireworks. For pro stuff, layer on Nmap.
  • Amp It Up: Add RTT stats, JSON exports, or GUI? Fork away!
  • Cisco Hack: Target .1 first—expect 22/80/443/161 if it’s stock.

Dig this? Share it or fuel my lab: http://github.com/www-lazy-guy-xyz/network-scanner

Leave a Comment