93c08f9de1
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>
222 lines
5.5 KiB
Go
222 lines
5.5 KiB
Go
package netaddr
|
|
|
|
import (
|
|
"errors"
|
|
"net"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// A scheme-less address must stay unix. Every deploy in the tree writes a bare
|
|
// path, so this is the test that says the transport change costs them nothing.
|
|
func TestParseSchemelessIsUnix(t *testing.T) {
|
|
a, err := Parse("/run/maven/stt.sock")
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
if !a.IsUnix() {
|
|
t.Fatalf("want unix, got %q", a.Network)
|
|
}
|
|
if a.Address != "/run/maven/stt.sock" {
|
|
t.Fatalf("address = %q", a.Address)
|
|
}
|
|
if a.Token != "" {
|
|
t.Fatalf("unix seam carries a token: %q", a.Token)
|
|
}
|
|
}
|
|
|
|
func TestParse(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
net, addr, tk string
|
|
wantErr bool
|
|
}{
|
|
{in: "", net: "unix", addr: ""},
|
|
{in: "unix:///run/maven/core.sock", net: "unix", addr: "/run/maven/core.sock"},
|
|
{in: "tcp://workstation:9310", net: "tcp", addr: "workstation:9310"},
|
|
{in: "tcp://workstation:9310?token=hunter2", net: "tcp", addr: "workstation:9310", tk: "hunter2"},
|
|
{in: "tcp://", wantErr: true},
|
|
{in: "udp://workstation:9310", wantErr: true},
|
|
}
|
|
for _, c := range cases {
|
|
a, err := Parse(c.in)
|
|
if c.wantErr {
|
|
if err == nil {
|
|
t.Errorf("Parse(%q) = %v, want error", c.in, a)
|
|
}
|
|
continue
|
|
}
|
|
if err != nil {
|
|
t.Errorf("Parse(%q): %v", c.in, err)
|
|
continue
|
|
}
|
|
if a.Network != c.net || a.Address != c.addr || a.Token != c.tk {
|
|
t.Errorf("Parse(%q) = %+v, want %s/%s/%s", c.in, a, c.net, c.addr, c.tk)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The token must never reach a log line.
|
|
func TestStringHidesToken(t *testing.T) {
|
|
a := MustParse("tcp://workstation:9310?token=hunter2")
|
|
if got := a.String(); got != "tcp://workstation:9310" {
|
|
t.Fatalf("String() = %q", got)
|
|
}
|
|
}
|
|
|
|
// A unix seam must round-trip with no handshake in front of the payload: the
|
|
// first bytes the listener sees are the caller's, exactly as before.
|
|
func TestUnixRoundTripHasNoHandshake(t *testing.T) {
|
|
a := MustParse(filepath.Join(t.TempDir(), "s.sock"))
|
|
ln, err := Listen(a)
|
|
if err != nil {
|
|
t.Fatalf("listen: %v", err)
|
|
}
|
|
defer ln.Close()
|
|
go echoOnce(ln)
|
|
|
|
c, err := Dial(a)
|
|
if err != nil {
|
|
t.Fatalf("dial: %v", err)
|
|
}
|
|
defer c.Close()
|
|
if got := roundTrip(t, c, "hello"); got != "hello" {
|
|
t.Fatalf("got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestTCPRoundTripWithToken(t *testing.T) {
|
|
ln, addr := listenLoopback(t, "s3cret")
|
|
defer ln.Close()
|
|
go echoOnce(ln)
|
|
|
|
c, err := Dial(addr)
|
|
if err != nil {
|
|
t.Fatalf("dial: %v", err)
|
|
}
|
|
defer c.Close()
|
|
if got := roundTrip(t, c, "hello"); got != "hello" {
|
|
t.Fatalf("got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestTCPWrongTokenIsRejected(t *testing.T) {
|
|
ln, addr := listenLoopback(t, "s3cret")
|
|
defer ln.Close()
|
|
// Accept keeps waiting past the bad peer, so nothing here should ever
|
|
// reach the echo. A conn that does means the token was not checked.
|
|
go echoOnce(ln)
|
|
|
|
bad := addr
|
|
bad.Token = "wrong"
|
|
if _, err := Dial(bad); !errors.Is(err, ErrUnauthorized) {
|
|
t.Fatalf("dial with wrong token: err = %v, want ErrUnauthorized", err)
|
|
}
|
|
}
|
|
|
|
// A stranger that speaks the protocol instead of the greeting is dropped, and
|
|
// the listener stays up for the peer that follows it.
|
|
func TestTCPUngreetedPeerDoesNotKillTheListener(t *testing.T) {
|
|
ln, addr := listenLoopback(t, "s3cret")
|
|
defer ln.Close()
|
|
go echoOnce(ln)
|
|
|
|
raw, err := net.Dial("tcp", addr.Address)
|
|
if err != nil {
|
|
t.Fatalf("raw dial: %v", err)
|
|
}
|
|
if _, err := raw.Write([]byte("GET / HTTP/1.1\n")); err != nil {
|
|
t.Fatalf("raw write: %v", err)
|
|
}
|
|
raw.Close()
|
|
|
|
c, err := Dial(addr)
|
|
if err != nil {
|
|
t.Fatalf("dial after stranger: %v", err)
|
|
}
|
|
defer c.Close()
|
|
if got := roundTrip(t, c, "still here"); got != "still here" {
|
|
t.Fatalf("got %q", got)
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
if _, err := Listen(MustParse("tcp://127.0.0.1:0")); err == nil {
|
|
t.Fatal("listen on a tokenless tcp seam succeeded")
|
|
}
|
|
}
|
|
|
|
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})
|
|
if err != nil {
|
|
t.Fatalf("listen: %v", err)
|
|
}
|
|
return ln, Addr{Network: "tcp", Address: ln.Addr().String(), Token: token}
|
|
}
|
|
|
|
func echoOnce(ln *Listener) {
|
|
c, err := ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer c.Close()
|
|
buf := make([]byte, 256)
|
|
n, err := c.Read(buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
_, _ = c.Write(buf[:n])
|
|
}
|
|
|
|
func roundTrip(t *testing.T, c net.Conn, msg string) string {
|
|
t.Helper()
|
|
if _, err := c.Write([]byte(msg)); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
buf := make([]byte, 256)
|
|
n, err := c.Read(buf)
|
|
if err != nil {
|
|
t.Fatalf("read: %v", err)
|
|
}
|
|
return string(buf[:n])
|
|
}
|