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
- Fire It Up:
python network_scanner.py
in your terminal. - Input Subnet: Drop in your CIDR, like
192.168.1.0/24
. - Options: Toggle MAC lookup (local LAN magic) and port scans.
- 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