diff --git a/server/go/server/server.go b/server/go/server/server.go index 80c6ab1..317424d 100644 --- a/server/go/server/server.go +++ b/server/go/server/server.go @@ -23,6 +23,15 @@ type Config struct { ReadTimeout time.Duration WriteTimeout time.Duration + // MaxBufferedBytes is a hard cap on unparsed inbound bytes buffered per + // connection. The protocol's own length field is validated to 50 MiB, but + // a peer that sends bytes that never assemble into a complete packet (e.g. + // an HTTP-mask envelope without its terminating CRLF CRLF, which Parse() + // waits on indefinitely) could otherwise grow the buffer without bound. + // Default leaves slack for the largest legitimate packet plus its mask + // envelope and a read chunk. + MaxBufferedBytes int + // Per-IP connection limits. The TCP listener (port 6543) is exposed // directly to the public internet, so a single host must not be able to // exhaust the global connection budget or flood new-connection attempts. @@ -43,6 +52,7 @@ func DefaultConfig() Config { KeepAliveTime: time.Minute * 5, ReadTimeout: time.Minute * 2, WriteTimeout: time.Second * 30, + MaxBufferedBytes: 50*1024*1024 + 64*1024, PerIPMaxConnections: 256, PerIPConnRateLimit: 120, PerIPConnRateWindow: time.Minute, @@ -399,6 +409,15 @@ func (s *Server) handleConnection(conn net.Conn) { // Write to input buffer _, _ = ctx.InBuffer.Write(buf[:n]) + // Hard cap on unparsed inbound bytes (memory-DoS guard). A peer + // that sends bytes that never assemble into a complete packet can + // otherwise grow this buffer without bound. + if ctx.InBuffer.Len() > s.config.MaxBufferedBytes { + s.logger.Warn("Inbound buffer exceeded %d bytes from ip=%s conn=%d — closing", + s.config.MaxBufferedBytes, ctx.GetPeerIP(), ctx.ID) + break + } + // Process received data s.processData(ctx) } diff --git a/server/go/server/server_test.go b/server/go/server/server_test.go index e7b8e0a..7a88855 100644 --- a/server/go/server/server_test.go +++ b/server/go/server/server_test.go @@ -1,6 +1,7 @@ package server import ( + "bytes" "net" "testing" "time" @@ -91,3 +92,41 @@ func TestPeerIP(t *testing.T) { } } } + +// A peer that sends bytes which never assemble into a complete packet (here an +// HTTP-mask envelope with no terminating CRLF CRLF) must be disconnected once +// the per-connection buffer cap is exceeded, rather than allowed to grow it +// without bound. +func TestHandleConnectionBufferCap(t *testing.T) { + cfg := DefaultConfig() + cfg.MaxBufferedBytes = 1024 // small cap for a fast, in-memory test + cfg.ReadTimeout = time.Second + + srv := New(cfg) + srv.running.Store(true) // bypass acceptLoop; exercise handleConnection directly + + client, serverConn := net.Pipe() + defer client.Close() + + done := make(chan struct{}) + go func() { + srv.handleConnection(serverConn) + close(done) + }() + + // "POST " prefix with no terminator, plus filler to exceed the cap. + payload := append([]byte("POST /x HTTP/1.1\r\n"), bytes.Repeat([]byte{'A'}, 4096)...) + writeErr := make(chan error, 1) + go func() { + _, err := client.Write(payload) + writeErr <- err + }() + + select { + case <-done: + // handleConnection returned because the cap was exceeded and it closed + // the connection. + case <-time.After(2 * time.Second): + t.Fatal("connection was not closed after exceeding the buffer cap") + } +}