Private/BfdSessionLoop.ps1
|
function Invoke-BfdSessionLoop { # Socket I/O around the protocol engine. Runs inside a dedicated runspace until $S.Stop is set. param($S) $c = $S.Config $rx = $null; $tx = $null; $echoRx = $null; $echoFailed = $false $clock = [Diagnostics.Stopwatch]::StartNew() $rand = [Random]::new() $ctx = $null $quiet = { param($sock) # Windows reports ICMP port unreachable as a receive error on UDP sockets; switch that off if ($IsWindows -or $PSVersionTable.PSEdition -eq 'Desktop') { try { $null = $sock.Client.IOControl(-1744830452, [byte[]]@(0, 0, 0, 0), $null) } catch { } } } try { $remoteIp = [ipaddress]$c.RemoteAddress $any = if ($remoteIp.AddressFamily -eq 'InterNetworkV6') { [ipaddress]::IPv6Any } else { [ipaddress]::Any } $bindIp = if ($c.LocalAddress) { [ipaddress]$c.LocalAddress } else { $any } $rx = [Net.Sockets.UdpClient]::new([Net.IPEndPoint]::new($bindIp, $c.ListenPort)) & $quiet $rx # RFC 5881 4: source port in 49152-65535, TTL/hop limit 255 for ($i = 0; $i -lt 20 -and -not $tx; $i++) { try { $tx = [Net.Sockets.UdpClient]::new([Net.IPEndPoint]::new($bindIp, $rand.Next(49152, 65535))) } catch { } } if (-not $tx) { throw 'Could not bind a source port in 49152-65535.' } & $quiet $tx $tx.Client.Ttl = 255 $remote = [Net.IPEndPoint]::new($remoteIp, $c.RemotePort) $hook = $null if ($c.OnStateChange) { $copy = [scriptblock]::Create($c.OnStateChange.ToString()) $mod = $ExecutionContext.SessionState.Module $hook = if ($mod) { $mod.NewBoundScriptBlock($copy) } else { $copy } } $ctx = New-BfdContext -Config $c -MyDiscriminator ([uint32]$rand.Next(1, [int]::MaxValue)) -Now $clock.Elapsed.TotalMilliseconds -TxSequence ([uint32]$rand.Next(1, 1000000)) $S.AuthFailures = 0 $S.State = 'Down' $S.LocalDiscriminator = $ctx.MyDisc $S.Ready = $true $nextTx = 0.0; $lastTx = 0.0; $echoNextTx = 0.0 while (-not $S.Stop) { $now = $clock.Elapsed.TotalMilliseconds if ($S.AdminDown -and $ctx.State -ne 'AdminDown') { Set-BfdContextState $ctx $c 'AdminDown' 7 $now } elseif (-not $S.AdminDown -and $ctx.State -eq 'AdminDown') { Set-BfdContextState $ctx $c 'Down' 0 $now; $ctx.YourDisc = 0 } try { while ($rx.Available -gt 0) { $ep = [Net.IPEndPoint]::new($any, 0) $data = $rx.Receive([ref]$ep) if (-not $ep.Address.Equals($remote.Address)) { continue } $r = Invoke-BfdReceive $ctx $c $data $now if ($r.Accepted) { $S.Received = $ctx.Received if ($c.RecordPackets) { Add-BfdEvent -State $S -Type 'Control' -Direction 'Rx' -Bytes $data -Summary ('rx from {0}' -f $ep.Address) } } else { $S.Dropped = $ctx.Dropped; $S.AuthFailures = $ctx.AuthFailures Add-BfdEvent -State $S -Type 'Drop' -Direction 'Rx' -Bytes $data -Summary "dropped: $($r.Reason)" } Publish-BfdChange $S $ctx $hook } } catch [Net.Sockets.SocketException] { } # echo function (RFC 5880 6.4): loop back the peer's echo packets, and originate our own when Active if ($c.EchoMode -and $c.EchoMode -ne 'Disabled' -and -not $echoRx -and -not $echoFailed) { try { $echoRx = [Net.Sockets.UdpClient]::new([Net.IPEndPoint]::new($bindIp, $c.EchoListenPort)); & $quiet $echoRx } catch { $echoFailed = $true; $S.LastError = "Echo socket: $($_.Exception.Message)" } } if ($echoRx) { try { while ($echoRx.Available -gt 0) { $ep = [Net.IPEndPoint]::new($any, 0) $data = $echoRx.Receive([ref]$ep) if ($data.Length -lt 20 -or $data[0] -ne 0x42 -or $data[1] -ne 0x46 -or $data[2] -ne 0x44 -or $data[3] -ne 0x45) { continue } if ((Get-BfdUInt32 $data 4) -eq $ctx.MyDisc) { $ctx.EchoLastRx = $now; $S.EchoReceived++ } elseif ($c.EchoMode -ne 'Disabled' -and -not $S.DropEcho -and $ep.Address.Equals($remote.Address)) { $null = $tx.Send($data, $data.Length, [Net.IPEndPoint]::new($ep.Address, $c.EchoPort)) $S.EchoReflected++ } } } catch [Net.Sockets.SocketException] { } } $wantEcho = $c.EchoMode -eq 'Active' -and $echoRx -and $ctx.State -eq 'Up' -and $ctx.RemoteState -eq 'Up' -and $ctx.RemoteRequiredEchoRx -gt 0 if ($wantEcho -and -not $ctx.EchoActive) { $ctx.EchoActive = $true; $ctx.EchoLastRx = $now; $echoNextTx = 0.0 Add-BfdEvent -State $S -Type 'Echo' -Summary 'echo function started' } elseif (-not $wantEcho -and $ctx.EchoActive) { $ctx.EchoActive = $false Add-BfdEvent -State $S -Type 'Echo' -Summary 'echo function stopped' } if ($ctx.EchoActive) { $echoMs = [Math]::Max([double]$c.EchoInterval, $ctx.RemoteRequiredEchoRx) / 1000.0 if ($now -ge $echoNextTx) { $e = [byte[]]::new(20) $e[0] = 0x42; $e[1] = 0x46; $e[2] = 0x44; $e[3] = 0x45 Set-BfdUInt32 $e 4 $ctx.MyDisc $ctx.EchoSeq = [uint32](([uint64]$ctx.EchoSeq + 1) % 4294967296) Set-BfdUInt32 $e 8 $ctx.EchoSeq [BitConverter]::GetBytes([int64][Diagnostics.Stopwatch]::GetTimestamp()).CopyTo($e, 12) $null = $tx.Send($e, $e.Length, [Net.IPEndPoint]::new($remoteIp, $c.EchoPort)) $S.EchoSent++ $echoNextTx = $now + $echoMs } if (($now - $ctx.EchoLastRx) -gt ($c.DetectMult * $echoMs)) { Add-BfdEvent -State $S -Type 'Echo' -Summary 'echo function failed' Set-BfdContextState $ctx $c 'Down' 2 $now } } Update-BfdTimers $ctx $c $now Publish-BfdChange $S $ctx $hook if ($S.Suppress) { $ctx.ReplyFinal = $false; $ctx.SendNow = $false } else { $intervalMs = Get-BfdTxIntervalMs $ctx # a shorter interval (state change, Poll sequence, peer lowering RequiredMinRx) must not wait for a slow schedule if ($nextTx -gt $lastTx + $intervalMs) { $nextTx = $lastTx + $intervalMs } $due = $now -ge $nextTx -and (Test-BfdPeriodicTx $ctx $c) if ($ctx.ReplyFinal -or $ctx.SendNow -or $due) { $pkt = New-BfdSessionPacket $ctx $c $S.Fault $S.Fault.AuthKey $null = $tx.Send($pkt, $pkt.Length, $remote) $S.Sent++ $lastTx = $now if ($c.RecordPackets) { Add-BfdEvent -State $S -Type 'Control' -Direction 'Tx' -Bytes $pkt -Summary ('tx to {0}' -f $remote.Address) } if ($due -or $nextTx -gt $now + $intervalMs) { $jitter = if ($c.DetectMult -eq 1) { 0.90 - 0.15 * $rand.NextDouble() } else { 1.0 - 0.25 * $rand.NextDouble() } $nextTx = $now + $intervalMs * $jitter } } } $S.RemoteState = $ctx.RemoteState $S.RemoteDiscriminator = $ctx.YourDisc $S.DetectionTimeMs = if ($ctx.RemoteMult) { Get-BfdDetectionTimeMs $ctx } else { $null } $S.TxIntervalMs = Get-BfdTxIntervalMs $ctx $S.PollPending = [bool]($ctx.PollPending -or $null -ne $ctx.DemandPollAt) $S.DemandActive = Test-BfdDemandActive $ctx $c $S.EchoActive = $ctx.EchoActive try { $null = $rx.Client.Poll(2000, [Net.Sockets.SelectMode]::SelectRead) } catch { Start-Sleep -Milliseconds 2 } } if (-not $S.Suppress -and $ctx) { $ctx.State = 'AdminDown'; $ctx.Diag = 7 $pkt = New-BfdSessionPacket $ctx $c $null $null = $tx.Send($pkt, $pkt.Length, $remote) } } catch { $S.LastError = $_.Exception.Message } finally { if ($rx) { $rx.Dispose() } if ($tx) { $tx.Dispose() } if ($echoRx) { $echoRx.Dispose() } $S.State = 'Stopped' $S.Ready = $true } } |