lx: rebind socket on handshake give-up (sing-box-lx SPEC 041 self-heal)
After ~90s of unanswered handshake initiations (the give-up branch of expiredRetransmitHandshake) the socket's 5-tuple is proven dead (expired NAT mapping / poisoned DPI flow entry after device sleep) and upstream retries into it forever; only a manual reconnect healed the peer. Reopen the bind once per give-up cycle (fresh ephemeral port unless the user pinned listen_port), then re-initiate immediately. Debounced via CAS; no timers or goroutines while healthy; a rebind racing Down()/Close() degrades to a no-op inside BindUpdate. Red/green e2e + unit tests.
This commit is contained in:
parent
37bc7b9f55
commit
c4e0bcf768
4 changed files with 330 additions and 0 deletions
|
|
@ -122,6 +122,23 @@ type Device struct {
|
|||
}
|
||||
|
||||
ipackets [5]*obfChain
|
||||
|
||||
// lx: SPEC 041 — passive self-heal on handshake give-up. When a peer's
|
||||
// handshake retry cycle exhausts (the give-up branch of
|
||||
// expiredRetransmitHandshake), the device reopens its bind once — with a
|
||||
// fresh ephemeral port when freshPort is set — and immediately
|
||||
// re-initiates. Heals dead per-flow path state (an expired NAT mapping or
|
||||
// a poisoned DPI flow entry) that otherwise pins every retry to the same
|
||||
// dead 5-tuple until a manual reconnect. Zero cost while healthy: no
|
||||
// timers, no goroutines — the trigger is the existing give-up event,
|
||||
// which only fires under traffic demand after ~90s of unanswered
|
||||
// initiations. Enabled by default; sing-box decides freshPort from
|
||||
// whether the user pinned listen_port.
|
||||
giveUpRebind struct {
|
||||
enabled atomic.Bool
|
||||
freshPort atomic.Bool
|
||||
last atomic.Int64 // unix seconds of the last rebind (debounce)
|
||||
}
|
||||
}
|
||||
|
||||
// deviceState represents the state of a Device.
|
||||
|
|
@ -326,6 +343,7 @@ func (device *Device) SetPrivateKey(sk NoisePrivateKey) error {
|
|||
func NewDevice(ctx context.Context, tunDevice tun.Device, bind conn.Bind, logger *Logger, workers int) *Device {
|
||||
device := new(Device)
|
||||
device.pauseManager = service.FromContext[pause.Manager](ctx)
|
||||
device.giveUpRebind.enabled.Store(true) // lx: SPEC 041 — self-heal on by default
|
||||
device.state.state.Store(uint32(deviceStateDown))
|
||||
device.closed = make(chan struct{})
|
||||
device.log = logger
|
||||
|
|
@ -785,6 +803,55 @@ func (device *Device) BindUpdate() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// lx: SPEC 041 — configure the handshake give-up self-heal (see the
|
||||
// giveUpRebind field comment). freshPort must be false when the user pinned
|
||||
// an explicit listen_port: the pinned port is preserved, at the cost of the
|
||||
// rebind not changing the 5-tuple.
|
||||
func (device *Device) SetGiveUpRebind(enabled, freshPort bool) {
|
||||
device.giveUpRebind.enabled.Store(enabled)
|
||||
device.giveUpRebind.freshPort.Store(freshPort)
|
||||
}
|
||||
|
||||
// lx: SPEC 041 — invoked from the give-up branch of
|
||||
// expiredRetransmitHandshake: ~90s of initiations went unanswered, so the
|
||||
// current socket's 5-tuple is proven dead. Reopen the bind (fresh ephemeral
|
||||
// port when allowed) and kick a new handshake cycle immediately. Runs the
|
||||
// heavy part in a goroutine so the timer callback never blocks on
|
||||
// BindUpdate's worker drain. Debounced to one rebind per RekeyAttemptTime
|
||||
// per device (CAS on `last` settles concurrent multi-peer give-ups). On a
|
||||
// down or closed device BindUpdate does not reopen the socket, so a rebind
|
||||
// racing idle-suspend (SPEC 020) or Close degrades to a no-op.
|
||||
func (device *Device) handleHandshakeGiveUp(peer *Peer) {
|
||||
if !device.giveUpRebind.enabled.Load() {
|
||||
return
|
||||
}
|
||||
if device.isClosed() {
|
||||
return
|
||||
}
|
||||
now := time.Now().Unix()
|
||||
last := device.giveUpRebind.last.Load()
|
||||
if now-last < int64(RekeyAttemptTime/time.Second) {
|
||||
return
|
||||
}
|
||||
if !device.giveUpRebind.last.CompareAndSwap(last, now) {
|
||||
return
|
||||
}
|
||||
fresh := device.giveUpRebind.freshPort.Load()
|
||||
go func() {
|
||||
if fresh {
|
||||
device.net.Lock()
|
||||
device.net.port = 0
|
||||
device.net.Unlock()
|
||||
}
|
||||
if err := device.BindUpdate(); err != nil {
|
||||
device.log.Errorf("%v - Failed to rebind after handshake give-up: %v", peer, err)
|
||||
return
|
||||
}
|
||||
device.log.Verbosef("%v - Rebound socket after handshake give-up (fresh port=%v)", peer, fresh)
|
||||
peer.SendHandshakeInitiation(false)
|
||||
}()
|
||||
}
|
||||
|
||||
func (device *Device) BindClose() error {
|
||||
device.net.Lock()
|
||||
err := closeBindLocked(device)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue