Private/BfdEngine.ps1

# BFD protocol engine (RFC 5880): session state machine, timer negotiation, Poll/Final, Demand mode, echo bookkeeping.
# Pure logic with no sockets, so it is unit-testable and shared by the I/O loop. Times are milliseconds on a monotonic
# clock supplied by the caller; intervals on the wire and in the config are microseconds.

function New-BfdContext {
    param($Config, [uint32]$MyDiscriminator, [double]$Now = 0, [uint32]$TxSequence = 1)
    $slowTx = [Math]::Max([double]$Config.DesiredMinTx, 1000000)
    @{
        State = 'Down'; Diag = 0
        MyDisc = $MyDiscriminator; YourDisc = [uint32]0
        RemoteState = $null; RemoteMult = 0; RemoteDesiredTx = 1000000; RemoteRequiredRx = 1000000
        RemoteRequiredEchoRx = 0; RemoteDemand = $false
        LastRx = $Now; UpSince = $null
        AdvTx = $slowTx; UsedTx = $slowTx; AdvRx = [double]$Config.RequiredMinRx; UsedRx = [double]$Config.RequiredMinRx
        PollPending = $false; DemandPollAt = $null; ReplyFinal = $false; SendNow = $false
        TxSeq = $TxSequence; RcvSeq = [uint32]0; RcvSeqKnown = $false
        EchoActive = $false; EchoLastRx = $Now; EchoSeq = [uint32]0
        Received = 0; Dropped = 0; AuthFailures = 0
        Changes = [System.Collections.Generic.List[object]]::new()
    }
}

function Set-BfdContextState {
    param($Ctx, $Config, [string]$New, [int]$Diag = 0, [double]$Now = 0)
    $Ctx.Diag = $Diag
    if ($New -eq $Ctx.State) { return }
    $Ctx.Changes.Add([pscustomobject]@{ Time = Get-Date; OldState = $Ctx.State; NewState = $New; Diagnostic = $Diag })
    $Ctx.State = $New
    $Ctx.TxSeq = [uint32](([uint64]$Ctx.TxSeq + 1) % 4294967296)
    $Ctx.PollPending = $false; $Ctx.DemandPollAt = $null; $Ctx.EchoActive = $false
    $Ctx.UpSince = if ($New -eq 'Up') { $Now } else { $null }
    if ($New -eq 'Down' -or $New -eq 'AdminDown') { $Ctx.RcvSeqKnown = $false }
    # Not Up: RFC 5880 6.8.3 keeps DesiredMinTx at 1 s or more; Up: Update-BfdTimers moves to the configured value by Poll sequence.
    $slow = [Math]::Max([double]$Config.DesiredMinTx, 1000000)
    $Ctx.AdvTx = $slow; $Ctx.UsedTx = $slow
    $Ctx.AdvRx = [double]$Config.RequiredMinRx; $Ctx.UsedRx = $Ctx.AdvRx
}

function Get-BfdDetectionTimeMs {
    param($Ctx)
    $Ctx.RemoteMult * [Math]::Max($Ctx.UsedRx, $Ctx.RemoteDesiredTx) / 1000.0
}

function Get-BfdTxIntervalMs {
    param($Ctx)
    [Math]::Max($Ctx.UsedTx, $Ctx.RemoteRequiredRx) / 1000.0
}

function Test-BfdDemandActive {
    # Local Demand mode is in effect: both ends Up and we asked the peer to stop its periodic packets.
    param($Ctx, $Config)
    [bool]($Config.DemandMode -and $Ctx.State -eq 'Up' -and $Ctx.RemoteState -eq 'Up')
}

function Test-BfdPeriodicTx {
    param($Ctx, $Config)
    if ($Ctx.State -eq 'AdminDown') { return $true }
    if ($Ctx.PollPending -or $null -ne $Ctx.DemandPollAt) { return $true }
    if ($Config.Passive -and $Ctx.YourDisc -eq 0) { return $false }
    if ($Ctx.RemoteRequiredRx -eq 0) { return $false }
    if ($Ctx.State -eq 'Up' -and $Ctx.RemoteState -eq 'Up' -and $Ctx.RemoteDemand) { return $false }
    $true
}

