I was scanning the Internat for a native PS way to run an ARP scan and came across your blog and hence GitHub repo. I thought "great, this is exactly what I need". I even thought "awesome" when I noted one of your examples was to pipe in the interface that has been assigned the default gateway, so ignoring VNets and the like. However, on running Find-LANHosts
with a set of IP addresses, I noted something odd.
The returned list from the arp cache was a lot smaller than I anticipated. Running a Wireshark scan after flushing the ARP cache, I noted that relatively few ARP requests were being sent. Slow the function down by running it through the VS Code debugger, it seems that seven though you are looping through the IPs in the relevant array, the Connect
method only connects to the first IP that was used after the Udpclient
was initiated. Hence, I only see an ARP request for the first IP in the array.
The function then only returns items that are already in the arp cache or makes it to the catch before it completes.
As an example, I think this does the trick (but I'm no expert here, so creating and closing Udpclients
like this might be inefficient :) )
<#
.SYNOPSIS
Quickly finds host on a local network using ARP for discovery
.DESCRIPTION
Uses ARP requests to determine whether a host is present on a network segment.
As APR is a Layer 2 mechanism, the list of IP addesses need to be on the same network as the device running the script.
.PARAMETER IP
Optional. Specifies one or more IP addresses to scan for. Typically this will be a list of all usable hosts on a network.
.PARAMETER NetAdapter
Optional. Specifies one or more NetAdaper (CimInstance) objects from Get-NetAdapter. These interfaces will have attached subnets detected and used for the scan.
If both the IP and NetAdapter parameters are omitted, all network adapters will be enumerated and local subnets automatically determined. This may require elevated priviliges.
Please note that this can include adapters with very high host counts (/16, etc) which will take considerable time to enumerate.
.PARAMETER DelayMS
Optional. Specifies the interpacket delay, default is 2ms. Can be increased if scanning unreliable or high latency networks.
.PARAMETER ClearARPCache
Optional. Clears the ARP cache before starting a scan. This is recommended, but may require elevated priviliges.
.EXAMPLE
Find-LANHosts
.EXAMPLE
Find-LANHosts -ClearARPCache -DelayMS 5
.EXAMPLE
Get-NetAdapter -Name Ethernet | Find-LANHosts
.EXAMPLE
Get-NetAdapter | ? {($_ | Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue) -ne $null} | Find-LANHosts
.EXAMPLE
Get-NetRoute -DestinationPrefix 0.0.0.0/0 | Get-NetAdapter | Find-LANHosts
.EXAMPLE
$IPs = 1..254 | % {"10.250.1.$_"}
Find-LANHosts -IP $IPs
.EXAMPLE
1..254 | % {"192.168.1.$_"} | Find-LANHosts -ClearARPCache
.EXAMPLE
1..254 | % {"10.1.1.$_"} | Find-LANHosts -DelayMS 5
.LINK
https://github.com/mdjx/PSLANScan
#>
function Find-LANHosts {
[Cmdletbinding(DefaultParameterSetName = "IPBlock")]
Param (
[Parameter(Mandatory = $false, ValueFromPipeline, ParameterSetName = "IPBlock")]
[string[]]$IP = $null,
[Parameter(Mandatory = $false, ValueFromPipeline, ParameterSetName = "Interface")]
[CimInstance[]]$NetAdapter = $null,
[Parameter(Mandatory = $false, Position = 2)]
[ValidateRange(0, 15000)]
[int]$DelayMS = 2,
[switch]$ClearARPCache
)
Begin {
$ASCIIEncoding = New-Object System.Text.ASCIIEncoding
$Bytes = $ASCIIEncoding.GetBytes("!")
#$UDP = New-Object System.Net.Sockets.Udpclient
if ($ClearARPCache) {
$ARPClear = arp -d 2>&1
if (($ARPClear.count -gt 0) -and ($ARPClear[0] -is [System.Management.Automation.ErrorRecord]) -and ($ARPClear[0].Exception -notmatch "The parameter is incorrect")) {
Throw $ARPClear[0].Exception
}
}
$IPList = [System.Collections.ArrayList]@()
$Timer = [System.Diagnostics.Stopwatch]::StartNew()
Write-Verbose "Beginning scan"
}
Process {
if (($null -eq $IP) -and ($null -eq $NetAdapter)) {
if ($VerbosePreference -eq "SilentlyContinue") { [array]$IP = Get-IPs -ReturnIntRange }
else {[array]$IP = Get-IPs -ReturnIntRange -Verbose }
}
if ($PsCmdlet.ParameterSetName -eq "Interface") {
if ($VerbosePreference -eq "SilentlyContinue") {[array]$IP = Get-IPs -NetAdapter $NetAdapter -ReturnIntRange }
else { [array]$IP = Get-IPs -NetAdapter $NetAdapter -ReturnIntRange -Verbose }
}
if ($IP.Count -lt 1) {
Write-Error "IP Count is less than 1, please check provided IPs or Adapter for valid address space"
}
if ($null -ne $IP.FirstIPInt) {
$IP | ForEach-Object {
$CurrentIPInt = $_.FirstIPInt
Do {
$UDP = New-Object System.Net.Sockets.Udpclient
$CurrIP = [IPAddress]$CurrentIPInt
$CurrIP = ($CurrIP).GetAddressBytes()
[Array]::Reverse($CurrIP)
$CurrIP = ([IPAddress]$CurrIP).IPAddressToString
#$UDP.Connect($CurrIP, 1)
#[void]$UDP.Send($Bytes, $Bytes.length)
[void]$UDP.Send($Bytes, $Bytes.length, $CurrIP, 1)
[void]$IPList.Add($CurrIP)
if ($DelayMS) {
[System.Threading.Thread]::Sleep($DelayMS)
}
$CurrentIPInt++
$UDP.Close()
} While ($CurrentIPInt -le $_.LastIPInt)
}
}
else {
$IP | ForEach-Object {
$UDP = New-Object System.Net.Sockets.Udpclient
#$UDP.Connect($_, 1)
#[void]$UDP.Send($Bytes, $Bytes.length)
[void]$UDP.Send($Bytes, $Bytes.length, $_, 1)
[void]$IPList.Add($_)
if ($DelayMS) {
[System.Threading.Thread]::Sleep($DelayMS)
}
$UDP.Close()
}
}
}
End {
$Hosts = arp -a
$Timer.Stop()
if ($Timer.Elapsed.TotalSeconds -gt 15) {
Write-Warning "Scan took longer than 15 seconds, ARP entries may have been flushed. Recommend lowering DelayMS parameter"
}
$Hosts = $Hosts | Where-Object { $_ -match "dynamic" } | % { ($_.trim() -replace " {1,}", ",") | ConvertFrom-Csv -Header "IP", "MACAddress" }
$Hosts = $Hosts | Where-Object { $_.IP -in $IPList }
Write-Output $Hosts
}
}
Find-LANHosts -IP $IPs