From 8fe9e3c3fef0049df82281a62afe35dda516dfe2 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Fri, 18 Sep 2026 17:48:26 -0400 Subject: fix: close combat ghost, admin traversal, login throttle, journal double-pay, and quality/economy gaps --- internal/net/server.go | 87 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 6 deletions(-) (limited to 'internal/net/server.go') diff --git a/internal/net/server.go b/internal/net/server.go index f56aebb..c818eef 100644 --- a/internal/net/server.go +++ b/internal/net/server.go @@ -141,6 +141,16 @@ func (sess *Session) AuthName() string { return "" } +// RemoteAddr returns the client's network address when the transport can +// report one, or "" for synthetic/test connections. It is immutable for the +// life of the connection, so it is safe to read off the owner goroutine. +func (sess *Session) RemoteAddr() string { + if rc, ok := sess.Conn.(interface{ RemoteAddr() string }); ok { + return rc.RemoteAddr() + } + return "" +} + // sessionSeq is the source of Session.seq values. It is incremented under the // adding Hub's mutex, so no extra atomicity is needed for correctness; atomic // access keeps the package-level default counter safe if Hub.Add is ever @@ -172,6 +182,11 @@ type Server struct { // admits against maxConnections so a pre-auth connection flood cannot // hold an unbounded number of reader goroutines, buffers, and outboxes. conns atomic.Int64 + // connsByIP bounds concurrent connections from one client address so a + // single source cannot exhaust the global cap with idle pre-auth + // sockets. Guarded by connsByIPMu. + connsByIPMu sync.Mutex + connsByIP map[string]int // idleTimeout returns the read timeout armed before each message. It is a // field so tests can drive the idle-drop path without waiting out the // production windows; nil means sessionIdleTimeout. @@ -204,6 +219,9 @@ const ( maxConnections = 1000 loginReadTimeout = 2 * time.Minute idleReadTimeout = 30 * time.Minute + // maxConnectionsPerIP stops one source from exhausting the global + // maxConnections with idle pre-auth sockets. + maxConnectionsPerIP = 8 ) // admitConnection reserves a connection slot, returning false when the server @@ -220,6 +238,52 @@ func (s *Server) releaseConnection() { s.conns.Add(-1) } +// admitIP reserves a per-address connection slot, returning false when the +// address already holds maxConnectionsPerIP. An empty address (synthetic +// connections) is always admitted. +func (s *Server) admitIP(addr string) bool { + ip := remoteHost(addr) + if ip == "" { + return true + } + s.connsByIPMu.Lock() + defer s.connsByIPMu.Unlock() + if s.connsByIP == nil { + s.connsByIP = make(map[string]int) + } + if s.connsByIP[ip] >= maxConnectionsPerIP { + return false + } + s.connsByIP[ip]++ + return true +} + +func (s *Server) releaseIP(addr string) { + ip := remoteHost(addr) + if ip == "" { + return + } + s.connsByIPMu.Lock() + defer s.connsByIPMu.Unlock() + if s.connsByIP[ip] <= 1 { + delete(s.connsByIP, ip) + return + } + s.connsByIP[ip]-- +} + +// remoteHost strips the port from a network address, returning it verbatim +// when it has none. +func remoteHost(addr string) string { + if addr == "" { + return "" + } + if host, _, err := net.SplitHostPort(addr); err == nil { + return host + } + return addr +} + // sessionIdleTimeout returns the read timeout to arm for this session. func (s *Server) sessionIdleTimeout(sess *Session) time.Duration { if s.idleTimeout != nil { @@ -531,8 +595,10 @@ func NewServer(cfg *config.Config) (*Server, error) { if cfg.HTTP.Enabled { s.httpServer = &http.Server{ - Addr: fmt.Sprintf(":%d", cfg.HTTP.Port), - Handler: newHTTPMux(s), + Addr: fmt.Sprintf(":%d", cfg.HTTP.Port), + Handler: newHTTPMux(s), + ReadHeaderTimeout: 10 * time.Second, + IdleTimeout: 60 * time.Second, } } @@ -546,8 +612,10 @@ func NewServer(cfg *config.Config) (*Server, error) { MinVersion: tls.VersionTLS12, } s.httpsServer = &http.Server{ - Addr: fmt.Sprintf(":%d", cfg.HTTPS.Port), - Handler: newHTTPMux(s), + Addr: fmt.Sprintf(":%d", cfg.HTTPS.Port), + Handler: newHTTPMux(s), + ReadHeaderTimeout: 10 * time.Second, + IdleTimeout: 60 * time.Second, } } @@ -785,6 +853,13 @@ func (s *Server) handleSession(sess *Session, handler func(*Session, string)) { } defer s.releaseConnection() + remoteAddr := sess.RemoteAddr() + if !s.admitIP(remoteAddr) { + sess.Write("Too many connections from your address. Please try again later.\r\n") + return + } + defer s.releaseIP(remoteAddr) + sess.Write("\033[2J\033[H") sess.Write(WelcomeBanner()) @@ -799,8 +874,8 @@ func (s *Server) handleSession(sess *Session, handler func(*Session, string)) { log.Printf("session read error: %v", err) return } - if len(line) > 1024 { - line = line[:1024] + if len(line) > maxLineLength { + line = truncateLine(line, maxLineLength) } handler(sess, line) } -- cgit v1.2.3