Private/Engine.ps1

function New-OspfSessionState {
    param(
        [Parameter(Mandatory)][string]$Name,
        [Parameter(Mandatory)][ipaddress]$RouterId,
        [Parameter(Mandatory)][ipaddress]$LocalAddress,
        [int]$PrefixLength = 24,
        [ipaddress]$AreaId = '0.0.0.0',
        [ValidateSet('Broadcast', 'PointToPoint')][string]$NetworkType = 'Broadcast',
        [int]$HelloInterval = 10,
        [long]$DeadInterval = 40,
        [int]$Priority = 1,
        [int]$Cost = 10,
        [int]$Mtu = 1500,
        [int]$InterfaceMtu = 1500,
        [int]$Options = 2,
        [ValidateSet('None', 'Simple', 'Md5')][string]$AuthType = 'None',
        [string]$AuthKey = '',
        [int]$KeyId = 1
    )
    $net = ConvertFrom-OspfPrefix "$LocalAddress/$PrefixLength"
    @{
        Name               = $Name
        RouterId           = $RouterId
        RouterIdLong       = ConvertTo-OspfLong $RouterId
        AreaId             = $AreaId
        LocalAddress       = $LocalAddress
        LocalAddressLong   = ConvertTo-OspfLong $LocalAddress
        PrefixLength       = $PrefixLength
        Mask               = $net.Mask
        NetworkLong        = $net.NetworkLong
        MaskLong           = $net.MaskLong
        NetworkType        = $NetworkType
        HelloInterval      = $HelloInterval
        DeadInterval       = $DeadInterval
        RxmtInterval       = 5
        Priority           = $Priority
        Cost               = $Cost
        Mtu                = $Mtu
        InterfaceMtu       = $InterfaceMtu
        Options            = $Options
        AuthType           = $AuthType
        AuthKey            = $AuthKey
        KeyId              = $KeyId
        CryptoSequence     = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
        InterfaceState     = 'Down'
        Dr                 = [long]0
        Bdr                = [long]0
        WaitDeadline       = [datetime]::MaxValue
        NextHello          = [datetime]::MinValue
        LastRefresh        = [datetime]::UtcNow
        Neighbors          = @{}
        Lsdb               = @{}
        Originated         = @{}
        SeqFloor           = @{}
        Prefixes           = [System.Collections.Generic.List[object]]::new()
        Externals          = [System.Collections.Generic.List[object]]::new()
        Summaries          = [System.Collections.Generic.List[object]]::new()
        Events             = [System.Collections.Generic.List[object]]::new()
        Outbox             = [System.Collections.Concurrent.ConcurrentQueue[object]]::new()
        Lock               = [object]::new()
        Status             = 'Stopped'
        SuppressHellos     = $false
        CorruptChecksum    = $false
        LsaDirty           = $false
        Cancel             = $false
        HoldDown           = @{}
        Bfd                = $null
        Socket             = $null
        Runspace           = $null
        PowerShell         = $null
        Handle             = $null
        Created            = [datetime]::UtcNow
    }
}

function Invoke-OspfLock {
    param($Session, [scriptblock]$ScriptBlock)
    [System.Threading.Monitor]::Enter($Session.Lock)
    try { & $ScriptBlock }
    finally { [System.Threading.Monitor]::Exit($Session.Lock) }
}

function Add-OspfEvent {
    param($Session, [string]$Direction, [string]$Type, [string]$Message, $Neighbor = $null, [byte[]]$Bytes = $null)
    $Session.Events.Add([pscustomobject]@{
            PSTypeName = 'PowerOSPF.Event'
            Time       = [datetime]::UtcNow
            Direction  = $Direction
            Type       = $Type
            Neighbor   = $Neighbor
            Message    = $Message
            Bytes      = $Bytes
        })
    while ($Session.Events.Count -gt 500) { $Session.Events.RemoveAt(0) }
}

