package client import ( "net" "sync" "testing" "time" ) // stalledHub accepts connections and then says nothing: it never answers the // Rekey frame with SessionReady, and never closes. This models a hub with a // wedged event loop, or a load balancer accepting on behalf of a dead backend. func stalledHub(t *testing.T) string { t.Helper() ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } var mu sync.Mutex var held []net.Conn t.Cleanup(func() { _ = ln.Close() mu.Lock() for _, c := range held { _ = c.Close() } mu.Unlock() }) go func() { for { c, err := ln.Accept() if err != nil { return } mu.Lock() held = append(held, c) mu.Unlock() } }() return ln.Addr().String() } // TestDialDoesNotWedgeOnStalledHub is the regression guard for the worst // failure mode found in the stability audit: Dial used to share a single // in-flight handshake, and the handshake read had no deadline. One // unresponsive hub therefore parked every present and future allocation // forever. 1:1 dials independently, but each must still fail on its own // HandshakeTimeout rather than block the other. func TestDialDoesNotWedgeOnStalledHub(t *testing.T) { c := New(&Config{ Server: stalledHub(t), PSK: "pool-test", MaxTunnels: 8, PingIntervalMs: 20000, Mappings: []Mapping{{Pattern: "mc.local", Destination: "127.0.0.1:1"}}, }) done := make(chan error, 2) go func() { _, err := c.pool.Dial(); done <- err }() time.Sleep(200 * time.Millisecond) // let the first caller get into the dial go func() { _, err := c.pool.Dial(); done <- err }() // Both must give up on their own; neither may be stuck behind the other. limit := time.After(HandshakeTimeout + 15*time.Second) for i := 0; i < 2; i++ { select { case err := <-done: if err == nil { t.Fatal("Dial succeeded against a hub that never answers") } case <-limit: t.Fatalf("Dial #%d never returned: a stalled hub wedged the other caller", i+1) } } } // TestDialRespectsMaxTunnels pins the concurrency cap: once live+dialing // equals maxTunnels, further Dial calls fail immediately rather than stacking. func TestDialRespectsMaxTunnels(t *testing.T) { p := newWorkerPool(&Client{}, 2) p.conns[&WorkerConn{id: 1}] = struct{}{} p.conns[&WorkerConn{id: 2}] = struct{}{} if _, err := p.Dial(); err != errTooManyTunnels { t.Fatalf("Dial at cap: got %v, want %v", err, errTooManyTunnels) } }