Private/Bfd.ps1

$script:BfdSessions = @{}
$script:BfdStateNames = @{ 0 = 'AdminDown'; 1 = 'Down'; 2 = 'Init'; 3 = 'Up' }
$script:BfdStateNumbers = @{ AdminDown = 0; Down = 1; Init = 2; Up = 3 }
$script:BfdAuthNumbers = @{ None = 0; Simple = 1; KeyedMd5 = 2; MeticulousKeyedMd5 = 3; KeyedSha1 = 4; MeticulousKeyedSha1 = 5 }
$script:BfdAuthNames = @{ 0 = 'None'; 1 = 'Simple'; 2 = 'KeyedMd5'; 3 = 'MeticulousKeyedMd5'; 4 = 'KeyedSha1'; 5 = 'MeticulousKeyedSha1' }

function Get-BfdDigest {
    param([byte[]]$Packet, [int]$AuthType, [string]$Key)
    $size = if ($AuthType -in 2, 3) { 16 } else { 20 }
    $k = [byte[]]::new($size)
    $kb = [System.Text.Encoding]::ASCII.GetBytes($Key)
    for ($i = 0; $i -lt $size -and $i -lt $kb.Length; $i++) { $k[$i] = $kb[$i] }
    $work = [byte[]]$Packet.Clone()
    for ($i = 0; $i -lt $size; $i++) { $work[32 + $i] = $k[$i] }
    if ($size -eq 16) { , [byte[]][System.Security.Cryptography.MD5]::HashData($work) }
    else { , [byte[]][System.Security.Cryptography.SHA1]::HashData($work) }
}

