Public/Network/Connectivity/Test-NetworkConnection.ps1
function Test-NetworkConnection { <# .SYNOPSIS Testing the internet connectivity functionality from both Test-Connection and Test-NetConnection .NOTES This is the Modified Version Of the default TNC function. Still Needs more Fixing and testings at the time #> [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", Mandatory = $True, ValueFromPipelineByPropertyName = $true)] [Alias('RemotePort')] [ValidateRange(1, 65535)] [Int] $Port = 0, [Parameter(ParameterSetName = "NetRouteDiagnostics", Mandatory = $True)] [Switch] $DiagnoseRouting, [Parameter(ParameterSetName = "NetRouteDiagnostics", Mandatory = $False)] [String] $ConstrainSourceAddress = "", [Parameter(ParameterSetName = "NetRouteDiagnostics", Mandatory = $False)] [UInt32] $ConstrainInterface = 0, [ValidateSet("Quiet", "Detailed", "Standard")] [String]$InformationLevel = "Standard", # Provide more description about the connectivity and issues that were found. [Parameter(Mandatory = $false)] [switch]$Describe ) Begin { # ActionPreferences # $ogeap = $ErrorActionPreference; $ErrorActionPreference = "SilentlyContinue" #region Functions ##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 = [System.Security.Principal.WindowsPrincipal]::new($CurrentIdentity) $AdminPrincipal = [System.Security.Principal.WindowsBuiltInRole]::Administrator return $CurrentSecurityPrincipal.IsInRole($AdminPrincipal) } ##Description: Resolves a hostname. ##Input: The user-provided computername that will be pinged/tested ##Output: The resolved IP addresses for computername function ResolveTargetName { param ($TargetName) $Addresses = $null try { $Addresses = [System.Net.Dns]::GetHostAddressesAsync($TargetName).GetAwaiter().GetResult() } catch { Write-Debug "Name resolution of $TargetName threw exception: $($_.Exception.Message)" } if ($null -eq $Addresses) { Write-Warning "Name resolution of $TargetName failed" } return $Addresses } ##Description: Pings a specified host ##Input: IP address to ping ##Output: PingReplyDetails for the ping attempt to host function PingTest { param ($TargetIPAddress) $Ping = [System.Net.NetworkInformation.Ping]::new() $PingReplyDetails = $null ##Indeterminate progress indication Write-Progress -Activity "Test-NetConnection :: $TargetIPAddress" -Status "Ping/ICMP Test" -CurrentOperation "Waiting for echo reply" -SecondsRemaining -1 -PercentComplete -1 try { $PingReplyDetails = $Ping.SendPingAsync($TargetIPAddress).GetAwaiter().Getresult() } catch { Write-Debug "Ping to $TargetIPAddress threw exception: $($_.Exception.Message)" } finally { $Ping.Dispose() } return $PingReplyDetails } ##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 = [System.Net.NetworkInformation.Ping]::new() $PingOptions = [System.Net.NetworkInformation.PingOptions]::new() $PingOptions.Ttl = 1 [Byte[]]$DataBuffer = @() 1..10 | ForEach-Object { $DataBuffer += [Byte]0 } $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.SendPingAsync($TargetIPAddress, 4000, $DataBuffer, $PingOptions).GetAwaiter().Getresult() if ($null -eq $PingReplyDetails.Address) { $ReturnTrace += $PingReplyDetails.Status.ToString() } else { $ReturnTrace += $PingReplyDetails.Address.IPAddressToString } } catch { Write-Debug "Ping to $TargetIPAddress threw exception: $($_.Exception.Message)" $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 } $Ping.Dispose() return $ReturnTrace } ##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) function TestTCP { param ($TargetIPAddress, $TargetPort) $ProgressString = "Test-NetConnection - " + $TargetIPAddress + ":" + $TargetPort Write-Progress -Activity $ProgressString -Status "Attempting TCP connect" -CurrentOperation "Waiting for response" -SecondsRemaining -1 -PercentComplete -1 $Success = $False $TCPClient = [System.Net.Sockets.TcpClient]::new($TargetIPAddress.AddressFamily) try { $null = $TCPClient.ConnectAsync($TargetIPAddress, $TargetPort).GetAwaiter().Getresult() $Success = $TCPClient.Connected; } catch { Write-Debug "TCP connect to ($TargetIPAddress : $TargetPort) threw exception: $($_.Exception.Message)" } finally { $TCPClient.Dispose() } return $Success } ##Description: Modifies the provided object with the correct local connectivty information ##Input: TestNetConnectionResults object that will be modified ##Output: Modified TestNetConnectionResult object function ResolveRoutingandAdapterWMIObjects { param ($TestNetConnectionResult) try { $TestNetConnectionResult.SourceAddress, $TestNetConnectionResult.NetRoute = Find-NetRoute -RemoteIPAddress $TestNetConnectionResult.RemoteAddress -ErrorAction SilentlyContinue $TestNetConnectionResult.NetAdapter = $TestNetConnectionResult.NetRoute | Get-NetAdapter -IncludeHidden -ErrorAction SilentlyContinue $TestNetConnectionResult.InterfaceAlias = $TestNetConnectionResult.NetRoute.InterfaceAlias $TestNetConnectionResult.InterfaceIndex = $TestNetConnectionResult.NetRoute.InterfaceIndex $TestNetConnectionResult.InterfaceDescription = $TestNetConnectionResult.NetAdapter.InterfaceDescription } catch { Write-Debug "ResolveRoutingandAdapterWMIObjects threw exception: $($_.Exception.Message)" } return $TestNetConnectionResult } ##Description: Resolves the DNS details for the computername ##Input: The TestNetConnectionResults object that will be "filled in" with DNS information ##Output: The modified TestNetConnectionResults object function ResolveDNSDetails { param ($TestNetConnectionResult) $TestNetConnectionResult.DNSOnlyRecords = @( Resolve-DnsName $ComputerName -DnsOnly -NoHostsFile -Type A_AAAA -ErrorAction SilentlyContinue | Where-Object { ($_.QueryType -eq "A") -or ($_.QueryType -eq "AAAA") -or ($_.QueryType -eq "PTR") } ) $TestNetConnectionResult.LLMNRNetbiosRecords = @( Resolve-DnsName $ComputerName -LlmnrNetbiosOnly -NoHostsFile -ErrorAction SilentlyContinue | Where-Object { ($_.QueryType -eq "A") -or ($_.QueryType -eq "AAAA") } ) $TestNetConnectionResult.BasicNameResolution = @(Resolve-DnsName $ComputerName -ErrorAction SilentlyContinue | Where-Object { ($_.QueryType -eq "A") -or ($_.QueryType -eq "AAAA") -or ($_.QueryType -eq "PTR") } ) $TestNetConnectionResult.AllNameResolutionResults = $Return.BasicNameResolution + $Return.DNSOnlyRecords + $Return.LLMNRNetbiosRecords | Sort-Object -Unique -Property Address return $TestNetConnectionResult } ##Description: Resolves the network security details for the computername ##Input: The TestNetConnectionResults object that will be "filled in" with network security information ##Output: Teh modified TestNetConnectionResults object function ResolveNetworkSecurityDetails { param ($TestNetConnectionResult) $TestNetConnectionResult.IsAdmin = CheckIfAdmin $NetworkIsolationInfo = Invoke-CimMethod -Namespace root\standardcimv2 -ClassName MSFT_NetAddressFilter -MethodName QueryIsolationType -Arguments @{InterfaceIndex = [uint32]$TestNetConnectionResult.InterfaceIndex; RemoteAddress = [string]$TestNetConnectionResult.RemoteAddress } -ErrorAction SilentlyContinue switch ($NetworkIsolationInfo.IsolationType) { 1 { $TestNetConnectionResult.NetworkIsolationContext = "Private Network"; } 0 { $TestNetConnectionResult.NetworkIsolationContext = "Loopback"; } 2 { $TestNetConnectionResult.NetworkIsolationContext = "Internet"; } } ##Elevation is required to read IPsec information for the connection. if ($TestNetConnectionResult.IsAdmin) { $TestNetConnectionResult.MatchingIPsecRules = Find-NetIPsecRule -RemoteAddress $TestNetConnectionResult.RemoteAddress -RemotePort $TestNetConnectionResult.RemotePort -Protocol TCP -ErrorAction SilentlyContinue } return $TestNetConnectionResult } ##Description: Diagnose route selection for a destination ##Input: RouteDiagnosticsResults object that will be filled on with route diagnostics information. ##Output: None function DiagnoseRouteSelection { param ([NetRouteDiagnostics] $RouteDiagnostics) $RouteDiagnostics.RouteDiagnosticsSucceeded = $False if ($RouteDiagnostics.Detailed) { Write-Progress -Activity "Test-NetConnection :: $($RouteDiagnostics.RemoteAddress)" -Status "RouteDiagnostics" -CurrentOperation "Starting Route Event Tracing" -SecondsRemaining -1 -PercentComplete -1 $LogFile = "" do { $LogFile = [System.IO.Path]::GetTempFileName().split(".")[0] + "Test-NetConnection.etl" } while (Test-Path -Path $LogFile -ErrorAction SilentlyContinue) $TraceResults = netsh trace start tracefile=$LogFile provider=Microsoft-Windows-TCPIP keywords=ut:TcpipRoute report=di perfmerge=no correlation=di session=tnc } Write-Progress -Activity "Test-NetConnection :: $($RouteDiagnostics.ComputerName)" -Status "RouteDiagnostics" -CurrentOperation "Resolving name" -SecondsRemaining -1 -PercentComplete -1 $RouteDiagnostics.ResolvedAddresses = ResolveTargetName -TargetName $RouteDiagnostics.ComputerName if ($null -eq $RouteDiagnostics.ResolvedAddresses) { netsh trace stop sessionname=tnc | Out-Null return } $RouteDiagnostics.RemoteAddress = $RouteDiagnostics.ResolvedAddresses[0] if ($null -eq $RouteDiagnostics.ConstrainSourceAddress) { if ($RouteDiagnostics.RemoteAddress.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork) { $RouteDiagnostics.ConstrainSourceAddress = [System.Net.IPAddress]::Any } else { $RouteDiagnostics.ConstrainSourceAddress = [System.Net.IPAddress]::IPv6Any } } if ($RouteDiagnostics.Detailed -and (Test-Path -Path $LogFile)) { ##Flush the destination cache to trigger a new route route lookup if ($RouteDiagnostics.RemoteAddress.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork) { netsh int ipv4 delete destinationcache | Out-Null } else { netsh int ipv6 delete destinationcache | Out-Null } } Write-Progress -Activity "Test-NetConnection :: $($RouteDiagnostics.RemoteAddress)" -Status "RouteDiagnostics" -CurrentOperation "Finding route" -SecondsRemaining -1 -PercentComplete -1 try { $RouteDiagnostics.SelectedSourceAddress, $RouteDiagnostics.SelectedNetRoute = ` Find-NetRoute -RemoteIPAddress $RouteDiagnostics.RemoteAddress -LocalIPAddress $RouteDiagnostics.ConstrainSourceAddress -InterfaceIndex $RouteDiagnostics.ConstrainInterfaceIndex -ErrorAction SilentlyContinue $RouteDiagnostics.OutgoingNetAdapter = $RouteDiagnostics.SelectedNetRoute | Get-NetAdapter -IncludeHidden -ErrorAction SilentlyContinue $RouteDiagnostics.OutgoingInterfaceAlias = $RouteDiagnostics.SelectedNetRoute.InterfaceAlias $RouteDiagnostics.OutgoingInterfaceIndex = $RouteDiagnostics.SelectedNetRoute.InterfaceIndex $RouteDiagnostics.OutgoingInterfaceDescription = $RouteDiagnostics.OutgoingNetAdapter.InterfaceDescription } catch { Write-Debug "Error occured while finding route information. Exception: $($_.Exception.Message)" netsh trace stop sessionname=tnc | Out-Null return } if ($RouteDiagnostics.Detailed) { Write-Progress -Activity "Test-NetConnection :: $($RouteDiagnostics.RemoteAddress)" -Status "RouteDiagnostics" -CurrentOperation "Parsing Route Events" -SecondsRemaining -1 -PercentComplete -1 $TraceResults += netsh trace stop sessionname=tnc if (!(Test-Path -Path $LogFile)) { $Message = "Error occured while collection routing events. Error: " + $TraceResults Write-Warning $Message return } ##Search for all relevant routing events: IpSourceAddressSelection (ID 1326), IpSortedAddressPairs (ID 1327), RouteLookup (ID 1370), IpRouteSelection (ID 1383), and IpRouteBlocked (ID 1384) $AllRoutingEvents = Get-WinEvent -Oldest -FilterHashtable @{ Path = $LogFile; ProviderName = 'Microsoft-Windows-TCPIP'; ID = @(1326, 1327, 1370, 1383, 1384) } | Where-Object { $null -ne $_.Message } if ($AllRoutingEvents.Count -eq 0) { ##There may be no route or address selection events if there was only one matching route, but there should've been at least one route lookup event. Write-Warning "No TCPIP routing events collected from $LogFile. The trace might have failed." } else { ##Get route selection events: [IpRouteSelection (ID 1383) and IpRouteBlocked (ID 1384)] containing the remote IP $RouteEvents = $AllRoutingEvents | Where-Object { (($_.Id -eq 1383) -or ($_.Id -eq 1384)) -and $_.Message.Contains("$($RouteDiagnostics.RemoteAddress) ") } foreach ($event in $RouteEvents) { if ($RouteDiagnostics.RouteSelectionEvents -notcontains $($event.Message)) { $RouteDiagnostics.RouteSelectionEvents += "$($event.Message)" } } ##Get source address selection events: [IpSourceAddressSelection (ID 1326)] containing the remote IP $SrcAddrEvents = $AllRoutingEvents | Where-Object { ($_.Id -eq 1326) -and $_.Message.Contains("$($RouteDiagnostics.RemoteAddress) ") } foreach ($event in $SrcAddrEvents) { if ($RouteDiagnostics.SourceAddressSelectionEvents -notcontains $($event.Message)) { $RouteDiagnostics.SourceAddressSelectionEvents += "$($event.Message)" } } ##Get destination address selection events: [IpSortedAddressPairs (ID 1327)] containing the resolved IPs $ResolvedAddrs = $RouteDiagnostics.ResolvedAddresses | ForEach-Object { $_.IPAddressToString } $DstAddrEvents = $AllRoutingEvents | Where-Object { ($_.Id -eq 1327) -and ($null -ne ($CurrMessage = $_.Message)) -and (($ResolvedAddrs | ForEach-Object { $CurrMessage.Contains("$_)") }) -contains $true) } foreach ($event in $DstAddrEvents) { if ($RouteDiagnostics.DestinationAddressSelectionEvents -notcontains $($event.Message)) { $RouteDiagnostics.DestinationAddressSelectionEvents += "$($event.Message)" } } } $RouteDiagnostics.LogFile = $LogFile } $RouteDiagnostics.RouteDiagnosticsSucceeded = $True return } #endregion } Process { switch ($PSCmdlet.ParameterSetName) { ##Route diagnostics parameterset "NetRouteDiagnostics" { $Return = [NetRouteDiagnostics]::new() $Return.ComputerName = $ComputerName if ($ConstrainSourceAddress -ne "") { $Return.ConstrainSourceAddress = $ConstrainSourceAddress } $Return.ConstrainInterfaceIndex = $ConstrainInterface $Return.Detailed = ($InformationLevel -eq "Detailed") if ($Return.Detailed -and (!(CheckIfAdmin))) { Write-Warning "'-InformationLevel Detailed' requires elevation (Run as administrator)." $Return.Detailed = $False } DiagnoseRouteSelection -RouteDiagnostics $Return return $Return } ##Test connection parametersets default { ##Construct the return object and fill basic details $Return = [TestNetConnectionResult]::new() $Return.ComputerName = $ComputerName $Return.Detailed = ($InformationLevel -eq "Detailed") #### Begin Name Resolution #### $Return.ResolvedAddresses = ResolveTargetName -TargetName $ComputerName if ($null -eq $Return.ResolvedAddresses) { if ($InformationLevel -eq "Quiet") { return $False } $Return.NameResolutionSucceeded = $False return $Return } $Return.RemoteAddress = $Return.ResolvedAddresses[0] $Return.NameResolutionSucceeded = $True #### End of Name Resolution #### #### Begin TCP test #### ##Attempt TCP test only if Port or CommonTCPPort is specified $AttemptTcpTest = ($PSCmdlet.ParameterSetName -eq "CommonTCPPort") -or ($PSCmdlet.ParameterSetName -eq "RemotePort") if ($AttemptTcpTest) { $Return.TcpTestSucceeded = $False switch ($CommonTCPPort) { "" { $Return.RemotePort = $Port } "HTTP" { $Return.RemotePort = 80 } "RDP" { $Return.RemotePort = 3389 } "SMB" { $Return.RemotePort = 445 } "WINRM" { $Return.RemotePort = 5985 } } ##Try TCP connect using all resolved addresses until it succeeds $Iter = 0 while (($Iter -lt $Return.ResolvedAddresses.Count) -and (!$Return.TcpTestSucceeded)) { $Return.TcpTestSucceeded = TestTCP -TargetIPAddress $Return.ResolvedAddresses[$Iter] -TargetPort $Return.RemotePort ##Output a warning message if the TCP test didn't succeed if (!$Return.TcpTestSucceeded) { Write-Warning "TCP connect to ($($Return.ResolvedAddresses[$Iter]) : $($Return.RemotePort)) failed" } $Iter++ } if ($Return.TcpTestSucceeded) { ##Get the remote address that was actually used for connection $Return.RemoteAddress = $Return.ResolvedAddresses[$Iter - 1] } ##If the user specified "quiet" then we should only return a boolean if ($InformationLevel -eq "Quiet") { return $Return.TcpTestSucceeded } } #### End of TCP test #### #### Begin Ping test #### ##Attempt Ping test only if TCP test is not attempted or TCP test failed $AttemptPingTest = (!$AttemptTcpTest) -or (!$Return.TcpTestSucceeded) if ($AttemptPingTest) { $Return.PingSucceeded = $False ##Try Ping using all resolved addresses until it succeeds $Iter = 0 while (($Iter -lt $Return.ResolvedAddresses.Count) -and (!$Return.PingSucceeded)) { $Return.PingReplyDetails = PingTest -TargetIPAddress $Return.ResolvedAddresses[$Iter] if ($null -ne $Return.PingReplyDetails) { $Return.PingSucceeded = ($Return.PingReplyDetails.Status -eq [System.Net.NetworkInformation.IPStatus]::Success) } ##Output a warning message if Ping didn't succeed if (!$Return.PingSucceeded) { $WarningString = "Ping to $($Return.ResolvedAddresses[$Iter]) failed" if ($null -ne $Return.PingReplyDetails) { $WarningString += " with status: $($Return.PingReplyDetails.Status)" } Write-Warning $WarningString } $Iter++ } if ($Return.PingSucceeded) { ##Get the remote address that was actually used for Ping $Return.RemoteAddress = $Return.ResolvedAddresses[$Iter - 1] } ##If the user specified "quiet" then we should only return a boolean if ($InformationLevel -eq "Quiet") { return $Return.PingSucceeded } } #### End of Ping test #### #### Begin TraceRoute #### ##TraceRoute, only occurs if switched by the user if ($TraceRoute -eq $True) { $Return.TraceRoute = TraceRoute -TargetIPAddress $Return.RemoteAddress -Hops $Hops } #### End of TraceRoute #### $Return = ResolveDNSDetails -TestNetConnectionResult $Return $Return = ResolveNetworkSecurityDetails -TestNetConnectionResult $Return $Return = ResolveRoutingandAdapterWMIObjects -TestNetConnectionResult $Return return $Return } } } end { # $ErrorActionPreference = $ogeap } } |