function Send-OspfPacket {
    param($Session, [int]$Type, [byte[]]$Body, [ipaddress]$Destination, [string]$Info = '', $Neighbor = $null)
    $Session.CryptoSequence++
    $pkt = New-OspfPacketBytes -Type $Type -RouterId $Session.RouterId -AreaId $Session.AreaId -Body $Body -AuthType $Session.AuthType `
        -AuthKey $Session.AuthKey -KeyId $Session.KeyId -CryptoSequence $Session.CryptoSequence -CorruptChecksum:([bool]$Session.CorruptChecksum)
    $Session.Outbox.Enqueue([pscustomobject]@{ Destination = $Destination; Bytes = $pkt })
    Add-OspfEvent -Session $Session -Direction 'Sent' -Type $script:PacketTypeNames[$Type] -Message "$Info to $Destination" -Neighbor $Neighbor -Bytes $pkt
}

function Send-OspfHello {
    param($Session)
    $n = [long[]]@($Session.Neighbors.Values | ForEach-Object { $_.RouterIdLong })
    $body = Get-OspfHelloBody -Mask $Session.Mask -HelloInterval $Session.HelloInterval -Options $Session.Options -Priority $Session.Priority `
        -DeadInterval $Session.DeadInterval -Dr $Session.Dr -Bdr $Session.Bdr -Neighbors $n
    Send-OspfPacket -Session $Session -Type 1 -Body $body -Destination $script:AllSpfRouters -Info "neighbors=$($n.Count)"
}

function Get-OspfLsaAge {
    param($Entry, [datetime]$Now)
    [int][math]::Min($script:OspfMaxAge, $Entry.Age0 + [math]::Floor(($Now - $Entry.InstallTime).TotalSeconds))
}

function Get-OspfEntryBytes {
    param($Entry, [datetime]$Now, [int]$AddAge = 0)
    $b = [byte[]]$Entry.Bytes.Clone()
    $age = [math]::Min($script:OspfMaxAge, (Get-OspfLsaAge $Entry $Now) + $AddAge)
    $b[0] = [byte]($age -shr 8)
    $b[1] = [byte]($age -band 0xFF)
    , $b
}

function Get-OspfEntryHeader {
    param($Entry, [datetime]$Now)
    Read-OspfLsaHeader -Data ([byte[]](Get-OspfEntryBytes $Entry $Now)) -Offset 0
}

function Install-OspfLsa {
    param($Session, [byte[]]$Lsa, [datetime]$Now, [switch]$Originated)
    $h = Read-OspfLsaHeader $Lsa 0
    $Session.Lsdb[$h.Key] = @{ Key = $h.Key; Bytes = $Lsa; Age0 = $h.Age; InstallTime = $Now; Originated = [bool]$Originated }
    $h.Key
}

function Set-OspfNeighborState {
    param($Session, $Neighbor, [string]$State, [string]$Reason, [datetime]$Now)
    $old = $Neighbor.State
    if ($old -eq $State) { return }
    $Neighbor.State = $State
    Add-OspfEvent -Session $Session -Direction 'Info' -Type 'NeighborState' -Neighbor $Neighbor.RouterId.ToString() -Message "$old -> $State ($Reason)"
    if ($script:NeighborRank[$State] -lt $script:NeighborRank['ExStart']) {
        $Neighbor.RequestList = @{}
        $Neighbor.Retrans = @{}
        $Neighbor.DbSummary = [System.Collections.Generic.List[string]]::new()
        $Neighbor.LastDbdBody = $null
        $Neighbor.LastRecv = $null
    }
    if ($old -eq 'Full' -or $State -eq 'Full') { $Session.LsaDirty = $true }
}

