Public/Start-BfdSession.ps1

function Start-BfdSession {
    <#
    .SYNOPSIS
        Starts a BFD session (RFC 5880) over UDP, TTL 255, source port 49152-65535. Single-hop (RFC 5881, port 3784) by default.
    .PARAMETER MultiHop
        Multihop BFD (RFC 5883): defaults -Port and -ListenPort to 4784 instead of 3784.
    .PARAMETER LocalAddress
        Bind to this local address, so packets leave from the address the peer expects. Two sessions can share a
        listen port when they bind different local addresses.
    .PARAMETER Passive
        Do not send control packets until the peer's discriminator is known.
    .PARAMETER DemandMode
        Request Demand mode (RFC 5880 6.6) once the session is Up: the peer stops its periodic packets and this side
        verifies connectivity with Poll sequences.
    .PARAMETER EchoMode
        Disabled, Reflect (advertise a required echo interval and loop the peer's echo packets back, UDP 3785) or
        Active (Reflect, plus originate echo packets and fail the session with diagnostic 2 when they stop coming back).
    .PARAMETER OnStateChange
        Notification hook, called with ($Event, $Context) on every state change, e.g. { param($e, $ctx) ... }.
        $Event has Session, Remote, OldState, NewState, Diagnostic, DiagnosticName, LocalDiscriminator,
        RemoteDiscriminator and IsUp. The hook runs in the session's runspace, so it cannot capture local variables:
        pass shared state with -HookContext. Keep it short: packets are not processed while it runs.
    .PARAMETER HookContext
        Object (typically a synchronized hashtable or a ConcurrentQueue) handed to -OnStateChange as its second argument.
    .PARAMETER AuthType
        None, SimplePassword, KeyedMD5, MeticulousKeyedMD5, KeyedSHA1 or MeticulousKeyedSHA1.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$RemoteAddress,
        [string]$Name,
        [ValidateRange(1, 65535)][int]$Port,
        [ValidateRange(1, 65535)][int]$ListenPort,
        [switch]$MultiHop,
        [ipaddress]$LocalAddress,
        [ValidateRange(1, 4294967)][uint32]$DesiredMinTxMs = 500,
        [ValidateRange(0, 4294967)][uint32]$RequiredMinRxMs = 500,
        [ValidateRange(1, 255)][int]$DetectMult = 3,
        [switch]$Passive,
        [switch]$DemandMode,
        [ValidateSet('Disabled', 'Reflect', 'Active')][string]$EchoMode = 'Disabled',
        [ValidateRange(1, 4294967)][uint32]$EchoIntervalMs = 100,
        [ValidateRange(1, 65535)][int]$EchoPort = 3785,
        [ValidateRange(1, 65535)][int]$EchoListenPort = 3785,
        [ValidateSet('None', 'SimplePassword', 'KeyedMD5', 'MeticulousKeyedMD5', 'KeyedSHA1', 'MeticulousKeyedSHA1')][string]$AuthType = 'None',
        [ValidateRange(0, 255)][int]$AuthKeyId = 1,
        [string]$AuthKey,
        [scriptblock]$OnStateChange,
        $HookContext,
        [bool]$RecordPackets = $true
    )
    $addr = $null
    if (-not [ipaddress]::TryParse($RemoteAddress, [ref]$addr)) { throw "-RemoteAddress '$RemoteAddress' is not an IP address." }
    Assert-BfdAuth -AuthType $AuthType -AuthKey $AuthKey

    $default = if ($MultiHop) { 4784 } else { 3784 }
    if (-not $PSBoundParameters.ContainsKey('Port')) { $Port = $default }
    if (-not $PSBoundParameters.ContainsKey('ListenPort')) { $ListenPort = $default }
    if (-not $Name) { $Name = $RemoteAddress }
    $reg = Get-BfdRegistry
    $existing = $reg.Bfd[$Name]
    if ($existing -and $existing.State -ne 'Stopped') { throw "BFD session '$Name' already exists." }
    if ($existing) { $null = $reg.Bfd.Remove($Name) }

    $S = [hashtable]::Synchronized(@{
            Name                = $Name
            Config              = [pscustomobject]@{
                RemoteAddress = $addr.ToString(); RemotePort = $Port; ListenPort = $ListenPort
                LocalAddress  = if ($LocalAddress) { $LocalAddress.ToString() } else { $null }
                MultiHop      = [bool]$MultiHop
                DesiredMinTx  = [uint32]($DesiredMinTxMs * 1000); RequiredMinRx = [uint32]($RequiredMinRxMs * 1000); DetectMult = $DetectMult
                Passive       = [bool]$Passive; DemandMode = [bool]$DemandMode
                EchoMode      = $EchoMode; EchoInterval = [uint32]($EchoIntervalMs * 1000); EchoPort = $EchoPort; EchoListenPort = $EchoListenPort
                AuthType      = $AuthType; AuthKeyId = $AuthKeyId; AuthKey = $AuthKey
                OnStateChange = $OnStateChange; HookContext = $HookContext; RecordPackets = $RecordPackets
            }
            State               = 'Down'
            RemoteState         = $null
            Diagnostic          = 0
            Stop                = $false
            Ready               = $false
            Suppress            = $false
            AdminDown           = $false
            DropEcho            = $false
            Fault               = [hashtable]::Synchronized(@{})
            Sent                = 0
            Received            = 0
            Dropped             = 0
            AuthFailures        = 0
            EchoSent            = 0
            EchoReceived        = 0
            EchoReflected       = 0
            LocalDiscriminator  = 0
            RemoteDiscriminator = 0
            DetectionTimeMs     = $null
            TxIntervalMs        = $null
            PollPending         = $false
            DemandActive        = $false
            EchoActive          = $false
            UpSince             = $null
            LastStateChange     = $null
            LastError           = $null
            History             = [System.Collections.Concurrent.ConcurrentQueue[string]]::new()
            Events              = [System.Collections.Concurrent.ConcurrentQueue[object]]::new()
            PowerShell          = $null
            Handle              = $null
        })
    $reg.Bfd[$Name] = $S
    Start-BfdRunspace -State $S
    Wait-BfdReady $S
    if ($S.LastError) {
        $err = $S.LastError
        Stop-BfdRunspace $S
        $null = $reg.Bfd.Remove($Name)
        throw "BFD session '$Name' failed to start: $err"
    }
    Get-BfdSession -Name $Name
}