Saturday, July 27, 2013

Test-Port function in PowerShell, modified when a Microsoft patch broke it

I frequently need to test whether a remote computer is listening on a particular TCP/IP port. I can use this to test connectivity, but I mostly use it as a simple test to determine if a server and/or a particular service are up and running. For example, it's great for determining if a server I restarted is back up and running SQL before I restart the servers that are dependent on it.

Long ago I wrote my Test-Port function. I have dropped it into scores of PowerShell scripts. It has been refined over the years as I have learned new tricks. As with most good snippets of code, I stole my first draft from the internet. It was long ago, and I regret I cannot credit the author I got it from.
I won't get into how the function works, I'll save that for a different day. This article is just to share how it was impacted by a Microsoft patch, and what I did about it.

The latest version of my old version is as follows:
function Test-Port

{

param ( [string]$Computer, [int]$Port )

#  Initialize object
$Test = new-object Net.Sockets.TcpClient

#  Attempt connection, 300 millisecond timeout, returns boolean
( $Test.BeginConnect( $Computer, $Port, $Null, $Null ) ).AsyncWaitHandle.WaitOne( 300 )

# Cleanup
$Test.Close()

}
Give it a resolvable computer name or IP address and a TCP port number to test, and it returns $True if it can establish a connection in under 300 milliseconds. (I used to use a 100 millisecond timeout, but I have users who test connections from 17,000 miles away, and 100 just wasn't enough to handle the latency.).

That old version of the function still works perfectly well in most circumstances.

The June 2013 Microsoft patches included a security patch for the way Windows handles TCP/IP packets. http://technet.microsoft.com/en-us/security/bulletin/ms13-049

This patch affected the behavior of Microsoft's software-based firewall, Forefront Threat Management Gateway 2010. I have one of these firewalls between my users and our test lab, and my function needs to be able to check connections through the firewall to various test servers. After the patch, the function returns $True for all connections through the TMG firewall if an appropriate allow rule exists, even if the server itself is not responding on that port.

I Googled my way to the MSDN article on [System.Net.Sockets.TCPClient] objects to learn more about them and to look for a different way to do the same thing. (In PowerShell, there is always a different way to do the same thing.)

The method .ConnectAsync() looked promising. I did some testing, and ended up with the following, which is only a minor variation on the original.
function Test-Port
    
{
 
param ( [string]$Computer, [int]$Port )
    
#  Initialize object
$Test = new-object Net.Sockets.TcpClient

#  Attempt connection, 300 millisecond timeout, returns boolean
( $Test.ConnectAsync( $Computer, $Port ) ).AsyncWaitHandle.WaitOne( 300 )
    
# Cleanup
$Test.Close()
    
} 
This version worked great. Until I tested it on a different workstation. It turns out that .ConnectAsync() is was first introduced in .Net 4.5. I need my function to work on end user workstations with a variety of down-level OS's and .Net versions. Someday everyone will be up to date, but for now I need something different.

The root problem is, I cannot trust .AsyncWaitHandle(). It returns a $True as soon as it gets a response that a packet was received, even if we never fully connect. (Some guesswork there, but close enough.) So I have to check for the connection "manually".

Instead of the elegant one-liner, I initiate the connection process as before, then I set a timeout end time, loop until successful or timed out, and then return the result.
function Test-Port
    
{
 
param ( [string]$Computer, [int]$Port )

#  Initialize object
$Test = New-Object Net.Sockets.TcpClient

#  Begin the attempt to connect
$Test.BeginConnect( $Gateway, $Port, $Null, $Null ) | Out-Null

#  Calculate the end of the timeout period
$Timeout = ( Get-Date ).AddMilliseconds( 300 )

#  Loop in 50 second increments until connected or timed out
While ( -not $Test.Connected -and ( Get-Date ) -lt $Timeout )
    { Sleep -Milliseconds 50 }

#  Return the connection status (Boolean)
$Test.Connected
    
# Cleanup
$Test.Close()
    
} 
Again, not as elegant, but it gets the job done. I replaced the function in my script, deployed it to my end users, and everyone is happy again. Thank Microsoft for giving us PowerShell so we can respond to all of the problems Microsoft creates. Love them or hate them. Or both at the same time.

No comments:

Post a Comment