Test-NetFlow.psm1

Add-Type -ReferencedAssemblies ([Microsoft.Management.Infrastructure.Ciminstance].Assembly.Location) -TypeDefinition @'
public class TestNetFlowResult
{
    public string ComputerName = null;
 
    //The Remote IP address used for connectivity
    public System.Net.IPAddress RemoteAddress = null;
 
    //Indicates if the Ping was successful
    public bool PingSucceeded;
 
    //Details of the ping
    public System.Net.NetworkInformation.PingReply PingReplyDetails = null;
 
    //The TCP socket
    public System.Net.Sockets.Socket TcpClientSocket = null;
 
    //If the test succeeded
    public bool TcpTestSucceeded;
 
    //Remote port used
    public uint RemotePort;
 
    //The results of the traceroute
    public string[] TraceRoute = null;
 
    //An indicator to the formatter that details should be shown
    public bool Detailed;
 
    //Information on the interface used for connectivity
    public string InterfaceAlias = null;
    public uint InterfaceIndex;
    public string InterfaceDescription = null;
    public Microsoft.Management.Infrastructure.CimInstance NetAdapter = null;
    public Microsoft.Management.Infrastructure.CimInstance NetRoute = null;
 
    //Source IP address
    public Microsoft.Management.Infrastructure.CimInstance SourceAddress = null;
 
    //DNS information
    public bool NameResolutionSucceeded;
    public object BasicNameResolution = null;
    public object LLMNRNetbiosRecords = null;
    public object DNSOnlyRecords = null;
    public object AllNameResolutionResults = null;
 
    //NetSec Info
    public bool IsAdmin; //If the test succeeded
    public string NetworkIsolationContext = null;
    public Microsoft.Management.Infrastructure.CimInstance[] MatchingIPsecRules = null;
 
     
    //Message Content and Resposne
    public string Transport;
    public string Message;
    public string Response;
}
'@



