From e5ad0f30efea726093ad4023a432a0a05d1d34b6 Mon Sep 17 00:00:00 2001 From: alexfrs69 Date: Tue, 29 Apr 2025 03:43:48 +0200 Subject: [PATCH 01/13] feat: add a fake status response when autoscaling is up --- cmd/mc-router/main.go | 5 +++- mcproto/types.go | 27 ++++++++++++++++++++++ mcproto/write.go | 54 +++++++++++++++++++++++++++++++++++++++++++ server/connector.go | 27 +++++++++++++++++++++- 4 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 mcproto/write.go diff --git a/cmd/mc-router/main.go b/cmd/mc-router/main.go index 9bf7c95..b7f6069 100644 --- a/cmd/mc-router/main.go +++ b/cmd/mc-router/main.go @@ -71,6 +71,9 @@ type Config struct { SimplifySRV bool `default:"false" usage:"Simplify fully qualified SRV records for mapping"` + FakeOnline bool `default:"false" usage:"Enable fake online MOTD when backend is offline and auto-scale-up is enabled"` + FakeOnlineMOTD string `default:"Server is sleeping\nJoin to wake it up" usage:"Custom MOTD to show when backend is offline and auto-scale-up is enabled"` + Webhook WebhookConfig `usage:"Webhook configuration"` } @@ -167,7 +170,7 @@ func main() { trustedIpNets = append(trustedIpNets, ipNet) } - connector := server.NewConnector(metricsBuilder.BuildConnectorMetrics(), config.UseProxyProtocol, config.ReceiveProxyProtocol, trustedIpNets, config.RecordLogins, autoScaleAllowDenyConfig) + connector := server.NewConnector(metricsBuilder.BuildConnectorMetrics(), config.UseProxyProtocol, config.ReceiveProxyProtocol, trustedIpNets, config.RecordLogins, autoScaleUpAllowDenyConfig, config.FakeOnline, config.FakeOnlineMOTD) clientFilter, err := server.NewClientFilter(config.ClientsToAllow, config.ClientsToDeny) if err != nil { diff --git a/mcproto/types.go b/mcproto/types.go index 9ff0f2f..4e7b48e 100644 --- a/mcproto/types.go +++ b/mcproto/types.go @@ -84,3 +84,30 @@ type ByteReader interface { const ( PacketLengthFieldBytes = 1 ) + +type StatusResponse struct { + Version StatusVersion `json:"version"` + Players StatusPlayers `json:"players"` + Description StatusText `json:"description"` + Favicon string `json:"favicon,omitempty"` +} + +type StatusVersion struct { + Name string `json:"name"` + Protocol int `json:"protocol"` +} + +type StatusPlayers struct { + Max int `json:"max"` + Online int `json:"online"` + Sample []PlayerEntry `json:"sample,omitempty"` +} + +type PlayerEntry struct { + Name string `json:"name"` + ID string `json:"id"` +} + +type StatusText struct { + Text string `json:"text"` +} \ No newline at end of file diff --git a/mcproto/write.go b/mcproto/write.go new file mode 100644 index 0000000..16a800f --- /dev/null +++ b/mcproto/write.go @@ -0,0 +1,54 @@ +package mcproto + +import ( + "encoding/json" + "io" +) + +func WriteStatusResponse(w io.Writer, motd string) error { + resp := StatusResponse{ + Version: StatusVersion{ + Name: "1.21.5", + Protocol: 770, + }, + Players: StatusPlayers{ + Max: 0, + Online: 0, + }, + Description: StatusText{ + Text: motd, + }, + } + data, err := json.Marshal(resp) + if err != nil { + return err + } + + jsonLen := encodeVarInt(len(data)) + payload := append(jsonLen, data...) + return WritePacket(w, 0x00, payload) +} + +func WritePacket(w io.Writer, packetID int, data []byte) error { + packet := append(encodeVarInt(packetID), data...) + length := encodeVarInt(len(packet)) + _, err := w.Write(append(length, packet...)) + return err +} + +// encodeVarInt encodes an int as a Minecraft VarInt. +func encodeVarInt(value int) []byte { + var buf []byte + for { + temp := byte(value & 0x7F) + value >>= 7 + if value != 0 { + temp |= 0x80 + } + buf = append(buf, temp) + if value == 0 { + break + } + } + return buf +} \ No newline at end of file diff --git a/server/connector.go b/server/connector.go index d805462..f3b009a 100644 --- a/server/connector.go +++ b/server/connector.go @@ -25,6 +25,7 @@ import ( const ( handshakeTimeout = 5 * time.Second + backendTimeout = 1 * time.Second ) var noDeadline time.Time @@ -137,6 +138,9 @@ type Connector struct { clientFilter *ClientFilter autoScaleUpAllowDenyConfig *AllowDenyConfig + fakeOnline bool + fakeOnlineMOTD string + connectionNotifier ConnectionNotifier } @@ -479,7 +483,7 @@ func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net. WithField("player", playerInfo). Info("Connecting to backend") - backendConn, err := net.Dial("tcp", backendHostPort) + backendConn, err := net.DialTimeout("tcp", backendHostPort, backendTimeout) if err != nil { logrus. WithError(err). @@ -497,6 +501,27 @@ func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net. } } + // Verify that the packet is a status request && autoScaleUp is enabled + if c.fakeOnline && waker != nil && nextState == mcproto.StateStatus { + logrus.Info("Server is offline, sending fakeOnlineMOTD") + + // Send a response to the client indicating that the server is sleeping + writeStatusErr := mcproto.WriteStatusResponse( + frontendConn, + c.fakeOnlineMOTD, + ) + + if writeStatusErr != nil { + logrus. + WithError(writeStatusErr). + WithField("client", clientAddr). + WithField("serverAddress", serverAddress). + WithField("backend", backendHostPort). + WithField("player", playerInfo). + Error("Failed to write status response") + } + } + return } From 5c0baf83d346337ca237cebfa1d6305d77dac1a7 Mon Sep 17 00:00:00 2001 From: alexfrs69 Date: Wed, 30 Apr 2025 01:30:46 +0200 Subject: [PATCH 02/13] feat: add client connection hang --- server/connector.go | 195 +++++++++++++++++++++++++++++--------------- 1 file changed, 131 insertions(+), 64 deletions(-) diff --git a/server/connector.go b/server/connector.go index f3b009a..72da9ab 100644 --- a/server/connector.go +++ b/server/connector.go @@ -25,7 +25,9 @@ import ( const ( handshakeTimeout = 5 * time.Second - backendTimeout = 1 * time.Second + backendTimeout = 30 * time.Second + backendRetryInterval = 5 * time.Second + backendStatusTimeout = 1 * time.Second ) var noDeadline time.Time @@ -428,7 +430,7 @@ func (c *Connector) cleanupBackendConnection(ctx context.Context, clientAddr net } func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net.Conn, - clientAddr net.Addr, preReadContent io.Reader, serverAddress string, playerInfo *PlayerInfo, nextState mcproto.State) { + clientAddr net.Addr, preReadContent io.Reader, serverAddress string, playerInfo *PlayerInfo, nextState mcproto.State) { backendHostPort, resolvedHost, waker, _ := Routes.FindBackendForServerAddress(ctx, serverAddress) cleanupMetrics := false @@ -458,72 +460,137 @@ func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net. } } - if backendHostPort == "" { + if backendHostPort == "" { + logrus. + WithField("serverAddress", serverAddress). + WithField("resolvedHost", resolvedHost). + WithField("player", playerInfo). + Warn("Unable to find registered backend") + c.metrics.Errors.With("type", "missing_backend").Add(1) + + if c.connectionNotifier != nil { + err := c.connectionNotifier.NotifyMissingBackend(ctx, clientAddr, serverAddress, playerInfo) + if err != nil { + logrus.WithError(err).Warn("failed to notify missing backend") + } + } + + return + } + + logrus. + WithField("client", clientAddr). + WithField("server", serverAddress). + WithField("backendHostPort", backendHostPort). + WithField("player", playerInfo). + Info("Connecting to backend") + + var backendConn net.Conn + var err error + + if nextState == mcproto.StateStatus { + // Status request: try to connect once with backendStatusTimeout + backendConn, err = net.DialTimeout("tcp", backendHostPort, backendStatusTimeout) + if err != nil { + logrus. + WithError(err). + WithField("client", clientAddr). + WithField("serverAddress", serverAddress). + WithField("backend", backendHostPort). + WithField("player", playerInfo). + Warn("Unable to connect to backend for status request") + c.metrics.Errors.With("type", "backend_failed").Add(1) + + if c.connectionNotifier != nil { + notifyErr := c.connectionNotifier.NotifyFailedBackendConnection(ctx, clientAddr, serverAddress, playerInfo, backendHostPort, err) + if notifyErr != nil { + logrus.WithError(notifyErr).Warn("failed to notify failed backend connection") + } + } + + // Return fakeOnline MOTD + if c.fakeOnline && waker != nil { + logrus.Info("Server is offline, sending fakeOnlineMOTD for status request") + writeStatusErr := mcproto.WriteStatusResponse( + frontendConn, + c.fakeOnlineMOTD, + ) + if writeStatusErr != nil { + logrus. + WithError(writeStatusErr). + WithField("client", clientAddr). + WithField("serverAddress", serverAddress). + WithField("backend", backendHostPort). + WithField("player", playerInfo). + Error("Failed to write status response") + } + } + return + } + } else if nextState == mcproto.StateLogin { + // Connect request + if waker != nil { + logrus.Debug("Connect: Autoscaler is enabled, waiting for backend to be ready") + // Autoscaler enabled: retry until backendTimeout is reached + deadline := time.Now().Add(backendTimeout) + for { + backendConn, err = net.DialTimeout("tcp", backendHostPort, backendRetryInterval) + logrus.Debug("Tries to connect to backend") + + if err == nil { + break + } + if time.Now().After(deadline) { + logrus. + WithError(err). + WithField("client", clientAddr). + WithField("serverAddress", serverAddress). + WithField("backend", backendHostPort). + WithField("player", playerInfo). + Warn("Unable to connect to backend after retries (autoscaler enabled)") + c.metrics.Errors.With("type", "backend_failed").Add(1) + if c.connectionNotifier != nil { + notifyErr := c.connectionNotifier.NotifyFailedBackendConnection(ctx, clientAddr, serverAddress, playerInfo, backendHostPort, err) + if notifyErr != nil { + logrus.WithError(notifyErr).Warn("failed to notify failed backend connection") + } + } + return + } + time.Sleep(backendRetryInterval) + } + } else { + logrus.Debug("Connect: Autoscaler is disabled, trying to connect once") + // Autoscaler disabled: try to connect once with backendTimeout + backendConn, err = net.DialTimeout("tcp", backendHostPort, backendTimeout) + if err != nil { + logrus. + WithError(err). + WithField("client", clientAddr). + WithField("serverAddress", serverAddress). + WithField("backend", backendHostPort). + WithField("player", playerInfo). + Warn("Unable to connect to backend (autoscaler disabled)") + c.metrics.Errors.With("type", "backend_failed").Add(1) + if c.connectionNotifier != nil { + notifyErr := c.connectionNotifier.NotifyFailedBackendConnection(ctx, clientAddr, serverAddress, playerInfo, backendHostPort, err) + if notifyErr != nil { + logrus.WithError(notifyErr).Warn("failed to notify failed backend connection") + } + } + return + } + } + } else { + // Unknown state, do nothing logrus. - WithField("serverAddress", serverAddress). - WithField("resolvedHost", resolvedHost). - WithField("player", playerInfo). - Warn("Unable to find registered backend") - c.metrics.Errors.With("type", "missing_backend").Add(1) - - if c.connectionNotifier != nil { - err := c.connectionNotifier.NotifyMissingBackend(ctx, clientAddr, serverAddress, playerInfo) - if err != nil { - logrus.WithError(err).Warn("failed to notify missing backend") - } - } - - return - } - - logrus. - WithField("client", clientAddr). - WithField("server", serverAddress). - WithField("backendHostPort", backendHostPort). - WithField("player", playerInfo). - Info("Connecting to backend") - - backendConn, err := net.DialTimeout("tcp", backendHostPort, backendTimeout) - if err != nil { - logrus. - WithError(err). WithField("client", clientAddr). WithField("serverAddress", serverAddress). - WithField("backend", backendHostPort). + WithField("nextState", nextState). WithField("player", playerInfo). - Warn("Unable to connect to backend") - c.metrics.Errors.With("type", "backend_failed").Add(1) - - if c.connectionNotifier != nil { - notifyErr := c.connectionNotifier.NotifyFailedBackendConnection(ctx, clientAddr, serverAddress, playerInfo, backendHostPort, err) - if notifyErr != nil { - logrus.WithError(notifyErr).Warn("failed to notify failed backend connection") - } - } - - // Verify that the packet is a status request && autoScaleUp is enabled - if c.fakeOnline && waker != nil && nextState == mcproto.StateStatus { - logrus.Info("Server is offline, sending fakeOnlineMOTD") - - // Send a response to the client indicating that the server is sleeping - writeStatusErr := mcproto.WriteStatusResponse( - frontendConn, - c.fakeOnlineMOTD, - ) - - if writeStatusErr != nil { - logrus. - WithError(writeStatusErr). - WithField("client", clientAddr). - WithField("serverAddress", serverAddress). - WithField("backend", backendHostPort). - WithField("player", playerInfo). - Error("Failed to write status response") - } - } - - return - } + Warn("Unknown state, unable to connect to backend") + return + } if c.connectionNotifier != nil { err := c.connectionNotifier.NotifyConnected(ctx, clientAddr, serverAddress, playerInfo, backendHostPort) From 18f8aaa935646c38ac2bc95a7f88215a53ca1186 Mon Sep 17 00:00:00 2001 From: alexfrs69 Date: Wed, 30 Apr 2025 02:08:32 +0200 Subject: [PATCH 03/13] fix: indentation convertion --- server/connector.go | 246 ++++++++++++++++++++++---------------------- 1 file changed, 123 insertions(+), 123 deletions(-) diff --git a/server/connector.go b/server/connector.go index 72da9ab..2c74fa8 100644 --- a/server/connector.go +++ b/server/connector.go @@ -141,7 +141,7 @@ type Connector struct { autoScaleUpAllowDenyConfig *AllowDenyConfig fakeOnline bool - fakeOnlineMOTD string + fakeOnlineMOTD string connectionNotifier ConnectionNotifier } @@ -430,7 +430,7 @@ func (c *Connector) cleanupBackendConnection(ctx context.Context, clientAddr net } func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net.Conn, - clientAddr net.Addr, preReadContent io.Reader, serverAddress string, playerInfo *PlayerInfo, nextState mcproto.State) { + clientAddr net.Addr, preReadContent io.Reader, serverAddress string, playerInfo *PlayerInfo, nextState mcproto.State) { backendHostPort, resolvedHost, waker, _ := Routes.FindBackendForServerAddress(ctx, serverAddress) cleanupMetrics := false @@ -460,137 +460,137 @@ func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net. } } - if backendHostPort == "" { - logrus. - WithField("serverAddress", serverAddress). - WithField("resolvedHost", resolvedHost). - WithField("player", playerInfo). - Warn("Unable to find registered backend") - c.metrics.Errors.With("type", "missing_backend").Add(1) - - if c.connectionNotifier != nil { - err := c.connectionNotifier.NotifyMissingBackend(ctx, clientAddr, serverAddress, playerInfo) - if err != nil { - logrus.WithError(err).Warn("failed to notify missing backend") - } - } - - return - } - - logrus. - WithField("client", clientAddr). - WithField("server", serverAddress). - WithField("backendHostPort", backendHostPort). - WithField("player", playerInfo). - Info("Connecting to backend") - - var backendConn net.Conn - var err error - - if nextState == mcproto.StateStatus { - // Status request: try to connect once with backendStatusTimeout - backendConn, err = net.DialTimeout("tcp", backendHostPort, backendStatusTimeout) - if err != nil { - logrus. - WithError(err). - WithField("client", clientAddr). - WithField("serverAddress", serverAddress). - WithField("backend", backendHostPort). - WithField("player", playerInfo). - Warn("Unable to connect to backend for status request") - c.metrics.Errors.With("type", "backend_failed").Add(1) - - if c.connectionNotifier != nil { - notifyErr := c.connectionNotifier.NotifyFailedBackendConnection(ctx, clientAddr, serverAddress, playerInfo, backendHostPort, err) - if notifyErr != nil { - logrus.WithError(notifyErr).Warn("failed to notify failed backend connection") - } - } - - // Return fakeOnline MOTD - if c.fakeOnline && waker != nil { - logrus.Info("Server is offline, sending fakeOnlineMOTD for status request") - writeStatusErr := mcproto.WriteStatusResponse( - frontendConn, - c.fakeOnlineMOTD, - ) - if writeStatusErr != nil { - logrus. - WithError(writeStatusErr). - WithField("client", clientAddr). - WithField("serverAddress", serverAddress). - WithField("backend", backendHostPort). - WithField("player", playerInfo). - Error("Failed to write status response") - } - } - return - } - } else if nextState == mcproto.StateLogin { - // Connect request - if waker != nil { + if backendHostPort == "" { + logrus. + WithField("serverAddress", serverAddress). + WithField("resolvedHost", resolvedHost). + WithField("player", playerInfo). + Warn("Unable to find registered backend") + c.metrics.Errors.With("type", "missing_backend").Add(1) + + if c.connectionNotifier != nil { + err := c.connectionNotifier.NotifyMissingBackend(ctx, clientAddr, serverAddress, playerInfo) + if err != nil { + logrus.WithError(err).Warn("failed to notify missing backend") + } + } + + return + } + + logrus. + WithField("client", clientAddr). + WithField("server", serverAddress). + WithField("backendHostPort", backendHostPort). + WithField("player", playerInfo). + Info("Connecting to backend") + + var backendConn net.Conn + var err error + + if nextState == mcproto.StateStatus { + // Status request: try to connect once with backendStatusTimeout + backendConn, err = net.DialTimeout("tcp", backendHostPort, backendStatusTimeout) + if err != nil { + logrus. + WithError(err). + WithField("client", clientAddr). + WithField("serverAddress", serverAddress). + WithField("backend", backendHostPort). + WithField("player", playerInfo). + Warn("Unable to connect to backend for status request") + c.metrics.Errors.With("type", "backend_failed").Add(1) + + if c.connectionNotifier != nil { + notifyErr := c.connectionNotifier.NotifyFailedBackendConnection(ctx, clientAddr, serverAddress, playerInfo, backendHostPort, err) + if notifyErr != nil { + logrus.WithError(notifyErr).Warn("failed to notify failed backend connection") + } + } + + // Return fakeOnline MOTD + if c.fakeOnline && waker != nil { + logrus.Info("Server is offline, sending fakeOnlineMOTD for status request") + writeStatusErr := mcproto.WriteStatusResponse( + frontendConn, + c.fakeOnlineMOTD, + ) + if writeStatusErr != nil { + logrus. + WithError(writeStatusErr). + WithField("client", clientAddr). + WithField("serverAddress", serverAddress). + WithField("backend", backendHostPort). + WithField("player", playerInfo). + Error("Failed to write status response") + } + } + return + } + } else if nextState == mcproto.StateLogin { + // Connect request + if waker != nil { logrus.Debug("Connect: Autoscaler is enabled, waiting for backend to be ready") - // Autoscaler enabled: retry until backendTimeout is reached - deadline := time.Now().Add(backendTimeout) - for { - backendConn, err = net.DialTimeout("tcp", backendHostPort, backendRetryInterval) + // Autoscaler enabled: retry until backendTimeout is reached + deadline := time.Now().Add(backendTimeout) + for { + backendConn, err = net.DialTimeout("tcp", backendHostPort, backendRetryInterval) logrus.Debug("Tries to connect to backend") - if err == nil { - break - } - if time.Now().After(deadline) { - logrus. - WithError(err). - WithField("client", clientAddr). - WithField("serverAddress", serverAddress). - WithField("backend", backendHostPort). - WithField("player", playerInfo). - Warn("Unable to connect to backend after retries (autoscaler enabled)") - c.metrics.Errors.With("type", "backend_failed").Add(1) - if c.connectionNotifier != nil { - notifyErr := c.connectionNotifier.NotifyFailedBackendConnection(ctx, clientAddr, serverAddress, playerInfo, backendHostPort, err) - if notifyErr != nil { - logrus.WithError(notifyErr).Warn("failed to notify failed backend connection") - } - } - return - } - time.Sleep(backendRetryInterval) - } - } else { + if err == nil { + break + } + if time.Now().After(deadline) { + logrus. + WithError(err). + WithField("client", clientAddr). + WithField("serverAddress", serverAddress). + WithField("backend", backendHostPort). + WithField("player", playerInfo). + Warn("Unable to connect to backend after retries (autoscaler enabled)") + c.metrics.Errors.With("type", "backend_failed").Add(1) + if c.connectionNotifier != nil { + notifyErr := c.connectionNotifier.NotifyFailedBackendConnection(ctx, clientAddr, serverAddress, playerInfo, backendHostPort, err) + if notifyErr != nil { + logrus.WithError(notifyErr).Warn("failed to notify failed backend connection") + } + } + return + } + time.Sleep(backendRetryInterval) + } + } else { logrus.Debug("Connect: Autoscaler is disabled, trying to connect once") - // Autoscaler disabled: try to connect once with backendTimeout - backendConn, err = net.DialTimeout("tcp", backendHostPort, backendTimeout) - if err != nil { - logrus. - WithError(err). - WithField("client", clientAddr). - WithField("serverAddress", serverAddress). - WithField("backend", backendHostPort). - WithField("player", playerInfo). - Warn("Unable to connect to backend (autoscaler disabled)") - c.metrics.Errors.With("type", "backend_failed").Add(1) - if c.connectionNotifier != nil { - notifyErr := c.connectionNotifier.NotifyFailedBackendConnection(ctx, clientAddr, serverAddress, playerInfo, backendHostPort, err) - if notifyErr != nil { - logrus.WithError(notifyErr).Warn("failed to notify failed backend connection") - } - } - return - } - } - } else { - // Unknown state, do nothing + // Autoscaler disabled: try to connect once with backendTimeout + backendConn, err = net.DialTimeout("tcp", backendHostPort, backendTimeout) + if err != nil { + logrus. + WithError(err). + WithField("client", clientAddr). + WithField("serverAddress", serverAddress). + WithField("backend", backendHostPort). + WithField("player", playerInfo). + Warn("Unable to connect to backend (autoscaler disabled)") + c.metrics.Errors.With("type", "backend_failed").Add(1) + if c.connectionNotifier != nil { + notifyErr := c.connectionNotifier.NotifyFailedBackendConnection(ctx, clientAddr, serverAddress, playerInfo, backendHostPort, err) + if notifyErr != nil { + logrus.WithError(notifyErr).Warn("failed to notify failed backend connection") + } + } + return + } + } + } else { + // Unknown state, do nothing logrus. WithField("client", clientAddr). WithField("serverAddress", serverAddress). WithField("nextState", nextState). WithField("player", playerInfo). Warn("Unknown state, unable to connect to backend") - return - } + return + } if c.connectionNotifier != nil { err := c.connectionNotifier.NotifyConnected(ctx, clientAddr, serverAddress, playerInfo, backendHostPort) From 0132bac7a20a46d1c54e550e774a973d45e61bb7 Mon Sep 17 00:00:00 2001 From: alexfrs69 Date: Wed, 30 Apr 2025 21:36:24 +0200 Subject: [PATCH 04/13] refactor: use retry library --- go.mod | 1 + go.sum | 2 + server/connector.go | 148 +++++++++++++++++--------------------------- 3 files changed, 61 insertions(+), 90 deletions(-) diff --git a/go.mod b/go.mod index 6b67d8d..0265fbb 100644 --- a/go.mod +++ b/go.mod @@ -50,6 +50,7 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect + github.com/sethvargo/go-retry v0.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect diff --git a/go.sum b/go.sum index dac882d..7658647 100644 --- a/go.sum +++ b/go.sum @@ -160,6 +160,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= +github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= diff --git a/server/connector.go b/server/connector.go index 2c74fa8..6b17995 100644 --- a/server/connector.go +++ b/server/connector.go @@ -21,12 +21,13 @@ import ( "github.com/juju/ratelimit" "github.com/pires/go-proxyproto" "github.com/sirupsen/logrus" + "github.com/sethvargo/go-retry" ) const ( handshakeTimeout = 5 * time.Second backendTimeout = 30 * time.Second - backendRetryInterval = 5 * time.Second + backendRetryInterval = 3 * time.Second backendStatusTimeout = 1 * time.Second ) @@ -485,110 +486,77 @@ func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net. WithField("player", playerInfo). Info("Connecting to backend") - var backendConn net.Conn - var err error - - if nextState == mcproto.StateStatus { - // Status request: try to connect once with backendStatusTimeout - backendConn, err = net.DialTimeout("tcp", backendHostPort, backendStatusTimeout) - if err != nil { + // We want to try to connect to the backend every backendRetryInterval + var backendTry retry.Backoff + + switch nextState { + case mcproto.StateStatus: + // Status request: try to connect once with backendStatusTimeout + backendTry = retry.NewConstant(backendStatusTimeout) + backendTry = retry.WithMaxRetries(0, backendTry) + case mcproto.StateLogin: + backendTry = retry.NewConstant(backendRetryInterval) + // Connect request: if autoscaler is enabled, try to connect until backendTimeout is reached + if waker != nil { + // Autoscaler enabled: retry until backendTimeout is reached + backendTry = retry.WithMaxDuration(backendTimeout, backendTry) + } else { + // Autoscaler disabled: try to connect once with backendRetryInterval + backendTry = retry.WithMaxRetries(0, backendTry) + } + default: + // Unknown state, do nothing logrus. - WithError(err). WithField("client", clientAddr). WithField("serverAddress", serverAddress). - WithField("backend", backendHostPort). + WithField("nextState", nextState). WithField("player", playerInfo). - Warn("Unable to connect to backend for status request") - c.metrics.Errors.With("type", "backend_failed").Add(1) + Error("Unknown state, unable to connect to backend") + return + } - if c.connectionNotifier != nil { - notifyErr := c.connectionNotifier.NotifyFailedBackendConnection(ctx, clientAddr, serverAddress, playerInfo, backendHostPort, err) - if notifyErr != nil { - logrus.WithError(notifyErr).Warn("failed to notify failed backend connection") - } - } + var backendConn net.Conn + if retryErr := retry.Do(ctx, backendTry, func(ctx context.Context) error { + logrus.Debug("Attempting to connect") + var err error + backendConn, err = net.Dial("tcp", backendHostPort) + if err != nil { return retry.RetryableError(err) } + return nil + }); retryErr != nil { + logrus. + WithError(retryErr). + WithField("client", clientAddr). + WithField("serverAddress", serverAddress). + WithField("backend", backendHostPort). + WithField("player", playerInfo). + Warn("Unable to connect to backend") + c.metrics.Errors.With("type", "backend_failed").Add(1) - // Return fakeOnline MOTD - if c.fakeOnline && waker != nil { - logrus.Info("Server is offline, sending fakeOnlineMOTD for status request") - writeStatusErr := mcproto.WriteStatusResponse( - frontendConn, - c.fakeOnlineMOTD, - ) - if writeStatusErr != nil { - logrus. - WithError(writeStatusErr). - WithField("client", clientAddr). - WithField("serverAddress", serverAddress). - WithField("backend", backendHostPort). - WithField("player", playerInfo). - Error("Failed to write status response") - } + if c.connectionNotifier != nil { + notifyErr := c.connectionNotifier.NotifyFailedBackendConnection(ctx, clientAddr, serverAddress, playerInfo, backendHostPort, retryErr) + if notifyErr != nil { + logrus.WithError(notifyErr).Warn("failed to notify failed backend connection") } - return } - } else if nextState == mcproto.StateLogin { - // Connect request - if waker != nil { - logrus.Debug("Connect: Autoscaler is enabled, waiting for backend to be ready") - // Autoscaler enabled: retry until backendTimeout is reached - deadline := time.Now().Add(backendTimeout) - for { - backendConn, err = net.DialTimeout("tcp", backendHostPort, backendRetryInterval) - logrus.Debug("Tries to connect to backend") - - if err == nil { - break - } - if time.Now().After(deadline) { - logrus. - WithError(err). - WithField("client", clientAddr). - WithField("serverAddress", serverAddress). - WithField("backend", backendHostPort). - WithField("player", playerInfo). - Warn("Unable to connect to backend after retries (autoscaler enabled)") - c.metrics.Errors.With("type", "backend_failed").Add(1) - if c.connectionNotifier != nil { - notifyErr := c.connectionNotifier.NotifyFailedBackendConnection(ctx, clientAddr, serverAddress, playerInfo, backendHostPort, err) - if notifyErr != nil { - logrus.WithError(notifyErr).Warn("failed to notify failed backend connection") - } - } - return - } - time.Sleep(backendRetryInterval) - } - } else { - logrus.Debug("Connect: Autoscaler is disabled, trying to connect once") - // Autoscaler disabled: try to connect once with backendTimeout - backendConn, err = net.DialTimeout("tcp", backendHostPort, backendTimeout) - if err != nil { + + if nextState == mcproto.StateStatus && c.fakeOnline && waker != nil { + logrus.Info("Server is offline, sending fakeOnlineMOTD for status request") + writeStatusErr := mcproto.WriteStatusResponse( + frontendConn, + c.fakeOnlineMOTD, + ) + + if writeStatusErr != nil { logrus. - WithError(err). + WithError(writeStatusErr). WithField("client", clientAddr). WithField("serverAddress", serverAddress). WithField("backend", backendHostPort). WithField("player", playerInfo). - Warn("Unable to connect to backend (autoscaler disabled)") - c.metrics.Errors.With("type", "backend_failed").Add(1) - if c.connectionNotifier != nil { - notifyErr := c.connectionNotifier.NotifyFailedBackendConnection(ctx, clientAddr, serverAddress, playerInfo, backendHostPort, err) - if notifyErr != nil { - logrus.WithError(notifyErr).Warn("failed to notify failed backend connection") - } - } - return + Error("Failed to write status response") } } - } else { - // Unknown state, do nothing - logrus. - WithField("client", clientAddr). - WithField("serverAddress", serverAddress). - WithField("nextState", nextState). - WithField("player", playerInfo). - Warn("Unknown state, unable to connect to backend") + return } From b0a5a2e93cec4ea4fdfe701f2e5670e2b2966b3a Mon Sep 17 00:00:00 2001 From: alexfrs69 Date: Thu, 1 May 2025 21:32:55 +0200 Subject: [PATCH 05/13] refactor: bundle config for connector --- cmd/mc-router/main.go | 13 ++++++++++++- server/connector.go | 21 +++++++-------------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/cmd/mc-router/main.go b/cmd/mc-router/main.go index b7f6069..ac71b4d 100644 --- a/cmd/mc-router/main.go +++ b/cmd/mc-router/main.go @@ -170,7 +170,18 @@ func main() { trustedIpNets = append(trustedIpNets, ipNet) } - connector := server.NewConnector(metricsBuilder.BuildConnectorMetrics(), config.UseProxyProtocol, config.ReceiveProxyProtocol, trustedIpNets, config.RecordLogins, autoScaleUpAllowDenyConfig, config.FakeOnline, config.FakeOnlineMOTD) + connectorConfig := server.ConnectorConfig{ + SendProxyProto: config.UseProxyProtocol, + ReceiveProxyProto: config.ReceiveProxyProtocol, + TrustedProxyNets: trustedIpNets, + RecordLogins: config.RecordLogins, + AutoScaleUpAllowDenyConfig: autoScaleUpAllowDenyConfig, + AutoScaleUp: config.AutoScaleUp, + FakeOnline: config.FakeOnline, + FakeOnlineMOTD: config.FakeOnlineMOTD, + } + + connector := server.NewConnector(metricsBuilder.BuildConnectorMetrics(), connectorConfig) clientFilter, err := server.NewClientFilter(config.ClientsToAllow, config.ClientsToDeny) if err != nil { diff --git a/server/connector.go b/server/connector.go index 6b17995..4474ff6 100644 --- a/server/connector.go +++ b/server/connector.go @@ -116,7 +116,6 @@ func NewConnector(metrics *ConnectorMetrics, sendProxyProto bool, receiveProxyPr return &Connector{ metrics: metrics, - sendProxyProto: sendProxyProto, connectionsCond: sync.NewCond(&sync.Mutex{}), receiveProxyProto: receiveProxyProto, trustedProxyNets: trustedProxyNets, @@ -129,20 +128,14 @@ func NewConnector(metrics *ConnectorMetrics, sendProxyProto bool, receiveProxyPr type Connector struct { state mcproto.State metrics *ConnectorMetrics - sendProxyProto bool - receiveProxyProto bool - recordLogins bool - trustedProxyNets []*net.IPNet activeConnections int32 serverMetrics *ServerMetrics connectionsCond *sync.Cond ngrokToken string clientFilter *ClientFilter - autoScaleUpAllowDenyConfig *AllowDenyConfig - fakeOnline bool - fakeOnlineMOTD string + config ConnectorConfig connectionNotifier ConnectionNotifier } @@ -187,7 +180,7 @@ func (c *Connector) createListener(ctx context.Context, listenAddress string) (n } logrus.WithField("listenAddress", listenAddress).Info("Listening for Minecraft client connections") - if c.receiveProxyProto { + if c.config.ReceiveProxyProto { proxyListener := &proxyproto.Listener{ Listener: listener, Policy: c.createProxyProtoPolicy(), @@ -201,7 +194,7 @@ func (c *Connector) createListener(ctx context.Context, listenAddress string) (n func (c *Connector) createProxyProtoPolicy() func(upstream net.Addr) (proxyproto.Policy, error) { return func(upstream net.Addr) (proxyproto.Policy, error) { - trustedIpNets := c.trustedProxyNets + trustedIpNets := c.config.TrustedProxyNets if len(trustedIpNets) == 0 { logrus.Debug("No trusted proxy networks configured, using the PROXY header by default") @@ -497,7 +490,7 @@ func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net. case mcproto.StateLogin: backendTry = retry.NewConstant(backendRetryInterval) // Connect request: if autoscaler is enabled, try to connect until backendTimeout is reached - if waker != nil { + if c.config.AutoScaleUp { // Autoscaler enabled: retry until backendTimeout is reached backendTry = retry.WithMaxDuration(backendTimeout, backendTry) } else { @@ -539,11 +532,11 @@ func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net. } } - if nextState == mcproto.StateStatus && c.fakeOnline && waker != nil { + if nextState == mcproto.StateStatus && c.config.FakeOnline && c.config.AutoScaleUp { logrus.Info("Server is offline, sending fakeOnlineMOTD for status request") writeStatusErr := mcproto.WriteStatusResponse( frontendConn, - c.fakeOnlineMOTD, + c.config.FakeOnlineMOTD, ) if writeStatusErr != nil { @@ -600,7 +593,7 @@ func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net. cleanupMetrics = true // PROXY protocol implementation - if c.sendProxyProto { + if c.config.SendProxyProto { // Determine transport protocol for the PROXY header by "analyzing" the frontend connection's address transportProtocol := proxyproto.TCPv4 From e2e7b91462f2b4bd66e719c75256c9ea8e7eb5b3 Mon Sep 17 00:00:00 2001 From: alexfrs69 Date: Thu, 1 May 2025 21:35:16 +0200 Subject: [PATCH 06/13] fix: fmt --- cmd/mc-router/main.go | 18 +++---- mcproto/types.go | 26 +++++----- mcproto/write.go | 76 ++++++++++++++--------------- server/allow_deny_list.go | 6 +-- server/allow_deny_list_test.go | 30 ++++++------ server/connector.go | 87 ++++++++++++++++++---------------- 6 files changed, 123 insertions(+), 120 deletions(-) diff --git a/cmd/mc-router/main.go b/cmd/mc-router/main.go index ac71b4d..1d51c8e 100644 --- a/cmd/mc-router/main.go +++ b/cmd/mc-router/main.go @@ -71,8 +71,8 @@ type Config struct { SimplifySRV bool `default:"false" usage:"Simplify fully qualified SRV records for mapping"` - FakeOnline bool `default:"false" usage:"Enable fake online MOTD when backend is offline and auto-scale-up is enabled"` - FakeOnlineMOTD string `default:"Server is sleeping\nJoin to wake it up" usage:"Custom MOTD to show when backend is offline and auto-scale-up is enabled"` + FakeOnline bool `default:"false" usage:"Enable fake online MOTD when backend is offline and auto-scale-up is enabled"` + FakeOnlineMOTD string `default:"Server is sleeping\nJoin to wake it up" usage:"Custom MOTD to show when backend is offline and auto-scale-up is enabled"` Webhook WebhookConfig `usage:"Webhook configuration"` } @@ -171,14 +171,14 @@ func main() { } connectorConfig := server.ConnectorConfig{ - SendProxyProto: config.UseProxyProtocol, - ReceiveProxyProto: config.ReceiveProxyProtocol, - TrustedProxyNets: trustedIpNets, - RecordLogins: config.RecordLogins, + SendProxyProto: config.UseProxyProtocol, + ReceiveProxyProto: config.ReceiveProxyProtocol, + TrustedProxyNets: trustedIpNets, + RecordLogins: config.RecordLogins, AutoScaleUpAllowDenyConfig: autoScaleUpAllowDenyConfig, - AutoScaleUp: config.AutoScaleUp, - FakeOnline: config.FakeOnline, - FakeOnlineMOTD: config.FakeOnlineMOTD, + AutoScaleUp: config.AutoScaleUp, + FakeOnline: config.FakeOnline, + FakeOnlineMOTD: config.FakeOnlineMOTD, } connector := server.NewConnector(metricsBuilder.BuildConnectorMetrics(), connectorConfig) diff --git a/mcproto/types.go b/mcproto/types.go index 4e7b48e..d20042c 100644 --- a/mcproto/types.go +++ b/mcproto/types.go @@ -86,28 +86,28 @@ const ( ) type StatusResponse struct { - Version StatusVersion `json:"version"` - Players StatusPlayers `json:"players"` - Description StatusText `json:"description"` - Favicon string `json:"favicon,omitempty"` + Version StatusVersion `json:"version"` + Players StatusPlayers `json:"players"` + Description StatusText `json:"description"` + Favicon string `json:"favicon,omitempty"` } type StatusVersion struct { - Name string `json:"name"` - Protocol int `json:"protocol"` + Name string `json:"name"` + Protocol int `json:"protocol"` } type StatusPlayers struct { - Max int `json:"max"` - Online int `json:"online"` - Sample []PlayerEntry `json:"sample,omitempty"` + Max int `json:"max"` + Online int `json:"online"` + Sample []PlayerEntry `json:"sample,omitempty"` } type PlayerEntry struct { - Name string `json:"name"` - ID string `json:"id"` + Name string `json:"name"` + ID string `json:"id"` } type StatusText struct { - Text string `json:"text"` -} \ No newline at end of file + Text string `json:"text"` +} diff --git a/mcproto/write.go b/mcproto/write.go index 16a800f..8088ab7 100644 --- a/mcproto/write.go +++ b/mcproto/write.go @@ -6,49 +6,49 @@ import ( ) func WriteStatusResponse(w io.Writer, motd string) error { - resp := StatusResponse{ - Version: StatusVersion{ - Name: "1.21.5", - Protocol: 770, - }, - Players: StatusPlayers{ - Max: 0, - Online: 0, - }, - Description: StatusText{ - Text: motd, - }, - } - data, err := json.Marshal(resp) - if err != nil { - return err - } + resp := StatusResponse{ + Version: StatusVersion{ + Name: "1.21.5", + Protocol: 770, + }, + Players: StatusPlayers{ + Max: 0, + Online: 0, + }, + Description: StatusText{ + Text: motd, + }, + } + data, err := json.Marshal(resp) + if err != nil { + return err + } - jsonLen := encodeVarInt(len(data)) - payload := append(jsonLen, data...) - return WritePacket(w, 0x00, payload) + jsonLen := encodeVarInt(len(data)) + payload := append(jsonLen, data...) + return WritePacket(w, 0x00, payload) } func WritePacket(w io.Writer, packetID int, data []byte) error { - packet := append(encodeVarInt(packetID), data...) - length := encodeVarInt(len(packet)) - _, err := w.Write(append(length, packet...)) - return err + packet := append(encodeVarInt(packetID), data...) + length := encodeVarInt(len(packet)) + _, err := w.Write(append(length, packet...)) + return err } // encodeVarInt encodes an int as a Minecraft VarInt. func encodeVarInt(value int) []byte { - var buf []byte - for { - temp := byte(value & 0x7F) - value >>= 7 - if value != 0 { - temp |= 0x80 - } - buf = append(buf, temp) - if value == 0 { - break - } - } - return buf -} \ No newline at end of file + var buf []byte + for { + temp := byte(value & 0x7F) + value >>= 7 + if value != 0 { + temp |= 0x80 + } + buf = append(buf, temp) + if value == 0 { + break + } + } + return buf +} diff --git a/server/allow_deny_list.go b/server/allow_deny_list.go index 3ad3198..9e23319 100644 --- a/server/allow_deny_list.go +++ b/server/allow_deny_list.go @@ -8,11 +8,11 @@ import ( type AllowDenyLists struct { Allowlist []PlayerInfo - Denylist []PlayerInfo + Denylist []PlayerInfo } type AllowDenyConfig struct { - Global AllowDenyLists + Global AllowDenyLists Servers map[string]AllowDenyLists } @@ -35,7 +35,7 @@ func entryMatchesPlayer(entry *PlayerInfo, userInfo *PlayerInfo) bool { if entry.Name == "" && entry.Uuid == uuid.Nil { return false } - + if entry.Name != "" && entry.Uuid != uuid.Nil { return *entry == *userInfo } diff --git a/server/allow_deny_list_test.go b/server/allow_deny_list_test.go index 45f500e..268ad1a 100644 --- a/server/allow_deny_list_test.go +++ b/server/allow_deny_list_test.go @@ -10,7 +10,7 @@ import ( func Test_allowDenyConfig_ServerAllowsPlayer(t *testing.T) { type args struct { serverAddress string - userInfo *PlayerInfo + userInfo *PlayerInfo } validUserInfo := &PlayerInfo{ Name: "player_name", @@ -27,20 +27,20 @@ func Test_allowDenyConfig_ServerAllowsPlayer(t *testing.T) { want bool }{ { - name: "nil config", + name: "nil config", allowDenyConfig: nil, args: args{ serverAddress: "server.my.domain", - userInfo: validUserInfo, + userInfo: validUserInfo, }, want: true, }, { - name: "empty config", + name: "empty config", allowDenyConfig: &AllowDenyConfig{}, args: args{ serverAddress: "server.my.domain", - userInfo: validUserInfo, + userInfo: validUserInfo, }, want: true, }, @@ -58,7 +58,7 @@ func Test_allowDenyConfig_ServerAllowsPlayer(t *testing.T) { }, args: args{ serverAddress: "server.my.domain", - userInfo: validUserInfo, + userInfo: validUserInfo, }, want: false, }, @@ -73,7 +73,7 @@ func Test_allowDenyConfig_ServerAllowsPlayer(t *testing.T) { }, args: args{ serverAddress: "server.my.domain", - userInfo: validUserInfo, + userInfo: validUserInfo, }, want: true, }, @@ -88,7 +88,7 @@ func Test_allowDenyConfig_ServerAllowsPlayer(t *testing.T) { }, args: args{ serverAddress: "server.my.domain", - userInfo: validUserInfo, + userInfo: validUserInfo, }, want: false, }, @@ -103,7 +103,7 @@ func Test_allowDenyConfig_ServerAllowsPlayer(t *testing.T) { }, args: args{ serverAddress: "server.my.domain", - userInfo: validUserInfo, + userInfo: validUserInfo, }, want: false, }, @@ -121,7 +121,7 @@ func Test_allowDenyConfig_ServerAllowsPlayer(t *testing.T) { }, args: args{ serverAddress: "server.my.domain", - userInfo: validUserInfo, + userInfo: validUserInfo, }, want: true, }, @@ -138,7 +138,7 @@ func Test_allowDenyConfig_ServerAllowsPlayer(t *testing.T) { }, args: args{ serverAddress: "server.my.domain", - userInfo: validUserInfo, + userInfo: validUserInfo, }, want: true, }, @@ -155,7 +155,7 @@ func Test_allowDenyConfig_ServerAllowsPlayer(t *testing.T) { }, args: args{ serverAddress: "server.my.domain", - userInfo: validUserInfo, + userInfo: validUserInfo, }, want: false, }, @@ -172,7 +172,7 @@ func Test_allowDenyConfig_ServerAllowsPlayer(t *testing.T) { }, args: args{ serverAddress: "server.my.domain", - userInfo: validUserInfo, + userInfo: validUserInfo, }, want: false, }, @@ -194,7 +194,7 @@ func Test_allowDenyConfig_ServerAllowsPlayer(t *testing.T) { }, args: args{ serverAddress: "server.my.domain", - userInfo: validUserInfo, + userInfo: validUserInfo, }, want: true, }, @@ -216,7 +216,7 @@ func Test_allowDenyConfig_ServerAllowsPlayer(t *testing.T) { }, args: args{ serverAddress: "server.my.domain", - userInfo: validUserInfo, + userInfo: validUserInfo, }, want: true, }, diff --git a/server/connector.go b/server/connector.go index 4474ff6..880a550 100644 --- a/server/connector.go +++ b/server/connector.go @@ -6,13 +6,14 @@ import ( "context" "errors" "fmt" - "github.com/google/uuid" "io" "net" "sync" "sync/atomic" "time" + "github.com/google/uuid" + "golang.ngrok.com/ngrok" "golang.ngrok.com/ngrok/config" @@ -20,13 +21,13 @@ import ( "github.com/itzg/mc-router/mcproto" "github.com/juju/ratelimit" "github.com/pires/go-proxyproto" - "github.com/sirupsen/logrus" "github.com/sethvargo/go-retry" + "github.com/sirupsen/logrus" ) const ( - handshakeTimeout = 5 * time.Second - backendTimeout = 30 * time.Second + handshakeTimeout = 5 * time.Second + backendTimeout = 30 * time.Second backendRetryInterval = 3 * time.Second backendStatusTimeout = 1 * time.Second ) @@ -126,16 +127,16 @@ func NewConnector(metrics *ConnectorMetrics, sendProxyProto bool, receiveProxyPr } type Connector struct { - state mcproto.State - metrics *ConnectorMetrics + state mcproto.State + metrics *ConnectorMetrics - activeConnections int32 - serverMetrics *ServerMetrics - connectionsCond *sync.Cond - ngrokToken string - clientFilter *ClientFilter + activeConnections int32 + serverMetrics *ServerMetrics + connectionsCond *sync.Cond + ngrokToken string + clientFilter *ClientFilter - config ConnectorConfig + config ConnectorConfig connectionNotifier ConnectionNotifier } @@ -481,31 +482,31 @@ func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net. // We want to try to connect to the backend every backendRetryInterval var backendTry retry.Backoff - + switch nextState { - case mcproto.StateStatus: - // Status request: try to connect once with backendStatusTimeout - backendTry = retry.NewConstant(backendStatusTimeout) + case mcproto.StateStatus: + // Status request: try to connect once with backendStatusTimeout + backendTry = retry.NewConstant(backendStatusTimeout) + backendTry = retry.WithMaxRetries(0, backendTry) + case mcproto.StateLogin: + backendTry = retry.NewConstant(backendRetryInterval) + // Connect request: if autoscaler is enabled, try to connect until backendTimeout is reached + if c.config.AutoScaleUp { + // Autoscaler enabled: retry until backendTimeout is reached + backendTry = retry.WithMaxDuration(backendTimeout, backendTry) + } else { + // Autoscaler disabled: try to connect once with backendRetryInterval backendTry = retry.WithMaxRetries(0, backendTry) - case mcproto.StateLogin: - backendTry = retry.NewConstant(backendRetryInterval) - // Connect request: if autoscaler is enabled, try to connect until backendTimeout is reached - if c.config.AutoScaleUp { - // Autoscaler enabled: retry until backendTimeout is reached - backendTry = retry.WithMaxDuration(backendTimeout, backendTry) - } else { - // Autoscaler disabled: try to connect once with backendRetryInterval - backendTry = retry.WithMaxRetries(0, backendTry) - } - default: - // Unknown state, do nothing - logrus. - WithField("client", clientAddr). - WithField("serverAddress", serverAddress). - WithField("nextState", nextState). - WithField("player", playerInfo). - Error("Unknown state, unable to connect to backend") - return + } + default: + // Unknown state, do nothing + logrus. + WithField("client", clientAddr). + WithField("serverAddress", serverAddress). + WithField("nextState", nextState). + WithField("player", playerInfo). + Error("Unknown state, unable to connect to backend") + return } var backendConn net.Conn @@ -513,16 +514,18 @@ func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net. logrus.Debug("Attempting to connect") var err error backendConn, err = net.Dial("tcp", backendHostPort) - if err != nil { return retry.RetryableError(err) } + if err != nil { + return retry.RetryableError(err) + } return nil }); retryErr != nil { logrus. - WithError(retryErr). - WithField("client", clientAddr). - WithField("serverAddress", serverAddress). - WithField("backend", backendHostPort). - WithField("player", playerInfo). - Warn("Unable to connect to backend") + WithError(retryErr). + WithField("client", clientAddr). + WithField("serverAddress", serverAddress). + WithField("backend", backendHostPort). + WithField("player", playerInfo). + Warn("Unable to connect to backend") c.metrics.Errors.With("type", "backend_failed").Add(1) if c.connectionNotifier != nil { From 9f3262cf3119defdcb011762592a683a8b09786a Mon Sep 17 00:00:00 2001 From: alexfrs69 Date: Fri, 2 May 2025 00:55:28 +0200 Subject: [PATCH 07/13] feat: add status cache --- cmd/mc-router/main.go | 27 ++++++++-- go.mod | 1 + go.sum | 55 +++++++++++++++++++ mcproto/write.go | 17 +----- server/cache.go | 122 ++++++++++++++++++++++++++++++++++++++++++ server/connector.go | 65 +++++++++++++++++----- 6 files changed, 256 insertions(+), 31 deletions(-) create mode 100644 server/cache.go diff --git a/cmd/mc-router/main.go b/cmd/mc-router/main.go index 1d51c8e..a4006cc 100644 --- a/cmd/mc-router/main.go +++ b/cmd/mc-router/main.go @@ -74,6 +74,9 @@ type Config struct { FakeOnline bool `default:"false" usage:"Enable fake online MOTD when backend is offline and auto-scale-up is enabled"` FakeOnlineMOTD string `default:"Server is sleeping\nJoin to wake it up" usage:"Custom MOTD to show when backend is offline and auto-scale-up is enabled"` + CacheStatus bool `default:"false" usage:"Cache status response for backends"` + CacheStatusInterval string `default:"30s" usage:"Interval to update the status cache"` + Webhook WebhookConfig `usage:"Webhook configuration"` } @@ -141,7 +144,6 @@ func main() { // Only one instance should be created server.DownScaler = server.NewDownScaler(ctx, downScalerEnabled, downScalerDelay) - c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) @@ -175,10 +177,11 @@ func main() { ReceiveProxyProto: config.ReceiveProxyProtocol, TrustedProxyNets: trustedIpNets, RecordLogins: config.RecordLogins, - AutoScaleUpAllowDenyConfig: autoScaleUpAllowDenyConfig, - AutoScaleUp: config.AutoScaleUp, + AutoScaleUpAllowDenyConfig: autoScaleAllowDenyConfig, + AutoScaleUp: config.AutoScale.Up, FakeOnline: config.FakeOnline, FakeOnlineMOTD: config.FakeOnlineMOTD, + CacheStatus: config.CacheStatus, } connector := server.NewConnector(metricsBuilder.BuildConnectorMetrics(), connectorConfig) @@ -198,6 +201,15 @@ func main() { server.NewWebhookNotifier(config.Webhook.Url, config.Webhook.RequireUser)) } + var cacheInterval time.Duration + if config.CacheStatus { + cacheInterval, err = time.ParseDuration(config.CacheStatusInterval) + if err != nil { + logrus.WithError(err).Fatal("Unable to parse cache status interval") + } + logrus.WithField("interval", config.CacheStatusInterval).Info("Using cache status interval") + } + if config.NgrokToken != "" { connector.UseNgrok(config.NgrokToken) } @@ -254,6 +266,15 @@ func main() { logrus.WithError(err).Fatal("Unable to start metrics reporter") } + if config.CacheStatus { + logrus.Info("Starting status cache updater") + connector.StatusCache.StartUpdater(connector, cacheInterval, func() map[string]string { + mappings := server.Routes.GetMappings() + logrus.WithField("mappings", mappings).Debug("Status cache updater") + return mappings + }) + } + // wait for process-stop signal <-c logrus.Info("Stopping. Waiting for connections to complete...") diff --git a/go.mod b/go.mod index 0265fbb..f77d726 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( ) require ( + github.com/Raqbit/mc-pinger v0.2.4 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/containerd/log v0.1.0 // indirect diff --git a/go.sum b/go.sum index 7658647..4b17c7a 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,15 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Raqbit/mc-pinger v0.0.0-20190414124050-6c3fd84faaa1 h1:s2UZugpc4wHQMBYoUI6wi2HE9ZR3TI8pmJrZpNhVFvU= +github.com/Raqbit/mc-pinger v0.0.0-20190414124050-6c3fd84faaa1/go.mod h1:r2rVvqOwaYCU3rYUNeSmCiJbcX2wJkDisXH7rZsjjuM= +github.com/Raqbit/mc-pinger v0.2.4 h1:s1iR1qQ/tGSktwPAmn8Lj94pjvn9xreipA++60ksmnw= +github.com/Raqbit/mc-pinger v0.2.4/go.mod h1:AeR7Gd9CW5VbYA5xA9vy0pvbWLOFoV8p8HP5/zpFthQ= github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= @@ -72,6 +78,9 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/subcommands v1.0.1 h1:/eqq+otEXm5vhfBrbREPCSVQbvofip6kIz+mX5TUH7k= +github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= @@ -80,6 +89,7 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rH github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/inconshreveable/log15 v3.0.0-testing.5+incompatible h1:VryeOTiaZfAzwx8xBcID1KlJCeoWSIpsNbSk+/D2LNk= @@ -88,8 +98,17 @@ github.com/inconshreveable/log15/v3 v3.0.0-testing.5 h1:h4e0f3kjgg+RJBlKOabrohjH github.com/inconshreveable/log15/v3 v3.0.0-testing.5/go.mod h1:3GQg1SVrLoWGfRv/kAZMsdyU5cp8eFc1P3cw+Wwku94= github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSHzRbhzK8RdXOsAdfDgO49TtqC1oZ+acxPrkfTxcCs= github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/influxdata/line-protocol v0.0.0-20190509173118-5712a8124a9a h1:p2OJKXyrNEo7OefeU+JNp1dpCOZJ8AOpJScpNK/MGDI= +github.com/influxdata/line-protocol v0.0.0-20190509173118-5712a8124a9a/go.mod h1:4kt73NQhadE3daL3WhR5EJ/J2ocX0PZzwxQ0gXJ7oFE= +github.com/itzg/go-flagsfiller v1.4.1/go.mod h1:mfQgTahSs4OHn8PYev2Wwi1LJXUiYiGuZVCpBLxzbYs= github.com/itzg/go-flagsfiller v1.15.0 h1:xspqfbiifTo1qnCpExtfkMN5fSfueB0nMsOsazcTETw= github.com/itzg/go-flagsfiller v1.15.0/go.mod h1:nR3jrF1gVJ7ZUfSews6/oPbXjBTG3ziIHfLaXstmxjE= +github.com/itzg/line-protocol-sender v0.1.1 h1:UA01VBt3/whRxpwO425w60pdNmgjnGV1tseR4qh6mC0= +github.com/itzg/line-protocol-sender v0.1.1/go.mod h1:Cd948iZ7YibnGcLt5D/11RfKmteh8lQyXpGUbY97WBw= +github.com/itzg/mc-monitor v0.1.6 h1:oNwWWqiFQ1TH/f+/aMrtmDXf76+Kdvyl/hlP0c/DqrA= +github.com/itzg/mc-monitor v0.1.6/go.mod h1:s5WgxgvI/H+lwtgSml9EvmP+rwQ40cpVuXDHd6EgfF4= +github.com/itzg/zapconfigs v0.1.0 h1:Gokocm8VaTNnZjvIiVA5NEhzZ1v7lEyXY/AbeBmq6YQ= +github.com/itzg/zapconfigs v0.1.0/go.mod h1:y4dArgRUOFbGRkUNJ8XSSw98FGn03wtkvMPy+OSA5Rc= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= @@ -102,6 +121,7 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -158,6 +178,7 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= @@ -173,6 +194,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -183,6 +205,7 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 h1:sv9kVfal0MK0wBMCOGr+HeJm9v803BkJxGrk2au7j08= @@ -201,24 +224,40 @@ go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= +go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= golang.ngrok.com/muxado/v2 v2.0.1 h1:jM9i6Pom6GGmnPrHKNR6OJRrUoHFkSZlJ3/S0zqdVpY= golang.ngrok.com/muxado/v2 v2.0.1/go.mod h1:wzxJYX4xiAtmwumzL+QsukVwFRXmPNv86vB8RPpOxyM= golang.ngrok.com/ngrok v1.13.0 h1:6SeOS+DAeIaHlkDmNH5waFHv0xjlavOV3wml0Z59/8k= golang.ngrok.com/ngrok v1.13.0/go.mod h1:BKOMdoZXfD4w6o3EtE7Cu9TVbaUWBqptrZRWnVcAuI4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= @@ -226,19 +265,24 @@ golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbht golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -248,11 +292,16 @@ golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -267,20 +316,26 @@ google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo= gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= diff --git a/mcproto/write.go b/mcproto/write.go index 8088ab7..1d5d8b6 100644 --- a/mcproto/write.go +++ b/mcproto/write.go @@ -5,21 +5,8 @@ import ( "io" ) -func WriteStatusResponse(w io.Writer, motd string) error { - resp := StatusResponse{ - Version: StatusVersion{ - Name: "1.21.5", - Protocol: 770, - }, - Players: StatusPlayers{ - Max: 0, - Online: 0, - }, - Description: StatusText{ - Text: motd, - }, - } - data, err := json.Marshal(resp) +func WriteStatusResponse(w io.Writer, status *StatusResponse) error { + data, err := json.Marshal(status) if err != nil { return err } diff --git a/server/cache.go b/server/cache.go new file mode 100644 index 0000000..8f64f2c --- /dev/null +++ b/server/cache.go @@ -0,0 +1,122 @@ +package server + +import ( + "net" + "strconv" + "sync" + "time" + + "github.com/itzg/mc-router/mcproto" + "github.com/sirupsen/logrus" + + mcpinger "github.com/Raqbit/mc-pinger" +) + +// CachedStatus holds the cached status response for a backend. +type CachedStatus struct { + Version mcproto.StatusVersion + Description mcproto.StatusText + Favicon string + Players mcproto.StatusPlayers + LastUpdated time.Time +} + +type StatusCache struct { + mu sync.RWMutex + cache map[string]*CachedStatus // key: serverAddress + ttl time.Duration +} + +func NewStatusCache(ttl time.Duration) *StatusCache { + return &StatusCache{ + cache: make(map[string]*CachedStatus), + ttl: ttl, + } +} + +func (sc *StatusCache) Get(serverAddress string) (*CachedStatus, bool) { + sc.mu.RLock() + defer sc.mu.RUnlock() + status, ok := sc.cache[serverAddress] + if !ok || time.Since(status.LastUpdated) > sc.ttl { + return nil, false + } + return status, true +} + +func (sc *StatusCache) Set(serverAddress string, status *CachedStatus) { + sc.mu.Lock() + defer sc.mu.Unlock() + sc.cache[serverAddress] = status +} + +func (sc *StatusCache) Delete(serverAddress string) { + sc.mu.Lock() + defer sc.mu.Unlock() + delete(sc.cache, serverAddress) +} + +func (sc *StatusCache) updateAll(getBackends func() map[string]string) { + for serverAddress, backendAddress := range getBackends() { + status, err := fetchBackendStatus(backendAddress) + if err == nil { + sc.Set(serverAddress, status) + } + } +} + +func (sc *StatusCache) StartUpdater(connector *Connector, interval time.Duration, getBackends func() map[string]string) { + // Update the status cache immediately + sc.updateAll(getBackends) + + // Start a goroutine to periodically update the status cache + go func() { + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + <-ticker.C + sc.updateAll(getBackends) + } + }() +} + +// fetchBackendStatus connects to the backend and retrieves its status. +func fetchBackendStatus(serverAddress string) (*CachedStatus, error) { + address, port, splitErr := net.SplitHostPort(serverAddress) + if splitErr != nil { + logrus. + WithError(splitErr). + WithField("serverAddress", serverAddress). + Error("Failed to split server address") + return nil, splitErr + } + + portInt, atoiErr := strconv.Atoi(port) + if atoiErr != nil { + logrus. + WithError(atoiErr). + WithField("serverAddress", serverAddress). + Error("Failed to convert port to int") + return nil, atoiErr + } + + // Create a new pinger instance with the address and port + pinger := mcpinger.New(address, uint16(portInt), mcpinger.WithTimeout(5*time.Second)) + + info, err := pinger.Ping() + if err != nil { + logrus. + WithError(err). + WithField("serverAddress", serverAddress). + Error("Failed to ping backend server") + return nil, err + } + + return &CachedStatus{ + Version: mcproto.StatusVersion{Name: info.Version.Name, Protocol: int(info.Version.Protocol)}, + Description: mcproto.StatusText{Text: info.Description.Text}, + Favicon: info.Favicon, + Players: mcproto.StatusPlayers{Max: int(info.Players.Max), Online: 0, Sample: []mcproto.PlayerEntry{}}, + LastUpdated: time.Now(), + }, nil +} diff --git a/server/connector.go b/server/connector.go index 880a550..ff79654 100644 --- a/server/connector.go +++ b/server/connector.go @@ -30,6 +30,7 @@ const ( backendTimeout = 30 * time.Second backendRetryInterval = 3 * time.Second backendStatusTimeout = 1 * time.Second + cacheTTL = 5 * time.Minute ) var noDeadline time.Time @@ -113,19 +114,29 @@ func (sm *ServerMetrics) ActiveConnectionsValue(serverAddress string) int { return 0 } -func NewConnector(metrics *ConnectorMetrics, sendProxyProto bool, receiveProxyProto bool, trustedProxyNets []*net.IPNet, recordLogins bool, autoScaleUpAllowDenyConfig *AllowDenyConfig) *Connector { +func NewConnector(metrics *ConnectorMetrics, cfg ConnectorConfig) *Connector { return &Connector{ - metrics: metrics, - connectionsCond: sync.NewCond(&sync.Mutex{}), - receiveProxyProto: receiveProxyProto, - trustedProxyNets: trustedProxyNets, - recordLogins: recordLogins, - autoScaleUpAllowDenyConfig: autoScaleUpAllowDenyConfig, - serverMetrics: NewServerMetrics(), + metrics: metrics, + connectionsCond: sync.NewCond(&sync.Mutex{}), + config: cfg, + serverMetrics: NewServerMetrics(), + StatusCache: NewStatusCache(backendTimeout), } } +type ConnectorConfig struct { + SendProxyProto bool + ReceiveProxyProto bool + TrustedProxyNets []*net.IPNet + RecordLogins bool + AutoScaleUpAllowDenyConfig *AllowDenyConfig + AutoScaleUp bool + FakeOnline bool + FakeOnlineMOTD string + CacheStatus bool +} + type Connector struct { state mcproto.State metrics *ConnectorMetrics @@ -138,6 +149,8 @@ type Connector struct { config ConnectorConfig + StatusCache *StatusCache + connectionNotifier ConnectionNotifier } @@ -410,7 +423,7 @@ func (c *Connector) cleanupBackendConnection(ctx context.Context, clientAddr net With("server_address", serverAddress). Set(float64(c.serverMetrics.ActiveConnectionsValue(serverAddress))) - if c.recordLogins && playerInfo != nil { + if c.config.RecordLogins && playerInfo != nil { c.metrics.ServerActivePlayer. With("player_name", playerInfo.Name). With("player_uuid", playerInfo.Uuid.String()). @@ -436,7 +449,7 @@ func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net. }() if waker != nil && nextState > mcproto.StateStatus { - serverAllowsPlayer := c.autoScaleUpAllowDenyConfig.ServerAllowsPlayer(serverAddress, playerInfo) + serverAllowsPlayer := c.config.AutoScaleUpAllowDenyConfig.ServerAllowsPlayer(serverAddress, playerInfo) logrus. WithField("client", clientAddr). WithField("server", serverAddress). @@ -536,10 +549,36 @@ func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net. } if nextState == mcproto.StateStatus && c.config.FakeOnline && c.config.AutoScaleUp { - logrus.Info("Server is offline, sending fakeOnlineMOTD for status request") + logrus.Info("Server is offline, sending fakeOnlineMOTD or cache for status request") + + var status *mcproto.StatusResponse + + if c.config.CacheStatus { + cachedStatus, ok := c.StatusCache.Get(serverAddress) + if ok { + logrus.WithField("cachedStatus", cachedStatus).Debug("Using cached status") + status = &mcproto.StatusResponse{ + Players: cachedStatus.Players, + Description: cachedStatus.Description, + Favicon: cachedStatus.Favicon, + } + } else { + logrus.WithField("serverAddress", serverAddress).Debug("No cached status found") + } + } + + if status == nil { + status = &mcproto.StatusResponse{ + Version: mcproto.StatusVersion{Name: "1.20.2", Protocol: 770}, + Players: mcproto.StatusPlayers{Max: 0, Online: 0}, + Description: mcproto.StatusText{Text: c.config.FakeOnlineMOTD}, + Favicon: "", + } + } + writeStatusErr := mcproto.WriteStatusResponse( frontendConn, - c.config.FakeOnlineMOTD, + status, ) if writeStatusErr != nil { @@ -573,7 +612,7 @@ func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net. With("server_address", serverAddress). Set(float64(c.serverMetrics.ActiveConnectionsValue(serverAddress))) - if c.recordLogins && playerInfo != nil { + if c.config.RecordLogins && playerInfo != nil { logrus. WithField("client", clientAddr). WithField("player", playerInfo). From 9ef54926535eb0902214237186debbb977863601 Mon Sep 17 00:00:00 2001 From: alexfrs69 Date: Sat, 3 May 2025 03:48:54 +0200 Subject: [PATCH 08/13] fix: test --- server/connector_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/connector_test.go b/server/connector_test.go index 357aa44..5d6f71b 100644 --- a/server/connector_test.go +++ b/server/connector_test.go @@ -56,7 +56,9 @@ func TestTrustedProxyNetworkPolicy(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { c := &Connector{ - trustedProxyNets: parseTrustedProxyNets(test.trustedNets), + config: ConnectorConfig{ + TrustedProxyNets: parseTrustedProxyNets(test.trustedNets), + }, } policy := c.createProxyProtoPolicy() From 8e71b2a83225b4d6c10920f3895e4b88ec3c3266 Mon Sep 17 00:00:00 2001 From: alexfrs69 Date: Sat, 3 May 2025 16:20:32 +0200 Subject: [PATCH 09/13] fix: enable fakeOnline only if autoscale up && kube --- cmd/mc-router/main.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/mc-router/main.go b/cmd/mc-router/main.go index a4006cc..404ff1f 100644 --- a/cmd/mc-router/main.go +++ b/cmd/mc-router/main.go @@ -172,6 +172,8 @@ func main() { trustedIpNets = append(trustedIpNets, ipNet) } + fakeOnlineEnabled := config.FakeOnline && config.AutoScale.Up && (config.InKubeCluster || config.KubeConfig != "") + connectorConfig := server.ConnectorConfig{ SendProxyProto: config.UseProxyProtocol, ReceiveProxyProto: config.ReceiveProxyProtocol, @@ -179,7 +181,7 @@ func main() { RecordLogins: config.RecordLogins, AutoScaleUpAllowDenyConfig: autoScaleAllowDenyConfig, AutoScaleUp: config.AutoScale.Up, - FakeOnline: config.FakeOnline, + FakeOnline: fakeOnlineEnabled, FakeOnlineMOTD: config.FakeOnlineMOTD, CacheStatus: config.CacheStatus, } From 861322b322e5a6ea512f458a565ada9c1e78b30c Mon Sep 17 00:00:00 2001 From: alexfrs69 Date: Sat, 3 May 2025 16:31:28 +0200 Subject: [PATCH 10/13] fix: logs --- server/connector.go | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/server/connector.go b/server/connector.go index ff79654..62cd0f0 100644 --- a/server/connector.go +++ b/server/connector.go @@ -448,7 +448,7 @@ func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net. c.cleanupBackendConnection(ctx, clientAddr, serverAddress, playerInfo, backendHostPort, cleanupMetrics, cleanupCheckScaleDown) }() - if waker != nil && nextState > mcproto.StateStatus { + if c.config.AutoScaleUp && waker != nil && nextState > mcproto.StateStatus { serverAllowsPlayer := c.config.AutoScaleUpAllowDenyConfig.ServerAllowsPlayer(serverAddress, playerInfo) logrus. WithField("client", clientAddr). @@ -524,7 +524,14 @@ func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net. var backendConn net.Conn if retryErr := retry.Do(ctx, backendTry, func(ctx context.Context) error { - logrus.Debug("Attempting to connect") + logrus. + WithField("client", clientAddr). + WithField("serverAddress", serverAddress). + WithField("backend", backendHostPort). + WithField("player", playerInfo). + WithField("nextState", nextState). + WithField("retryInterval", backendTry). + Debug("Attempting to connect to backend") var err error backendConn, err = net.Dial("tcp", backendHostPort) if err != nil { @@ -549,14 +556,23 @@ func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net. } if nextState == mcproto.StateStatus && c.config.FakeOnline && c.config.AutoScaleUp { - logrus.Info("Server is offline, sending fakeOnlineMOTD or cache for status request") + logrus. + WithField("client", clientAddr). + WithField("serverAddress", serverAddress). + WithField("backend", backendHostPort). + Info("Server is offline, sending fakeOnlineMOTD or cache for status request") var status *mcproto.StatusResponse if c.config.CacheStatus { cachedStatus, ok := c.StatusCache.Get(serverAddress) if ok { - logrus.WithField("cachedStatus", cachedStatus).Debug("Using cached status") + logrus. + WithField("cachedStatus", cachedStatus). + WithField("client", clientAddr). + WithField("serverAddress", serverAddress). + WithField("backend", backendHostPort). + Debug("Using cached status") status = &mcproto.StatusResponse{ Players: cachedStatus.Players, Description: cachedStatus.Description, From 9cb38c618c0dc4ed8d5b065341cf0d39f7c2966a Mon Sep 17 00:00:00 2001 From: alexfrs69 Date: Sun, 4 May 2025 00:24:44 +0200 Subject: [PATCH 11/13] refactor: separate functions --- cmd/mc-router/main.go | 34 ++++--- server/cache.go | 37 ++++--- server/connector.go | 217 ++++++++++++++++++++++-------------------- 3 files changed, 147 insertions(+), 141 deletions(-) diff --git a/cmd/mc-router/main.go b/cmd/mc-router/main.go index 404ff1f..5fe1e47 100644 --- a/cmd/mc-router/main.go +++ b/cmd/mc-router/main.go @@ -34,10 +34,14 @@ type WebhookConfig struct { } type AutoScale struct { - Up bool `usage:"Increase Kubernetes StatefulSet Replicas (only) from 0 to 1 on respective backend servers when accessed"` - Down bool `default:"false" usage:"Decrease Kubernetes StatefulSet Replicas (only) from 1 to 0 on respective backend servers after there are no connections"` - DownAfter string `default:"10m" usage:"Server scale down delay after there are no connections"` - AllowDeny string `usage:"Path to config for server allowlists and denylists. If a global/server entry is specified, only players allowed to connect to the server will be able to trigger a scale up when -auto-scale-up is enabled or cancel active down scalers when -auto-scale-down is enabled"` + Up bool `usage:"Increase Kubernetes StatefulSet Replicas (only) from 0 to 1 on respective backend servers when accessed"` + Down bool `default:"false" usage:"Decrease Kubernetes StatefulSet Replicas (only) from 1 to 0 on respective backend servers after there are no connections"` + DownAfter string `default:"10m" usage:"Server scale down delay after there are no connections"` + AllowDeny string `usage:"Path to config for server allowlists and denylists. If a global/server entry is specified, only players allowed to connect to the server will be able to trigger a scale up when -auto-scale-up is enabled or cancel active down scalers when -auto-scale-down is enabled"` + FakeOnline bool `default:"false" usage:"Enable fake online status when backend is offline and auto-scale-up is enabled"` + FakeOnlineMOTD string `default:"Server is sleeping\nJoin to wake it up" usage:"Custom MOTD to show when backend is offline, status has been cached and auto-scale-up is enabled"` + CacheStatus bool `default:"false" usage:"Cache status response for backends"` + CacheStatusInterval string `default:"30s" usage:"Interval to update the status cache"` } type Config struct { @@ -71,12 +75,6 @@ type Config struct { SimplifySRV bool `default:"false" usage:"Simplify fully qualified SRV records for mapping"` - FakeOnline bool `default:"false" usage:"Enable fake online MOTD when backend is offline and auto-scale-up is enabled"` - FakeOnlineMOTD string `default:"Server is sleeping\nJoin to wake it up" usage:"Custom MOTD to show when backend is offline and auto-scale-up is enabled"` - - CacheStatus bool `default:"false" usage:"Cache status response for backends"` - CacheStatusInterval string `default:"30s" usage:"Interval to update the status cache"` - Webhook WebhookConfig `usage:"Webhook configuration"` } @@ -172,7 +170,7 @@ func main() { trustedIpNets = append(trustedIpNets, ipNet) } - fakeOnlineEnabled := config.FakeOnline && config.AutoScale.Up && (config.InKubeCluster || config.KubeConfig != "") + fakeOnlineEnabled := config.AutoScale.FakeOnline && config.AutoScale.Up && (config.InKubeCluster || config.KubeConfig != "") connectorConfig := server.ConnectorConfig{ SendProxyProto: config.UseProxyProtocol, @@ -182,8 +180,8 @@ func main() { AutoScaleUpAllowDenyConfig: autoScaleAllowDenyConfig, AutoScaleUp: config.AutoScale.Up, FakeOnline: fakeOnlineEnabled, - FakeOnlineMOTD: config.FakeOnlineMOTD, - CacheStatus: config.CacheStatus, + FakeOnlineMOTD: config.AutoScale.FakeOnlineMOTD, + CacheStatus: config.AutoScale.CacheStatus, } connector := server.NewConnector(metricsBuilder.BuildConnectorMetrics(), connectorConfig) @@ -204,12 +202,12 @@ func main() { } var cacheInterval time.Duration - if config.CacheStatus { - cacheInterval, err = time.ParseDuration(config.CacheStatusInterval) + if config.AutoScale.CacheStatus { + cacheInterval, err = time.ParseDuration(config.AutoScale.CacheStatusInterval) if err != nil { logrus.WithError(err).Fatal("Unable to parse cache status interval") } - logrus.WithField("interval", config.CacheStatusInterval).Info("Using cache status interval") + logrus.WithField("interval", config.AutoScale.CacheStatusInterval).Debug("Using cache status interval") } if config.NgrokToken != "" { @@ -268,11 +266,11 @@ func main() { logrus.WithError(err).Fatal("Unable to start metrics reporter") } - if config.CacheStatus { + if config.AutoScale.CacheStatus { logrus.Info("Starting status cache updater") connector.StatusCache.StartUpdater(connector, cacheInterval, func() map[string]string { mappings := server.Routes.GetMappings() - logrus.WithField("mappings", mappings).Debug("Status cache updater") + logrus.WithField("mappings", mappings).Debug("Updating status cache with mappings") return mappings }) } diff --git a/server/cache.go b/server/cache.go index 8f64f2c..7a8cd49 100644 --- a/server/cache.go +++ b/server/cache.go @@ -12,39 +12,30 @@ import ( mcpinger "github.com/Raqbit/mc-pinger" ) -// CachedStatus holds the cached status response for a backend. -type CachedStatus struct { - Version mcproto.StatusVersion - Description mcproto.StatusText - Favicon string - Players mcproto.StatusPlayers - LastUpdated time.Time -} - type StatusCache struct { mu sync.RWMutex - cache map[string]*CachedStatus // key: serverAddress + cache map[string]*mcproto.StatusResponse // key: serverAddress ttl time.Duration } func NewStatusCache(ttl time.Duration) *StatusCache { return &StatusCache{ - cache: make(map[string]*CachedStatus), + cache: make(map[string]*mcproto.StatusResponse), ttl: ttl, } } -func (sc *StatusCache) Get(serverAddress string) (*CachedStatus, bool) { +func (sc *StatusCache) Get(serverAddress string) (*mcproto.StatusResponse, bool) { sc.mu.RLock() defer sc.mu.RUnlock() status, ok := sc.cache[serverAddress] - if !ok || time.Since(status.LastUpdated) > sc.ttl { + if !ok { return nil, false } return status, true } -func (sc *StatusCache) Set(serverAddress string, status *CachedStatus) { +func (sc *StatusCache) Set(serverAddress string, status *mcproto.StatusResponse) { sc.mu.Lock() defer sc.mu.Unlock() sc.cache[serverAddress] = status @@ -58,6 +49,11 @@ func (sc *StatusCache) Delete(serverAddress string) { func (sc *StatusCache) updateAll(getBackends func() map[string]string) { for serverAddress, backendAddress := range getBackends() { + logrus. + WithField("serverAddress", serverAddress). + WithField("backendAddress", backendAddress). + Debug("Updating status cache") + status, err := fetchBackendStatus(backendAddress) if err == nil { sc.Set(serverAddress, status) @@ -81,12 +77,12 @@ func (sc *StatusCache) StartUpdater(connector *Connector, interval time.Duration } // fetchBackendStatus connects to the backend and retrieves its status. -func fetchBackendStatus(serverAddress string) (*CachedStatus, error) { - address, port, splitErr := net.SplitHostPort(serverAddress) +func fetchBackendStatus(backendHost string) (*mcproto.StatusResponse, error) { + address, port, splitErr := net.SplitHostPort(backendHost) if splitErr != nil { logrus. WithError(splitErr). - WithField("serverAddress", serverAddress). + WithField("backend", backendHost). Error("Failed to split server address") return nil, splitErr } @@ -95,7 +91,7 @@ func fetchBackendStatus(serverAddress string) (*CachedStatus, error) { if atoiErr != nil { logrus. WithError(atoiErr). - WithField("serverAddress", serverAddress). + WithField("serverAddress", backendHost). Error("Failed to convert port to int") return nil, atoiErr } @@ -107,16 +103,15 @@ func fetchBackendStatus(serverAddress string) (*CachedStatus, error) { if err != nil { logrus. WithError(err). - WithField("serverAddress", serverAddress). + WithField("backend", backendHost). Error("Failed to ping backend server") return nil, err } - return &CachedStatus{ + return &mcproto.StatusResponse{ Version: mcproto.StatusVersion{Name: info.Version.Name, Protocol: int(info.Version.Protocol)}, Description: mcproto.StatusText{Text: info.Description.Text}, Favicon: info.Favicon, Players: mcproto.StatusPlayers{Max: int(info.Players.Max), Online: 0, Sample: []mcproto.PlayerEntry{}}, - LastUpdated: time.Now(), }, nil } diff --git a/server/connector.go b/server/connector.go index 62cd0f0..7971832 100644 --- a/server/connector.go +++ b/server/connector.go @@ -493,54 +493,13 @@ func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net. WithField("player", playerInfo). Info("Connecting to backend") - // We want to try to connect to the backend every backendRetryInterval - var backendTry retry.Backoff - - switch nextState { - case mcproto.StateStatus: - // Status request: try to connect once with backendStatusTimeout - backendTry = retry.NewConstant(backendStatusTimeout) - backendTry = retry.WithMaxRetries(0, backendTry) - case mcproto.StateLogin: - backendTry = retry.NewConstant(backendRetryInterval) - // Connect request: if autoscaler is enabled, try to connect until backendTimeout is reached - if c.config.AutoScaleUp { - // Autoscaler enabled: retry until backendTimeout is reached - backendTry = retry.WithMaxDuration(backendTimeout, backendTry) - } else { - // Autoscaler disabled: try to connect once with backendRetryInterval - backendTry = retry.WithMaxRetries(0, backendTry) - } - default: - // Unknown state, do nothing - logrus. - WithField("client", clientAddr). - WithField("serverAddress", serverAddress). - WithField("nextState", nextState). - WithField("player", playerInfo). - Error("Unknown state, unable to connect to backend") - return - } + // Try to connect to the backend with a different logic depending on the state and the auto-scaling + backendConn, err := c.retryBackendConnection(ctx, backendHostPort, nextState) - var backendConn net.Conn - if retryErr := retry.Do(ctx, backendTry, func(ctx context.Context) error { - logrus. - WithField("client", clientAddr). - WithField("serverAddress", serverAddress). - WithField("backend", backendHostPort). - WithField("player", playerInfo). - WithField("nextState", nextState). - WithField("retryInterval", backendTry). - Debug("Attempting to connect to backend") - var err error - backendConn, err = net.Dial("tcp", backendHostPort) - if err != nil { - return retry.RetryableError(err) - } - return nil - }); retryErr != nil { + // Failed to connect to the backend + if err != nil { logrus. - WithError(retryErr). + WithError(err). WithField("client", clientAddr). WithField("serverAddress", serverAddress). WithField("backend", backendHostPort). @@ -549,75 +508,28 @@ func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net. c.metrics.Errors.With("type", "backend_failed").Add(1) if c.connectionNotifier != nil { - notifyErr := c.connectionNotifier.NotifyFailedBackendConnection(ctx, clientAddr, serverAddress, playerInfo, backendHostPort, retryErr) + notifyErr := c.connectionNotifier.NotifyFailedBackendConnection(ctx, clientAddr, serverAddress, playerInfo, backendHostPort, err) if notifyErr != nil { logrus.WithError(notifyErr).Warn("failed to notify failed backend connection") } } - if nextState == mcproto.StateStatus && c.config.FakeOnline && c.config.AutoScaleUp { - logrus. - WithField("client", clientAddr). - WithField("serverAddress", serverAddress). - WithField("backend", backendHostPort). - Info("Server is offline, sending fakeOnlineMOTD or cache for status request") - - var status *mcproto.StatusResponse - - if c.config.CacheStatus { - cachedStatus, ok := c.StatusCache.Get(serverAddress) - if ok { - logrus. - WithField("cachedStatus", cachedStatus). - WithField("client", clientAddr). - WithField("serverAddress", serverAddress). - WithField("backend", backendHostPort). - Debug("Using cached status") - status = &mcproto.StatusResponse{ - Players: cachedStatus.Players, - Description: cachedStatus.Description, - Favicon: cachedStatus.Favicon, - } - } else { - logrus.WithField("serverAddress", serverAddress).Debug("No cached status found") - } + if c.connectionNotifier != nil { + err := c.connectionNotifier.NotifyConnected(ctx, clientAddr, serverAddress, playerInfo, backendHostPort) + if err != nil { + logrus.WithError(err).Warn("failed to notify connected") } - if status == nil { - status = &mcproto.StatusResponse{ - Version: mcproto.StatusVersion{Name: "1.20.2", Protocol: 770}, - Players: mcproto.StatusPlayers{Max: 0, Online: 0}, - Description: mcproto.StatusText{Text: c.config.FakeOnlineMOTD}, - Favicon: "", - } - } + } - writeStatusErr := mcproto.WriteStatusResponse( - frontendConn, - status, - ) - - if writeStatusErr != nil { - logrus. - WithError(writeStatusErr). - WithField("client", clientAddr). - WithField("serverAddress", serverAddress). - WithField("backend", backendHostPort). - WithField("player", playerInfo). - Error("Failed to write status response") - } + // If the backend is offline and we are in status state, we can send a fake online status + if nextState == mcproto.StateStatus && c.config.FakeOnline { + c.sendFakeOnlineStatus(frontendConn, serverAddress) } return } - if c.connectionNotifier != nil { - err := c.connectionNotifier.NotifyConnected(ctx, clientAddr, serverAddress, playerInfo, backendHostPort) - if err != nil { - logrus.WithError(err).Warn("failed to notify connected") - } - } - c.metrics.ConnectionsBackend.With("host", resolvedHost).Add(1) c.metrics.ActiveConnections.Set(float64( @@ -736,6 +648,107 @@ func (c *Connector) pumpConnections(ctx context.Context, frontendConn, backendCo } } +func (c *Connector) retryBackendConnection(ctx context.Context, backendHostPort string, nextState mcproto.State) (net.Conn, error) { + // We want to try to connect to the backend every backendRetryInterval + var backendTry retry.Backoff + + // Set the retry timeouts based on the next state and autoscaler + switch nextState { + case mcproto.StateStatus: + // Status request: try to connect once with backendStatusTimeout + backendTry = retry.NewConstant(backendStatusTimeout) + backendTry = retry.WithMaxRetries(0, backendTry) + case mcproto.StateLogin: + backendTry = retry.NewConstant(backendRetryInterval) + // Connect request: if autoscaler is enabled, try to connect until backendTimeout is reached + if c.config.AutoScaleUp { + // Autoscaler enabled: retry until backendTimeout is reached + backendTry = retry.WithMaxDuration(backendTimeout, backendTry) + } else { + // Autoscaler disabled: try to connect once with backendRetryInterval + backendTry = retry.WithMaxRetries(0, backendTry) + } + default: + // Unknown state, return error + logrus. + WithField("backend", backendHostPort). + WithField("nextState", nextState). + Error("Unknown state, unable to connect to backend") + return nil, fmt.Errorf("unknown state: %d", nextState) + } + + var backendConn net.Conn + if err := retry.Do(ctx, backendTry, func(ctx context.Context) error { + logrus. + WithField("backend", backendHostPort). + WithField("nextState", nextState). + Debug("Attempting to connect to backend") + + var err error + backendConn, err = net.Dial("tcp", backendHostPort) + if err != nil { + return retry.RetryableError(err) + } + return nil + }); err != nil { + return nil, err + } + + return backendConn, nil +} + +func (c *Connector) getFakeOnlineStatus(serverAddress string) *mcproto.StatusResponse { + // Try to get the status from the cache + status, hit := c.StatusCache.Get(serverAddress) + if !hit { + logrus. + WithField("serverAddress", serverAddress). + Debug("Failed to get status from cache, sending default status") + + // If we can't get the status from the cache, send a default status + return &mcproto.StatusResponse{ + Version: mcproto.StatusVersion{ + Name: "UNKNOWN", + Protocol: 0, + }, + Players: mcproto.StatusPlayers{ + Max: 0, + Online: 0, + Sample: []mcproto.PlayerEntry{}, + }, + Description: mcproto.StatusText{ + Text: c.config.FakeOnlineMOTD, + }, + } + } + + logrus. + WithField("serverAddress", serverAddress). + Debug("Fetched status from cache") + + // We got the status from the cache + return status +} + +func (c *Connector) sendFakeOnlineStatus(frontendConn net.Conn, serverAddress string) { + // Get the fake online status + status := c.getFakeOnlineStatus(serverAddress) + // Send the status to the client + if err := mcproto.WriteStatusResponse(frontendConn, status); err != nil { + logrus. + WithError(err). + WithField("client", frontendConn.RemoteAddr()). + WithField("status", status). + Error("Failed to send fake online status") + return + } + + logrus. + WithField("client", frontendConn.RemoteAddr()). + WithField("status", status). + Debug("Sent fake online status") +} + func (c *Connector) pumpFrames(incoming io.Reader, outgoing io.Writer, errors chan<- error, from, to string, clientAddr net.Addr, playerInfo *PlayerInfo) { amount, err := io.Copy(outgoing, incoming) From 8b025520f97d019c7ad14afb4967a37566346108 Mon Sep 17 00:00:00 2001 From: alexfrs69 Date: Sun, 4 May 2025 00:28:16 +0200 Subject: [PATCH 12/13] fix: remove ttl from cache --- server/cache.go | 4 +--- server/connector.go | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/server/cache.go b/server/cache.go index 7a8cd49..ae1c4d4 100644 --- a/server/cache.go +++ b/server/cache.go @@ -15,13 +15,11 @@ import ( type StatusCache struct { mu sync.RWMutex cache map[string]*mcproto.StatusResponse // key: serverAddress - ttl time.Duration } -func NewStatusCache(ttl time.Duration) *StatusCache { +func NewStatusCache() *StatusCache { return &StatusCache{ cache: make(map[string]*mcproto.StatusResponse), - ttl: ttl, } } diff --git a/server/connector.go b/server/connector.go index 7971832..cbf2b12 100644 --- a/server/connector.go +++ b/server/connector.go @@ -30,7 +30,6 @@ const ( backendTimeout = 30 * time.Second backendRetryInterval = 3 * time.Second backendStatusTimeout = 1 * time.Second - cacheTTL = 5 * time.Minute ) var noDeadline time.Time @@ -121,7 +120,7 @@ func NewConnector(metrics *ConnectorMetrics, cfg ConnectorConfig) *Connector { connectionsCond: sync.NewCond(&sync.Mutex{}), config: cfg, serverMetrics: NewServerMetrics(), - StatusCache: NewStatusCache(backendTimeout), + StatusCache: NewStatusCache(), } } From 096cdb6bd83a05bc13877eac057ca02af10d5840 Mon Sep 17 00:00:00 2001 From: alexfrs69 Date: Sun, 4 May 2025 00:30:23 +0200 Subject: [PATCH 13/13] fix: `StatusPlayerEntry` --- mcproto/types.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mcproto/types.go b/mcproto/types.go index d20042c..172107f 100644 --- a/mcproto/types.go +++ b/mcproto/types.go @@ -2,6 +2,7 @@ package mcproto import ( "fmt" + "github.com/google/uuid" ) @@ -98,12 +99,12 @@ type StatusVersion struct { } type StatusPlayers struct { - Max int `json:"max"` - Online int `json:"online"` - Sample []PlayerEntry `json:"sample,omitempty"` + Max int `json:"max"` + Online int `json:"online"` + Sample []StatusPlayerEntry `json:"sample,omitempty"` } -type PlayerEntry struct { +type StatusPlayerEntry struct { Name string `json:"name"` ID string `json:"id"` }