fix(web): bound the panel's open connections and share asset gzip

The engine-wide gzip middleware gave every in-flight request its own
compressor of about 1 MiB, and the public /assets bundles let an
anonymous client keep a handler blocked in Write until WriteTimeout, so
a thousand stalled downloads held a gigabyte. The listener itself took
any number of connections, each holding buffers and a goroutine before
any login.

The bundles are now served from a gzip copy made once per file, with
the assets prefix left out of the per-request middleware, and the panel
listener closes a new connection once 2048 are open or its source (an
IPv4 address or IPv6 /64) holds 256; a loopback reverse proxy counts
only toward the total.
This commit is contained in:
MHSanaei
2026-11-01 01:01:38 +01:00
parent 1c748adadf
commit 3526a65f5d
7 changed files with 436 additions and 11 deletions
+98
View File
@@ -0,0 +1,98 @@
package network
import (
"net"
"net/netip"
"sync"
)
// Every open panel connection holds buffers and a goroutine before any login, so their
// number is bounded overall and per source; a loopback reverse proxy counts only overall.
const (
MaxPanelConns = 2048
MaxPanelConnsPerSource = 256
)
type limitListener struct {
net.Listener
total, perSource int
mu sync.Mutex
open int
bySource map[netip.Prefix]int
}
// NewLimitListener closes an accepted connection at once when the total or its source
// (an IPv4 address or IPv6 /64) is at its limit, rather than leaving it in the backlog.
func NewLimitListener(l net.Listener, total, perSource int) net.Listener {
return &limitListener{Listener: l, total: total, perSource: perSource, bySource: map[netip.Prefix]int{}}
}
func (l *limitListener) Accept() (net.Conn, error) {
for {
c, err := l.Listener.Accept()
if err != nil {
return nil, err
}
source, counted := connSource(c.RemoteAddr())
if !l.acquire(source, counted) {
_ = c.Close()
continue
}
return &limitConn{Conn: c, release: func() { l.release(source, counted) }}, nil
}
}
func (l *limitListener) acquire(source netip.Prefix, counted bool) bool {
l.mu.Lock()
defer l.mu.Unlock()
if l.open >= l.total || (counted && l.bySource[source] >= l.perSource) {
return false
}
l.open++
if counted {
l.bySource[source]++
}
return true
}
func (l *limitListener) release(source netip.Prefix, counted bool) {
l.mu.Lock()
defer l.mu.Unlock()
l.open--
if counted {
if l.bySource[source]--; l.bySource[source] <= 0 {
delete(l.bySource, source)
}
}
}
// connSource is the per-source bucket of addr; loopback and unparseable sources have none.
func connSource(addr net.Addr) (netip.Prefix, bool) {
ap, err := netip.ParseAddrPort(addr.String())
if err != nil {
return netip.Prefix{}, false
}
ip := ap.Addr().Unmap()
if ip.IsLoopback() {
return netip.Prefix{}, false
}
bits := 32
if ip.Is6() {
bits = 64
}
prefix, err := ip.Prefix(bits)
return prefix, err == nil
}
type limitConn struct {
net.Conn
once sync.Once
release func()
}
func (c *limitConn) Close() error {
err := c.Conn.Close()
c.once.Do(c.release)
return err
}
+77
View File
@@ -109,3 +109,80 @@ func TestProductionHTTPServersUseServeHTTPWrapper(t *testing.T) {
t.Fatalf("scan production Go files: %v", err)
}
}
// queueListener hands out conns from a channel, each claiming the given remote address.
type queueListener struct{ conns chan net.Conn }
func (l queueListener) Accept() (net.Conn, error) {
c, ok := <-l.conns
if !ok {
return nil, net.ErrClosed
}
return c, nil
}
func (queueListener) Close() error { return nil }
func (queueListener) Addr() net.Addr { return testAddr("queue") }
type remoteConn struct {
net.Conn
remote net.Addr
closed chan struct{}
}
func (c *remoteConn) RemoteAddr() net.Addr { return c.remote }
func (c *remoteConn) Close() error { close(c.closed); return nil }
func newRemoteConn(addr string) *remoteConn {
return &remoteConn{remote: &net.TCPAddr{IP: net.ParseIP(addr), Port: 40000}, closed: make(chan struct{})}
}
// Every panel connection holds buffers before any login, so one source opening
// thousands of them used to take the whole panel's memory.
func TestLimitListenerCapsConnectionsPerSourceAndInTotal(t *testing.T) {
queue := queueListener{conns: make(chan net.Conn, 16)}
l := NewLimitListener(queue, 4, 2)
accept := func(c *remoteConn) net.Conn {
t.Helper()
queue.conns <- c
accepted, err := l.Accept()
if err != nil {
t.Fatalf("Accept: %v", err)
}
return accepted
}
first := accept(newRemoteConn("203.0.113.1"))
accept(newRemoteConn("203.0.113.1"))
refused := newRemoteConn("203.0.113.1")
queue.conns <- refused
other := accept(newRemoteConn("2001:db8::1"))
select {
case <-refused.closed:
default:
t.Fatal("a source past its share was accepted")
}
if other.RemoteAddr().String() != "[2001:db8::1]:40000" {
t.Fatalf("accepted %s, want the other source", other.RemoteAddr())
}
// Loopback is exempt from the per-source share but counts toward the total.
accept(newRemoteConn("127.0.0.1"))
overTotal := newRemoteConn("127.0.0.1")
queue.conns <- overTotal
next := make(chan net.Conn, 1)
go func() {
c, _ := l.Accept()
next <- c
}()
select {
case <-overTotal.closed:
case c := <-next:
t.Fatalf("a connection past the total was accepted: %s", c.RemoteAddr())
}
_ = first.Close()
queue.conns <- newRemoteConn("203.0.113.1")
again := <-next
if again.RemoteAddr().String() != "203.0.113.1:40000" {
t.Fatalf("a closed connection did not free its slot: accepted %s", again.RemoteAddr())
}
}