function Invoke-BfdReceive {
    # Processes one received datagram (RFC 5880 6.8.6). Returns @{ Accepted; Reason }.
    param($Ctx, $Config, [byte[]]$Data, [double]$Now)
    $reject = { param($why) $Ctx.Dropped++; @{ Accepted = $false; Reason = $why } }

    if ($Ctx.State -eq 'AdminDown') { return & $reject 'SessionAdminDown' }
    if ($Data.Length -lt 24) { return & $reject 'TooShort' }
    if (([int]$Data[0] -shr 5) -ne 1) { return & $reject 'Version' }
    $len = [int]$Data[3]
    $authBit = ($Data[1] -band 0x04) -ne 0
    if ($len -lt $(if ($authBit) { 26 } else { 24 }) -or $len -gt $Data.Length) { return & $reject 'Length' }
    if ($Data[2] -eq 0) { return & $reject 'DetectMultZero' }
    if ($Data[1] -band 0x01) { return & $reject 'Multipoint' }
    $myDisc = Get-BfdUInt32 $Data 4
    $yourDisc = Get-BfdUInt32 $Data 8
    if ($myDisc -eq 0) { return & $reject 'MyDiscriminatorZero' }
    $poll = ($Data[1] -band 0x20) -ne 0
    $final = ($Data[1] -band 0x10) -ne 0
    if ($poll -and $final) { return & $reject 'PollAndFinal' }
    $remoteState = $script:BfdStates[[int]($Data[1] -shr 6)]
    if ($yourDisc -ne 0 -and $yourDisc -ne $Ctx.MyDisc) { return & $reject 'YourDiscriminatorUnknown' }
    if ($yourDisc -eq 0 -and $remoteState -notin 'Down', 'AdminDown') { return & $reject 'YourDiscriminatorZero' }

    $type = if ($Config.AuthType) { $Config.AuthType } else { 'None' }
    if ($type -ne 'None') {
        if (-not $authBit -or -not (Test-BfdAuth -Packet $Data -Type $type -KeyId $Config.AuthKeyId -Key $Config.AuthKey)) {
            $Ctx.AuthFailures++
            return & $reject 'AuthFailed'
        }
        $info = $script:BfdAuthTypes[$type]
        if ($type -ne 'SimplePassword') {
            $seq = Get-BfdUInt32 $Data 28
            if (-not (Test-BfdAuthSequence -Sequence $seq -Last $Ctx.RcvSeq -Known $Ctx.RcvSeqKnown -Meticulous $info.Meticulous -DetectMult $Ctx.RemoteMult)) {
                $Ctx.AuthFailures++
                return & $reject 'AuthSequence'
            }
            $Ctx.RcvSeq = $seq; $Ctx.RcvSeqKnown = $true
        }
    }
    elseif ($authBit) {
        $Ctx.AuthFailures++
        return & $reject 'AuthUnexpected'
    }

    $Ctx.Received++
    $Ctx.LastRx = $Now
    $Ctx.YourDisc = $myDisc
    $Ctx.RemoteState = $remoteState
    $Ctx.RemoteMult = [int]$Data[2]
    $Ctx.RemoteDemand = ($Data[1] -band 0x02) -ne 0
    $Ctx.RemoteDesiredTx = [double](Get-BfdUInt32 $Data 12)
    $Ctx.RemoteRequiredRx = [double](Get-BfdUInt32 $Data 16)
    $Ctx.RemoteRequiredEchoRx = [double](Get-BfdUInt32 $Data 20)

    if ($final -and ($Ctx.PollPending -or $null -ne $Ctx.DemandPollAt)) {
        # Poll sequence terminated: new timer values take full effect
        $Ctx.PollPending = $false; $Ctx.DemandPollAt = $null
        $Ctx.UsedTx = $Ctx.AdvTx; $Ctx.UsedRx = $Ctx.AdvRx
    }

    switch ($Ctx.State) {
        'Down' {
            if ($remoteState -eq 'Down') { Set-BfdContextState $Ctx $Config 'Init' 0 $Now }
            elseif ($remoteState -eq 'Init') { Set-BfdContextState $Ctx $Config 'Up' 0 $Now }
        }
        'Init' {
            if ($remoteState -eq 'AdminDown') { Set-BfdContextState $Ctx $Config 'Down' 3 $Now }
            elseif ($remoteState -in 'Init', 'Up') { Set-BfdContextState $Ctx $Config 'Up' 0 $Now }
        }
        'Up' {
            if ($remoteState -in 'Down', 'AdminDown') { Set-BfdContextState $Ctx $Config 'Down' 3 $Now }
        }
    }
    if ($poll) { $Ctx.ReplyFinal = $true; $Ctx.SendNow = $true }
    @{ Accepted = $true; Reason = $null }
}