function New-OspfNeighbor {
    param([long]$RouterIdLong, [long]$Address, [datetime]$Now)
    @{
        RouterId     = ConvertFrom-OspfLong $RouterIdLong
        RouterIdLong = $RouterIdLong
        Address      = $Address
        State        = 'Down'
        Priority     = 0
        Dr           = [long]0
        Bdr          = [long]0
        Options      = 0
        Deadline     = $Now
        DdSeq        = [long]0
        IsMaster     = $false
        RequestList  = @{}
        Retrans      = @{}
        DbSummary    = [System.Collections.Generic.List[string]]::new()
        LastDbdBody  = $null
        LastDbdMore  = $false
        LastDbdTime  = $Now
        LastLsrTime  = $Now
        LastRecv     = $null
        LastCrypto   = [long]-1
        Since        = $Now
    }
}

function Select-OspfDesignatedRouter {
    param($Candidates)
    $pick = {
        param($list)
        @($list | Sort-Object -Property @{ Expression = { $_.Priority }; Descending = $true }, @{ Expression = { $_.Id }; Descending = $true }) | Select-Object -First 1
    }
    $bdrPool = @($Candidates | Where-Object { $_.Dr -ne $_.Ip })
    $declBdr = @($bdrPool | Where-Object { $_.Bdr -eq $_.Ip })
    $b = if ($declBdr.Count) { & $pick $declBdr } else { & $pick $bdrPool }
    $drPool = @($Candidates | Where-Object { $_.Dr -eq $_.Ip })
    $d = if ($drPool.Count) { & $pick $drPool } else { $b }
    [pscustomobject]@{ Dr = $(if ($d) { [long]$d.Ip } else { [long]0 }); Bdr = $(if ($b) { [long]$b.Ip } else { [long]0 }) }
}

function Invoke-OspfElection {
    param($Session, [datetime]$Now)
    if ($Session.NetworkType -ne 'Broadcast') { Update-OspfAdjacencies -Session $Session -Now $Now; return }
    $others = @($Session.Neighbors.Values | Where-Object { $script:NeighborRank[$_.State] -ge 2 -and $_.Priority -gt 0 } |
            ForEach-Object { [pscustomobject]@{ Id = $_.RouterIdLong; Ip = $_.Address; Priority = $_.Priority; Dr = $_.Dr; Bdr = $_.Bdr } })
    $self = [pscustomobject]@{ Id = $Session.RouterIdLong; Ip = $Session.LocalAddressLong; Priority = $Session.Priority; Dr = $Session.Dr; Bdr = $Session.Bdr }
    $oldDr = $Session.Dr; $oldBdr = $Session.Bdr
    $result = $null
    for ($pass = 0; $pass -lt 2; $pass++) {
        $cands = @($others)
        if ($Session.Priority -gt 0) { $cands += $self }
        $result = Select-OspfDesignatedRouter $cands
        $changed = (($result.Dr -eq $self.Ip) -ne ($self.Dr -eq $self.Ip)) -or (($result.Bdr -eq $self.Ip) -ne ($self.Bdr -eq $self.Ip))
        if (-not $changed) { break }
        $self.Dr = $result.Dr; $self.Bdr = $result.Bdr
    }
    $Session.Dr = $result.Dr
    $Session.Bdr = $result.Bdr
    $Session.InterfaceState = if ($Session.Dr -eq $Session.LocalAddressLong) { 'DR' } elseif ($Session.Bdr -eq $Session.LocalAddressLong) { 'BDR' } else { 'DROther' }
    $Session.WaitDeadline = [datetime]::MaxValue
    if ($oldDr -ne $Session.Dr -or $oldBdr -ne $Session.Bdr) {
        Add-OspfEvent -Session $Session -Direction 'Info' -Type 'Election' -Message "DR=$(ConvertFrom-OspfLong $Session.Dr) BDR=$(ConvertFrom-OspfLong $Session.Bdr) state=$($Session.InterfaceState)"
        $Session.NextHello = $Now
        $Session.LsaDirty = $true
    }
    Update-OspfAdjacencies -Session $Session -Now $Now
}

