diff --git a/internal/netaddr/netaddr.go b/internal/netaddr/netaddr.go index 11e1403..9dd2c74 100644 --- a/internal/netaddr/netaddr.go +++ b/internal/netaddr/netaddr.go @@ -188,6 +188,8 @@ func (l *Listener) Accept() (net.Conn, error) { case err := <-l.errc: l.errc <- err return nil, err + case <-l.done: + return nil, net.ErrClosed } } diff --git a/internal/netaddr/netaddr_test.go b/internal/netaddr/netaddr_test.go index 273e0bb..2358a44 100644 --- a/internal/netaddr/netaddr_test.go +++ b/internal/netaddr/netaddr_test.go @@ -184,6 +184,37 @@ func TestTCPListenRequiresToken(t *testing.T) { } } +func TestTCPAcceptInFlightReturnsWhenListenerCloses(t *testing.T) { + ln, addr := listenLoopback(t, "s3cret") + + accepted := make(chan error, 1) + go func() { + _, err := ln.Accept() + accepted <- err + }() + + // Make the underlying accept loop observable: a silent raw peer is accepted + // by TCP and held in the concurrent handshake, while the public Accept is + // still waiting for an authorised connection. + peer, err := net.Dial("tcp", addr.Address) + if err != nil { + t.Fatalf("dial silent peer: %v", err) + } + defer peer.Close() + + if err := ln.Close(); err != nil { + t.Fatalf("close: %v", err) + } + select { + case err := <-accepted: + if !errors.Is(err, net.ErrClosed) { + t.Fatalf("Accept error = %v, want net.ErrClosed", err) + } + case <-time.After(time.Second): + t.Fatal("in-flight Accept remained blocked after Close") + } +} + func listenLoopback(t *testing.T, token string) (*Listener, Addr) { t.Helper() ln, err := Listen(Addr{Network: "tcp", Address: "127.0.0.1:0", Token: token})