mirror of
https://github.com/soypat/lneto.git
synced 2026-08-31 11:59: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:
@@ -53,6 +53,10 @@ type StackAsync struct {
|
||||
lookup dns.Message
|
||||
dnssv netip.Addr
|
||||
|
||||
// ephPort drives sequential ephemeral-port allocation (see
|
||||
// [StackAsync.ephemeralPort]); zero means not yet seeded.
|
||||
ephPort uint32
|
||||
|
||||
ntpUDP internet.StackUDPPort
|
||||
ntp ntp.Client
|
||||
|
||||
@@ -352,6 +356,23 @@ func (s *StackAsync) Prand32() (randval uint32) {
|
||||
return randval
|
||||
}
|
||||
|
||||
// ephemeralPort returns the next port of the IANA dynamic range (49152-65535,
|
||||
// RFC 6335 §6), allocated sequentially from a random per-stack start so a port is
|
||||
// revisited only after the full 16384-port cycle. Random selection instead reuses
|
||||
// a recent port at birthday-paradox rates, and a reused 4-tuple can collide with
|
||||
// state the previous conversation left behind (a TIME-WAIT, a NAT flow entry)
|
||||
// which swallows the new SYN.
|
||||
func (s *StackAsync) ephemeralPort() uint16 {
|
||||
s.mu.Lock()
|
||||
if s.ephPort == 0 {
|
||||
s.ephPort = s.prand32()%16384 | 1
|
||||
}
|
||||
port := 49152 + s.ephPort%16384
|
||||
s.ephPort++
|
||||
s.mu.Unlock()
|
||||
return uint16(port)
|
||||
}
|
||||
|
||||
func (s *StackAsync) prand32() uint32 {
|
||||
/* Algorithm "xor" from p. 4 of Marsaglia, "Xorshift RNGs" */
|
||||
seed := internal.Prand32(s.prng)
|
||||
|
||||
Reference in New Issue
Block a user