function Test-NetFlow
{
    [CmdletBinding( )]
    Param(
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0)] [Alias('RemoteAddress','cn')] [string] $ComputerName = "internetbeacon.msedge.net",

        [Parameter(ParameterSetName = "ICMP", Mandatory = $False)]  [Switch]$TraceRoute ,
        [Parameter(ParameterSetName = "ICMP", Mandatory = $False)] [ValidateRange(1,120)] [int]$Hops = 30,

        [Parameter(ParameterSetName = "CommonTCPPort", Mandatory = $True, Position = 1)] [ValidateSet("HTTP","RDP","SMB","WINRM")] [String]$CommonTCPPort ="",
        [Parameter(ParameterSetName = "RemotePort")][Parameter(ParameterSetName = "UDP", Mandatory = $True, ValueFromPipelineByPropertyName = $true)] [Alias('RemotePort')] [ValidateRange(1,65535)] [int]$Port = 0,
        
        [Parameter(ParameterSetName = "CommonTCPPort")] [Parameter(ParameterSetName = "RemotePort")] [Parameter(ParameterSetName = "UDP")] [String]$Message ="",

        [Parameter(ParameterSetName = "UDP", Mandatory = $True)]  [Switch]$UDP ,


        [Parameter()] [ValidateSet("Quiet","Detailed")] [String]$InformationLevel = "Standard"
    )

    Begin
    {

        ##Description: Checks if the local execution context is elevated
        ##Input: None
        ##Output: Boolean. True if the local execution context is elevated.
        function CheckIfAdmin 
        {

            $CurrentIdentity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
            $CurrentSecurityPrincipal = new-object System.Security.Principal.WindowsPrincipal($CurrentIdentity)
            $AdminPrincipal = [System.Security.Principal.WindowsBuiltInRole]::Administrator
            return $CurrentSecurityPrincipal.IsInRole($AdminPrincipal)
        }

        ##Description: Returns the remote IP address used for connectivity.
        ##Input: The user-provided computername that will be pinged/tested
        ##Output: An IP address for the remote host
        function ResolveTargetName
           {

                param($TargetName,$TargetPort)

                $RemoteAddress = [System.Net.IPAddress]::Loopback
                if ([System.Net.IPAddress]::TryParse($TargetName, [ref]$RemoteAddress))
                {
                    return $RemoteAddress
                }

                try
                {
                    $UDPClient = new-object System.Net.Sockets.UDPClient($TargetName,$TargetPort)
                    $UDPClient.Connect($TargetName,$TargetPort)
                    return $UDPClient.Client.RemoteEndPoint.Address
                }
                catch [System.Net.Sockets.SocketException]
                {
                    $Message = "Name resolution of $TargetName failed -- Status: " + $_.Exception.InnerException.SocketErrorCode.ToString()
                    Write-Warning $Message
                    return $null
                }
           }

        ##Description: Pings a specified host
        ##Input: IP address to ping
        ##Output: PingReplyDetails for the ping attempt to host
        function PingTest
        {
            param($TargetIPAddress)
            $Ping = new-object System.Net.NetworkInformation.Ping

            ##Indeterminate progress indication
            Write-Progress  -Activity "Test-NetConnection :: $TargetIPAddress" -Status "Ping/ICMP Test" -CurrentOperation "Waiting for echo reply" -SecondsRemaining -1 -PercentComplete -1

            try
            {
                return $Ping.Send($TargetIPAddress)
            }
            catch [System.Net.NetworkInformation.PingException]
            {
                $Message = "Ping to $TargetIPAddress failed -- Status: " + $_.Exception.InnerException.Message.ToString()
                Write-Warning $Message
                return $null
            }
        }

        ##Description: Traces a route to a specified IP address using repetitive echo requests
        ##Input: IP address to trace against
        ##Output: Array of IP addresses representing the traced route. The message from the ping reply status is emmited, if there is no response.
        function TraceRoute
        {
            param($TargetIPAddress,$Hops)
            $Ping = new-object System.Net.NetworkInformation.Ping
            $PingOptions = new-object System.Net.NetworkInformation.PingOptions
            $PingOptions.Ttl=1
            $DataBuffer=new-object byte[] 10
            $ReturnTrace = @()
          
            do
            {
                try
                {
                    $CurrentHop = [int] $PingOptions.Ttl
                    write-progress -CurrentOperation "TTL = $CurrentHop" -Status "ICMP Echo Request (Max TTL = $Hops)" -Activity "TraceRoute" -PercentComplete -1 -SecondsRemaining -1
                    $PingReplyDetails = $Ping.Send($TargetIPAddress, 4000, $DataBuffer, $PingOptions)

                    if($PingReplyDetails.Address -eq $null)
                    {
                         $ReturnTrace += $PingReplyDetails.Status.ToString()
                    }
                    else
                    {
                         $ReturnTrace += $PingReplyDetails.Address.IPAddressToString
                    }
              }
              catch
              {
                    Write-Debug "Exception thrown in PING send"
                 $ReturnTrace += "..."
              }
              $PingOptions.Ttl++
          }
          while(($PingReplyDetails.Status -ne 'Success') -and ($PingOptions.Ttl -le $Hops))

            ##If the last entry in the trace does not equal the target, then the trace did not successfully complete
            if($ReturnTrace[-1] -ne $TargetIPAddress)
                {
                    $OutputString = "Trace route to destination " + $TargetIPAddress + " did not complete. Trace terminated :: " + $ReturnTrace[-1]
                    Write-Warning $OutputString
                }

           return $ReturnTrace
        }

        ##Description: Sends a UDP datagram and waits 4 seconds for a response.
        ##Input: IP address and port to connect to
        ##Output: If the connection succeeded (as a boolean), and the socket, and then the data
        function TestUDP
        {
        
            param($TargetName,$TargetPort)
             try{

                $UDPclient = $null;
                                
                $ProgressString = "Test-NetConnection - " + $TargetName + ":" + $TargetPort
                Write-Progress -Activity $ProgressString -Status "Preparing to send UDP datagram" -CurrentOperation "Binding" -SecondsRemaining -1 -PercentComplete -1
                $UDPclient = new-object System.Net.Sockets.UDPclient($TargetName, $TargetPort);
                Write-Debug $TargetName
                Write-Debug $TargetPort
                
                $UDPclient.Client.ReceiveTimeout = 3000;
                $Encoding = new-object System.Text.ASCIIEncoding;
                $encodedTransmission = $Encoding.GetBytes($Message);
                $void = [Array]::Resize([ref] $encodedTransmission,500);
                $void = $UDPclient.Send($encodedTransmission,500);
                Write-Debug "Sending UDP echo request."
                Start-Sleep -Seconds 1
            } catch [Exception]
            {
               Write-Debug "Exception thrown in UDP bind/sends;"
               
                Write-Debug $_.Exception.Message;
                throw $_.Exception;

                return $null
            }
                
            

            try{
                $Response = "";
                 $receivedTransmission = new-object byte[] 500;
                $remoteendpoint = new-object System.Net.IPEndpoint([System.Net.IPAddress]::Any,0)
                $receivedTransmission  = $UDPclient.Receive([ref] $remoteendpoint);
                $Response = $Encoding.GetString($receivedTransmission);
                $Response = $Encoding.GetString($receivedTransmission).Replace("`0","");
                return  $Response;
                
            } catch [Exception]
            {
               Write-Debug "Exception thrown in UDP receive;"
               
                Write-Debug $_.Exception.Message;
                throw $_.Exception;

                return $null
            }
           




        }



        ##Description: Attempts a TCP connection against a specified IP address
        ##Input: IP address and port to connect to
        ##Output: If the connection succeeded (as a boolean), and the socket, and then the data
        function TestTCP
        {
            param($TargetName,$TargetPort)

            try
            {
                $ProgressString = "Test-NetConnection - " + $TargetName + ":" + $TargetPort
                Write-Progress -Activity $ProgressString -Status "Attempting TCP connect" -CurrentOperation "Waiting for response" -SecondsRemaining -1 -PercentComplete -1
                $TCPClient = new-object System.Net.Sockets.TcpClient($TargetName, $TargetPort);
                $Response = "";
              
                if($Message -ne "")
                {

                     $testStream = $TCPClient.GetStream();
                     $Encoding = new-object System.Text.ASCIIEncoding;

                     $encodedTransmission = $Encoding.GetBytes($Message);
                    
            
                     $void = [Array]::Resize([ref] $encodedTransmission,500);
                     $testStream.Write($encodedTransmission,0,500);
                     $receivedTransmission = new-object byte[] 500;
                     $void = $testStream.Read($receivedTransmission,0,500);
                     $Response = $Encoding.GetString($receivedTransmission).Replace("`0","");
                     Write-Debug "Response: $Response"
                }
            
                return $TCPClient.Client, $TCPClient.Connected, $Response;
            }
            catch [Exception]
            {
                Write-Debug "Exception thrown in TCP connect"
                Write-Debug $_.Exception.Message
                return $null
            }
        }

        ##Description: Modifies the provided object with the correct local connectivty information
        ##Input: TestNetFlowResults object that will be modified
        ##Output: Modified TestNetFlowResult object
        function ResolveRoutingandAdapterWMIObjects
        {
            param($TestNetFlowResult)

            try
            {
                $TestNetFlowResult.SourceAddress, $TestNetFlowResult.NetRoute = Find-NetRoute -RemoteIPAddress $TestNetFlowResult.RemoteAddress -ErrorAction SilentlyContinue
                $TestNetFlowResult.NetAdapter = $TestNetFlowResult.NetRoute | Get-NetAdapter -IncludeHidden -ErrorAction SilentlyContinue

                $TestNetFlowResult.InterfaceAlias = $TestNetFlowResult.NetRoute.InterfaceAlias
                $TestNetFlowResult.InterfaceIndex = $TestNetFlowResult.NetRoute.InterfaceIndex
                $TestNetFlowResult.InterfaceDescription =  $TestNetFlowResult.NetAdapter.InterfaceDescription
            }
            catch
            {
                Write-Debug "Exception thrown in ResolveRoutingandAdapterWMIObjects"
            }
            return $TestNetFlowResult
        }

        ##Description: Resolves the DNS details for the computername
        ##Input: The TestNetFlowResults object that will be "filled in" with DNS information
        ##Output: The modified TestNetFlowResults object
        function ResolveDNSDetails
        {
            param($TestNetFlowResult)
            $TestNetFlowResult.DNSOnlyRecords = @( Resolve-DnsName $ComputerName -DnsOnly -NoHostsFile -Type A_AAAA -ErrorAction SilentlyContinue | where-object {($_.QueryType -eq "A") -or ($_.QueryType -eq "AAAA") } )
            $TestNetFlowResult.LLMNRNetbiosRecords = @( Resolve-DnsName $ComputerName -LlmnrNetbiosOnly   -NoHostsFile -ErrorAction SilentlyContinue | where-object {($_.QueryType -eq "A") -or ($_.QueryType -eq "AAAA") } )
            $TestNetFlowResult.BasicNameResolution = @(Resolve-DnsName $ComputerName -ErrorAction SilentlyContinue | where-object {($_.QueryType -eq "A") -or ($_.QueryType -eq "AAAA")} )

            $TestNetFlowResult.AllNameResolutionResults = $Return.BasicNameResolution  + $Return.DNSOnlyRecords + $Return.LLMNRNetbiosRecords | Sort-Object -Unique -Property Address
            return $TestNetFlowResult
        }

        ##Description: Resolves the network security details for the computername
        ##Input: The TestNetFlowResults object that will be "filled in" with network security information
        ##Output: Teh modified TestNetFlowResults object
        function ResolveNetworkSecurityDetails
        {
            param($TestNetFlowResult)
            $TestNetFlowResult.IsAdmin  = CheckIfAdmin
            $NetworkIsolationInfo = Invoke-WmiMethod -Namespace root\standardcimv2 -Class MSFT_NetAddressFilter -Name QueryIsolationType -ArgumentList $TestNetFlowResult.InterfaceIndex,$TestNetFlowResult.RemoteAddress -ErrorAction SilentlyContinue

            switch ($NetworkIsolationInfo.IsolationType)
            {
                1 {$TestNetFlowResult.NetworkIsolationContext = "Private Network";}
                0 {$TestNetFlowResult.NetworkIsolationContext = "Loopback";}
                2 {$TestNetFlowResult.NetworkIsolationContext = "Internet";}
            }

            ##Elevation is required to read IPsec information for the connection.
            if($TestNetFlowResult.IsAdmin)
            {
                $TestNetFlowResult.MatchingIPsecRules = Find-NetIPsecRule -RemoteAddress $TestNetFlowResult.RemoteAddress  -RemotePort $TestNetFlowResult.RemotePort -Protocol TCP -ErrorAction SilentlyContinue
            }

            return $TestNetFlowResult
        }
    }

    Process
    {
        ##Construct the return object and fill basic details
        $Return = new-object TestNetFlowResult
        $Return.ComputerName = $ComputerName
        $Return.Detailed = ($InformationLevel -eq "Detailed")
        $Return.Transport = "TCP"
        if($UDP)
        {
            if($Message -eq "")
            {
                Write-Debug "No Message Specified for UDP";
                $Message = "UDP Echo";
            }
            $Return.Transport = "UDP";
        }

        $Return.Message = $Message

        #UDP connect is done, to simplify name resolution and source address detection
        $Return.RemoteAddress = ResolveTargetName -TargetName $ComputerName -TargetPort $Return.RemotePort

        if($Return.RemoteAddress -eq $null)
        {
            if($InformationLevel -eq "Quiet")
            {
                return  $false
            }
            return $Return
        }
        else
        {
            $Return.NameResolutionSucceeded = $True
        }

        ##Ping
        $Return.PingReplyDetails = PingTest -TargetIPAddress $Return.RemoteAddress
        if ($Return.PingReplyDetails -ne $null)
        {
            $Return.PingSucceeded = ($Return.PingReplyDetails.Status -eq [System.Net.NetworkInformation.IPStatus]::Success)

            ##Output a warning message if the ping did not succeed
            if(!$Return.PingSucceeded)
            {
                $WarningString = "Ping to $ComputerName failed -- Status: " + $Return.PingReplyDetails.Status.ToString()
                write-warning $WarningString
            }
        }

        #### Begin TCP test ####
        $TCPTestAttempted = $False

            ##Check if the user specified a port directly
            if ($Port -ne 0)
            {
                Write-Debug "User specified a port directly."
                $Return.RemotePort = $Port;
                if($UDP)
                {
                    $Return.Response = TestUDP  -TargetName $Return.ComputerName -TargetPort $Return.RemotePort
                }
                else
                {
                    $TCPTestAttempted = $True
                    $Return.TcpClientSocket, $Return.TcpTestSucceeded, $Return.Response = TestTCP -TargetName $Return.ComputerName -TargetPort $Return.RemotePort
                }
            }
            ##If no port was specified directly, then we check to see if a CommonTCPPort was specified
            if ($CommonTCPPort -ne "")
            {
                switch ($CommonTCPPort)
                {
                    "HTTP" {$Return.RemotePort = 80}
                    "RDP" {$Return.RemotePort = 3389}
                    "SMB" {$Return.RemotePort = 445}
                    "WINRM" {$Return.RemotePort = 5985}
                }

                
                if($UDP)
                {
                    $Return.Response = TestUDP  -TargetName $Return.ComputerName -TargetPort $Return.RemotePort
                }
                else
                {
                    $TCPTestAttempted = $True
                    $Return.TcpClientSocket, $Return.TcpTestSucceeded, $Return.Response = TestTCP -TargetName $Return.ComputerName -TargetPort $Return.RemotePort
                }
            }

            ##If the user specified "quiet" then we should only return a boolean
            if($InformationLevel -eq "Quiet")
            {
                if($TCPTestAttempted)
                    {
                        return  $Return.TcpTestSucceeded
                    }
                else
                    {
                        return  $Return.PingSucceeded
                    }
            }

            ##If we did a TCP test and it failed (did not succeed) then we need to write a warning
            if((!$Return.TcpTestSucceeded) -and ($TCPTestAttempted))
            {
               $WarningString = "TCP connect to $ComputerName"+ ":" + $Return.RemotePort +" failed"
                write-warning $WarningString
            }
        #### End of TCP test ####

        ##TraceRoute, only occurs if switched by the user
        if($TraceRoute -eq $True)
        {
            $Return.TraceRoute = TraceRoute -TargetIPAddress $Return.RemoteAddress -Hops $Hops
        }

        $Return = ResolveDNSDetails -TestNetFlowResult $Return
        $Return = ResolveNetworkSecurityDetails -TestNetFlowResult $Return
        $Return = ResolveRoutingandAdapterWMIObjects -TestNetFlowResult $Return


        return $Return
    }
}

##Export cmdlet and alias to module
New-Alias TNC Test-NetFlow
Export-ModuleMember -Alias TNC -Function Test-NetFlow