From bba88324fa5a4413115039266740701713c14602 Mon Sep 17 00:00:00 2001 From: Morquin Date: Mon, 11 Aug 2025 21:54:53 +0200 Subject: [PATCH 01/11] Add reverse proxy IP header support for web server logging Added: - getClientIP helper function to extract real client IPs from proxy headers - X-Real-IP header parsing (higher priority) - X-Forwarded-For header parsing with comma-separated IP support - Security check to only trust headers from localhost connections Changed: - Web request logging to use getClientIP instead of r.RemoteAddr - Log output now shows real client IPs when behind trusted reverse proxy --- internal/web/web.go | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/internal/web/web.go b/internal/web/web.go index 0192756c..5eab3e57 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -42,6 +42,47 @@ type WebNav struct { Target string } +// getClientIP extracts the real client IP address from the request. +// It checks for X-Real-IP and X-Forwarded-For headers when the direct +// connection is from localhost (trusted proxy), otherwise returns the +// direct connection IP. +func getClientIP(r *http.Request) string { + // Get the direct connection IP + remoteAddr := r.RemoteAddr + + // Extract just the IP part (remove port) + host, _, err := net.SplitHostPort(remoteAddr) + if err != nil { + // If splitting fails, use the whole remoteAddr + host = remoteAddr + } + + // Only trust proxy headers if the connection is from localhost + if host == "127.0.0.1" || host == "::1" || host == "localhost" { + // Check X-Real-IP first (higher priority) + if realIP := r.Header.Get("X-Real-IP"); realIP != "" { + return realIP + } + + // Check X-Forwarded-For (may contain multiple IPs) + if forwardedFor := r.Header.Get("X-Forwarded-For"); forwardedFor != "" { + // X-Forwarded-For can contain comma-separated IPs + // The first one is the original client + ips := strings.Split(forwardedFor, ",") + if len(ips) > 0 { + // Trim any whitespace from the IP + clientIP := strings.TrimSpace(ips[0]) + if clientIP != "" { + return clientIP + } + } + } + } + + // Return the direct connection IP if no proxy headers or not from localhost + return host +} + type WebPlugin interface { NavLinks() map[string]string // Name=>Path pairs WebRequest(r *http.Request) (html string, templateData map[string]any, ok bool) // Get the first handler of a given request @@ -106,7 +147,7 @@ func serveTemplate(w http.ResponseWriter, r *http.Request) { } if !pageFound || len(fileBase) > 0 && fileBase[0] == '_' { - mudlog.Info("Web", "ip", r.RemoteAddr, "ref", r.Header.Get("Referer"), "file path", fullPath, "file extension", fileExt, "error", "Not found") + mudlog.Info("Web", "ip", getClientIP(r), "ref", r.Header.Get("Referer"), "file path", fullPath, "file extension", fileExt, "error", "Not found") fullPath = filepath.Join(httpRoot, `404.html`) fInfo, err = os.Stat(fullPath) @@ -122,7 +163,7 @@ func serveTemplate(w http.ResponseWriter, r *http.Request) { } // Log the request - mudlog.Info("Web", "ip", r.RemoteAddr, "ref", r.Header.Get("Referer"), "file path", fullPath, "file extension", fileExt, "file source", source, "size", fmt.Sprintf(`%.2fk`, float64(fSize)/1024)) + mudlog.Info("Web", "ip", getClientIP(r), "ref", r.Header.Get("Referer"), "file path", fullPath, "file extension", fileExt, "file source", source, "size", fmt.Sprintf(`%.2fk`, float64(fSize)/1024)) // For non-HTML files, serve them statically. if fileExt != ".html" { From 7cf81b450bb51bdede32e98bd4bca9948c5018a5 Mon Sep 17 00:00:00 2001 From: Morquin Date: Tue, 12 Aug 2025 06:59:22 +0200 Subject: [PATCH 02/11] Add secure telnet port configuration and improve web display Added: - SecureTelnetPort configuration field to Network struct - SecureTelnetPorts tracking in web Stats struct - Secure telnet port parsing in world stats update - Conditional display of secure telnet ports on homepage Changed: - Web request logging to show real client IPs behind proxy - Underlay width for ports display reduced for better visual balance --- _datafiles/config.yaml | 5 ++++ _datafiles/html/public/index.html | 43 ++++++++++++++++++++++++------ internal/configs/config.network.go | 1 + internal/web/stats.go | 15 ++++++----- world.go | 7 +++++ 5 files changed, 57 insertions(+), 14 deletions(-) diff --git a/_datafiles/config.yaml b/_datafiles/config.yaml index d42ea917..fb83a2b2 100755 --- a/_datafiles/config.yaml +++ b/_datafiles/config.yaml @@ -383,6 +383,11 @@ Network: # The port the server listens on for telnet connections. Listen on multiple # ports by separating them with commas. For example, [33333, 33334, 33335] TelnetPort: [33333, 44444] + # - SecureTelnetPort - + # The port the server listens on for secure telnet connections. Listen on multiple + # ports by separating them with commas. For example, [33334, 33335] + # Set to [0] to disable secure telnet. + SecureTelnetPort: [0] # - LocalPort - # A port that can only be accessed via localhost, but will not limit based on connection count LocalPort: 9999 diff --git a/_datafiles/html/public/index.html b/_datafiles/html/public/index.html index 8315eac6..fe60aaca 100644 --- a/_datafiles/html/public/index.html +++ b/_datafiles/html/public/index.html @@ -1,13 +1,40 @@ {{template "header" .}} -
- - Play - + +

 

