netaddr: greet a tcp peer off the accept path (V-581)

A peer that connected and then said nothing froze the whole seam. The token
handshake ran inline in Listener.Accept, so the five seconds of handshakeTimeout
the silent peer was owed were five seconds no other connection could be
accepted. One unauthenticated stranger holding a socket open was a denial of
service on every daemon behind a tcp seam, which is the path V-515 is about to
put mavwaked and mavenclient on.

Accept now takes authorized connections off a channel. A background loop pulls
from the wrapped listener and greets each connection in its own goroutine, so a
slow greeting costs only its own connection. Listener.Close releases anything
still waiting to be handed over.

A unix seam delegates straight to the wrapped listener and grows no machinery,
because it has no handshake to run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 03:23:49 +04:00
parent 69270f4cfb
commit 93c08f9de1
2 changed files with 105 additions and 11 deletions
+69 -11
View File
@@ -31,6 +31,7 @@ import (
"os"
"path/filepath"
"strings"
"sync"
"time"
"golang.org/x/sys/unix"
@@ -154,31 +155,78 @@ func clientHandshake(c net.Conn, token string) error {
// Listener wraps a net.Listener so Accept performs the token check for a tcp
// seam. A connection that fails the check is closed and never surfaces, so
// the protocol above this layer only ever sees authorized peers.
//
// A unix seam takes none of that machinery: Accept delegates straight to the
// wrapped listener, which is what it did before the token existed.
type Listener struct {
net.Listener
addr Addr
start sync.Once
closeOnce sync.Once
conns chan net.Conn
errc chan error // buffered 1, re-armed so every Accept sees the error
done chan struct{}
}
// Accept returns the next authorized connection. Unauthorized peers are
// dropped and Accept keeps waiting: a bad token is a rejected stranger, not a
// reason to stop serving.
//
// Each tcp handshake runs in its own goroutine rather than inline here. A peer
// that connects and then says nothing holds its greeting open for
// handshakeTimeout, and inline that peer stalls every other connection for
// five seconds — one silent stranger was enough to freeze the seam.
func (l *Listener) Accept() (net.Conn, error) {
if l.addr.IsUnix() {
return l.Listener.Accept()
}
l.start.Do(func() { go l.acceptLoop() })
select {
case c := <-l.conns:
return c, nil
case err := <-l.errc:
l.errc <- err
return nil, err
}
}
// acceptLoop takes connections off the wrapped listener and greets each one
// concurrently. It ends on the first listener error, which every later Accept
// then reports.
func (l *Listener) acceptLoop() {
for {
c, err := l.Listener.Accept()
if err != nil {
return nil, err
select {
case l.errc <- err:
case <-l.done:
}
return
}
if l.addr.IsUnix() {
return c, nil
}
if err := serverHandshake(c, l.addr.Token); err != nil {
_ = c.Close()
continue
}
return c, nil
go l.greet(c)
}
}
func (l *Listener) greet(c net.Conn) {
if err := serverHandshake(c, l.addr.Token); err != nil {
_ = c.Close()
return
}
select {
case l.conns <- c:
case <-l.done:
_ = c.Close()
}
}
// Close stops the listener and releases any connection still waiting to be
// handed to Accept.
func (l *Listener) Close() error {
l.closeOnce.Do(func() { close(l.done) })
return l.Listener.Close()
}
// Addr reports the parsed seam address this listener was built from.
func (l *Listener) SeamAddr() Addr { return l.addr }
@@ -236,7 +284,7 @@ func Listen(a Addr) (*Listener, error) {
if err != nil {
return nil, err
}
return &Listener{Listener: ln, addr: a}, nil
return wrap(ln, a), nil
}
if a.Token == "" {
return nil, fmt.Errorf("netaddr: listen %s: tcp seam requires a token", a)
@@ -245,7 +293,17 @@ func Listen(a Addr) (*Listener, error) {
if err != nil {
return nil, fmt.Errorf("netaddr: listen %s: %w", a, err)
}
return &Listener{Listener: ln, addr: a}, nil
return wrap(ln, a), nil
}
func wrap(ln net.Listener, a Addr) *Listener {
return &Listener{
Listener: ln,
addr: a,
conns: make(chan net.Conn),
errc: make(chan error, 1),
done: make(chan struct{}),
}
}
func listenUnix(path string) (net.Listener, error) {
+36
View File
@@ -5,6 +5,7 @@ import (
"net"
"path/filepath"
"testing"
"time"
)
// A scheme-less address must stay unix. Every deploy in the tree writes a bare
@@ -140,6 +141,41 @@ func TestTCPUngreetedPeerDoesNotKillTheListener(t *testing.T) {
}
}
// A peer that connects and never speaks must not hold the seam. The greeting
// it owes is bounded by handshakeTimeout, so serving it on the accept path
// costs every later connection those five seconds.
func TestTCPSilentPeerDoesNotStallTheSeam(t *testing.T) {
ln, addr := listenLoopback(t, "s3cret")
defer ln.Close()
go echoOnce(ln)
mute, err := net.Dial("tcp", addr.Address)
if err != nil {
t.Fatalf("mute dial: %v", err)
}
defer mute.Close()
done := make(chan string, 1)
go func() {
c, err := Dial(addr)
if err != nil {
done <- "dial: " + err.Error()
return
}
defer c.Close()
done <- roundTrip(t, c, "still here")
}()
select {
case got := <-done:
if got != "still here" {
t.Fatalf("got %q", got)
}
case <-time.After(handshakeTimeout / 2):
t.Fatal("a silent peer stalled the listener")
}
}
// A tcp seam with no token is a misconfiguration, and it must fail at bind
// rather than serve the owner's turns to anyone who connects.
func TestTCPListenRequiresToken(t *testing.T) {