mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-28 07:19:08 +01:00
[proxy] IPv6 in-place apply and TCP accept-loop hardening on netstack listeners
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user