Search code examples
powershellpowershell-remotingwindows-server-2016

Check if port is open even if no service is listening on remote machine


Scenario: I'm into a corporate network and need to automate some network checks. One of these is to verify if specific ports are not firewall blocked but very often there is no service listening on those ports on remote machines and, per my understanding, both Test-NetConnection and portqry, return false/filtered if there is no response from the other side but it doesn't actually mean the firewall is blocking the port, right?

Idea: I first use Test-NetConnection to see if I get any reply, in that case the test is ok. If the remote end is not responding on the port:

  • I create a TCPClient object
  • Invoke a ScriptBlock to the remote computer to create a Listener on the specific port
  • try to connect from Source computer to the target so I can be sure the port is open

By now here is what I came up with:

function Test-Port([string] $server, [int] $port) {
    $out = $false;
    if(Test-NetConnection -ComputerName $server -Port $port | Select-Object -ExpandProperty TcpTestSucceeded) {
        $out = $true;
    } else {
        $TcpClient = New-Object System.Net.Sockets.TcpClient;
        try {
            $Session = New-PSSession -ComputerName $server -ErrorAction Stop;
            if($null -ne $Session) {
                $RemoteLastExitCode = Invoke-Command -Session $Session -ScriptBlock {
                    param($p)
                    try {
                        $Listener = [System.Net.Sockets.TcpListener]$p;
                        $Listener.Start();
                        while($true) {
                            $client = $Listener.AcceptTcpClient();
                            $client.Close();
                            Exit $p;
                        }
                    } catch [System.Net.Sockets.SocketException] {
                        Write-Host "Socket Error: $($_)";
                    } finally {
                        $Listener.Stop();
                    }
                    $LASTEXITCODE;
                } -ArgumentList $port
                try {
                    $TcpClient.Connect($server, $port);
                } catch [System.Net.Sockets.SocketException] {
                    Write-Host "TCP Client Error: $($_)";
                } finally {
                    if($RemoteLastExitCode -eq $port) { $out = $true; }
                    $TcpClient.Close();
                }
            }
        } catch [System.Management.Automation.Remoting.PSRemotingTransportException] {
            Write-Host "Error: Unable to open remote session to $($server). Skipping." -ForegroundColor Red;
        } finally {
            Remove-PSSession -Session $Session -ErrorAction SilentlyContinue;
        }
    }
    return $out;
}

However, it doesn't seem to work. Additionally, when this runs again after the first time, I receive the error "Only one usage of each socket address (protocol/network address/port) is normally permitted". I suppose either the Socket listener or the TCPClient doesn't get closed/Stopped properly.

I'm also not sure the Invoke-Command is execution blocking or not. I also tried to run it with -AsJob parameter but the port test fails. Just to say, I made a manual test (logged into target computer, created a listener and connected from source) to be sure the port is open as prove that this test should return True.

Thanks in advance.


Solution

  • I finally got a solution. My problems were:

    • Invoke-command, even if remote, is execution-blocking
    • TCPListener was not closed if the connection couldn't be established
    • I was not properly returning a result from the Remote invoke-command execution

    Here is a working version which does the following:

    • uses Test-NetConnection to check if $port is open on $server and LISTENING;
    • if not, create TCPClient + TCPListener
    • Launch an Invoke-Command job on the remote $server starting the TCPlistener so we have an endpoint where to test if the firewall is blocking the $port or not;
    • tries to connect the TCPClient to the specified $port on the $server
    • if it succeeded, the invoke-command job returns the port number, otherwise is null;
    • the TCPListener gets closed after 10 seconds if not connection comes;

    P.S.: in my example, I look for a specific session name already existing, you can change that part.

    function Test-Port([string] $server, [int] $port) {
        $out = $false;
        if(Test-NetConnection -ComputerName $server -Port $port | Select-Object -ExpandProperty TcpTestSucceeded) {
            $out = $true;
        } else {
            $removeSession = $false;
            $TcpClient = New-Object System.Net.Sockets.TcpClient;
            try {
                $Session = Get-PSSession | Where-Object -FilterScript { $_.State -eq "Opened" -and ($_.Name -eq "$($env:COMPUTERNAME)$($server)" -or $_.Name -eq "$($server)$($env:COMPUTERNAME)") };
                if($null -eq $Session) { 
                    $Session = New-PSSession -ComputerName $server -ErrorAction Stop; 
                    $removeSession = $true;
                }
    
                Invoke-Command -Session $Session -AsJob -JobName PortTest -ScriptBlock {
                    param($p)
                    $Script:return = $null;
                    try {
                        $IPAddress = Get-NetIPConfiguration | Where-Object -FilterScript { $null -ne $_.IPv4DefaultGateway } | 
                            Select-Object -ExpandProperty IPv4Address | Select-Object -ExpandProperty IPAddress;
                        $Listener = [System.Net.Sockets.TcpListener]::new($IPAddress, $p);
                        $Listener.Start();
    
                        $counter = 0;
                        while($true) {
                            if(!$Listener.Pending()) {
                                if($counter -eq 20) {
                                    $Listener.Stop();
                                    break;
                                }
                                $counter++;
                                Start-Sleep -Milliseconds 500;
                                continue;
                            }
                            $client = $Listener.AcceptTcpClient();
                            $Script:return = $p;
                            $client.Close();
                            break;
                        }
                    } catch [System.Net.Sockets.SocketException] {
                        Write-Host "Socket Error: $($_)";
                    } finally {
                        $Listener.Stop();
                    }
                    return $Script:return;
                } -ArgumentList $port
            
                $TcpClient.ReceiveTimeout = 10000;
                $TcpClient.SendTimeout = 10000;
                $TcpClient.Connect($server, $port);
                $RemoteReturn = Wait-Job -Name PortTest | Receive-Job;
            } catch [System.Management.Automation.Remoting.PSRemotingTransportException] {
                Write-Host "Error: Unable to open remote session to $($server). Skipping." -ForegroundColor Red;
            } catch [System.Net.Sockets.SocketException] {  
                Write-Host "TCP Client Error: $($_)";
            } finally {
                if($RemoteReturn -eq $port) { $out = $true; }
                $TcpClient.Close();
                if($removeSession) { Remove-PSSession -Session $Session -ErrorAction SilentlyContinue; }
            }
        }
        return $out;
    }