Public/Start-BfdSession.ps1

function Start-BfdSession {
    <#
    .SYNOPSIS
        Starts a BFD session (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. Use it when the peer is
        not on the same L2 segment (single-hop peers drop packets whose TTL is below 255) and supports multihop BFD.
    .PARAMETER LocalAddress
        Bind to this local IPv4 address, so packets leave from the address the peer expects.
    .PARAMETER ListenPort
        Local UDP port to receive on (3784 normally, 4784 with -MultiHop).
    .PARAMETER Port
        Remote UDP port to send to (3784 normally, 4784 with -MultiHop).
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$RemoteAddress,
        [string]$Name,
        [int]$Port,
        [int]$ListenPort,
        [switch]$MultiHop,
        [ipaddress]$LocalAddress,
        [uint32]$DesiredMinTxMs = 500,
        [uint32]$RequiredMinRxMs = 500,
        [ValidateRange(1, 255)][int]$DetectMult = 3,
        [ValidateSet('None', 'SimplePassword', 'KeyedSHA1')][string]$AuthType = 'None',
        [ValidateRange(0, 255)][int]$AuthKeyId = 1,
        [string]$AuthKey
    )
    if ($AuthType -ne 'None' -and -not $AuthKey) { throw "-AuthKey is required with -AuthType $AuthType." }
    $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-PowerBgpRegistry
    $existing = $reg.Bfd[$Name]
    if ($existing -and $existing.State -ne 'Stopped') { throw "BFD session '$Name' already exists." }

    $S = [hashtable]::Synchronized(@{
            Name               = $Name
            Config             = [pscustomobject]@{
                RemoteAddress = $RemoteAddress; RemotePort = $Port; ListenPort = $ListenPort
                LocalAddress = if ($LocalAddress) { $LocalAddress.ToString() } else { $null }
                DesiredMinTx = $DesiredMinTxMs * 1000; RequiredMinRx = $RequiredMinRxMs * 1000; DetectMult = $DetectMult
                AuthType = $AuthType; AuthKeyId = $AuthKeyId; AuthKey = $AuthKey
            }
            State              = 'Down'
            RemoteState        = $null
            Stop               = $false
            Suppress           = $false
            Sent               = 0
            Received           = 0
            LocalDiscriminator = 0
            LastError          = $null
            History            = [System.Collections.Concurrent.ConcurrentQueue[string]]::new()
            PowerShell         = $null
            Handle             = $null
        })
    $reg.Bfd[$Name] = $S
    Start-PowerBgpRunspace -State $S -Entry 'Invoke-BfdSessionLoop'
    Get-BfdSession -Name $Name
}

function Get-BfdSession {
    [CmdletBinding()]
    param([string]$Name = '*')
    foreach ($S in @((Get-PowerBgpRegistry).Bfd.Values)) {
        if ($S.Name -notlike $Name) { continue }
        [pscustomobject]@{
            PSTypeName    = 'PowerBGP.BfdSession'
            Name          = $S.Name
            State         = $S.State
            RemoteState   = $S.RemoteState
            Remote        = '{0}:{1}' -f $S.Config.RemoteAddress, $S.Config.RemotePort
            LocalAddress  = $S.Config.LocalAddress
            TxMs          = $S.Config.DesiredMinTx / 1000
            RxMs          = $S.Config.RequiredMinRx / 1000
            DetectMult    = $S.Config.DetectMult
            AuthType      = if ($S.Config.AuthType) { $S.Config.AuthType } else { 'None' }
            AuthFailures  = [int]$S.AuthFailures
            Sent          = $S.Sent
            Received      = $S.Received
            Suppressed    = [bool]$S.Suppress
            LastError     = $S.LastError
            History       = $S.History.ToArray()
        }
    }
}

function Stop-BfdSession {
    <#
    .SYNOPSIS
        Stops a BFD session. It stays configured (State 'Stopped') so Resume-BfdSession can restart it;
        use Remove-BfdSession to delete it.
    #>

    [CmdletBinding()]
    param([Parameter(Mandatory, ValueFromPipelineByPropertyName)][string]$Name)
    process {
        $S = (Get-PowerBgpRegistry).Bfd[$Name]
        if (-not $S) { Write-Error "No such BFD session '$Name'."; return }
        Stop-PowerBgpRunspace $S
        $S.State = 'Stopped'
    }
}

