Private/Test-WinClatTcp.ps1

Function Test-WinClatTcp {

    <#

    .SYNOPSIS
        Runs the TCP functional test against an IPv4 target and port.

    .DESCRIPTION
        Opens one TCP connection with System.Net.Sockets.TcpClient constructed with AddressFamily.InterNetwork, which guarantees an IPv4 socket, with a three-second connect timeout (DESIGN.md, section 8.4):
          - Connected returns Pass.
          - Connection refused returns Pass, because a translated reset proves the CLAT path.
          - No response within the timeout returns Warning; the target may be unreachable or filtered.
          - Any other socket error returns Fail, because it means there is no working IPv4 path.

        Returns an object with Result and Detail. Detail records the method, target, and port.

        This function must also be available in the remote session. See DESIGN.md, section 4.3.

    .PARAMETER Target
        The IPv4 address to test. Validated by Test-WinClatIPv4Target before it reaches this function.

    .PARAMETER Port
        The TCP port to connect to.

    .PARAMETER TimeoutMilliseconds
        The connect timeout. The default is 3000.

    .INPUTS
        None. This function does not accept pipeline input.

    .OUTPUTS
        System.Management.Automation.PSCustomObject with Result and Detail properties.

    .EXAMPLE
        Test-WinClatTcp -Target '192.0.2.10' -Port 443

        Returns Result Pass when a connection to 192.0.2.10 port 443 succeeds or is refused.

    .LINK
        https://learn.microsoft.com/dotnet/api/system.net.sockets.tcpclient

    .NOTES
        Version: 1.0.0
        Creation Date: October 28, 2026
        Last Updated: October 28, 2026
        Author: Richard Hicks
        Organization: Richard M. Hicks Consulting, Inc.
        Contact: rich@richardhicks.com
        Website: https://www.richardhicks.com/

    #>


    [CmdletBinding()]
    [OutputType([PSCustomObject])]

    Param (

        [Parameter(Mandatory)]
        [string]$Target,

        [Parameter(Mandatory)]
        [ValidateRange(1, 65535)]
        [int]$Port,

        [ValidateRange(100, 60000)]
        [int]$TimeoutMilliseconds = 3000

    )

    $Client = New-Object -TypeName System.Net.Sockets.TcpClient -ArgumentList ([System.Net.Sockets.AddressFamily]::InterNetwork)

    Try {

        $Task = $Client.ConnectAsync($Target, $Port)

        If ($Task.Wait($TimeoutMilliseconds)) {

            Return [PSCustomObject]@{ Result = 'Pass'; Detail = "TCP connection to $Target port ${Port}: connected." }

        }

        [PSCustomObject]@{ Result = 'Warning'; Detail = "TCP connection to $Target port ${Port}: no response within $TimeoutMilliseconds ms. The target may be unreachable or filtered." }

    }

    Catch {

        # Find the socket error inside the method invocation and aggregate exceptions.
        $Exception = $_.Exception

        While ($Exception -and $Exception -isnot [System.Net.Sockets.SocketException]) {

            $Exception = $Exception.InnerException

        }

        If ($Exception -and $Exception.SocketErrorCode -eq [System.Net.Sockets.SocketError]::ConnectionRefused) {

            Return [PSCustomObject]@{ Result = 'Pass'; Detail = "TCP connection to $Target port ${Port}: connection refused. A translated reset proves the CLAT path." }

        }

        $Message = $_.Exception.Message

        If ($Exception) {

            $Message = "$($Exception.SocketErrorCode): $($Exception.Message)"

        }

        [PSCustomObject]@{ Result = 'Fail'; Detail = "TCP connection to $Target port ${Port}: $Message There is no working IPv4 path." }

    }

    Finally {

        $Client.Close()

    }

}