+
+
+
+
+

+ Telnet Port{{ if gt (len .CONFIG.Network.TelnetPort) 1 }}s{{end}} +

+

+ : {{ join .CONFIG.Network.TelnetPort ", " }} +

+
+ {{ if and .CONFIG.Network.SecureTelnetPort (ne (index + .CONFIG.Network.SecureTelnetPort 0) "0") }} +
+
+

+ Secure Telnet Port{{ if gt (len .CONFIG.Network.SecureTelnetPort) 1 + }}s{{end}} +

+

+ : {{ join .CONFIG.Network.SecureTelnetPort ", " }} +

+
+ {{ end }}
-

 

-
-

Telnet Port{{ if gt (len .CONFIG.Network.TelnetPort) 1 }}s{{end}}: {{ join .CONFIG.Network.TelnetPort ", " }}

+
-{{template "footer" .}} \ No newline at end of file +{{template "footer" .}} diff --git a/internal/configs/config.network.go b/internal/configs/config.network.go index 89130f2b..ad562f53 100644 --- a/internal/configs/config.network.go +++ b/internal/configs/config.network.go @@ -3,6 +3,7 @@ package configs type Network struct { MaxTelnetConnections ConfigInt `yaml:"MaxTelnetConnections"` // Maximum number of telnet connections to accept TelnetPort ConfigSliceString `yaml:"TelnetPort"` // One or more Ports used to accept telnet connections + SecureTelnetPort ConfigSliceString `yaml:"SecureTelnetPort"` // One or more Ports used to accept secure telnet connections LocalPort ConfigInt `yaml:"LocalPort"` // Port used for admin connections, localhost only HttpPort ConfigInt `yaml:"HttpPort"` // Port used for web requests HttpsPort ConfigInt `yaml:"HttpsPort"` // Port used for web https requests diff --git a/internal/web/stats.go b/internal/web/stats.go index e7ebd011..35f9231d 100644 --- a/internal/web/stats.go +++ b/internal/web/stats.go @@ -7,17 +7,19 @@ import ( ) type Stats struct { - OnlineUsers []users.OnlineInfo - TelnetPorts []int - WebSocketPort int + OnlineUsers []users.OnlineInfo + TelnetPorts []int + SecureTelnetPorts []int + WebSocketPort int } var ( statsLock = sync.RWMutex{} serverStats = Stats{ - WebSocketPort: 0, - OnlineUsers: []users.OnlineInfo{}, - TelnetPorts: []int{}, + WebSocketPort: 0, + OnlineUsers: []users.OnlineInfo{}, + TelnetPorts: []int{}, + SecureTelnetPorts: []int{}, } ) @@ -41,4 +43,5 @@ func (s *Stats) Reset() { s.WebSocketPort = 0 s.OnlineUsers = []users.OnlineInfo{} s.TelnetPorts = []int{} + s.SecureTelnetPorts = []int{} } diff --git a/world.go b/world.go index 16842d08..5ef0fb04 100644 --- a/world.go +++ b/world.go @@ -1057,6 +1057,13 @@ func (w *World) UpdateStats() { } } + for _, t := range c.SecureTelnetPort { + p, _ := strconv.Atoi(t) + if p > 0 { + s.SecureTelnetPorts = append(s.SecureTelnetPorts, p) + } + } + s.WebSocketPort = int(c.HttpPort) web.UpdateStats(s) From 0758235bae55a1c7ede6ab72da45b8a5340d7987 Mon Sep 17 00:00:00 2001 From: Morquin Date: Tue, 12 Aug 2025 07:59:40 +0200 Subject: [PATCH 03/11] Add connection type tracking and secure telnet port support Overview: Secure ports only listen to localhost like the LocalPort, and will display on the website as secure if added to config. Added: - ConnectionType field to OnlineInfo struct showing "Web", "Telnet", or "Secure" - GetLocalPort method to ConnectionDetails for port detection - GetConnectionPort helper function in connections package - SecureTelnetPort configuration field for localhost-only secure connections - Automatic listening on SecureTelnetPort (localhost-only, like LocalPort) - Connection type detection based on WebSocket status and port number - Connection type column to online users HTML table Changed: - GetOnlineInfo method to determine and include connection type - Online page to display how each user is connected - Config documentation to clarify SecureTelnetPort behavior --- _datafiles/config.yaml | 6 ++--- _datafiles/html/public/online.html | 2 ++ internal/connections/connectiondetails.go | 27 +++++++++++++++++++++++ internal/connections/connections.go | 11 +++++++++ internal/users/onlineinfo.go | 19 ++++++++-------- internal/users/userrecord.go | 19 ++++++++++++++++ internal/web/web.go | 3 ++- main.go | 9 ++++++++ 8 files changed, 83 insertions(+), 13 deletions(-) diff --git a/_datafiles/config.yaml b/_datafiles/config.yaml index fb83a2b2..6adb8fcc 100755 --- a/_datafiles/config.yaml +++ b/_datafiles/config.yaml @@ -384,9 +384,9 @@ Network: # ports by separating them with commas. For example, [33333, 33334, 33335] TelnetPort: [33333, 44444] # - SecureTelnetPort - - # The port the server listens on for secure telnet connections. Listen on multiple - # ports by separating them with commas. For example, [33334, 33335] - # Set to [0] to disable secure telnet. + # Localhost-only ports for secure telnet connections (e.g., TLS proxy forwarding). + # Like LocalPort but shown on website. Multiple ports supported: [33334, 33335] + # Set to [0] to disable. SecureTelnetPort: [0] # - LocalPort - # A port that can only be accessed via localhost, but will not limit based on connection count diff --git a/_datafiles/html/public/online.html b/_datafiles/html/public/online.html index 216da07a..31007d9f 100644 --- a/_datafiles/html/public/online.html +++ b/_datafiles/html/public/online.html @@ -12,6 +12,7 @@

Users Online:

Alignment Profession Time Online + Connection Role {{range $index, $uInfo := .STATS.OnlineUsers}} @@ -22,6 +23,7 @@

Users Online:

{{ $uInfo.Alignment }} {{ $uInfo.Profession }} {{ $uInfo.OnlineTimeStr }}{{ if $uInfo.IsAFK }} (AFK){{end}} + {{ $uInfo.ConnectionType }} {{ $uInfo.Role }} {{end}} diff --git a/internal/connections/connectiondetails.go b/internal/connections/connectiondetails.go index bcdfda48..8df05aa3 100644 --- a/internal/connections/connectiondetails.go +++ b/internal/connections/connectiondetails.go @@ -3,6 +3,7 @@ package connections import ( "errors" "net" + "strconv" "strings" "sync" "sync/atomic" @@ -278,6 +279,32 @@ func (cd *ConnectionDetails) RemoteAddr() net.Addr { return cd.conn.RemoteAddr() } +// GetLocalPort returns the local port the connection came in on +func (cd *ConnectionDetails) GetLocalPort() int { + var localAddr net.Addr + if cd.wsConn != nil { + localAddr = cd.wsConn.LocalAddr() + } else if cd.conn != nil { + localAddr = cd.conn.LocalAddr() + } else { + return 0 + } + + // Extract port from address + if tcpAddr, ok := localAddr.(*net.TCPAddr); ok { + return tcpAddr.Port + } + + // Try parsing as string + _, portStr, err := net.SplitHostPort(localAddr.String()) + if err != nil { + return 0 + } + + port, _ := strconv.Atoi(portStr) + return port +} + // get for uniqueId func (cd *ConnectionDetails) ConnectionId() ConnectionId { return ConnectionId(atomic.LoadUint64((*uint64)(&cd.connectionId))) diff --git a/internal/connections/connections.go b/internal/connections/connections.go index 5b3161ba..3fb3e6f7 100644 --- a/internal/connections/connections.go +++ b/internal/connections/connections.go @@ -80,6 +80,17 @@ func IsWebsocket(id ConnectionId) bool { return false } +func GetConnectionPort(id ConnectionId) int { + lock.Lock() + defer lock.Unlock() + + if cd, ok := netConnections[id]; ok { + return cd.GetLocalPort() + } + + return 0 +} + func GetAllConnectionIds() []ConnectionId { lock.Lock() diff --git a/internal/users/onlineinfo.go b/internal/users/onlineinfo.go index 63e147f9..49d28952 100644 --- a/internal/users/onlineinfo.go +++ b/internal/users/onlineinfo.go @@ -1,13 +1,14 @@ package users type OnlineInfo struct { - Username string - CharacterName string - Level int - Alignment string - Profession string - OnlineTime int64 - OnlineTimeStr string - IsAFK bool - Role string + Username string + CharacterName string + Level int + Alignment string + Profession string + OnlineTime int64 + OnlineTimeStr string + IsAFK bool + Role string + ConnectionType string // "Web", "Telnet", or "Secure" } diff --git a/internal/users/userrecord.go b/internal/users/userrecord.go index f98d36ef..49e9cd1d 100644 --- a/internal/users/userrecord.go +++ b/internal/users/userrecord.go @@ -5,6 +5,7 @@ import ( "fmt" "math" "math/big" + "strconv" "strings" "time" @@ -619,6 +620,23 @@ func (u *UserRecord) GetOnlineInfo() OnlineInfo { isAfk = true } + // Determine connection type + connectionType := "Telnet" + if connections.IsWebsocket(u.connectionId) { + connectionType = "Web" + } else { + // Check if connected through a secure telnet port + port := connections.GetConnectionPort(u.connectionId) + networkConfig := configs.GetNetworkConfig() + for _, securePortStr := range networkConfig.SecureTelnetPort { + securePort, _ := strconv.Atoi(securePortStr) + if securePort > 0 && port == securePort { + connectionType = "Secure" + break + } + } + } + return OnlineInfo{ u.Username, u.Character.Name, @@ -629,6 +647,7 @@ func (u *UserRecord) GetOnlineInfo() OnlineInfo { timeStr, isAfk, u.Role, + connectionType, } } diff --git a/internal/web/web.go b/internal/web/web.go index 5eab3e57..2dea44ba 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -58,7 +58,8 @@ func getClientIP(r *http.Request) string { } // Only trust proxy headers if the connection is from localhost - if host == "127.0.0.1" || host == "::1" || host == "localhost" { + // Check for various localhost representations + if host == "127.0.0.1" || host == "::1" || host == "localhost" || host == "0.0.0.0" { // Check X-Real-IP first (higher priority) if realIP := r.Header.Get("X-Real-IP"); realIP != "" { return realIP diff --git a/main.go b/main.go index c782abfa..f5f3ae5c 100644 --- a/main.go +++ b/main.go @@ -271,6 +271,15 @@ func main() { TelnetListenOnPort(`127.0.0.1`, int(c.Network.LocalPort), &wg, 0) } + // Secure telnet ports - same as LocalPort but tracked differently + for _, port := range c.Network.SecureTelnetPort { + if p, err := strconv.Atoi(port); err == nil && p > 0 { + mudlog.Info("Telnet", "stage", "Listening on secure port (localhost only)", "port", p) + // Same as LocalPort - localhost only, no connection limit + TelnetListenOnPort(`127.0.0.1`, p, &wg, 0) + } + } + go worldManager.InputWorker(workerShutdownChan, &wg) go worldManager.MainWorker(workerShutdownChan, &wg) From 87d4df4d39421a5c4abde94cd78880def9c2d9a2 Mon Sep 17 00:00:00 2001 From: Morquin Date: Tue, 12 Aug 2025 09:42:46 +0200 Subject: [PATCH 04/11] Secure display testing --- internal/users/userrecord.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/users/userrecord.go b/internal/users/userrecord.go index 49e9cd1d..696527ad 100644 --- a/internal/users/userrecord.go +++ b/internal/users/userrecord.go @@ -628,10 +628,14 @@ func (u *UserRecord) GetOnlineInfo() OnlineInfo { // Check if connected through a secure telnet port port := connections.GetConnectionPort(u.connectionId) networkConfig := configs.GetNetworkConfig() + + // Debug logging + mudlog.Debug("Connection type check", "connectionId", u.connectionId, "port", port, "securePorts", networkConfig.SecureTelnetPort) + for _, securePortStr := range networkConfig.SecureTelnetPort { securePort, _ := strconv.Atoi(securePortStr) if securePort > 0 && port == securePort { - connectionType = "Secure" + connectionType = "TLS" break } } From 61b0a6d29b9ede503ed36b18a224ea8bb88dba0b Mon Sep 17 00:00:00 2001 From: Morquin Date: Tue, 12 Aug 2025 09:53:01 +0200 Subject: [PATCH 05/11] Redesign secure telnet port configuration for TLS proxy architecture - SecureTelnetPort is now display-only (shown on website, not bound) - Added SecureTelnetLocalPort for internal binding (where TLS proxy forwards) - Updated connection detection to check SecureTelnetLocalPort - This allows proper stunnel4/HAProxy integration: * TLS proxy binds to public SecureTelnetPort (e.g., 33334) * TLS proxy forwards to SecureTelnetLocalPort (e.g., 9998) * Game binds to SecureTelnetLocalPort on localhost only * Connections via SecureTelnetLocalPort show as 'TLS' on online page --- _datafiles/config.yaml | 13 ++++++++++--- internal/configs/config.network.go | 13 +++++++------ internal/users/userrecord.go | 12 ++++-------- main.go | 12 +++++------- 4 files changed, 26 insertions(+), 24 deletions(-) diff --git a/_datafiles/config.yaml b/_datafiles/config.yaml index 6adb8fcc..492d3966 100755 --- a/_datafiles/config.yaml +++ b/_datafiles/config.yaml @@ -384,10 +384,17 @@ Network: # ports by separating them with commas. For example, [33333, 33334, 33335] TelnetPort: [33333, 44444] # - SecureTelnetPort - - # Localhost-only ports for secure telnet connections (e.g., TLS proxy forwarding). - # Like LocalPort but shown on website. Multiple ports supported: [33334, 33335] - # Set to [0] to disable. + # Display-only: External ports where users connect via TLS proxy (e.g., stunnel4). + # These ports are shown on the website but NOT bound by the game server. + # Example: [33334] if stunnel4 listens on port 33334 + # Set to [0] to disable display. SecureTelnetPort: [0] + # - SecureTelnetLocalPort - + # Internal port where TLS proxy forwards secure connections (localhost only). + # Game server binds to this port to receive forwarded TLS connections. + # Example: 9998 if stunnel4 forwards to localhost:9998 + # Set to 0 to disable. + SecureTelnetLocalPort: 0 # - LocalPort - # A port that can only be accessed via localhost, but will not limit based on connection count LocalPort: 9999 diff --git a/internal/configs/config.network.go b/internal/configs/config.network.go index ad562f53..a643ffa9 100644 --- a/internal/configs/config.network.go +++ b/internal/configs/config.network.go @@ -1,12 +1,13 @@ package configs type Network struct { - MaxTelnetConnections ConfigInt `yaml:"MaxTelnetConnections"` // Maximum number of telnet connections to accept - TelnetPort ConfigSliceString `yaml:"TelnetPort"` // One or more Ports used to accept telnet connections - SecureTelnetPort ConfigSliceString `yaml:"SecureTelnetPort"` // One or more Ports used to accept secure telnet connections - LocalPort ConfigInt `yaml:"LocalPort"` // Port used for admin connections, localhost only - HttpPort ConfigInt `yaml:"HttpPort"` // Port used for web requests - HttpsPort ConfigInt `yaml:"HttpsPort"` // Port used for web https requests + MaxTelnetConnections ConfigInt `yaml:"MaxTelnetConnections"` // Maximum number of telnet connections to accept + TelnetPort ConfigSliceString `yaml:"TelnetPort"` // One or more Ports used to accept telnet connections + SecureTelnetPort ConfigSliceString `yaml:"SecureTelnetPort"` // Display-only: external ports where users connect via TLS + SecureTelnetLocalPort ConfigInt `yaml:"SecureTelnetLocalPort"` // Internal port where TLS proxy forwards to (localhost only) + LocalPort ConfigInt `yaml:"LocalPort"` // Port used for admin connections, localhost only + HttpPort ConfigInt `yaml:"HttpPort"` // Port used for web requests + HttpsPort ConfigInt `yaml:"HttpsPort"` // Port used for web https requests HttpsRedirect ConfigBool `yaml:"HttpsRedirect"` // If true, http traffic will be redirected to https AfkSeconds ConfigInt `yaml:"AfkSeconds"` // How long until a player is marked as afk? MaxIdleSeconds ConfigInt `yaml:"MaxIdleSeconds"` // How many seconds a player can go without a command in game before being kicked. diff --git a/internal/users/userrecord.go b/internal/users/userrecord.go index 696527ad..7d817cd3 100644 --- a/internal/users/userrecord.go +++ b/internal/users/userrecord.go @@ -625,19 +625,15 @@ func (u *UserRecord) GetOnlineInfo() OnlineInfo { if connections.IsWebsocket(u.connectionId) { connectionType = "Web" } else { - // Check if connected through a secure telnet port + // Check if connected through the secure telnet local port (where TLS proxy forwards) port := connections.GetConnectionPort(u.connectionId) networkConfig := configs.GetNetworkConfig() // Debug logging - mudlog.Debug("Connection type check", "connectionId", u.connectionId, "port", port, "securePorts", networkConfig.SecureTelnetPort) + mudlog.Debug("Connection type check", "connectionId", u.connectionId, "port", port, "secureLocalPort", networkConfig.SecureTelnetLocalPort) - for _, securePortStr := range networkConfig.SecureTelnetPort { - securePort, _ := strconv.Atoi(securePortStr) - if securePort > 0 && port == securePort { - connectionType = "TLS" - break - } + if networkConfig.SecureTelnetLocalPort > 0 && port == int(networkConfig.SecureTelnetLocalPort) { + connectionType = "TLS" } } diff --git a/main.go b/main.go index f5f3ae5c..bb34b0be 100644 --- a/main.go +++ b/main.go @@ -271,13 +271,11 @@ func main() { TelnetListenOnPort(`127.0.0.1`, int(c.Network.LocalPort), &wg, 0) } - // Secure telnet ports - same as LocalPort but tracked differently - for _, port := range c.Network.SecureTelnetPort { - if p, err := strconv.Atoi(port); err == nil && p > 0 { - mudlog.Info("Telnet", "stage", "Listening on secure port (localhost only)", "port", p) - // Same as LocalPort - localhost only, no connection limit - TelnetListenOnPort(`127.0.0.1`, p, &wg, 0) - } + // Secure telnet local port - where TLS proxy forwards to + if c.Network.SecureTelnetLocalPort > 0 { + mudlog.Info("Telnet", "stage", "Listening on secure local port (localhost only)", "port", c.Network.SecureTelnetLocalPort) + // Same as LocalPort - localhost only, no connection limit + TelnetListenOnPort(`127.0.0.1`, int(c.Network.SecureTelnetLocalPort), &wg, 0) } go worldManager.InputWorker(workerShutdownChan, &wg) From 420a4be7fc0010e27d0d83c6f169283fe8f0992b Mon Sep 17 00:00:00 2001 From: Morquin Date: Tue, 12 Aug 2025 11:57:47 +0200 Subject: [PATCH 06/11] Change SecureTelnetLocalPort to support multiple ports - Changed SecureTelnetLocalPort from single ConfigInt to ConfigSliceString - Now supports multiple secure local ports: [9998, 9997] - Updated connection detection to check all ports in the slice - Allows multiple TLS proxies to forward to different local ports - Consistent with SecureTelnetPort being a slice --- _datafiles/config.yaml | 11 ++++++----- internal/configs/config.network.go | 2 +- internal/users/userrecord.go | 12 ++++++++---- main.go | 12 +++++++----- 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/_datafiles/config.yaml b/_datafiles/config.yaml index 492d3966..43000486 100755 --- a/_datafiles/config.yaml +++ b/_datafiles/config.yaml @@ -390,11 +390,12 @@ Network: # Set to [0] to disable display. SecureTelnetPort: [0] # - SecureTelnetLocalPort - - # Internal port where TLS proxy forwards secure connections (localhost only). - # Game server binds to this port to receive forwarded TLS connections. - # Example: 9998 if stunnel4 forwards to localhost:9998 - # Set to 0 to disable. - SecureTelnetLocalPort: 0 + # Internal ports where TLS proxy forwards secure connections (localhost only). + # Game server binds to these ports to receive forwarded TLS connections. + # Example: [9998] if stunnel4 forwards to localhost:9998 + # Multiple ports supported: [9998, 9997] for multiple TLS proxies + # Set to [0] to disable. + SecureTelnetLocalPort: [0] # - LocalPort - # A port that can only be accessed via localhost, but will not limit based on connection count LocalPort: 9999 diff --git a/internal/configs/config.network.go b/internal/configs/config.network.go index a643ffa9..860daabd 100644 --- a/internal/configs/config.network.go +++ b/internal/configs/config.network.go @@ -4,7 +4,7 @@ type Network struct { MaxTelnetConnections ConfigInt `yaml:"MaxTelnetConnections"` // Maximum number of telnet connections to accept TelnetPort ConfigSliceString `yaml:"TelnetPort"` // One or more Ports used to accept telnet connections SecureTelnetPort ConfigSliceString `yaml:"SecureTelnetPort"` // Display-only: external ports where users connect via TLS - SecureTelnetLocalPort ConfigInt `yaml:"SecureTelnetLocalPort"` // Internal port where TLS proxy forwards to (localhost only) + SecureTelnetLocalPort ConfigSliceString `yaml:"SecureTelnetLocalPort"` // Internal ports where TLS proxy forwards to (localhost only) LocalPort ConfigInt `yaml:"LocalPort"` // Port used for admin connections, localhost only HttpPort ConfigInt `yaml:"HttpPort"` // Port used for web requests HttpsPort ConfigInt `yaml:"HttpsPort"` // Port used for web https requests diff --git a/internal/users/userrecord.go b/internal/users/userrecord.go index 7d817cd3..952be64d 100644 --- a/internal/users/userrecord.go +++ b/internal/users/userrecord.go @@ -625,15 +625,19 @@ func (u *UserRecord) GetOnlineInfo() OnlineInfo { if connections.IsWebsocket(u.connectionId) { connectionType = "Web" } else { - // Check if connected through the secure telnet local port (where TLS proxy forwards) + // Check if connected through a secure telnet local port (where TLS proxy forwards) port := connections.GetConnectionPort(u.connectionId) networkConfig := configs.GetNetworkConfig() // Debug logging - mudlog.Debug("Connection type check", "connectionId", u.connectionId, "port", port, "secureLocalPort", networkConfig.SecureTelnetLocalPort) + mudlog.Debug("Connection type check", "connectionId", u.connectionId, "port", port, "secureLocalPorts", networkConfig.SecureTelnetLocalPort) - if networkConfig.SecureTelnetLocalPort > 0 && port == int(networkConfig.SecureTelnetLocalPort) { - connectionType = "TLS" + for _, securePortStr := range networkConfig.SecureTelnetLocalPort { + securePort, _ := strconv.Atoi(securePortStr) + if securePort > 0 && port == securePort { + connectionType = "TLS" + break + } } } diff --git a/main.go b/main.go index bb34b0be..0542d7a2 100644 --- a/main.go +++ b/main.go @@ -271,11 +271,13 @@ func main() { TelnetListenOnPort(`127.0.0.1`, int(c.Network.LocalPort), &wg, 0) } - // Secure telnet local port - where TLS proxy forwards to - if c.Network.SecureTelnetLocalPort > 0 { - mudlog.Info("Telnet", "stage", "Listening on secure local port (localhost only)", "port", c.Network.SecureTelnetLocalPort) - // Same as LocalPort - localhost only, no connection limit - TelnetListenOnPort(`127.0.0.1`, int(c.Network.SecureTelnetLocalPort), &wg, 0) + // Secure telnet local ports - where TLS proxy forwards to + for _, port := range c.Network.SecureTelnetLocalPort { + if p, err := strconv.Atoi(port); err == nil && p > 0 { + mudlog.Info("Telnet", "stage", "Listening on secure local port (localhost only)", "port", p) + // Same as LocalPort - localhost only, no connection limit + TelnetListenOnPort(`127.0.0.1`, p, &wg, 0) + } } go worldManager.InputWorker(workerShutdownChan, &wg) From 289c78192fb5429f7d8d6cb428e711b59c242984 Mon Sep 17 00:00:00 2001 From: Morquin Date: Tue, 12 Aug 2025 17:35:14 +0200 Subject: [PATCH 07/11] Update onlineinfo.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- internal/users/onlineinfo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/users/onlineinfo.go b/internal/users/onlineinfo.go index 49d28952..e4e80a4d 100644 --- a/internal/users/onlineinfo.go +++ b/internal/users/onlineinfo.go @@ -10,5 +10,5 @@ type OnlineInfo struct { OnlineTimeStr string IsAFK bool Role string - ConnectionType string // "Web", "Telnet", or "Secure" + ConnectionType string // "Web", "Telnet", or "TLS" } From 05fbd0331fd839df44113bc15edd23f04b580655 Mon Sep 17 00:00:00 2001 From: Morquin Date: Tue, 12 Aug 2025 17:35:41 +0200 Subject: [PATCH 08/11] Update web.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- internal/web/web.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/web/web.go b/internal/web/web.go index 2dea44ba..b72185be 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -59,7 +59,7 @@ func getClientIP(r *http.Request) string { // Only trust proxy headers if the connection is from localhost // Check for various localhost representations - if host == "127.0.0.1" || host == "::1" || host == "localhost" || host == "0.0.0.0" { + if host == "127.0.0.1" || host == "::1" || host == "localhost" { // Check X-Real-IP first (higher priority) if realIP := r.Header.Get("X-Real-IP"); realIP != "" { return realIP From fd20aca0d9cb5c0585d4f9d8a019f6d46ce7bd89 Mon Sep 17 00:00:00 2001 From: Morquin Date: Tue, 12 Aug 2025 21:24:22 +0200 Subject: [PATCH 09/11] Removed debug logging from connection type detection Removed: - Debug logging in GetOnlineInfo that was firing every 10 seconds with stats updates - These logs were creating unnecessary noise in production environments --- internal/users/userrecord.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/users/userrecord.go b/internal/users/userrecord.go index 952be64d..aa813d3c 100644 --- a/internal/users/userrecord.go +++ b/internal/users/userrecord.go @@ -629,9 +629,6 @@ func (u *UserRecord) GetOnlineInfo() OnlineInfo { port := connections.GetConnectionPort(u.connectionId) networkConfig := configs.GetNetworkConfig() - // Debug logging - mudlog.Debug("Connection type check", "connectionId", u.connectionId, "port", port, "secureLocalPorts", networkConfig.SecureTelnetLocalPort) - for _, securePortStr := range networkConfig.SecureTelnetLocalPort { securePort, _ := strconv.Atoi(securePortStr) if securePort > 0 && port == securePort { From 1ae985b7af792f501a6f61ac2fd2be07f4da24c8 Mon Sep 17 00:00:00 2001 From: Morquin Date: Fri, 15 Aug 2025 11:06:46 +0200 Subject: [PATCH 10/11] Small commit to hopefully permanently fix fmtcheck changes Certain files - config.network.go, mapper.go, rooms.go and userrecords.go always error out when doing the fmtcheck during the make process. This hopefully fixes this permanently by comitting the change always enforced by fmtcheck. --- internal/configs/config.network.go | 12 ++++++------ internal/mapper/mapper.go | 8 ++++---- internal/rooms/rooms.go | 1 - internal/users/userrecord.go | 2 +- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/internal/configs/config.network.go b/internal/configs/config.network.go index 860daabd..712bcb37 100644 --- a/internal/configs/config.network.go +++ b/internal/configs/config.network.go @@ -8,12 +8,12 @@ type Network struct { LocalPort ConfigInt `yaml:"LocalPort"` // Port used for admin connections, localhost only HttpPort ConfigInt `yaml:"HttpPort"` // Port used for web requests HttpsPort ConfigInt `yaml:"HttpsPort"` // Port used for web https requests - HttpsRedirect ConfigBool `yaml:"HttpsRedirect"` // If true, http traffic will be redirected to https - AfkSeconds ConfigInt `yaml:"AfkSeconds"` // How long until a player is marked as afk? - MaxIdleSeconds ConfigInt `yaml:"MaxIdleSeconds"` // How many seconds a player can go without a command in game before being kicked. - TimeoutMods ConfigBool `yaml:"TimeoutMods"` // Whether to kick admin/mods when idle too long. - ZombieSeconds ConfigInt `yaml:"ZombieSeconds"` // How many seconds a player will be a zombie allowing them to reconnect. - LogoutRounds ConfigInt `yaml:"LogoutRounds"` // How many rounds of uninterrupted meditation must be completed to log out. + HttpsRedirect ConfigBool `yaml:"HttpsRedirect"` // If true, http traffic will be redirected to https + AfkSeconds ConfigInt `yaml:"AfkSeconds"` // How long until a player is marked as afk? + MaxIdleSeconds ConfigInt `yaml:"MaxIdleSeconds"` // How many seconds a player can go without a command in game before being kicked. + TimeoutMods ConfigBool `yaml:"TimeoutMods"` // Whether to kick admin/mods when idle too long. + ZombieSeconds ConfigInt `yaml:"ZombieSeconds"` // How many seconds a player will be a zombie allowing them to reconnect. + LogoutRounds ConfigInt `yaml:"LogoutRounds"` // How many rounds of uninterrupted meditation must be completed to log out. } func (n *Network) Validate() { diff --git a/internal/mapper/mapper.go b/internal/mapper/mapper.go index 044d5d50..a6c15492 100644 --- a/internal/mapper/mapper.go +++ b/internal/mapper/mapper.go @@ -998,15 +998,15 @@ func PreCacheMaps() { func validateRoomBiomes() { missingBiomeCount := 0 invalidBiomeCount := 0 - + for _, roomId := range rooms.GetAllRoomIds() { room := rooms.LoadRoom(roomId) if room == nil { continue } - + originalBiome := room.Biome - + // Check if room has no biome if originalBiome == "" { zoneBiome := rooms.GetZoneBiome(room.Zone) @@ -1022,7 +1022,7 @@ func validateRoomBiomes() { } } } - + if missingBiomeCount > 0 || invalidBiomeCount > 0 { mudlog.Info("Biome validation complete", "missing", missingBiomeCount, "invalid", invalidBiomeCount) } diff --git a/internal/rooms/rooms.go b/internal/rooms/rooms.go index 49906960..285d34f1 100644 --- a/internal/rooms/rooms.go +++ b/internal/rooms/rooms.go @@ -2198,7 +2198,6 @@ func (r *Room) Validate() error { } } - // Make sure all items are validated (and have uids) for i := range r.Items { r.Items[i].Validate() diff --git a/internal/users/userrecord.go b/internal/users/userrecord.go index aa813d3c..9200a00a 100644 --- a/internal/users/userrecord.go +++ b/internal/users/userrecord.go @@ -628,7 +628,7 @@ func (u *UserRecord) GetOnlineInfo() OnlineInfo { // Check if connected through a secure telnet local port (where TLS proxy forwards) port := connections.GetConnectionPort(u.connectionId) networkConfig := configs.GetNetworkConfig() - + for _, securePortStr := range networkConfig.SecureTelnetLocalPort { securePort, _ := strconv.Atoi(securePortStr) if securePort > 0 && port == securePort { From bbc8f3e3a54281e6b97c06c2688f0e6833da4ea7 Mon Sep 17 00:00:00 2001 From: Morquin Date: Fri, 15 Aug 2025 12:30:17 +0200 Subject: [PATCH 11/11] Fixing comments Removed excessive debug comments and "what" comments. Only retained the relevent stuff. --- internal/web/web.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/internal/web/web.go b/internal/web/web.go index b72185be..611843ed 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -47,31 +47,25 @@ type WebNav struct { // connection is from localhost (trusted proxy), otherwise returns the // direct connection IP. func getClientIP(r *http.Request) string { - // Get the direct connection IP remoteAddr := r.RemoteAddr - // Extract just the IP part (remove port) host, _, err := net.SplitHostPort(remoteAddr) if err != nil { - // If splitting fails, use the whole remoteAddr host = remoteAddr } // Only trust proxy headers if the connection is from localhost - // Check for various localhost representations if host == "127.0.0.1" || host == "::1" || host == "localhost" { - // Check X-Real-IP first (higher priority) + // X-Real-IP has higher priority than X-Forwarded-For if realIP := r.Header.Get("X-Real-IP"); realIP != "" { return realIP } - // Check X-Forwarded-For (may contain multiple IPs) if forwardedFor := r.Header.Get("X-Forwarded-For"); forwardedFor != "" { // X-Forwarded-For can contain comma-separated IPs // The first one is the original client ips := strings.Split(forwardedFor, ",") if len(ips) > 0 { - // Trim any whitespace from the IP clientIP := strings.TrimSpace(ips[0]) if clientIP != "" { return clientIP @@ -80,7 +74,6 @@ func getClientIP(r *http.Request) string { } } - // Return the direct connection IP if no proxy headers or not from localhost return host }