1 package testutil 2 3 import ( 4 "context" 5 "net" 6 "sync" 7 "testing" 8 ) 9 10 // BlackholeListener starts a TCP listener that accepts connections but 11 // never reads, writes, or closes them. Each accepted connection runs in 12 // its own goroutine that blocks until the listener is shut down. 13 // 14 // This simulates a half-open TCP connection: the client sees an 15 // established TCP session, but no application-layer response ever arrives. 16 // The kernel keeps the connection open until the local read deadline (if 17 // any) fires or the peer gives up. Callers pair this with a context-bound 18 // round-trip to convert such stalls into bounded client-side failures. 19 // 20 // The address is registered for cleanup via t.Cleanup, so a single 21 // BlackholeListener(t) call is leak-free under normal test runs. The 22 // returned close func is idempotent and may also be invoked explicitly by 23 // the test for early teardown. 24 // 25 // Concurrency: the accept loop and any number of accepted connections run 26 // concurrently. Accepted goroutines block until ctx is cancelled; the close 27 // func waits for them via wg.Wait before returning. 28 func BlackholeListener(t *testing.T) (string, func()) { 29 t.Helper() 30 31 ln, err := net.Listen("tcp", "127.0.0.1:0") 32 if err != nil { 33 t.Fatalf("blackhole: listen: %v", err) 34 } 35 36 ctx, cancel := context.WithCancel(context.Background()) 37 var wg sync.WaitGroup 38 39 // Accept loop. Stops as soon as the listener is closed. 40 go func() { 41 for { 42 conn, err := ln.Accept() 43 if err != nil { 44 return 45 } 46 wg.Add(1) 47 go func(net.Conn) { 48 defer wg.Done() 49 // Hold the connection until the listener is closed. 50 // Intentionally no read/write/close — we are simulating 51 // a peer that silently dropped packets. 52 <-ctx.Done() 53 }(conn) 54 } 55 }() 56 57 c := func() { 58 cancel() 59 _ = ln.Close() 60 wg.Wait() 61 } 62 t.Cleanup(c) 63 return ln.Addr().String(), c 64 } 65