Last Update: Sep 04, 2024 | Published: Apr 20, 2015
I hope you’ve been following along in this series as we build a PowerShell tool to ping a range of IP addresses in a given subnet. In the last article, we left with a pretty decent tool that displayed the IP address for computers that responded. Although that may be all you need, lets try to get a more rich result since PowerShell is already writing an object to the pipeline. It would probably be helpful to see the computer name.
PowerShell Ping Sweep Tool Article Series
If you are running Windows 8 or later, you can easily use the Resolve-DnsName cmdlet with an IP address.
Excellent. I can see that the NameHost property is what I want. Although I'm not a big fan of using raw .NET when there is a perfectly good cmdlet to use, this can serve as an alternative method:
This approach provides us with a different property name but gives us the same result. It would be nice to be able to write an object to the pipeline with the IP address and its host name. It's possible that the IP address is not registered with DNS, so I should handle that error. Here's the relevant change to the function.
Here's what happens when I run the new version. Another useful entry might the device's MAC address. I can't guarantee that the host will be a Windows host or that I will have permissions to access it, so I can't rely on WMI or CIM cmdlets. However, there is an old-school alternative. When I ping a remote IP address, an entry will be made in the arp table.
I can also get a specific IP address. The physical address is the MAC address. To obtain the MAC address, I will have to parse the ARP output. I'll need to get just the line with the MAC. There are a few ways I could parse the text output. Here's one approach: I can split this line into an array of three elements. I am splitting on the white spaces between the columns using a regular expression pattern.
I find it helpful to trim the line first so that there are no extra spaces. The MAC address is the second element.
I can use this code in my function so that if the address can be pinged, I can get the MAC and add it to the output. Here's the new version of Test-Subnet.
[cmdletbinding()] Param( [Parameter(Position=0,HelpMessage="Enter an IPv4 subnet ending in 0.")] [ValidatePattern("d{1,3}.d{1,3}.d{1,3}.0")] [string]$Subnet= ((Get-NetIPAddress -AddressFamily IPv4).Where({$_.InterfaceAlias -notmatch "Bluetooth|Loopback"}).IPAddress -replace "d{1,3}$","0"), [ValidateRange(1,255)] [int]$Start = 1, [ValidateRange(1,255)] [int]$End = 254, [ValidateRange(1,10)] [Alias("count")] [int]$Ping = 1 ) Write-Verbose "Pinging $subnet from $start to $end" Write-Verbose "Testing with $ping pings(s)" #a hash table of parameter values to splat to Write-Progress $progHash = @{ Activity = "Ping Sweep" CurrentOperation = "None" Status = "Pinging IP Address" PercentComplete = 0 } #How many addresses need to be pinged? $count = ($end - $start)+1 <# take the subnet and split it into an array then join the first 3 elements back into a string separated by a period. This will be used to construct an IP address. #> $base = $subnet.split(".")[0..2] -join "." #Initialize a counter $i = 0 #loop while the value of $start is <= $end while ($start -le $end) { #increment the counter write-Verbose $start $i++ #calculate % processed for Write-Progress $progHash.PercentComplete = ($i/$count)*100 #define the IP address to be pinged by using the current value of $start $IP = "$base.$start" #Use the value in Write-Progress $proghash.currentoperation = $IP Write-Progress @proghash #get local IP $local = (Get-NetIPAddress -AddressFamily IPv4).Where({$_.InterfaceAlias -notmatch "Bluetooth|Loopback"}) #test the connection if (Test-Connection -ComputerName $IP -Count $ping -Quiet) { #if the IP is not local get the MAC if ($IP -ne $Local.IPAddress) { #get MAC entry from ARP table Try { $arp = (arp -a $IP | where {$_ -match $IP}).trim() -split "s+" $MAC = $arp[1] } Catch { #this should never happen but just in case Write-Warning "Failed to resolve MAC for $IP" $MAC = "unknown" } } else { #get local MAC $MAC = ($local | Get-NetAdapter).MACAddress } #attempt to resolve the hostname Try { $iphost = (Resolve-DNSName -Name $IP -ErrorAction Stop).Namehost } Catch { Write-Verbose "Failed to resolve host name for $IP" #set a value $iphost = "unknown" } Finally { #create a custom object [pscustomobject]@{ IPAddress = $IP Hostname = $iphost MAC = $MAC } } } #if test ping #increment the value $start by 1 $start++ } #close while loop } #end function
After testing, I realized I needed to take the local IP address into account.
The local address won't be in the ARP table. If the address I'm pinging is the same as the local address, I'll need to take other steps to get the MAC.
I also included some error handling in the rare situation where the address can be pinged but not retrieved from the ARP cache. I don't think this is even possible, but it is only a few lines of code, so I included it to be safe. Most of my tests have been with a small subnet subset, but I should really test the entire thing end to end. I'm also a little curious about how long this will take.
Subtracting the begin and finish times shows this took about 6 minutes. You might get different results depending on the quality of your network and especially name resolution. In fact all of the extra bits could be totally optional. You could create the function so that the basic output is the IP address and if you want the additional detail, it is specified by parameter. There are some advanced things we could do to really speed up performance with runspaces or the use of background jobs, but that is definitely advanced material and beyond the scope of this series. However, there is one more feature I think we can add and I'll cover that next time.