From 48db3a11d038db642dfe903ac3cb6d323ad29f30 Mon Sep 17 00:00:00 2001 From: Marius Mutu Date: Sat, 6 Sep 2025 22:42:41 +0300 Subject: [PATCH] Enhance Windows network scanning with improved device discovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ARP-first scanning approach to find devices that don't respond to ping - Implement local host MAC address detection for Windows host machines - Support multiple network prefix lengths (/16, /20, /24) with proper network calculation - Add Get-LocalHostMacAddress function with 3 detection methods: * Direct IP-to-adapter matching via Get-NetIPAddress/Get-NetAdapter * WMI-based detection via Win32_NetworkAdapterConfiguration * Same-subnet fallback detection - Skip ping for devices already found in ARP table (performance improvement) - Improved detection of host.docker.internal MAC addresses This resolves issues where local Windows host devices were found without MAC addresses, enabling proper Wake-on-LAN functionality for all network devices including the host machine. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- scripts/windows-network-scan.ps1 | 302 ++++++++++++++++++++++++------- 1 file changed, 238 insertions(+), 64 deletions(-) diff --git a/scripts/windows-network-scan.ps1 b/scripts/windows-network-scan.ps1 index 7243beb..4bf162f 100644 --- a/scripts/windows-network-scan.ps1 +++ b/scripts/windows-network-scan.ps1 @@ -30,25 +30,34 @@ function Get-LocalNetworks { $networkAddr = $adapter.IPAddress $prefixLength = $adapter.PrefixLength - # Simple network calculation for /24 networks (most common) - if ($prefixLength -eq 24) { - $parts = $networkAddr.Split('.') - if ($parts.Length -eq 4) { + # Network calculation for different prefix lengths + $parts = $networkAddr.Split('.') + if ($parts.Length -eq 4) { + if ($prefixLength -eq 24) { $networkAddress = "$($parts[0]).$($parts[1]).$($parts[2]).0" $networks += "$networkAddress/24" Write-Log "Found network: $networkAddress/24 from adapter $($adapter.IPAddress)" } - } - elseif ($prefixLength -eq 16) { - $parts = $networkAddr.Split('.') - if ($parts.Length -eq 4) { + elseif ($prefixLength -eq 20) { + # For /20, calculate the network base (e.g., 10.0.20.144/20 -> 10.0.16.0/20) + $thirdOctet = [int]$parts[2] + $networkThird = $thirdOctet - ($thirdOctet % 16) # Round down to nearest /20 boundary + $networkAddress = "$($parts[0]).$($parts[1]).$networkThird.0" + $networks += "$networkAddress/20" + Write-Log "Found network: $networkAddress/20 from adapter $($adapter.IPAddress)" + } + elseif ($prefixLength -eq 16) { $networkAddress = "$($parts[0]).$($parts[1]).0.0" $networks += "$networkAddress/16" Write-Log "Found network: $networkAddress/16 from adapter $($adapter.IPAddress)" } - } - else { - Write-Log "Skipping unsupported prefix length /$prefixLength for $networkAddr" + else { + Write-Log "Unsupported prefix length /$prefixLength for $networkAddr, trying /24" + # Fallback to /24 for unknown prefix lengths + $networkAddress = "$($parts[0]).$($parts[1]).$($parts[2]).0" + $networks += "$networkAddress/24" + Write-Log "Fallback network: $networkAddress/24 from adapter $($adapter.IPAddress)" + } } } @@ -74,7 +83,21 @@ function Show-NetworkSelectionMenu { foreach ($adapter in $adapters) { $parts = $adapter.IPAddress.Split('.') if ($parts.Length -eq 4) { - $networkAddr = "$($parts[0]).$($parts[1]).$($parts[2]).0/24" + $prefixLength = $adapter.PrefixLength + + if ($prefixLength -eq 24) { + $networkAddr = "$($parts[0]).$($parts[1]).$($parts[2]).0/24" + } elseif ($prefixLength -eq 20) { + $thirdOctet = [int]$parts[2] + $networkThird = $thirdOctet - ($thirdOctet % 16) + $networkAddr = "$($parts[0]).$($parts[1]).$networkThird.0/20" + } elseif ($prefixLength -eq 16) { + $networkAddr = "$($parts[0]).$($parts[1]).0.0/16" + } else { + # Fallback to /24 + $networkAddr = "$($parts[0]).$($parts[1]).$($parts[2]).0/24" + } + if ($detectedNetworks -notcontains $networkAddr) { $detectedNetworks += $networkAddr } @@ -170,6 +193,81 @@ function Test-NetworkAddress { } } +function Get-LocalHostMacAddress { + param([string]$IPAddress) + + Write-Log "Attempting to find MAC address for potential local host: $IPAddress" + + try { + # Method 1: Check if this IP matches any of the local network adapters + $localAdapters = Get-NetIPAddress -AddressFamily IPv4 | Where-Object { + $_.IPAddress -ne "127.0.0.1" -and ($_.PrefixOrigin -eq "Dhcp" -or $_.PrefixOrigin -eq "Manual") + } + + foreach ($adapter in $localAdapters) { + if ($adapter.IPAddress -eq $IPAddress) { + Write-Log "Found matching local IP address: $IPAddress on interface $($adapter.InterfaceIndex)" + + # Get the corresponding network adapter + $netAdapter = Get-NetAdapter -InterfaceIndex $adapter.InterfaceIndex -ErrorAction SilentlyContinue + if ($netAdapter -and $netAdapter.MacAddress) { + $cleanMac = $netAdapter.MacAddress.Replace('-', ':').ToLower() + Write-Log "Found local MAC address: $cleanMac for IP $IPAddress" + return $cleanMac + } + } + } + + # Method 2: Use WMI to find network adapters with the matching IP + $wmiAdapters = Get-WmiObject -Class Win32_NetworkAdapterConfiguration | Where-Object { + $_.IPEnabled -eq $true -and $_.IPAddress + } + + foreach ($wmiAdapter in $wmiAdapters) { + if ($wmiAdapter.IPAddress -contains $IPAddress) { + if ($wmiAdapter.MACAddress) { + $cleanMac = $wmiAdapter.MACAddress.Replace('-', ':').ToLower() + Write-Log "Found local MAC via WMI: $cleanMac for IP $IPAddress" + return $cleanMac + } + } + } + + # Method 3: Check if IP is in the same subnet and try to find the default adapter + $localIPs = $localAdapters | ForEach-Object { $_.IPAddress } + foreach ($localIP in $localIPs) { + $localParts = $localIP -split '\.' + $targetParts = $IPAddress -split '\.' + + # Simple check for same /24 network + if ($localParts.Length -eq 4 -and $targetParts.Length -eq 4) { + if ($localParts[0] -eq $targetParts[0] -and + $localParts[1] -eq $targetParts[1] -and + $localParts[2] -eq $targetParts[2]) { + + # This IP is in the same subnet, find the adapter for the local IP + $matchingAdapter = $localAdapters | Where-Object { $_.IPAddress -eq $localIP } + if ($matchingAdapter) { + $netAdapter = Get-NetAdapter -InterfaceIndex $matchingAdapter.InterfaceIndex -ErrorAction SilentlyContinue + if ($netAdapter -and $netAdapter.MacAddress) { + $cleanMac = $netAdapter.MacAddress.Replace('-', ':').ToLower() + Write-Log "Found MAC for same subnet adapter: $cleanMac (local IP: $localIP, target: $IPAddress)" + return $cleanMac + } + } + } + } + } + + Write-Log "Could not determine MAC address for IP: $IPAddress" + return "" + } + catch { + Write-Log "Error finding local MAC address: $($_.Exception.Message)" + return "" + } +} + function Get-NetworkDevices { param([string]$NetworkCIDR) @@ -188,22 +286,107 @@ function Get-NetworkDevices { if ($prefixLength -eq 24) { $startIP = 1 $endIP = 254 + } elseif ($prefixLength -eq 20) { + # For /20 networks (like 10.0.20.0/20), scan the specific /24 subnet where the network is + $thirdOctet = [int]$networkParts[2] + $baseIP = "$($networkParts[0]).$($networkParts[1]).$thirdOctet" + $startIP = 1 + $endIP = 254 + Write-Log "Scanning /20 network as /24 subnet: $baseIP.0/24" + } elseif ($prefixLength -eq 16) { + # For /16 networks, we need to scan multiple /24 subnets + # For now, just scan the specific /24 subnet where the network address is + $thirdOctet = [int]$networkParts[2] + $baseIP = "$($networkParts[0]).$($networkParts[1]).$thirdOctet" + $startIP = 1 + $endIP = 254 + Write-Log "Scanning /16 network as /24 subnet: $baseIP.0/24" } else { - Write-Log "Only /24 networks are supported in this version" + Write-Log "Unsupported prefix length /$prefixLength. Supported: /16, /20, /24" return @() } $devices = @() - $aliveIPs = @() - - # Simplified ping sweep with progress indication - Write-Log "Starting ping sweep for $($endIP - $startIP + 1) addresses..." - - # Use the configurable batch size parameter $totalIPs = $endIP - $startIP + 1 - $processed = 0 - Write-Log "Scanning $totalIPs addresses in batches of $BatchSize..." + # STEP 1: Get ARP table first to find devices that might not respond to ping + Write-Log "Step 1: Retrieving devices from ARP table..." + $arpEntries = @{} + $arpDevices = @{} + + try { + # Use Get-NetNeighbor for Windows 8/Server 2012 and later + $neighbors = Get-NetNeighbor -AddressFamily IPv4 | Where-Object { $_.State -ne "Unreachable" } + foreach ($neighbor in $neighbors) { + if ($neighbor.LinkLayerAddress -and $neighbor.LinkLayerAddress -ne "00-00-00-00-00-00") { + $cleanMac = $neighbor.LinkLayerAddress.Replace('-', ':').ToLower() + $arpEntries[$neighbor.IPAddress] = $cleanMac + + # Check if this IP is in our scan range + $ipParts = $neighbor.IPAddress -split '\.' + if ($ipParts.Length -eq 4) { + $currentBaseIP = "$($ipParts[0]).$($ipParts[1]).$($ipParts[2])" + $lastOctet = [int]$ipParts[3] + + if ($currentBaseIP -eq $baseIP -and $lastOctet -ge $startIP -and $lastOctet -le $endIP) { + $arpDevices[$neighbor.IPAddress] = @{ + ip = $neighbor.IPAddress + mac = $cleanMac + hostname = "" + status = "arp" + } + Write-Log "Found in ARP: $($neighbor.IPAddress) -> $cleanMac" + } + } + } + } + } + catch { + Write-Log "Get-NetNeighbor failed, trying arp command..." + + # Fallback to arp command + try { + $arpOutput = arp -a + foreach ($line in $arpOutput) { + if ($line -match '(\d+\.\d+\.\d+\.\d+)\s+([0-9a-fA-F-]{17})\s+\w+') { + $ip = $matches[1] + $mac = $matches[2].Replace('-', ':').ToLower() + if ($mac -ne "00:00:00:00:00:00") { + $arpEntries[$ip] = $mac + + # Check if this IP is in our scan range + $ipParts = $ip -split '\.' + if ($ipParts.Length -eq 4) { + $currentBaseIP = "$($ipParts[0]).$($ipParts[1]).$($ipParts[2])" + $lastOctet = [int]$ipParts[3] + + if ($currentBaseIP -eq $baseIP -and $lastOctet -ge $startIP -and $lastOctet -le $endIP) { + $arpDevices[$ip] = @{ + ip = $ip + mac = $mac + hostname = "" + status = "arp" + } + Write-Log "Found in ARP: $ip -> $mac" + } + } + } + } + } + } + catch { + Write-Log "Warning: Could not retrieve ARP table" + } + } + + Write-Log "Found $($arpDevices.Count) devices in ARP table for this network" + + # STEP 2: Ping sweep for remaining IPs not found in ARP + Write-Log "Step 2: Starting ping sweep for remaining addresses..." + + $aliveIPs = @() + $processed = 0 + $skippedCount = 0 # Process IPs in smaller batches to avoid overwhelming the system for ($batch = $startIP; $batch -le $endIP; $batch += $BatchSize) { @@ -212,9 +395,16 @@ function Get-NetworkDevices { Write-Log "Batch progress: $([Math]::Floor(($batch - $startIP) * 100 / $totalIPs))% - Scanning IPs $batch to $batchEnd" - # Create jobs for current batch + # Create jobs for current batch (skip IPs already found in ARP) for ($i = $batch; $i -le $batchEnd; $i++) { $ip = "$baseIP.$i" + + if ($arpDevices.ContainsKey($ip)) { + $processed++ + $skippedCount++ + continue # Skip IPs already found in ARP + } + $job = Start-Job -ScriptBlock { param($IPAddress, $TimeoutMs) try { @@ -246,7 +436,7 @@ function Get-NetworkDevices { if ($result -and $result.Success) { $aliveIPs += $result.IP - Write-Log "Found alive host: $($result.IP) ($($result.ResponseTime)ms)" + Write-Log "Found alive host via ping: $($result.IP) ($($result.ResponseTime)ms)" } $processed++ @@ -254,49 +444,24 @@ function Get-NetworkDevices { # Show progress more frequently $progressPercent = [Math]::Floor($processed * 100 / $totalIPs) - Write-Log "Progress: $processed/$totalIPs addresses scanned ($progressPercent%)" + Write-Log "Progress: $processed/$totalIPs addresses scanned ($progressPercent%) - Skipped $skippedCount (already in ARP)" # Small delay between batches to prevent system overload Start-Sleep -Milliseconds 100 } - Write-Log "Found $($aliveIPs.Count) alive hosts" + Write-Log "Found $($aliveIPs.Count) additional hosts via ping" - # Get ARP table to find MAC addresses - Write-Log "Retrieving MAC addresses from ARP table..." - $arpEntries = @{} + # STEP 3: Combine results and resolve hostnames + Write-Log "Step 3: Building final device list..." - try { - # Use Get-NetNeighbor for Windows 8/Server 2012 and later - $neighbors = Get-NetNeighbor -AddressFamily IPv4 | Where-Object { $_.State -ne "Unreachable" } - foreach ($neighbor in $neighbors) { - if ($neighbor.LinkLayerAddress -and $neighbor.LinkLayerAddress -ne "00-00-00-00-00-00") { - $arpEntries[$neighbor.IPAddress] = $neighbor.LinkLayerAddress.Replace('-', ':').ToLower() - } - } - } - catch { - Write-Log "Get-NetNeighbor failed, trying arp command..." - - # Fallback to arp command - try { - $arpOutput = arp -a - foreach ($line in $arpOutput) { - if ($line -match '(\d+\.\d+\.\d+\.\d+)\s+([0-9a-fA-F-]{17})\s+\w+') { - $ip = $matches[1] - $mac = $matches[2].Replace('-', ':').ToLower() - if ($mac -ne "00:00:00:00:00:00") { - $arpEntries[$ip] = $mac - } - } - } - } - catch { - Write-Log "Warning: Could not retrieve ARP table" - } + # Add devices found in ARP + foreach ($arpDevice in $arpDevices.Values) { + $arpDevice.status = "online" # Change from "arp" to "online" + $devices += $arpDevice } - # Build device list + # Add devices found via ping (and try to get their MAC from ARP if available) foreach ($ip in $aliveIPs) { $device = @{ ip = $ip @@ -304,11 +469,14 @@ function Get-NetworkDevices { hostname = "" status = "online" } - - # Try to resolve hostname + $devices += $device + } + + # Resolve hostnames and fix MAC addresses for local host + foreach ($device in $devices) { try { - $hostname = [System.Net.Dns]::GetHostEntry($ip).HostName - if ($hostname -and $hostname -ne $ip) { + $hostname = [System.Net.Dns]::GetHostEntry($device.ip).HostName + if ($hostname -and $hostname -ne $device.ip) { $device.hostname = $hostname } } @@ -316,12 +484,18 @@ function Get-NetworkDevices { # Hostname resolution failed, leave empty } - $devices += $device + # Special handling for local host (Windows machine running the script) + if (-not $device.mac -or $device.mac -eq "") { + $device.mac = Get-LocalHostMacAddress -IPAddress $device.ip + if ($device.mac) { + Write-Log "Found local host MAC: $($device.ip) -> $($device.mac)" + } + } if ($device.mac) { - Write-Log "Device found: $ip -> $($device.mac) ($($device.hostname))" + Write-Log "Final device: $($device.ip) -> $($device.mac) ($($device.hostname))" } else { - Write-Log "Device found: $ip (no MAC address available)" + Write-Log "Final device: $($device.ip) (no MAC address available) ($($device.hostname))" } }