function Update-BfdTimers {
    # Periodic step: timer changes via Poll sequence (RFC 5880 6.8.3), Demand-mode verification, detection time expiry.
    param($Ctx, $Config, [double]$Now)
    $cfgTx = [double]$Config.DesiredMinTx
    $cfgRx = [double]$Config.RequiredMinRx

    if ($Ctx.State -eq 'Up') {
        $wantTx = if ($Ctx.EchoActive) { [Math]::Max($cfgTx, 1000000) } else { $cfgTx }
        if ($wantTx -ne $Ctx.AdvTx -or $cfgRx -ne $Ctx.AdvRx) {
            # raising DesiredMinTx / lowering RequiredMinRx only takes effect once the peer answers with Final
            $Ctx.UsedTx = [Math]::Min($Ctx.UsedTx, $wantTx)
            $Ctx.UsedRx = [Math]::Max($Ctx.UsedRx, $cfgRx)
            $Ctx.AdvTx = $wantTx; $Ctx.AdvRx = $cfgRx
            $Ctx.PollPending = $true; $Ctx.SendNow = $true
        }
    }
    elseif ($Ctx.State -ne 'AdminDown') {
        $slow = [Math]::Max($cfgTx, 1000000)
        $Ctx.AdvTx = $slow; $Ctx.UsedTx = $slow
        $Ctx.AdvRx = $cfgRx; $Ctx.UsedRx = $cfgRx
    }

    if ($Ctx.State -notin 'Init', 'Up' -or $Ctx.RemoteMult -le 0) { return }
    $detect = Get-BfdDetectionTimeMs $Ctx

    if (Test-BfdDemandActive $Ctx $Config) {
        if ($null -eq $Ctx.DemandPollAt) {
            if (($Now - $Ctx.LastRx) -gt ($detect / 2)) { $Ctx.DemandPollAt = $Now; $Ctx.SendNow = $true }
        }
        elseif (($Now - $Ctx.DemandPollAt) -gt $detect) {
            Set-BfdContextState $Ctx $Config 'Down' 1 $Now
            $Ctx.YourDisc = 0
        }
        return
    }
    $Ctx.DemandPollAt = $null

    # a peer in Demand mode is silent by design, so only enforce the detection time otherwise
    $remoteQuiet = $Ctx.State -eq 'Up' -and $Ctx.RemoteState -eq 'Up' -and $Ctx.RemoteDemand
    if (-not $remoteQuiet -and ($Now - $Ctx.LastRx) -gt $detect) {
        Set-BfdContextState $Ctx $Config 'Down' 1 $Now
        $Ctx.YourDisc = 0
    }
}

function New-BfdSessionPacket {
    # Builds the next periodic/Poll/Final control packet for the session. $Fault overrides advertised fields.
    param($Ctx, $Config, $Fault, [string]$AuthKeyOverride)
    $f = if ($Fault) { $Fault } else { @{} }
    $state = if ($null -ne $f.State) { $f.State } else { $Ctx.State }
    $a = @{
        State             = $state
        Diagnostic        = $(if ($null -ne $f.Diagnostic) { [int]$f.Diagnostic } else { [int]$Ctx.Diag })
        DetectMult        = $(if ($null -ne $f.DetectMult) { [int]$f.DetectMult } else { [int]$Config.DetectMult })
        MyDiscriminator   = $(if ($null -ne $f.MyDiscriminator) { [uint32]$f.MyDiscriminator } else { $Ctx.MyDisc })
        YourDiscriminator = $(if ($null -ne $f.YourDiscriminator) { [uint32]$f.YourDiscriminator } else { $Ctx.YourDisc })
        DesiredMinTx      = $(if ($null -ne $f.DesiredMinTx) { [uint32]$f.DesiredMinTx } else { [uint32]$Ctx.AdvTx })
        RequiredMinRx     = $(if ($null -ne $f.RequiredMinRx) { [uint32]$f.RequiredMinRx } else { [uint32]$Ctx.AdvRx })
        RequiredMinEchoRx = $(if ($Config.EchoMode -and $Config.EchoMode -ne 'Disabled') { [uint32]$Config.EchoInterval } else { [uint32]0 })
        Final             = [bool]$Ctx.ReplyFinal
        Poll              = [bool](-not $Ctx.ReplyFinal -and ($Ctx.PollPending -or $null -ne $Ctx.DemandPollAt))
        Demand            = [bool](Test-BfdDemandActive $Ctx $Config)
    }
    $type = if ($Config.AuthType) { $Config.AuthType } else { 'None' }
    if ($type -ne 'None') {
        $info = $script:BfdAuthTypes[$type]
        $a.AuthType = $type
        $a.AuthKeyId = [int]$Config.AuthKeyId
        $a.AuthKey = if ($AuthKeyOverride) { $AuthKeyOverride } else { $Config.AuthKey }
        $a.AuthSequence = $Ctx.TxSeq
        if ($info.Meticulous) { $Ctx.TxSeq = [uint32](([uint64]$Ctx.TxSeq + 1) % 4294967296) }
    }
    $Ctx.ReplyFinal = $false; $Ctx.SendNow = $false
    New-BfdControl @a
}