Private/Invoke-BgpPeerLoop.ps1

function Invoke-BgpPeerLoop {
    param($S)

    $cfg = $S.Config
    $listener = $null
    $delay = $cfg.ReconnectDelay
    try {
        if ($cfg.Passive) {
            $listener = [Net.Sockets.TcpListener]::new($(if ($cfg.LocalAddress) { [ipaddress]$cfg.LocalAddress } elseif ('IPv6' -in $cfg.AddressFamily -and -not $cfg.Password) { [ipaddress]::IPv6Any } else { [ipaddress]::Any }), $cfg.Port)
            if ($listener.Server.AddressFamily -eq 'InterNetworkV6') { $listener.Server.DualMode = $true }
            if ($cfg.Password) { Set-BgpTcpMd5 -Socket $listener.Server -Address $cfg.RemoteAddress -Password $cfg.Password }
            $listener.Start()
        }
        while (-not $S.Stop) {
            Invoke-BgpPeerSession $S $listener
            if ($S.Stop -or -not $cfg.AutoReconnect) { break }

            # A session that reached Established resets the backoff
            if ($S.LastEstablished) { $delay = $cfg.ReconnectDelay; $S.LastEstablished = $false }
            $S.Reconnects++
            $S.NextRetry = (Get-Date).AddSeconds($delay)
            for ($i = 0; $i -lt ($delay * 10) -and -not $S.Stop; $i++) { Start-Sleep -Milliseconds 100 }
            $delay = [Math]::Min($delay * 2, $cfg.ReconnectMaxDelay)
            $S.NextRetry = $null
        }
    }
    catch { $S.LastError = $_.Exception.Message }
    finally {
        if ($listener) { $listener.Stop() }
        $S.State = 'Idle'
    }
}

