mirror of
https://github.com/soypat/lneto.git
synced 2026-09-01 04:19:05 +00:00
fix(xnet): allocate ephemeral ports sequentially, not randomly (#180)
Ephemeral ports were drawn at random from the 16384-port dynamic range on every dial. Under connection-per-request churn the birthday paradox reuses a recently-released port after only a few dozen dials (~1% per dial at 200 outstanding-in-teardown), and a reused 4-tuple lands on whatever state the previous conversation left along the path, such as the peer's TIME-WAIT socket or a NAT's flow-table entry, which silently swallows the new SYN. Measured end to end: an HTTP client with keep-alives off against a macOS peer hit a dead dial after ~286 connections and stayed dark for ~30 seconds, exactly one 2MSL TIME-WAIT expiry. Allocate sequentially from a per-stack random starting offset instead: a port is only revisited after the full 16384-port cycle, and the random start keeps a rebooted node off the ports its previous life just used. TestEphemeralPortSequence pins the full-cycle-no-reuse property. Co-authored-by: Derek den Haas <d.haas@directcode.com>
This commit is contained in:
+3
-2
@@ -102,8 +102,9 @@ func (s StackGo) SocketNetip(ctx context.Context, network string, family, sotype
|
||||
isDial := raddr.IsValid() && !raddr.Addr().IsUnspecified()
|
||||
if laddr.Port() == 0 {
|
||||
// Auto-assign an ephemeral port for both outbound dials and for listeners
|
||||
// that did not request a fixed port.
|
||||
laddr = netip.AddrPortFrom(laddr.Addr(), uint16(49152+s.blk.async.Prand32()%16384))
|
||||
// that did not request a fixed port. Sequential, not random: see
|
||||
// [StackAsync.ephemeralPort] for why random selection breaks dial churn.
|
||||
laddr = netip.AddrPortFrom(laddr.Addr(), s.blk.async.ephemeralPort())
|
||||
}
|
||||
if laddr.Addr().IsUnspecified() {
|
||||
// Fill in the stack's configured address for the requested family.
|
||||
|
||||
Reference in New Issue
Block a user