function Test-OspfAdjacencyNeeded {
    param($Session, $Neighbor)
    if ($Session.NetworkType -eq 'PointToPoint') { return $true }
    $me = $Session.LocalAddressLong
    ($Session.Dr -eq $me -or $Session.Bdr -eq $me -or $Neighbor.Address -eq $Session.Dr -or $Neighbor.Address -eq $Session.Bdr)
}

function Update-OspfAdjacencies {
    param($Session, [datetime]$Now)
    foreach ($n in @($Session.Neighbors.Values)) {
        $rank = $script:NeighborRank[$n.State]
        $need = Test-OspfAdjacencyNeeded $Session $n
        if ($n.State -eq '2-Way' -and $need) { Start-OspfAdjacency $Session $n $Now }
        elseif ($rank -ge 3 -and -not $need) { Set-OspfNeighborState $Session $n '2-Way' 'AdjOK? no adjacency needed' $Now }
    }
}

function Start-OspfAdjacency {
    param($Session, $Neighbor, [datetime]$Now)
    Set-OspfNeighborState $Session $Neighbor 'ExStart' 'AdjOK?' $Now
    $Neighbor.IsMaster = $true
    $Neighbor.DdSeq = [long]([DateTimeOffset]::UtcNow.ToUnixTimeSeconds() -band 0x7FFFFFFF)
    $Neighbor.RequestList = @{}
    $Neighbor.Retrans = @{}
    $Neighbor.LastRecv = $null
    $Neighbor.DbSummary = [System.Collections.Generic.List[string]]::new()
    foreach ($e in $Session.Lsdb.Values) {
        if ((Get-OspfLsaAge $e $Now) -lt $script:OspfMaxAge) { $Neighbor.DbSummary.Add($e.Key) }
    }
    Send-OspfInitialDbd $Session $Neighbor $Now
}

function Send-OspfDbdPacket {
    param($Session, $Neighbor, [byte[]]$Body, [datetime]$Now, [string]$Info)
    $Neighbor.LastDbdBody = $Body
    $Neighbor.LastDbdTime = $Now
    Send-OspfPacket -Session $Session -Type 2 -Body $Body -Destination (ConvertFrom-OspfLong $Neighbor.Address) -Info $Info -Neighbor $Neighbor.RouterId.ToString()
}

function Send-OspfInitialDbd {
    param($Session, $Neighbor, [datetime]$Now)
    $body = Get-OspfDbdBody -InterfaceMtu $Session.Mtu -Options $Session.Options -Flags 7 -Sequence $Neighbor.DdSeq
    $Neighbor.LastDbdMore = $true
    Send-OspfDbdPacket $Session $Neighbor $body $Now "init seq=$($Neighbor.DdSeq)"
}

function Send-OspfDbdChunk {
    param($Session, $Neighbor, [datetime]$Now)
    $limit = [math]::Max(1, [int][math]::Floor(($Session.InterfaceMtu - 52) / 20))
    $headers = [System.Collections.Generic.List[byte[]]]::new()
    while ($Neighbor.DbSummary.Count -gt 0 -and $headers.Count -lt $limit) {
        $key = $Neighbor.DbSummary[0]
        $Neighbor.DbSummary.RemoveAt(0)
        $e = $Session.Lsdb[$key]
        if ($e) { $headers.Add([byte[]](Get-OspfEntryBytes $e $Now)[0..19]) }
    }
    $more = $Neighbor.DbSummary.Count -gt 0
    $flags = 0
    if ($more) { $flags = $flags -bor 2 }
    if ($Neighbor.IsMaster) { $flags = $flags -bor 1 }
    $Neighbor.LastDbdMore = $more
    $body = Get-OspfDbdBody -InterfaceMtu $Session.Mtu -Options $Session.Options -Flags $flags -Sequence $Neighbor.DdSeq -Headers $headers.ToArray()
    Send-OspfDbdPacket $Session $Neighbor $body $Now "seq=$($Neighbor.DdSeq) headers=$($headers.Count) more=$more"
}