function Invoke-BgpPeerSession {
    param($S, $Listener)

    $cfg = $S.Config
    $client = $null
    $stream = $null
    $clock = [Diagnostics.Stopwatch]::StartNew()
    $buf = [System.Collections.Generic.List[byte]]::new()
    $tmp = [byte[]]::new(8192)

    $send = {
        param([byte[]]$Bytes)
        $stream.Write($Bytes, 0, $Bytes.Length)
        $S.Sent++
        $script:lastSent = $clock.Elapsed.TotalSeconds
        Add-BgpEvent $S 'Sent' $Bytes
    }

    try {
        $S.State = 'Active'

        if ($cfg.Passive) {
            while (-not $S.Stop -and -not $Listener.Pending()) { Start-Sleep -Milliseconds 100 }
            if ($S.Stop) { return }
            $client = $Listener.AcceptTcpClient()
        }
        else {
            while (-not $S.Stop -and -not $client) {
                $S.State = 'Connect'
                $c = if ($cfg.LocalAddress) { [Net.Sockets.TcpClient]::new([Net.IPEndPoint]::new([ipaddress]$cfg.LocalAddress, 0)) }
                elseif ($cfg.Password) { [Net.Sockets.TcpClient]::new([Net.Sockets.AddressFamily]::InterNetwork) }
                else { [Net.Sockets.TcpClient]::new() }
                if ($cfg.Password) { Set-BgpTcpMd5 -Socket $c.Client -Address $cfg.RemoteAddress -Password $cfg.Password }
                $task = $c.ConnectAsync($cfg.RemoteAddress, $cfg.Port)
                while (-not $task.IsCompleted -and -not $S.Stop) { Start-Sleep -Milliseconds 50 }
                if (-not $S.Stop -and $task.Status -eq 'RanToCompletion' -and $c.Connected) { $client = $c; break }
                $S.LastError = if ($task.IsFaulted) { $task.Exception.InnerException.Message } else { $S.LastError }
                $c.Dispose()
                $S.State = 'Active'
                for ($i = 0; $i -lt 30 -and -not $S.Stop; $i++) { Start-Sleep -Milliseconds 100 }
            }
            if ($S.Stop) { return }
        }

        $S.LastError = $null
        $client.NoDelay = $true
        if ($cfg.Ttl) { $client.Client.Ttl = [int16]$cfg.Ttl }
        $stream = $client.GetStream()
        $S.RemoteEndpoint = $client.Client.RemoteEndPoint.ToString()
        $S.LocalAddress = $client.Client.LocalEndPoint.Address.ToString()

        $script:lastSent = $clock.Elapsed.TotalSeconds
        $lastRecv = $clock.Elapsed.TotalSeconds
        $hold = 240
        & $send (New-BgpOpen -AS $cfg.LocalAS -RouterId $cfg.RouterId -HoldTime $cfg.HoldTime -AddressFamily $(if ($cfg.AddressFamily) { $cfg.AddressFamily } else { 'IPv4' }) -GracefulRestart:([bool]$cfg.GracefulRestart) -RestartTime $(if ($cfg.RestartTime) { $cfg.RestartTime } else { 120 }))
        $S.State = 'OpenSent'
        $done = $false

        while (-not $S.Stop -and -not $done) {
            $item = $null
            while ($S.Outbound.TryDequeue([ref]$item)) { & $send $item }

            if ($stream.DataAvailable) {
                $n = $stream.Read($tmp, 0, $tmp.Length)
                if ($n -eq 0) { $S.LastError = 'Connection closed by peer'; break }
                $buf.AddRange([byte[]]$tmp[0..($n - 1)])
            }
            elseif ($client.Client.Poll(0, [Net.Sockets.SelectMode]::SelectRead) -and -not $stream.DataAvailable) {
                $S.LastError = 'Connection closed by peer'
                break
            }

            while ($buf.Count -ge 19 -and -not $done) {
                $len = $buf[16] * 256 + $buf[17]
                if ($len -lt 19 -or $len -gt 4096) {
                    & $send (New-BgpNotification -Code 1 -Subcode 2)
                    $S.LastError = "Bad message length $len"; $done = $true; break
                }
                if ($buf.Count -lt $len) { break }
                $msg = $buf.GetRange(0, $len).ToArray()
                $buf.RemoveRange(0, $len)
                $lastRecv = $clock.Elapsed.TotalSeconds
                $S.Received++
                Add-BgpEvent $S 'Received' $msg
                $parsed = $null
                try { $parsed = ConvertFrom-BgpMessage $msg } catch { }

                switch ([int]$msg[18]) {
                    1 {
                        if (-not $parsed -or $S.State -ne 'OpenSent') { $done = $true; break }
                        if ($cfg.RemoteAS -and $parsed.Asn -ne $cfg.RemoteAS) {
                            & $send (New-BgpNotification -Code 2 -Subcode 2)
                            $S.LastError = "Bad peer AS $($parsed.Asn), expected $($cfg.RemoteAS)"; $done = $true; break
                        }
                        $S.PeerOpen = $parsed
                        if (-not (Get-BgpPeerGracefulRestart $parsed)) { $S.StaleExpires = [datetime]::MinValue; Remove-BgpStaleRoute $S }
                        $S.PeerFourOctet = [bool]($parsed.Capabilities | Where-Object Code -EQ 65)
                        $hold = [Math]::Min($cfg.HoldTime, $parsed.HoldTime)
                        $S.HoldTime = $hold
                        & $send (New-BgpKeepalive)
                        $S.State = 'OpenConfirm'
                    }
                    2 {
                        if ($S.State -ne 'Established') { $done = $true; break }
                        $u = ConvertFrom-BgpMessage $msg -TwoOctetAs:(-not $S.PeerFourOctet)
                        $dummy = $null
                        foreach ($p in $u.Withdrawn) { $null = $S.Learned.TryRemove($p, [ref]$dummy) }
                        if ($u.EndOfRib) { Remove-BgpStaleRoute $S -Family $u.EndOfRib }
                        $rules = $S.Config.InboundFilter; $defaultAction = $S.Config.InboundDefault
                        foreach ($p in $u.Nlri) {
                            if (($rules -or $defaultAction -eq 'Deny') -and -not (Test-BgpInboundFilter -Rules $rules -Default $defaultAction -Prefix $p -Update $u)) {
                                $S.Filtered++
                                $null = $S.Learned.TryRemove($p, [ref]$dummy)
                                continue
                            }
                            $S.Learned[$p] = [pscustomobject]@{
                                Peer = $S.Name; Prefix = $p; NextHop = "$($u.NextHop)"; AsPath = ($u.AsPath -join ' ')
                                Origin = $u.Origin; Med = $u.Med; LocalPref = $u.LocalPref; Community = ($u.Community -join ' '); Received = Get-Date; Stale = $false
                            }
                        }
                    }
                    3 { $S.LastError = "Notification received: $($parsed.Summary)"; $done = $true }
                    4 {
                        if ($S.State -eq 'OpenConfirm') {
                            $S.State = 'Established'; $S.Established = Get-Date; $S.LastEstablished = $true
                            # Replay routes advertised before the session dropped
                            foreach ($e in @($S.Advertised.Values)) {
                                if (-not $e.Params) { continue }
                                $q = @{} + $e.Params
                                $q.Nlri = @($e.Prefix); $q.Remove('Withdrawn')
                                $S.Outbound.Enqueue([byte[]](New-BgpUpdate @q))
                            }
                            if ($cfg.GracefulRestart) {
                                foreach ($af in @($(if ($cfg.AddressFamily) { $cfg.AddressFamily } else { 'IPv4' }))) {
                                    $S.Outbound.Enqueue([byte[]](New-BgpUpdate -EndOfRib -AddressFamily $af))
                                }
                            }
                        }
                    }
                }
            }
            if ($done) { break }

            Remove-BgpStaleRoute $S
            $now = $clock.Elapsed.TotalSeconds
            if ($hold -gt 0 -and ($now - $lastRecv) -gt $hold) {
                & $send (New-BgpNotification -Code 4 -Subcode 0)
                $S.LastError = 'Hold timer expired'
                break
            }
            if ($hold -gt 0 -and $S.State -in 'OpenConfirm', 'Established' -and -not $S.SuppressKeepalives -and ($now - $script:lastSent) -ge ($hold / 3)) {
                & $send (New-BgpKeepalive)
            }
            Start-Sleep -Milliseconds 20
        }

        if ($S.Stop -and $stream) { try { & $send (New-BgpNotification -Code 6 -Subcode 0) } catch { } }
    }
    catch { $S.LastError = $_.Exception.Message }
    finally {
        if ($client) { $client.Dispose() }
        $S.State = 'Idle'
        $S.Established = $null
        $peerGr = Get-BgpPeerGracefulRestart $S.PeerOpen
        if ($cfg.GracefulRestart -and $peerGr -and $peerGr.RestartTime -gt 0 -and -not $S.Stop -and $S.Learned.Count) {
            # RFC 4724 receiving speaker: keep routes as stale until the peer returns (End-of-RIB) or Restart Time passes
            foreach ($v in @($S.Learned.Values)) { $v.Stale = $true }
            $S.StaleExpires = (Get-Date).AddSeconds($peerGr.RestartTime)
        }
        else { $S.Learned.Clear() }
        $dropped = $null
        while ($S.Outbound.TryDequeue([ref]$dropped)) { }
    }
}