function New-BfdPacketBytes {
    param(
        [int]$State = 1, [int]$Diagnostic = 0, [int]$DetectMult = 3,
        [long]$MyDiscriminator = 1, [long]$YourDiscriminator = 0,
        [long]$DesiredMinTx = 1000000, [long]$RequiredMinRx = 1000000, [long]$RequiredMinEchoRx = 0,
        [bool]$Poll = $false, [bool]$Final = $false, [bool]$ControlPlaneIndependent = $false, [bool]$Demand = $false, [bool]$Multipoint = $false,
        [string]$AuthType = 'None', [string]$AuthKey = '', [int]$KeyId = 1, [long]$Sequence = 0
    )
    $au = $script:BfdAuthNumbers[$AuthType]
    $flags = 0
    if ($Poll) { $flags = $flags -bor 0x20 }
    if ($Final) { $flags = $flags -bor 0x10 }
    if ($ControlPlaneIndependent) { $flags = $flags -bor 0x08 }
    if ($au -ne 0) { $flags = $flags -bor 0x04 }
    if ($Demand) { $flags = $flags -bor 0x02 }
    if ($Multipoint) { $flags = $flags -bor 0x01 }
    $auth = [byte[]]@()
    switch ($au) {
        1 {
            $pw = [System.Text.Encoding]::ASCII.GetBytes($AuthKey)
            if ($pw.Length -lt 1 -or $pw.Length -gt 16) { throw 'BFD simple passwords are 1-16 characters' }
            $auth = [byte[]](Join-OspfBytes (ConvertTo-OspfBytes 1 1) (ConvertTo-OspfBytes (3 + $pw.Length) 1) (ConvertTo-OspfBytes $KeyId 1) $pw)
        }
        { $_ -in 2, 3 } { $auth = [byte[]](Join-OspfBytes (ConvertTo-OspfBytes $au 1) (ConvertTo-OspfBytes 24 1) (ConvertTo-OspfBytes $KeyId 1) (ConvertTo-OspfBytes 0 1) (ConvertTo-OspfBytes $Sequence 4) ([byte[]]::new(16))) }
        { $_ -in 4, 5 } { $auth = [byte[]](Join-OspfBytes (ConvertTo-OspfBytes $au 1) (ConvertTo-OspfBytes 28 1) (ConvertTo-OspfBytes $KeyId 1) (ConvertTo-OspfBytes 0 1) (ConvertTo-OspfBytes $Sequence 4) ([byte[]]::new(20))) }
    }
    $len = 24 + $auth.Length
    $pkt = [byte[]](Join-OspfBytes (ConvertTo-OspfBytes ((1 -shl 5) -bor $Diagnostic) 1) (ConvertTo-OspfBytes (($State -shl 6) -bor $flags) 1) (ConvertTo-OspfBytes $DetectMult 1) (ConvertTo-OspfBytes $len 1) `
            (ConvertTo-OspfBytes $MyDiscriminator 4) (ConvertTo-OspfBytes $YourDiscriminator 4) (ConvertTo-OspfBytes $DesiredMinTx 4) (ConvertTo-OspfBytes $RequiredMinRx 4) (ConvertTo-OspfBytes $RequiredMinEchoRx 4) $auth)
    if ($au -ge 2) {
        $d = Get-BfdDigest -Packet $pkt -AuthType $au -Key $AuthKey
        for ($i = 0; $i -lt $d.Length; $i++) { $pkt[32 + $i] = $d[$i] }
    }
    , $pkt
}

function Test-BfdAuth {
    param([byte[]]$Data, [string]$AuthKey)
    if (($Data[1] -band 0x04) -eq 0 -or $Data.Length -lt 26) { return $false }
    $au = [int]$Data[24]
    switch ($au) {
        1 { $pw = [System.Text.Encoding]::ASCII.GetBytes($AuthKey); return ([System.Convert]::ToHexString($pw) -eq [System.Convert]::ToHexString([byte[]]$Data[27..($Data.Length - 1)])) }
        { $_ -in 2, 3, 4, 5 } {
            $size = if ($au -in 2, 3) { 16 } else { 20 }
            if ($Data.Length -lt 32 + $size) { return $false }
            $d = Get-BfdDigest -Packet $Data -AuthType $au -Key $AuthKey
            return ([System.Convert]::ToHexString($d) -eq [System.Convert]::ToHexString([byte[]]$Data[32..(31 + $size)]))
        }
        default { return $false }
    }
}

function New-BfdSessionState {
    param([string]$Name, [ipaddress]$Peer, [ipaddress]$LocalAddress, [int]$DetectMult, [long]$DesiredMinTx, [long]$RequiredMinRx, [string]$AuthType, [string]$AuthKey, [int]$KeyId)
    @{
        Name = $Name; Peer = $Peer; LocalAddress = $LocalAddress; State = 'Down'; Diagnostic = 0
        MyDisc = [long](Get-Random -Minimum 1 -Maximum 2147483647); YourDisc = [long]0
        DetectMult = $DetectMult; DesiredMinTx = $DesiredMinTx; RequiredMinRx = $RequiredMinRx
        RemoteState = 'Down'; RemoteDetectMult = 3; RemoteDesiredMinTx = [long]1000000; RemoteRequiredMinRx = [long]1000000
        AuthType = $AuthType; AuthKey = $AuthKey; KeyId = $KeyId; AuthSeq = [long]([DateTimeOffset]::UtcNow.ToUnixTimeSeconds()); RemoteAuthSeq = [long]-1
        NextTx = [datetime]::MinValue; DetectDeadline = [datetime]::MaxValue; WasUp = $false; PollPending = $false; FinalPending = $false
        SuppressTx = $false; Status = 'Stopped'; Cancel = $false
        Events = [System.Collections.Generic.List[object]]::new()
        Outbox = [System.Collections.Concurrent.ConcurrentQueue[object]]::new()
        Lock = [object]::new(); Socket = $null; RxSocket = $null; Runspace = $null; PowerShell = $null; Handle = $null
    }
}

function Set-BfdState {
    param($S, [string]$State, [int]$Diag, [string]$Reason)
    if ($S.State -eq $State) { return }
    Add-OspfEvent -Session $S -Direction 'Info' -Type 'BfdState' -Message "$($S.State) -> $State ($Reason)"
    $S.State = $State
    $S.Diagnostic = $Diag
    if ($State -eq 'Up') { $S.WasUp = $true }
    if ($State -eq 'Down' -or $State -eq 'AdminDown') { $S.YourDisc = if ($State -eq 'AdminDown') { $S.YourDisc } else { [long]0 } }
}

function Get-BfdTxInterval {
    param($S)
    if ($S.State -eq 'Up') { [long][math]::Max($S.DesiredMinTx, $S.RemoteRequiredMinRx) } else { [long][math]::Max($S.DesiredMinTx, 1000000) }
}

function Send-BfdPacket {
    param($S, [datetime]$Now, [bool]$Final = $false)
    $S.AuthSeq++
    $desired = if ($S.State -eq 'Up') { $S.DesiredMinTx } else { [long][math]::Max($S.DesiredMinTx, 1000000) }
    $pkt = New-BfdPacketBytes -State $script:BfdStateNumbers[$S.State] -Diagnostic $S.Diagnostic -DetectMult $S.DetectMult -MyDiscriminator $S.MyDisc -YourDiscriminator $S.YourDisc `
        -DesiredMinTx $desired -RequiredMinRx $S.RequiredMinRx -Poll $S.PollPending -Final $Final -AuthType $S.AuthType -AuthKey $S.AuthKey -KeyId $S.KeyId -Sequence $S.AuthSeq
    $S.Outbox.Enqueue([pscustomobject]@{ Destination = $S.Peer; Bytes = $pkt })
    Add-OspfEvent -Session $S -Direction 'Sent' -Type 'BfdControl' -Message "state=$($S.State) to $($S.Peer)" -Bytes $pkt
}

function Invoke-BfdReceive {
    param($S, [byte[]]$Data, [ipaddress]$Source, [datetime]$Now = [datetime]::UtcNow)
    if ($S.Status -ne 'Running' -or -not $Source.Equals($S.Peer)) { return }
    $m = ConvertFrom-BfdControl -Packet $Data -AuthKey $S.AuthKey
    $drop = { param($why) Add-OspfEvent -Session $S -Direction 'Received' -Type 'BfdControl' -Message "Dropped: $why" -Bytes $Data }
    if (-not $m.Valid) { & $drop $m.Error; return }
    if ($m.Version -ne 1) { & $drop "version $($m.Version)"; return }
    if ($m.DetectMult -eq 0 -or $m.Multipoint -or $m.MyDiscriminator -eq 0) { & $drop 'invalid fields'; return }
    if ($m.YourDiscriminator -eq 0 -and $m.State -notin 'Down', 'AdminDown') { & $drop 'YourDiscriminator 0 in non-Down state'; return }
    if ($m.YourDiscriminator -ne 0 -and $m.YourDiscriminator -ne $S.MyDisc) { & $drop 'unknown YourDiscriminator'; return }
    if ($m.AuthType -ne $S.AuthType) { & $drop "auth type mismatch ($($m.AuthType))"; return }
    if ($S.AuthType -ne 'None') {
        if (-not $m.AuthValid) { & $drop 'authentication failed'; return }
        if ($S.AuthType -like 'Meticulous*') { if ($m.Sequence -le $S.RemoteAuthSeq) { & $drop 'sequence not increasing'; return } }
        elseif ($S.AuthType -like 'Keyed*' -and $S.RemoteAuthSeq -ge 0 -and $m.Sequence -lt $S.RemoteAuthSeq) { & $drop 'sequence went backwards'; return }
        $S.RemoteAuthSeq = $m.Sequence
    }
    Add-OspfEvent -Session $S -Direction 'Received' -Type 'BfdControl' -Message "state=$($m.State) from $Source" -Bytes $Data
    $S.YourDisc = $m.MyDiscriminator
    $S.RemoteState = $m.State
    $S.RemoteDetectMult = $m.DetectMult
    $S.RemoteDesiredMinTx = $m.DesiredMinTxInterval
    $S.RemoteRequiredMinRx = $m.RequiredMinRxInterval
    if ($m.Final) { $S.PollPending = $false }
    if ($S.State -ne 'AdminDown') {
        if ($m.State -eq 'AdminDown') {
            if ($S.State -ne 'Down') { Set-BfdState $S 'Down' 3 'neighbor signaled AdminDown' }
        }
        else {
            switch ($S.State) {
                'Down' { if ($m.State -eq 'Down') { Set-BfdState $S 'Init' 0 'remote Down' } elseif ($m.State -eq 'Init') { Set-BfdState $S 'Up' 0 'remote Init' } }
                'Init' { if ($m.State -in 'Init', 'Up') { Set-BfdState $S 'Up' 0 "remote $($m.State)" } }
                'Up' { if ($m.State -eq 'Down') { Set-BfdState $S 'Down' 3 'neighbor signaled Down' } }
            }
        }
    }
    $detect = [long]$S.RemoteDetectMult * [math]::Max($S.RequiredMinRx, $S.RemoteDesiredMinTx)
    $S.DetectDeadline = $Now.AddTicks($detect * 10)
    if ($m.Poll) { Send-BfdPacket $S $Now $true }
}

function Invoke-BfdTick {
    param($S, [datetime]$Now = [datetime]::UtcNow)
    if ($S.Status -ne 'Running') { return }
    if ($S.State -in 'Init', 'Up' -and $Now -gt $S.DetectDeadline) {
        Set-BfdState $S 'Down' 1 'control detection time expired'
        $S.DetectDeadline = [datetime]::MaxValue
    }
    if ($Now -ge $S.NextTx) {
        if (-not $S.SuppressTx) { Send-BfdPacket $S $Now }
        $interval = Get-BfdTxInterval $S
        $S.NextTx = $Now.AddTicks([long]($interval * 10 * (0.75 + 0.25 * (Get-Random -Minimum 0.0 -Maximum 1.0))))
    }
}

function Invoke-BfdRuntime {
    param($S)
    $buf = [byte[]]::new(2048)
    while (-not $S.Cancel) {
        try {
            if ($S.RxSocket.Poll(20000, [System.Net.Sockets.SelectMode]::SelectRead)) {
                $ep = [System.Net.EndPoint][System.Net.IPEndPoint]::new([ipaddress]::Any, 0)
                $n = $S.RxSocket.ReceiveFrom($buf, [ref]$ep)
                $data = [byte[]]$buf[0..($n - 1)]
                $src = $ep.Address
                Invoke-OspfLock $S { Invoke-BfdReceive -S $S -Data $data -Source $src }
            }
            Invoke-OspfLock $S { Invoke-BfdTick -S $S }
        }
        catch { Invoke-OspfLock $S { Add-OspfEvent -Session $S -Direction 'Info' -Type 'Error' -Message $_.Exception.Message } }
        $item = $null
        while ($S.Outbox.TryDequeue([ref]$item)) {
            try { $null = $S.Socket.SendTo([byte[]]$item.Bytes, [System.Net.IPEndPoint]::new($item.Destination, 3784)) } catch { }
        }
    }
}

function Start-BfdRuntime {
    param($S, [switch]$NoNetwork)
    Invoke-OspfLock $S {
        $S.Status = 'Running'
        $S.Cancel = $false
        $S.State = 'Down'; $S.Diagnostic = 0; $S.YourDisc = [long]0; $S.WasUp = $false
        $S.NextTx = [datetime]::UtcNow
        $S.DetectDeadline = [datetime]::MaxValue
        Add-OspfEvent -Session $S -Direction 'Info' -Type 'Session' -Message 'BFD started'
    }
    if ($NoNetwork) { return }
    $mk = {
        param($port)
        $u = [System.Net.Sockets.Socket]::new([System.Net.Sockets.AddressFamily]::InterNetwork, [System.Net.Sockets.SocketType]::Dgram, [System.Net.Sockets.ProtocolType]::Udp)
        $u.Bind([System.Net.IPEndPoint]::new($S.LocalAddress, $port))
        $u.Ttl = 255
        $u
    }
    try {
        $S.RxSocket = & $mk 3784
        $S.Socket = & $mk 0
    }
    catch {
        foreach ($k in 'Socket', 'RxSocket') { if ($S[$k]) { $S[$k].Dispose(); $S[$k] = $null } }
        $S.Status = 'Stopped'
        throw "Cannot open BFD UDP sockets on $($S.LocalAddress) (port 3784): $($_.Exception.Message)"
    }
    $rs = [runspacefactory]::CreateRunspace()
    $rs.Open()
    $ps = [powershell]::Create()
    $ps.Runspace = $rs
    $null = $ps.AddScript({
            param($manifest, $state)
            Import-Module $manifest -Force
            & (Get-Module PowerOSPF) { param($s) Invoke-BfdRuntime -S $s } $state
        }).AddArgument($script:ModuleManifest).AddArgument($S)
    $S.Runspace = $rs; $S.PowerShell = $ps; $S.Handle = $ps.BeginInvoke()
}

function Stop-BfdRuntime {
    param($S, [switch]$Silent)
    Invoke-OspfLock $S {
        if ($S.Status -eq 'Running' -and -not $Silent) {
            Set-BfdState $S 'AdminDown' 7 'administratively down'
            Send-BfdPacket $S ([datetime]::UtcNow)
        }
    }
    $item = $null
    if ($S.Socket -and -not $S.PowerShell) { while ($S.Outbox.TryDequeue([ref]$item)) { try { $null = $S.Socket.SendTo([byte[]]$item.Bytes, [System.Net.IPEndPoint]::new($item.Destination, 3784)) } catch { } } }
    elseif ($S.PowerShell) { Start-Sleep -Milliseconds 60 }
    $S.Cancel = $true
    if ($S.PowerShell) {
        try { $null = $S.Handle.AsyncWaitHandle.WaitOne(3000); $S.PowerShell.Stop() } catch { }
        $S.PowerShell.Dispose(); $S.Runspace.Dispose()
    }
    foreach ($k in 'Socket', 'RxSocket') { if ($S[$k]) { $S[$k].Dispose(); $S[$k] = $null } }
    $S.PowerShell = $null; $S.Runspace = $null; $S.Handle = $null
    Invoke-OspfLock $S { $S.Status = 'Stopped'; $S.State = 'Down' }
}