function Send-OspfLsrTo {
    param($Session, $Neighbor, [datetime]$Now)
    if ($Neighbor.RequestList.Count -eq 0) { return }
    $reqs = @($Neighbor.RequestList.Values | Select-Object -First 50 | ForEach-Object {
            [pscustomobject]@{ Type = $_.Type; LinkStateId = $_.LinkStateId; AdvertisingRouter = $_.AdvertisingRouter } })
    $Neighbor.LastLsrTime = $Now
    Send-OspfPacket -Session $Session -Type 3 -Body (Get-OspfLsrBody -Requests $reqs) -Destination (ConvertFrom-OspfLong $Neighbor.Address) -Info "requests=$($reqs.Count)" -Neighbor $Neighbor.RouterId.ToString()
}

function Send-OspfLsuTo {
    param($Session, $Neighbor, $Lsas)
    $max = $Session.InterfaceMtu - 60
    $batch = [System.Collections.Generic.List[byte[]]]::new()
    $size = 0
    $flush = {
        if ($batch.Count -gt 0) {
            Send-OspfPacket -Session $Session -Type 4 -Body (Get-OspfLsuBody -Lsas $batch.ToArray()) -Destination (ConvertFrom-OspfLong $Neighbor.Address) -Info "lsas=$($batch.Count)" -Neighbor $Neighbor.RouterId.ToString()
            $batch.Clear()
        }
    }
    foreach ($l in (ConvertTo-OspfByteList $Lsas)) {
        if ($size + $l.Length -gt $max -and $batch.Count -gt 0) { & $flush; $size = 0 }
        $batch.Add($l)
        $size += $l.Length
    }
    & $flush
}

function Send-OspfAck {
    param($Session, $Neighbor, $Headers)
    $h = ConvertTo-OspfByteList $Headers
    if ($h.Count -eq 0) { return }
    Send-OspfPacket -Session $Session -Type 5 -Body (Get-OspfLsAckBody -Headers $h) -Destination (ConvertFrom-OspfLong $Neighbor.Address) -Info "acks=$($h.Count)" -Neighbor $Neighbor.RouterId.ToString()
}

function Send-OspfFlood {
    param($Session, [string[]]$Keys, [datetime]$Now, $Except = $null)
    foreach ($n in @($Session.Neighbors.Values)) {
        if ($script:NeighborRank[$n.State] -lt $script:NeighborRank['Exchange']) { continue }
        if ($null -ne $Except -and $n.RouterIdLong -eq $Except.RouterIdLong) { continue }
        $lsas = [System.Collections.Generic.List[byte[]]]::new()
        foreach ($k in $Keys) {
            $e = $Session.Lsdb[$k]
            if ($e) {
                $h = Read-OspfLsaHeader $e.Bytes 0
                $n.Retrans[$k] = @{ Sequence = $h.Sequence; Time = $Now }
                $lsas.Add([byte[]](Get-OspfEntryBytes $e $Now 1))
            }
        }
        if ($lsas.Count -gt 0) { Send-OspfLsuTo $Session $n $lsas.ToArray() }
    }
}

function Restart-OspfAdjacency {
    param($Session, $Neighbor, [string]$Reason, [datetime]$Now)
    Add-OspfEvent -Session $Session -Direction 'Info' -Type 'Adjacency' -Neighbor $Neighbor.RouterId.ToString() -Message $Reason
    Set-OspfNeighborState $Session $Neighbor '2-Way' $Reason $Now
    if (Test-OspfAdjacencyNeeded $Session $Neighbor) { Start-OspfAdjacency $Session $Neighbor $Now }
}

function Complete-OspfExchange {
    param($Session, $Neighbor, [datetime]$Now)
    if ($Neighbor.RequestList.Count -eq 0) {
        Set-OspfNeighborState $Session $Neighbor 'Full' 'ExchangeDone' $Now
    }
    else {
        Set-OspfNeighborState $Session $Neighbor 'Loading' 'ExchangeDone' $Now
        Send-OspfLsrTo $Session $Neighbor $Now
    }
}