Files
wol/scripts/windows-network-scan.ps1
Marius Mutu 48db3a11d0 Enhance Windows network scanning with improved device discovery
- 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 <noreply@anthropic.com>
2025-09-06 22:42:41 +03:00

577 lines
22 KiB
PowerShell

# Windows Network Scanner for WOL Manager
# This script scans the local network for devices and extracts their MAC addresses
# Results are saved to a JSON file that can be read by the Docker container
param(
[string]$Network = "",
[string]$OutputPath = ".\data\network-scan-results.json",
[int]$TimeoutMs = 1000,
[int]$BatchSize = 10,
[switch]$Verbose
)
function Write-Log {
param([string]$Message)
if ($Verbose) {
Write-Host "[$(Get-Date -Format 'HH:mm:ss')] $Message" -ForegroundColor Green
}
}
function Get-LocalNetworks {
Write-Log "Detecting local networks..."
$networks = @()
# Get all network adapters that are up and have an IP address
$adapters = Get-NetIPAddress -AddressFamily IPv4 | Where-Object {
$_.IPAddress -ne "127.0.0.1" -and ($_.PrefixOrigin -eq "Dhcp" -or $_.PrefixOrigin -eq "Manual")
}
foreach ($adapter in $adapters) {
$networkAddr = $adapter.IPAddress
$prefixLength = $adapter.PrefixLength
# 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 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 "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)"
}
}
}
# Add common default networks if none found
if ($networks.Count -eq 0) {
$networks += "192.168.1.0/24"
Write-Log "No networks auto-detected, using default: 192.168.1.0/24"
}
return $networks
}
function Show-NetworkSelectionMenu {
Write-Host "`nWOL Manager - Selectare Retea" -ForegroundColor Yellow
Write-Host "===============================" -ForegroundColor Yellow
# Try to detect local networks
$detectedNetworks = @()
$adapters = Get-NetIPAddress -AddressFamily IPv4 | Where-Object {
$_.IPAddress -ne "127.0.0.1" -and ($_.PrefixOrigin -eq "Dhcp" -or $_.PrefixOrigin -eq "Manual")
}
foreach ($adapter in $adapters) {
$parts = $adapter.IPAddress.Split('.')
if ($parts.Length -eq 4) {
$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
}
}
}
# Show menu
Write-Host "`nSelecteaza reteaua de scanat:" -ForegroundColor White
Write-Host ""
$menuOptions = @()
$optionIndex = 1
# Add detected networks
if ($detectedNetworks.Count -gt 0) {
Write-Host "Retele detectate automat:" -ForegroundColor Green
foreach ($detectedNetwork in $detectedNetworks) {
Write-Host " $optionIndex. $detectedNetwork" -ForegroundColor Cyan
$menuOptions += $detectedNetwork
$optionIndex++
}
Write-Host ""
}
# Add common networks
Write-Host "Retele comune:" -ForegroundColor Green
$commonNetworks = @("192.168.1.0/24", "192.168.0.0/24", "10.0.0.0/24", "10.0.20.0/24", "172.16.0.0/24")
foreach ($commonNetwork in $commonNetworks) {
if ($detectedNetworks -notcontains $commonNetwork) {
Write-Host " $optionIndex. $commonNetwork" -ForegroundColor Cyan
$menuOptions += $commonNetwork
$optionIndex++
}
}
Write-Host ""
Write-Host " $optionIndex. Introdu manual reteaua" -ForegroundColor Yellow
$menuOptions += "custom"
Write-Host ""
Write-Host " 0. Iesire" -ForegroundColor Red
Write-Host ""
# Get user choice
do {
$choice = Read-Host "Selecteaza optiunea (0-$($menuOptions.Count))"
# Convert to integer for comparison
try {
$choiceNum = [int]$choice
} catch {
Write-Host "Te rog introdu un numar valid!" -ForegroundColor Red
continue
}
if ($choiceNum -eq 0) {
Write-Host "Scanare anulata." -ForegroundColor Yellow
exit 0
}
elseif ($choiceNum -ge 1 -and $choiceNum -le $menuOptions.Count) {
if ($menuOptions[$choiceNum - 1] -eq "custom") {
do {
$customNetwork = Read-Host "Introdu reteaua in format CIDR (ex: 192.168.100.0/24)"
if ($customNetwork -match '^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/\d{1,2}$') {
return @($customNetwork)
} else {
Write-Host "Format invalid! Foloseste formatul: 192.168.1.0/24" -ForegroundColor Red
}
} while ($true)
} else {
return @($menuOptions[$choiceNum - 1])
}
} else {
Write-Host "Optiune invalida! Selecteaza intre 0 si $($menuOptions.Count)." -ForegroundColor Red
}
} while ($true)
}
function Test-NetworkAddress {
param(
[string]$IPAddress,
[int]$TimeoutMs = 1000
)
try {
$ping = New-Object System.Net.NetworkInformation.Ping
$result = $ping.Send($IPAddress, $TimeoutMs)
return $result.Status -eq "Success"
}
catch {
return $false
}
}
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)
Write-Log "Scanning network: $NetworkCIDR"
# Parse network CIDR
$parts = $NetworkCIDR -split '/'
$networkAddress = $parts[0]
$prefixLength = [int]$parts[1]
# Calculate IP range
$networkParts = $networkAddress -split '\.'
$baseIP = "$($networkParts[0]).$($networkParts[1]).$($networkParts[2])"
# Determine scan range based on prefix length
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 "Unsupported prefix length /$prefixLength. Supported: /16, /20, /24"
return @()
}
$devices = @()
$totalIPs = $endIP - $startIP + 1
# 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) {
$batchEnd = [Math]::Min($batch + $BatchSize - 1, $endIP)
$jobs = @()
Write-Log "Batch progress: $([Math]::Floor(($batch - $startIP) * 100 / $totalIPs))% - Scanning IPs $batch to $batchEnd"
# 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 {
$ping = New-Object System.Net.NetworkInformation.Ping
$result = $ping.Send($IPAddress, $TimeoutMs)
return @{
IP = $IPAddress
Success = ($result.Status -eq "Success")
ResponseTime = if ($result.Status -eq "Success") { $result.RoundtripTime } else { $null }
}
}
catch {
return @{
IP = $IPAddress
Success = $false
ResponseTime = $null
}
}
} -ArgumentList $ip, $TimeoutMs
$jobs += @{ Job = $job; IP = $ip }
}
# Wait for batch to complete with timeout
$maxWaitTime = ($TimeoutMs * 2) / 1000 # Convert to seconds, double the ping timeout
foreach ($jobInfo in $jobs) {
$result = Wait-Job -Job $jobInfo.Job -Timeout $maxWaitTime | Receive-Job
Remove-Job -Job $jobInfo.Job -Force
if ($result -and $result.Success) {
$aliveIPs += $result.IP
Write-Log "Found alive host via ping: $($result.IP) ($($result.ResponseTime)ms)"
}
$processed++
}
# Show progress more frequently
$progressPercent = [Math]::Floor($processed * 100 / $totalIPs)
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) additional hosts via ping"
# STEP 3: Combine results and resolve hostnames
Write-Log "Step 3: Building final device list..."
# Add devices found in ARP
foreach ($arpDevice in $arpDevices.Values) {
$arpDevice.status = "online" # Change from "arp" to "online"
$devices += $arpDevice
}
# Add devices found via ping (and try to get their MAC from ARP if available)
foreach ($ip in $aliveIPs) {
$device = @{
ip = $ip
mac = if ($arpEntries.ContainsKey($ip)) { $arpEntries[$ip] } else { "" }
hostname = ""
status = "online"
}
$devices += $device
}
# Resolve hostnames and fix MAC addresses for local host
foreach ($device in $devices) {
try {
$hostname = [System.Net.Dns]::GetHostEntry($device.ip).HostName
if ($hostname -and $hostname -ne $device.ip) {
$device.hostname = $hostname
}
}
catch {
# Hostname resolution failed, leave empty
}
# 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 "Final device: $($device.ip) -> $($device.mac) ($($device.hostname))"
} else {
Write-Log "Final device: $($device.ip) (no MAC address available) ($($device.hostname))"
}
}
return $devices
}
# Main execution
try {
Write-Log "Starting Windows network scan..."
# Determine networks to scan
$networksToScan = @()
if ($Network) {
# Use specified network
$networksToScan += $Network
} else {
# Show interactive menu for network selection
$networksToScan = Show-NetworkSelectionMenu
}
if ($networksToScan.Count -eq 0) {
Write-Error "No networks to scan found"
exit 1
}
$allDevices = @()
# Scan each network
foreach ($net in $networksToScan) {
Write-Log "Processing network: $net"
$devices = Get-NetworkDevices -NetworkCIDR $net
$allDevices += $devices
}
# Prepare result object
$result = @{
success = $true
timestamp = (Get-Date -Format "yyyy-MM-ddTHH:mm:ss.fffZ")
networks_scanned = $networksToScan
computers = $allDevices
message = "Scanare completata cu succes. Gasite $($allDevices.Count) dispozitive."
}
# Ensure output directory exists
$outputDir = Split-Path -Parent $OutputPath
if (-not (Test-Path $outputDir)) {
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
}
# Save results to JSON
$result | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutputPath -Encoding UTF8
Write-Log "Results saved to: $OutputPath"
Write-Log "Scan completed successfully. Found $($allDevices.Count) devices."
# Also output to console for immediate feedback
if ($allDevices.Count -gt 0) {
Write-Host "`nDevices found:" -ForegroundColor Yellow
foreach ($device in $allDevices) {
$macInfo = if ($device.mac) { $device.mac } else { "No MAC" }
$hostInfo = if ($device.hostname) { " ($($device.hostname))" } else { "" }
Write-Host " $($device.ip) -> $macInfo$hostInfo" -ForegroundColor Cyan
}
}
}
catch {
$errorResult = @{
success = $false
timestamp = (Get-Date -Format "yyyy-MM-ddTHH:mm:ss.fffZ")
message = "Eroare la scanare: $($_.Exception.Message)"
computers = @()
}
$errorResult | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutputPath -Encoding UTF8
Write-Error "Scan failed: $($_.Exception.Message)"
exit 1
}