Skip to content

Instantly share code, notes, and snippets.

@dutchLuck
Last active February 16, 2026 08:36
Show Gist options
  • Select an option

  • Save dutchLuck/cff79b7a93dc656263eb746a42102347 to your computer and use it in GitHub Desktop.

Select an option

Save dutchLuck/cff79b7a93dc656263eb746a42102347 to your computer and use it in GitHub Desktop.
Python script to leverage ping and arp to discover IP to ID/name association via known MAC addresses on the local network when DHCP shifts IP around and local DNS is dodgy.
#!/usr/bin/env python3
#
# N E T W O R K _ S C A N . P Y
#
# network_scan.py last edited on Wed Feb 16 19:26:35 2026 as 0v8
#
# Ping scan the local network for devices with known MAC addresses
# and record their IPv4 addresses and ping status. The results are
# displayed in two tables, one sorted by device name and the other
# sorted by IP address.
#
#
# This code was produced by AI from a prompt originating
# in a desire to work around the local network suffering
# from unreliable DNS and the DHCP occasionally shifting
# the IPv4 addresses of devices on the local network.
#
#
# Please DO NOT use this script on any network that
# you do not own or do not have the permission of the
# network CZAR to scan. It is only applicable to a
# home network with just a few devices attached. If used
# in a work environment it may trigger anti-virus security
# monitoring systems etc.
#
#
# Leverage the Operating System ping and arp/ip utility
# programs to find the IP addresses of devices expected
# to be on the local network. The known devices are read
# from afile named "devices.txt" which matches identifiers
# like names, although not necessarily DNS names, with
# an associated MAC address.
# An example of the file (IEEE MAC format) is; -
#
# # ComputerName MAC
# Apple_1 26-6B-81-44-D6-75
# PC_1 01-06-AC-21-EC-85
# PC_2 51-33-E3-93-5F-3B
# Gateway 1C-21-B4-25-07-2D
# Broadcast_Address FF-FF-FF-FF-FF-FF
#
#
# This code should work on an Apple mac, Linux,
# and Microsoft Windows.
#
# It is known to run, with a 15 or 20 seconds delay
# on macOS Tahoe 26.2
# (AI suggested the "arpscan" utility was a fast
# alternative, if you are on linux.)
# On macOS and Windows, native ping and arp utilities are used.
# On Linux, native ping and ip utilities are used.
#
# There are no command line parameters to specify so run with; -
#
# python3 network_scan.py
#
# OR on macOS and Linux after using the command "chmod u+x network_scan.py"
#
# ./network_scan.py
#
#
# For example, a hypothetical run may produce the
# following output after a minute or two; -
#
# % ./network_scan.py
# network_scan.py 0v7 interpreted by python version 3.14.0
# Name IP Address MAC Address Ping Result
# --------------------------------------------------------------------
# Apple_1 192.168.1.100 26-6B-81-44-D6-75 Success
# Gateway 192.168.1.1 1C-21-B4-25-07-2D Success
# Multicast_Address 224.0.0.251 01-00-5E-00-00-FB unknown
# PC_1 192.168.1.102 01-06-AC-21-EC-85 Failed
#
# IP Address Name MAC Address Ping Result
# --------------------------------------------------------------------
# 192.168.1.1 Gateway 1C-21-B4-25-07-2D Success
# 192.168.1.100 Apple_1 26-6B-81-44-D6-75 Success
# 192.168.1.102 PC_1 01-06-AC-21-EC-85 Failed
# 224.0.0.251 Multicast_Address 01-00-5E-00-00-FB unknown
# %
#
# Note that a device with a "Failed" Ping Result may still be active,
# but have a firewall that blocks ping access or it may be off/asleep,
# but was active a short time ago and its entry has not yet aged out
# of the macOS arp cache.
#
#
# If your local network uses a different IP range then edit
# the SUBNET, NETWORK_START and NETWORK_END IP addresses in the
# "Constants" section below.
#
#
# 0v8 Convert all MAC address use to IEEE format (E.g. 01-20-33-04-AA-FF).
# 0v7 Added Windows support with proper ping and arp parsing.
# 0v6 Switch from arp to ip neigh on linux.
# 0v5 First cut at running on both macOS and linux.
# 0v4 Fixed input parsing to restore name output.
# 0v3 Changed input parsing to cope with IEEE MAC format.
# 0v2 Changed to threaded implementation to speed up the response.
# 0v1 Output in two tables 1 sorted by ID/Name and the other by IP..
#
#------------------------------
# Start of python script
#------------------------------
import platform # python_version()
import subprocess
import ipaddress
import time
import re # regular expressions
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor, as_completed
#----------------------------
# Constants
#----------------------------
VERSION_ID = "0v8"
DEVICE_FILE = "devices.txt"
SUBNET = "192.168.1"
NETWORK_START = "192.168.1.1" # Start of DHCP allocation range
NETWORK_END = "192.168.1.254" # End of IP scan range
MAX_THREADS = 50 # sweet spot for most home networks
#------------------------------
# Print version information
#------------------------------
print(f"network_scan.py {VERSION_ID} interpreted by python version {platform.python_version()}")
OS_TYPE = platform.system().lower() # 'darwin', 'linux', or 'windows'
#-----------------------
# OS dependent Constants
#-----------------------
WAIT_TIME = 2000 # Default (macOS) ping timeout in milliseconds
INCOMPLETE_FLAG = "(incomplete)"
PING_COUNT_FLAG = "-c"
PING_TIMEOUT_FLAG = "-W"
if OS_TYPE == "linux":
WAIT_TIME = 2 # Linux ping uses seconds
INCOMPLETE_FLAG = "<incomplete>"
elif OS_TYPE == "windows":
WAIT_TIME = 2000 # Windows ping uses milliseconds
PING_COUNT_FLAG = "-n"
PING_TIMEOUT_FLAG = "-w"
#---------------------------
# Helper formatting function
#---------------------------
def any_mac_address_to_IEEE_format(mac: str) -> str:
"""
Convert any H/W MAC Address format (E.G. 0:11:2:33:40:5a)
to IEEE format (00-11-02-33-40-5A) with hyphens, leading zeros and uppercase
"""
parts = re.split(r"[:-]", mac)
return "-".join(f"{int(part, 16):02X}" for part in parts)
# --------------------------------
# Load known devices MAC addresses
# --------------------------------
known_devices = {}
with open(DEVICE_FILE, "r") as f:
for line in f:
if line.strip():
if not line or line.startswith("#"): #skip comments or blank lines
continue
name, mac = line.strip().split() # Expecting "Name MAC" format in devices.txt
mac = any_mac_address_to_IEEE_format(mac) # Convert to IEEE format for consistency
known_devices[mac] = name
# -----------------------------
# Ping sweep
# -----------------------------
def ping(ip):
result = subprocess.run(
["ping", PING_COUNT_FLAG, "1", PING_TIMEOUT_FLAG, str(WAIT_TIME), str(ip)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
return ip, (result.returncode == 0)
#-----------------------------------
# Generate IP List
#----------------------------------
start_ip = int(ipaddress.IPv4Address(NETWORK_START))
end_ip = int(ipaddress.IPv4Address(NETWORK_END))
ip_list = [str(ipaddress.IPv4Address(ip)) for ip in range(start_ip, end_ip + 1)]
ping_result_table = {}
# -----------------------------
# Parallel ping sweep
# -----------------------------
with ThreadPoolExecutor(max_workers=MAX_THREADS) as executor:
futures = [executor.submit(ping, ip) for ip in ip_list]
for future in as_completed(futures):
result_ip, result = future.result()
if result:
ping_result_table[str(result_ip)] = "Success"
else:
ping_result_table[str(result_ip)] = "Failed"
# Let ARP cache settle
time.sleep(1)
# -----------------------------------
# Read ARP table - platform dependent
#
# The arp utility on macOS and Windows,
# and the ip neigh utility on linux
# all show the ARP cache, but in
# different formats. Some linux
# distributions may not have the arp
# utility by default, but all should
# have ip neigh.
# -----------------------------------
responsive_arp_ips = []
if OS_TYPE == "darwin":
arp_output = subprocess.check_output(["arp", "-a"], text=True) # macOS arp -a format
elif OS_TYPE == "linux":
arp_output = subprocess.check_output(["ip", "neigh", "show"], text=True) # Linux ip neigh format
elif OS_TYPE == "windows":
arp_output = subprocess.check_output(["arp", "-a"], text=True) # Windows arp -a format
arp_table = {}
for line in arp_output.splitlines():
line = line.strip()
if not line:
continue
if OS_TYPE == "linux": # Linux ip neigh output format
if not line.startswith(SUBNET + "."):
continue
parts = line.split()
if len(parts) < 5:
continue
ip = str(ipaddress.IPv4Address(parts[0])) # Validate and standardize IP format
arp_table[ip] = any_mac_address_to_IEEE_format(parts[4].lower())
if SUBNET in ip:
responsive_arp_ips.append(str(ip))
elif OS_TYPE == "darwin": # macOS arp -a output format
parts = line.split()
if len(parts) >= 4 and parts[3] != INCOMPLETE_FLAG:
ip = str(ipaddress.IPv4Address(parts[1].strip("()"))) # Validate and standardize IP format
arp_table[ip] = any_mac_address_to_IEEE_format(parts[3].lower())
if SUBNET in ip:
responsive_arp_ips.append(str(ip))
elif OS_TYPE == "windows": # Windows arp -a output format
# Format: " 192.168.1.1 aa-bb-cc-dd-ee-ff dynamic"
# Skip header lines and empty lines
if "Interface" in line or "Address" in line or "---" in line or not line: # Skip non-data lines
continue
parts = line.split()
if len(parts) >= 2:
try:
# Validate IP address format and standardize it
ip = str(ipaddress.IPv4Address(parts[0])) # Validate and standardize IP format
# Get MAC address (second column)
mac = parts[1].lower() if len(parts) > 1 else None
# Validate MAC format (should have hyphens or colons)
if mac and ("-" in mac or ":" in mac):
arp_table[ip] = any_mac_address_to_IEEE_format(mac)
if SUBNET in ip:
responsive_arp_ips.append(str(ip))
except (ValueError, ipaddress.AddressValueError):
# Skip invalid IP addresses
continue
# -----------------------------
# Build results
# -----------------------------
results = []
known_name_max = 0
known_ip_max = 0
known_mac_max = 0
for ip in responsive_arp_ips:
mac = arp_table.get(ip, "unknown")
name = known_devices.get(mac, "Unknown Device")
ping_success = ping_result_table.get(ip, "unknown")
results.append((name, ip, mac, ping_success))
if len(name) > known_name_max:
known_name_max = len(name)
if len(ip) > known_ip_max:
known_ip_max = len(ip)
if len(mac) > known_mac_max:
known_mac_max = len(mac)
name_width = known_name_max + 2 # set up output width
ip_width = known_ip_max + 2 # set up output width
mac_width = known_mac_max + 2 # set up output width
# -------------------------------------
# Output table in Name, IP, Mac columns
# -------------------------------------
# Sort by name
results.sort(key=lambda x: x[0].lower())
print(f"{'Name':{name_width}} {'IP Address':{ip_width}} {'MAC Address':{mac_width}} {'Ping Result'}")
print("-" * (14 + name_width + ip_width + mac_width))
for name, ip, mac, ping_success in results:
print(f"{name:{name_width}} {ip:{ip_width}} {mac:{mac_width}} {ping_success}")
# -------------------------------------
# Output table in IP, Name, Mac columns
# -------------------------------------
# Sort by IP
results.sort(key=lambda x: x[1].lower())
print(f"\n{'IP Address':{ip_width}} {'Name':{name_width}} {'MAC Address':{mac_width}} {'Ping Result'}")
print("-" * (14 + name_width + ip_width + mac_width))
for name, ip, mac, ping_success in results:
print(f"{ip:{ip_width}} {name:{name_width}} {mac:{mac_width}} {ping_success}")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment