mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-20 21:59:07 +02:00
86 lines
2.7 KiB
Go
86 lines
2.7 KiB
Go
package tcp
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// gvisorInvalidEndpointMsg is the canonical text gVisor netstack returns
|
|
// when Accept() is called on a listener whose underlying endpoint has
|
|
// been destroyed (peer rekey, embedded-client reset, account churn).
|
|
// There is no exported sentinel from gvisor.dev/gvisor/pkg/tcpip that
|
|
// survives gonet's *net.OpError wrapping in a way errors.Is can match,
|
|
// so we fall back to a string check. Stable across the gVisor versions
|
|
// netbird pins.
|
|
const gvisorInvalidEndpointMsg = "endpoint is in invalid state"
|
|
|
|
// IsClosedListenerErr reports whether err signals that an accept loop
|
|
// should exit because the underlying listener can no longer serve
|
|
// connections. It recognises:
|
|
//
|
|
// - net.ErrClosed for stdlib listeners (Listener.Close was called).
|
|
// - gVisor's "endpoint is in invalid state" for netstack-backed
|
|
// listeners whose endpoint was destroyed out from under them
|
|
// (typically when a per-account WireGuard netstack is reset without
|
|
// also tearing the listener entry down).
|
|
//
|
|
// Without the gVisor branch an accept loop on a netstack listener spins
|
|
// CPU-hot forever after the endpoint dies, because Accept never blocks
|
|
// again and the error neither matches net.ErrClosed nor cancels ctx.
|
|
func IsClosedListenerErr(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
if errors.Is(err, net.ErrClosed) {
|
|
return true
|
|
}
|
|
return strings.Contains(err.Error(), gvisorInvalidEndpointMsg)
|
|
}
|
|
|
|
// AcceptBackoff implements the exponential backoff used by
|
|
// net/http.Server.Serve for transient Accept errors. Without it a loop
|
|
// hitting a sticky unknown error burns a full CPU core. The zero value
|
|
// is ready to use; call Reset after a successful Accept.
|
|
type AcceptBackoff struct {
|
|
delay time.Duration
|
|
}
|
|
|
|
// minAcceptDelay / maxAcceptDelay mirror the stdlib defaults
|
|
// (net/http.Server.Serve) and keep us well below 1 log line per second
|
|
// per orphaned listener.
|
|
const (
|
|
minAcceptDelay = 5 * time.Millisecond
|
|
maxAcceptDelay = time.Second
|
|
)
|
|
|
|
// Backoff waits the next exponential delay (5ms doubling up to 1s) and
|
|
// returns true when the wait completed. Returns false if ctx fired
|
|
// during the wait — callers should treat that as "exit the loop".
|
|
func (b *AcceptBackoff) Backoff(ctx context.Context) bool {
|
|
b.advance()
|
|
select {
|
|
case <-ctx.Done():
|
|
return false
|
|
case <-time.After(b.delay):
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Reset clears the accumulated delay so the next failure starts at the
|
|
// minimum delay again. Call after a successful Accept.
|
|
func (b *AcceptBackoff) Reset() { b.delay = 0 }
|
|
|
|
func (b *AcceptBackoff) advance() {
|
|
if b.delay == 0 {
|
|
b.delay = minAcceptDelay
|
|
} else {
|
|
b.delay *= 2
|
|
}
|
|
if b.delay > maxAcceptDelay {
|
|
b.delay = maxAcceptDelay
|
|
}
|
|
}
|