function Resume-BfdSession {
    [CmdletBinding()]
    param([Parameter(Mandatory, ValueFromPipelineByPropertyName)][string]$Name)
    process {
        $old = (Get-PowerBgpRegistry).Bfd[$Name]
        if (-not $old) { Write-Error "No such BFD session '$Name'."; return }
        $c = $old.Config
        $la = if ($c.LocalAddress) { @{ LocalAddress = [ipaddress]$c.LocalAddress } } else { @{} }
        $null = Start-BfdSession -Name $Name -RemoteAddress $c.RemoteAddress -Port $c.RemotePort -ListenPort $c.ListenPort @la `
            -DesiredMinTxMs ($c.DesiredMinTx / 1000) -RequiredMinRxMs ($c.RequiredMinRx / 1000) -DetectMult $c.DetectMult `
            -AuthType $(if ($c.AuthType) { $c.AuthType } else { 'None' }) -AuthKeyId $(if ($null -ne $c.AuthKeyId) { $c.AuthKeyId } else { 1 }) -AuthKey $c.AuthKey
        Get-BfdSession -Name $Name
    }
}

function Update-BfdSession {
    <#
    .SYNOPSIS
        Changes a BFD session's stored settings. A running session is restarted (it goes Down and renegotiates).
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Name,
        [int]$Port,
        [int]$ListenPort,
        [ipaddress]$LocalAddress,
        [uint32]$DesiredMinTxMs,
        [uint32]$RequiredMinRxMs,
        [ValidateRange(1, 255)][int]$DetectMult,
        [ValidateSet('None', 'SimplePassword', 'KeyedSHA1')][string]$AuthType,
        [ValidateRange(0, 255)][int]$AuthKeyId,
        [string]$AuthKey
    )
    $S = (Get-PowerBgpRegistry).Bfd[$Name]
    if (-not $S) { throw "No such BFD session '$Name'." }
    $wasRunning = $S.State -ne 'Stopped'
    if ($wasRunning) { Stop-BfdSession -Name $Name }
    $c = $S.Config
    if ($PSBoundParameters.ContainsKey('Port')) { $c.RemotePort = $Port }
    if ($PSBoundParameters.ContainsKey('ListenPort')) { $c.ListenPort = $ListenPort }
    if ($PSBoundParameters.ContainsKey('LocalAddress')) { $c.LocalAddress = $LocalAddress.ToString() }
    if ($PSBoundParameters.ContainsKey('DesiredMinTxMs')) { $c.DesiredMinTx = $DesiredMinTxMs * 1000 }
    if ($PSBoundParameters.ContainsKey('RequiredMinRxMs')) { $c.RequiredMinRx = $RequiredMinRxMs * 1000 }
    if ($PSBoundParameters.ContainsKey('DetectMult')) { $c.DetectMult = $DetectMult }
    foreach ($k in 'AuthType', 'AuthKeyId', 'AuthKey') { if ($PSBoundParameters.ContainsKey($k)) { $c.$k = $PSBoundParameters[$k] } }
    if ($c.AuthType -ne 'None' -and -not $c.AuthKey) { throw "-AuthKey is required with -AuthType $($c.AuthType)." }
    if ($wasRunning) { Resume-BfdSession -Name $Name } else { Get-BfdSession -Name $Name }
}

function Remove-BfdSession {
    [CmdletBinding(SupportsShouldProcess)]
    param([Parameter(Mandatory, ValueFromPipelineByPropertyName)][string]$Name)
    process {
        $reg = Get-PowerBgpRegistry
        $S = $reg.Bfd[$Name]
        if (-not $S) { Write-Error "No such BFD session '$Name'."; return }
        if ($PSCmdlet.ShouldProcess($Name, 'Remove BFD session')) {
            Stop-PowerBgpRunspace $S
            $reg.Bfd.Remove($Name)
        }
    }
}

function Set-BfdSession {
    <#
    .SYNOPSIS
        Fault injection: -Suppress $true stops sending BFD control packets while the session object stays alive.
    #>

    [CmdletBinding()]
    param([Parameter(Mandatory)][string]$Name, [Parameter(Mandatory)][bool]$Suppress)
    $S = (Get-PowerBgpRegistry).Bfd[$Name]
    if (-not $S) { throw "No such BFD session '$Name'." }
    $S.Suppress = $Suppress
}