Building a PowerShell Ping Sweep Tool: Adding a Port Check

Over the last few articles on the Petri IT Knowledgebase, I’ve been walking you through the process of creating a PowerShell-based tool that you could use to ping a range of subnet addresses. I hope you’ve been following along and if not, then take a few minutes to get caught up. Hopefully you’ll notice I’ve followed an iterative process, where I started with a basic command and slowly added pieces onto the tool. If I had tried to write everything all at once and then test, then it’s more than likely that something would not work, and it might take longer to figure out why. Today, I want to add the final piece to the tool, where we’ll add a port check to our existing tool.

PowerShell Ping Sweep Tool Article Series

  1. Building a Ping Sweep Tool with PowerShell
  2. PowerShell Ping Sweep Tool: Adding Parameter Validation
  3. Adding Trace Information to a PowerShell Ping Tool
  4. Identifying a Computer Name with a PowerShell Ping Sweep Tool
  5. Building a PowerShell Ping Sweep Tool: Adding a Port Check

Philosophically, PowerShell tools, that is scripts and functions, should be designed to do one thing and write a single object type to the pipeline. One of the features I want to add is a quick port check. On one hand, this seems like it should be a completely separate tool. But I’m going to limit my port scan to a single user-defined port. My assumption is that you might want to use the sweep tool to not only see what IP addresses are up, but also see which IT addresses that might have a specific port open.
First, let’s test some code to handle the port scan. I want to test a single server and port.

$IP = "172.16.30.11"
$Port = 80

I already know this port is open, so whatever code I come up with needs to reflect that fact. There’s no easy way around this, but we’ll have to use the .NET Framework directly. How did I know what to use? Google.

$tcp = New-Object System.Net.Sockets.TcpClient

But now you know as well. Here’s what we have.

Using $tcp in Windows PowerShell. (Image Credit: Jeff Hicks)
Using $tcp in Windows PowerShell. (Image Credit: Jeff Hicks)

The object has a method, which you could discover by piping to Get-Member or reading the MSDN documentation called ConnectAsync. It needs an IP address and port.

$tcpconnection = $tcp.ConnectAsync($IP,$Port)

This has made a connection. There is also a Connect method, which you could have used to test. But I used ConnectAsync for performance reasons. Here’s what the object looks like.

Using $tcpconnection in Windows PowerShell. (Image Credit: Jeff Hicks)
Using $tcpconnection in Windows PowerShell. (Image Credit: Jeff Hicks)

You might think you could use the IsCompleted property, but as far as I can tell that has to do with whether the connection process is complete. It turns out we need to dig a bit deeper and look at the AsyncWaitHandle property. This property is a nested object that has a method called WaitOne that will wait for a connection to be made. I’m going to give PowerShell 1000 milliseconds to connect to the port.

$wait = $tcpconnection.AsyncWaitHandle.WaitOne(1000,$False)

The value of $wait will be true or false, which is very handy in PowerShell.

Our $wait property has returned true. (Image Credit: Jeff Hicks)
Our $wait property has returned true. (Image Credit: Jeff Hicks)

Of course, I should verify that with another port that I know is not open.

$port = 22
$tcp = New-Object System.Net.Sockets.TcpClient
$tcpconnection = $tcp.ConnectAsync($IP,$Port)
$wait = $tcpconnection.AsyncWaitHandle.WaitOne(1000,$False)

Our $wait property has returned false. (Image Credit: Jeff Hicks)
Our $wait property has returned false. (Image Credit: Jeff Hicks)

Just what I expected. While PowerShell will probably clean up for me, it is a good practice to close and dispose of the connection.

$tcpconnection.Dispose()
$tcp.Dispose()

In my function, I’ll add a parameter called Port, but I won’t assign a default value. As the function processes each IP address, if there is a port value, I can test using the code from above.

If you recall, the function is writing a custom object to the pipeline and I’m going to add a property called Port. If the port is open, I’ll include the value in the output. Otherwise I’ll set the value to null. If no port is tested, then the property will also be null. Here is the finished function.

Function 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,
[int]$Port
)
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
    }
    #test Port if specified
    if ($Port) {
        $tcp = New-Object System.Net.Sockets.TcpClient
        $tcpconnection = $tcp.ConnectAsync($IP,$Port)
        $wait = $tcpconnection.AsyncWaitHandle.WaitOne(1000,$False)
        If ($wait) {
            #assign port number of it responds
            $PortTest = $Port
            $tcpconnection.Dispose()
        }
        else {
            Write-Verbose "Port $port not open"
            #assign Null if port not open
            $PortTest = $Null
        }
        $tcp.Dispose()
    } #if $Port
    else {
        #if not testing a port set value to Null
        $PortTest = $Null
    }
    #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
         Port = $PortTest
       }
    }
  } #if test ping
  #increment the value $start by 1
  $start++
} #close while loop
} #end function


With the function loaded into my PowerShell session, I can test a range of addresses and see what is listening on port 53.

test-subnet 172.16.30.0 -Start 200 -end 215 -Port 53

Testing port 53 with our new function. (Image Credit: Jeff Hicks)
Testing port 53 with our new function. (Image Credit: Jeff Hicks)

Because I have an object, I can use my tool like any other command in a PowerShell expression.

test-subnet 172.16.30.0 -Port 80 | where {$_.port -eq 80} | Select IPAddress,Hostname

031315 1235 BuildingaPo6
The only task that remains is to insert some comment based help, which I will leave for you to write. I’ll be the first to admit this is far from a perfect PowerShell tool, but I hope the process served as a learning platform. If you have any questions on what I did or why, I hope you’ll leave a comment.