Private/Connectivity.psm1

using namespace System
using namespace System.Net
using namespace System.Net.NetworkInformation
using namespace System.Net.Sockets

using module ./Models.psm1
using module ./DNS.psm1
using module ./IPManagement.psm1


class AsyncPingResult {
  [string]$Result
  [string]$IPAddress
  [int]$ResponseTime
  [int]$BufferSize
  hidden [string]$ComputerName
  hidden [bool]$DontFragment
  hidden [string]$Source
  hidden $TimeToLive
  hidden $Timeout
}

class Connectivity {
  static [object] ConnectRemoteDesktop([string[]]$ComputerName, [System.Management.Automation.PSCredential]$Credential, [bool]$Admin, [bool]$MultiMon, [bool]$FullScreen, [bool]$Public, [int]$Width, [int]$Height, [bool]$Wait) {
    [string]$MstscArguments = -join $(
      switch ($true) {
        { $Admin } { '/admin ' }
        { $MultiMon } { '/multimon ' }
        { $FullScreen } { '/f ' }
        { $Public } { '/public ' }
        { $Width } { "/w:$Width " }
        { $Height } { "/h:$Height " }
      }
    )
    foreach ($Computer in $ComputerName) {
      $ProcessInfo = [System.Diagnostics.ProcessStartInfo]::new()
      $Process = [System.Diagnostics.Process]::new()
      $ComputerCmdkey = if ($Computer.Contains(':')) { ($Computer -split ':')[0] } else { $Computer }
      # Store credential via cmdkey
      $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Credential.Password)
      $plainPwd = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)
      & cmdkey.exe /generic:"TERMSRV/$ComputerCmdkey" /user:$Credential.UserName /pass:$plainPwd | Out-Null
      [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
      $ProcessInfo.FileName = "$Env:SystemRoot\system32\mstsc.exe"
      $ProcessInfo.Arguments = "$MstscArguments /v $Computer"
      $ProcessInfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Normal
      $Process.StartInfo = $ProcessInfo
      [void]$Process.Start()
      if ($Wait) { $null = $Process.WaitForExit() }
    }
    return $null
  }


  static [PSCustomObject] GetPSSessionInfo() {
    $pid_val = [System.Diagnostics.Process]::GetCurrentProcess().Id
    $proc = Get-Process -Id $pid_val
    $cmd = $null
    $parent = $null
    $psVT = (Get-Variable PSVersionTable -ValueOnly -ErrorAction SilentlyContinue)
    $psEd = if ($psVT) { $psVT.PSEdition } else { 'Core' }
    if ($psEd -eq 'Desktop') {
      $cim = Get-CimInstance -ClassName Win32_process -Filter "processID = $($proc.Id)" -Property CommandLine, ParentProcessID
      $cmd = $cim.CommandLine
      $parent = Get-Process -Id $cim.ParentProcessId -ErrorAction SilentlyContinue
    } else {
      $cmd = $proc.CommandLine
      $parent = $proc.Parent
    }
    $psver = (Get-Variable PSVersionTable -ValueOnly -ErrorAction SilentlyContinue).PSVersion
    $info = [PSCustomObject]@{
      PSTypeName = 'PSSessionInfo'
      ProcessID  = $pid_val
      Command    = $cmd
      Host       = (Get-Variable Host -ValueOnly -ErrorAction SilentlyContinue).Name
      Started    = $proc.StartTime
      PSVersion  = $psver
      Elevated   = $false
      Parent     = $parent
    }
    Update-TypeData -TypeName PSSessionInfo -MemberType ScriptProperty -MemberName Runtime -Value { (Get-Date) - $this.Started } -Force
    Update-TypeData -TypeName PSSessionInfo -MemberType ScriptProperty -MemberName Memory -Value { (Get-Process -Id $this.ProcessID).WorkingSet / 1MB -as [int32] } -Force
    return $info
  }


  static [object] InvokeDig([string]$Name, [string]$Type, [string]$Server) {
    if (![string]::IsNullOrWhiteSpace($Server)) {
      $r = Resolve-DnsName -Name $Name -Type $Type -Server $Server | Format-Table -AutoSize | Out-String
    } else {
      $r = Resolve-DnsName -Name $Name -Type $Type -ErrorAction SilentlyContinue | Format-Table -AutoSize | Out-String
    }
    if (!$r) { Write-Warning "Unable to resolve [$Name] :(" }
    return $r
  }

  static [object] InvokePing([string]$Name, [int]$Count, [bool]$IPv6) {
    if ($IPv6) {
      $r = & ping.exe $Name -n $Count -6 -a
    } else {
      $r = & ping.exe $Name -n $Count -4 -a
    }
    return ($r -join "`n")
  }

  static [object] SendPingAsync([string[]]$ips) {
    $t = $ips | ForEach-Object { ([Net.NetworkInformation.Ping]::new()).SendPingAsync($_, 250) }
    [Threading.Tasks.Task]::WaitAll($t)
    return $t.Result
  }


  static [void] SendTCP([string]$TargetIP, [int]$TargetPort, [string]$Message) {
    try {
      if ([string]::IsNullOrEmpty($TargetIP)) { $TargetIP = Read-Host 'Enter target IP address' }
      if ($TargetPort -eq 0) { $TargetPort = [int](Read-Host 'Enter target port') }
      if ([string]::IsNullOrEmpty($Message)) { $Message = Read-Host 'Enter message to send' }

      $IP = [Dns]::GetHostAddresses($TargetIP)
      $Address = [IPAddress]::Parse($IP[0].ToString())
      $Socket = [TcpClient]::new($Address, $TargetPort)
      $Stream = $Socket.GetStream()
      $Writer = [System.IO.StreamWriter]::new($Stream)
      $Writer.WriteLine($Message)
      $Writer.Flush()
      $Stream.Close()
      $Socket.Close()
      Write-Host "Done." -ForegroundColor Green
    } catch {
      Write-Verbose "Errored: $($_.CategoryInfo.Category) : $($_.CategoryInfo.Reason) : $($_.Exception.Message)"
    }
  }

  static [void] SendUDP([string]$TargetIP, [int]$TargetPort, [string]$Message) {
    try {
      if ([string]::IsNullOrEmpty($TargetIP)) { $TargetIP = Read-Host 'Enter target IP address' }
      if ($TargetPort -eq 0) { $TargetPort = [int](Read-Host 'Enter target port') }
      if ([string]::IsNullOrEmpty($Message)) { $Message = Read-Host 'Enter message to send' }

      $IP = [Dns]::GetHostAddresses($TargetIP)
      $Address = [IPAddress]::Parse($IP[0].ToString())
      $EndPoints = [System.Net.IPEndPoint]::new($Address, $TargetPort)
      $Socket = [UdpClient]::new()
      $EncodedText = [Text.Encoding]::ASCII.GetBytes($Message)
      [void]$Socket.Send($EncodedText, $EncodedText.Length, $EndPoints)
      $Socket.Close()
      Write-Host "Done." -ForegroundColor Green
    } catch {
      Write-Verbose "Errored: $($_.CategoryInfo.Category) : $($_.CategoryInfo.Reason) : $($_.Exception.Message)"
    }
  }

  static [object[]] StartCimSession([string[]]$ComputerName, [System.Management.Automation.PSCredential]$Credential) {
    $Opt = New-CimSessionOption -Protocol Dcom
    $result = @()
    $SessionParams = @{ ErrorAction = 'Stop' }
    if ($null -ne $Credential) { $SessionParams.Credential = $Credential }
    foreach ($Computer in $ComputerName) {
      $SessionParams.ComputerName = $Computer
      if ((Test-WSMan -ComputerName $Computer -ErrorAction SilentlyContinue).productversion -match 'Stack: ([3-9]|[1-9][0-9]+)\.[0-9]+') {
        try {
          Write-Verbose "Attempting to connect to $Computer using the WSMAN protocol."
          $result += New-CimSession @SessionParams
        } catch {
          Write-Warning "Unable to connect to $Computer using the WSMAN protocol. Verify your credentials and try again."
        }
      } else {
        $SessionParams.SessionOption = $Opt
        try {
          Write-Verbose "Attempting to connect to $Computer using the DCOM protocol."
          $result += New-CimSession @SessionParams
        } catch {
          Write-Warning "Unable to connect to $Computer using the WSMAN or DCOM protocol. Verify $Computer is online and try again."
        }
        $SessionParams.Remove('SessionOption')
      }
    }
    return $result
  }


  static [object] TestConnectionAsync([string[]]$ComputerName, [int]$Timeout, [int]$TimeToLive, [int]$BufferSize, [bool]$IncludeSource, [bool]$Full) {
    $result = @()
    $Source = if ($IncludeSource) { $Env:COMPUTERNAME } else { $null }
    $Buffer = [System.Collections.ArrayList]::new()
    1..$BufferSize | ForEach-Object { $null = $Buffer.Add(([byte][char]'A')) }
    $PingOptions = [PingOptions]::new()
    $PingOptions.Ttl = $TimeToLive
    $DontFragment = $BufferSize -le 1500
    $PingOptions.DontFragment = $DontFragment

    $Task = foreach ($Computer in $ComputerName) {
      [pscustomobject]@{
        ComputerName = $Computer
        Task         = [Ping]::new().SendPingAsync($Computer, $Timeout, $Buffer, $PingOptions)
      }
    }
    try {
      [void][Threading.Tasks.Task]::WaitAll($Task.Task)
    } catch {
      Write-Error -Message "Error checking connections: $($_.Exception.Message)"
    }
    $Task | ForEach-Object {
      $r = [AsyncPingResult]::new()
      $r.ComputerName = $_.ComputerName
      $r.BufferSize = $BufferSize
      $r.Timeout = $Timeout
      $r.TimeToLive = $TimeToLive
      $r.DontFragment = $DontFragment
      if ($IncludeSource) { $r.Source = $Source }
      if ($_.Task.IsFaulted) {
        $r.Result = $_.Task.Exception.InnerException.InnerException.Message
        $r.IPAddress = $null
        $r.ResponseTime = 0
      } else {
        $r.Result = $_.Task.Result.Status
        $r.IPAddress = $_.Task.Result.Address.ToString()
        $r.ResponseTime = $_.Task.Result.RoundtripTime
      }
      $result += $r
    }
    return $result
  }


  static [void] TestDns([string[]]$InterfaceAlias, [int]$ThrottleLimit) {
    try {
      $StopWatch = [System.Diagnostics.Stopwatch]::StartNew()
      $PathToRepo = [IO.Path]::Combine((Get-Variable PSScriptRoot -ValueOnly), '..')
      $Table = Import-Csv "$PathToRepo/Data/domain-names.csv"
      foreach ($Row in $Table) {
        Write-Progress "Resolving $($Row.Domain) ..."
        $isLin = (Get-Variable IsLinux -ValueOnly -ErrorAction SilentlyContinue) -eq $true
        if ($isLin) {
          $null = & nslookup $Row.Domain
        } else {
          $null = Resolve-DnsName $Row.Domain -ErrorAction SilentlyContinue
        }
      }
      $Count = $Table.Count
      [int]$Elapsed = $StopWatch.Elapsed.TotalSeconds
      $Average = [math]::round($Count / [math]::Max($Elapsed, 1), 1)
      Write-Verbose "Resolved $Count domains. Average: $Average domains/sec"
    } catch {
      Write-Verbose "Errored: $($_.CategoryInfo.Category) : $($_.CategoryInfo.Reason) : $($_.Exception.Message)"
    }
  }


  static [object] TestNetwork([string[]]$Subnet) {
    $result = $null
    foreach ($curSubnet in $Subnet) {
      $net = [IPManagement]::GetIpRange($curSubnet)
      $result = [Connectivity]::TestConnectionAsync($net, 5000, 64, 32, $false, $false) |
        Select-Object -Property @{Name = 'IpAddress'; Expr = { $_.ComputerName } }, @{Name = 'ComputerName'; Expr = { $null } }, Result
      $Name = [DNS]::GetDNSHostEntryAsync($net)
      foreach ($curResult in $result) {
        $tmp = $Name | Where-Object { $_.ComputerName -eq $curResult.IpAddress }
        if ($tmp) {
          $curResult.ComputerName = if ($tmp.Result -eq 'No such host is known') { 'UNKNOWN' } else { $tmp.Result.ToLower() }
        }
        if ($curResult.Result -ne 'Success') { $curResult.Result = 'TimeOut' }
      }
      $result = $result | Where-Object { !($_.ComputerName -eq 'UNKNOWN' -and $_.Result -eq 'TimeOut') }
    }
    return $result
  }


  static [object] TestNetworkConnection([string]$ComputerName, [bool]$TraceRoute, [int]$Hops, [string]$CommonTCPPort, [int]$Port, [bool]$DiagnoseRouting, [string]$ConstrainSourceAddress, [uint32]$ConstrainInterface, [string]$InformationLevel, [bool]$Describe) {
    $result = $null
    if ($DiagnoseRouting) {
      $Return = [NetRouteDiagnostics]::new()
      $Return.ComputerName = $ComputerName
      if (![string]::IsNullOrEmpty($ConstrainSourceAddress)) { $Return.ConstrainSourceAddress = $ConstrainSourceAddress }
      $Return.ConstrainInterfaceIndex = $ConstrainInterface
      $Return.Detailed = ($InformationLevel -eq 'Detailed')
      if ($Return.Detailed -and (!([Connectivity]::CheckIfAdmin()))) {
        Write-Warning "'-InformationLevel Detailed' requires elevation (Run as administrator)."
        $Return.Detailed = $false
      }
      [Connectivity]::DiagnoseRouteSelection($Return)
      $result = $Return
    } else {
      $Return = [TestNetConnectionResult]::new()
      $Return.ComputerName = $ComputerName
      $Return.Detailed = ($InformationLevel -eq 'Detailed')
      $Return.ResolvedAddresses = [Connectivity]::ResolveTargetName($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

      $AttemptTcpTest = ![string]::IsNullOrEmpty($CommonTCPPort) -or $Port -gt 0
      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 }
        }
        $Iter = 0
        while (($Iter -lt $Return.ResolvedAddresses.Count) -and (!$Return.TcpTestSucceeded)) {
          $Return.TcpTestSucceeded = [Connectivity]::TestTCP($Return.ResolvedAddresses[$Iter], $Return.RemotePort)
          if (!$Return.TcpTestSucceeded) { Write-Warning "TCP connect to ($($Return.ResolvedAddresses[$Iter]) : $($Return.RemotePort)) failed" }
          $Iter++
        }
        if ($Return.TcpTestSucceeded) { $Return.RemoteAddress = $Return.ResolvedAddresses[$Iter - 1] }
        if ($InformationLevel -eq 'Quiet') { return $Return.TcpTestSucceeded }
      }

      $AttemptPingTest = (!$AttemptTcpTest) -or (!$Return.TcpTestSucceeded)
      if ($AttemptPingTest) {
        $Return.PingSucceeded = $false
        $Iter = 0
        while (($Iter -lt $Return.ResolvedAddresses.Count) -and (!$Return.PingSucceeded)) {
          $Return.PingReplyDetails = [Connectivity]::PingTest($Return.ResolvedAddresses[$Iter])
          if ($null -ne $Return.PingReplyDetails) {
            $Return.PingSucceeded = ($Return.PingReplyDetails.Status -eq [IPStatus]::Success)
          }
          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) { $Return.RemoteAddress = $Return.ResolvedAddresses[$Iter - 1] }
        if ($InformationLevel -eq 'Quiet') { return $Return.PingSucceeded }
      }

      if ($TraceRoute) { $Return.TraceRoute = [Connectivity]::TraceRoute($Return.RemoteAddress, $Hops) }
      $Return = [Connectivity]::ResolveDNSDetails($Return, $ComputerName)
      $Return = [Connectivity]::ResolveNetworkSecurityDetails($Return)
      $Return = [Connectivity]::ResolveRoutingandAdapterWMIObjects($Return)
      $result = $Return
    }
    return $result
  }


  static [bool] CheckIfAdmin() {
    $id = [Security.Principal.WindowsIdentity]::GetCurrent()
    $pri = [Security.Principal.WindowsPrincipal]::new($id)
    return $pri.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
  }

  static [object] ResolveTargetName([string]$TargetName) {
    $Addresses = $null
    try {
      $Addresses = [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
  }

  static [object] PingTest([object]$TargetIPAddress) {
    $Ping = [Ping]::new()
    $PingReplyDetails = $null
    Write-Progress -Activity "Connectivity :: PingTest" -Status "Waiting for echo reply from $TargetIPAddress" -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
  }

  static [object] TraceRoute([object]$TargetIPAddress, [object]$Hops) {
    $Ping = [Ping]::new()
    $PingOptions = [PingOptions]::new()
    $PingOptions.Ttl = 1
    [Byte[]]$DataBuffer = @(0) * 10
    $ReturnTrace = @()
    $PingReplyDetails = $null
    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()
        $ReturnTrace += if ($null -eq $PingReplyDetails.Address) { $PingReplyDetails.Status.ToString() } else { $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 ($ReturnTrace[-1] -ne $TargetIPAddress) {
      Write-Warning "Trace route to destination $TargetIPAddress did not complete. Trace terminated :: $($ReturnTrace[-1])"
    }
    $Ping.Dispose()
    return $ReturnTrace
  }

  static [object] TestTCP([object]$TargetIPAddress, [object]$TargetPort) {
    Write-Progress -Activity "Connectivity :: TestTCP $TargetIPAddress`:$TargetPort" -Status "Attempting TCP connect" -SecondsRemaining -1 -PercentComplete -1
    $Success = $false
    $TCPClient = [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
  }

  static [object] ResolveRoutingandAdapterWMIObjects([object]$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
  }

  static [object] ResolveDNSDetails([object]$TestNetConnectionResult, [string]$ComputerName) {
    $TestNetConnectionResult.DNSOnlyRecords = @(Resolve-DnsName $ComputerName -DnsOnly -NoHostsFile -Type A_AAAA -ErrorAction SilentlyContinue | Where-Object { $_.QueryType -in 'A', 'AAAA', 'PTR' })
    $TestNetConnectionResult.LLMNRNetbiosRecords = @(Resolve-DnsName $ComputerName -LlmnrNetbiosOnly -NoHostsFile -ErrorAction SilentlyContinue | Where-Object { $_.QueryType -in 'A', 'AAAA' })
    $TestNetConnectionResult.BasicNameResolution = @(Resolve-DnsName $ComputerName -ErrorAction SilentlyContinue | Where-Object { $_.QueryType -in 'A', 'AAAA', 'PTR' })
    $TestNetConnectionResult.AllNameResolutionResults = $TestNetConnectionResult.BasicNameResolution + $TestNetConnectionResult.DNSOnlyRecords + $TestNetConnectionResult.LLMNRNetbiosRecords | Sort-Object -Unique -Property Address
    return $TestNetConnectionResult
  }

  static [object] ResolveNetworkSecurityDetails([object]$TestNetConnectionResult) {
    $TestNetConnectionResult.IsAdmin = [Connectivity]::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' }
    }
    if ($TestNetConnectionResult.IsAdmin) {
      $TestNetConnectionResult.MatchingIPsecRules = Find-NetIPsecRule -RemoteAddress $TestNetConnectionResult.RemoteAddress -RemotePort $TestNetConnectionResult.RemotePort -Protocol TCP -ErrorAction SilentlyContinue
    }
    return $TestNetConnectionResult
  }

  static [object] DiagnoseRouteSelection([object]$RouteDiagnostics) {
    $RouteDiagnostics.RouteDiagnosticsSucceeded = $false
    $LogFile = ""
    $TraceResults = $null
    if ($RouteDiagnostics.Detailed) {
      Write-Progress -Activity "DiagnoseRouteSelection" -Status "Starting Route Event Tracing" -SecondsRemaining -1 -PercentComplete -1
      do {
        $LogFile = [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
    }
    $RouteDiagnostics.ResolvedAddresses = [Connectivity]::ResolveTargetName($RouteDiagnostics.ComputerName)
    if ($null -eq $RouteDiagnostics.ResolvedAddresses) {
      netsh trace stop sessionname=tnc | Out-Null
      return $null
    }
    $RouteDiagnostics.RemoteAddress = $RouteDiagnostics.ResolvedAddresses[0]
    if ($null -eq $RouteDiagnostics.ConstrainSourceAddress) {
      $RouteDiagnostics.ConstrainSourceAddress = if ($RouteDiagnostics.RemoteAddress.AddressFamily -eq [AddressFamily]::InterNetwork) {
        [IPAddress]::Any
      } else {
        [IPAddress]::IPv6Any
      }
    }
    if ($RouteDiagnostics.Detailed -and (Test-Path -Path $LogFile)) {
      if ($RouteDiagnostics.RemoteAddress.AddressFamily -eq [AddressFamily]::InterNetwork) {
        netsh int ipv4 delete destinationcache | Out-Null
      } else {
        netsh int ipv6 delete destinationcache | Out-Null
      }
    }
    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 finding route: $($_.Exception.Message)"
      netsh trace stop sessionname=tnc | Out-Null
      return $null
    }
    if ($RouteDiagnostics.Detailed) {
      $TraceResults += netsh trace stop sessionname=tnc
      if (!(Test-Path -Path $LogFile)) {
        Write-Warning "Error collecting routing events. Error: $TraceResults"
        return $null
      }
      $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) {
        Write-Warning "No TCPIP routing events collected from $LogFile."
      } else {
        $RouteEvents = $AllRoutingEvents | Where-Object { ($_.Id -in 1383, 1384) -and $_.Message.Contains("$($RouteDiagnostics.RemoteAddress) ") }
        foreach ($event in $RouteEvents) {
          if ($RouteDiagnostics.RouteSelectionEvents -notcontains $event.Message) { $RouteDiagnostics.RouteSelectionEvents += "$($event.Message)" }
        }
        $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)" }
        }
        $ResolvedAddrs = $RouteDiagnostics.ResolvedAddresses | ForEach-Object { $_.IPAddressToString }
        $DstAddrEvents = $AllRoutingEvents | Where-Object { ($_.Id -eq 1327) -and (($ResolvedAddrs | ForEach-Object { $_.Message.Contains("$_)") }) -contains $true) }
        foreach ($event in $DstAddrEvents) {
          if ($RouteDiagnostics.DestinationAddressSelectionEvents -notcontains $event.Message) { $RouteDiagnostics.DestinationAddressSelectionEvents += "$($event.Message)" }
        }
      }
      $RouteDiagnostics.LogFile = $LogFile
    }
    $RouteDiagnostics.RouteDiagnosticsSucceeded = $true
    return $null
  }

  static [object] TestNetworkPort([string]$ComputerName, [int]$Port, [string]$Protocol, [int]$TcpTimeout, [int]$UdpTimeout) {
    $result = $false
    if ($Protocol -eq 'TCP') {
      $TcpClient = [TcpClient]::new()
      $Connect = $TcpClient.BeginConnect($ComputerName, $Port, $null, $null)
      $Wait = $Connect.AsyncWaitHandle.WaitOne($TcpTimeout, $false)
      if (!$Wait) {
        $TcpClient.Close()
      } else {
        $TcpClient.EndConnect($Connect)
        $TcpClient.Close()
        $result = $true
      }
      $TcpClient.Dispose()
    } elseif ($Protocol -eq 'UDP') {
      $UdpClient = [UdpClient]::new()
      $UdpClient.Client.ReceiveTimeout = $UdpTimeout
      $UdpClient.Connect($ComputerName, $Port)
      $enc = [Text.ASCIIEncoding]::new()
      $byte = $enc.GetBytes([datetime]::Now.ToString())
      [void]$UdpClient.Send($byte, $byte.Length)
      $endpoint = [System.Net.IPEndPoint]::new([IPAddress]::Any, 0)
      try {
        $receivebytes = $UdpClient.Receive([ref]$endpoint)
        if ($enc.GetString($receivebytes)) { $result = $true }
      } catch {
        Write-Error "$ComputerName failed port test on port '$Protocol`:$Port' with error '$($_.Exception.Message)'"
      }
      $UdpClient.Dispose()
    }
    return $result
  }


  static [object] TestPort([string[]]$ComputerName, [uint16[]]$Port, [int]$Timeout, [bool]$TCP, [bool]$UDP) {
    if (!$TCP -and !$UDP) { $TCP = $true }
    $report = @()
    foreach ($c in $ComputerName) {
      foreach ($p in $Port) {
        if ($TCP) {
          $temp = [pscustomobject]@{ ComputerName = $c; Protocol = 'TCP'; Port = $p; Open = $false; Notes = '' }
          $tcpobject = [TcpClient]::new()
          $connect = $tcpobject.BeginConnect($c, $p, $null, $null)
          $wait = $connect.AsyncWaitHandle.WaitOne($Timeout, $false)
          if (!$wait) {
            $tcpobject.Close()
            $temp.Notes = 'Connection to Port Timed Out'
          } else {
            $failed = $false
            try { $null = $tcpobject.EndConnect($connect) } catch { $failed = $true; $temp.Notes = $_.Exception.Message }
            $tcpobject.Close()
            if (!$failed) { $temp.Open = $true; $temp.Notes = "Successful link to $c TCP port $p" }
          }
          $report += $temp
        }
        if ($UDP) {
          $temp = [pscustomobject]@{ ComputerName = $c; Protocol = 'UDP'; Port = $p; Open = $false; Notes = '' }
          $Socket = [Net.Sockets.Socket]::new('InterNetwork', 'Dgram', 'Udp')
          $Socket.SendTimeOut = $Timeout
          $Socket.ReceiveTimeOut = $Timeout
          try {
            $Socket.Connect($c, $p)
            $Buffer = [byte[]]::new(48); $Buffer[0] = 27
            $null = $Socket.Send($Buffer)
            $null = $Socket.Receive($Buffer)
            $temp.Open = $true
          } catch {
            $temp.Notes = $_.Exception.Message
          }
          $Socket.Dispose()
          $report += $temp
        }
      }
    }
    return $report
  }


  static [object] WaitPing([string]$ComputerName, [int]$Timeout, [int]$CheckEvery, [bool]$Offline) {
    $timer = $null
    try {
      $timer = [System.Diagnostics.Stopwatch]::StartNew()
      Write-Verbose "Waiting for [$ComputerName] to become pingable"
      if ($Offline) {
        while (Test-Connection -ComputerName $ComputerName -Quiet -Count 1) {
          Write-Verbose "Waiting for [$ComputerName] to go offline..."
          if ($timer.Elapsed.TotalSeconds -ge $Timeout) { throw "Timeout exceeded waiting for [$ComputerName] to go offline" }
          Start-Sleep -Seconds $CheckEvery
        }
        Write-Verbose "[$ComputerName] is now offline. Waited $([Math]::Round($timer.Elapsed.TotalSeconds, 0)) seconds"
      } else {
        while (!(Test-Connection -ComputerName $ComputerName -Quiet -Count 1)) {
          Write-Verbose "Waiting for [$ComputerName] to become pingable..."
          if ($timer.Elapsed.TotalSeconds -ge $Timeout) { throw "Timeout exceeded waiting for ping to [$ComputerName]" }
          Start-Sleep -Seconds $CheckEvery
        }
        Write-Verbose "Ping now available on [$ComputerName]. Waited $([Math]::Round($timer.Elapsed.TotalSeconds, 0)) seconds"
      }
    } catch {
      Write-Error -Message $_.Exception.Message
    } finally {
      if ($null -ne $timer) { $timer.Stop() }
    }
    return $null
  }
}