diff --git a/cmd/mc-router/main.go b/cmd/mc-router/main.go index 9bf7c95..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 { @@ -138,7 +142,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) @@ -167,7 +170,21 @@ func main() { trustedIpNets = append(trustedIpNets, ipNet) } - connector := server.NewConnector(metricsBuilder.BuildConnectorMetrics(), config.UseProxyProtocol, config.ReceiveProxyProtocol, trustedIpNets, config.RecordLogins, autoScaleAllowDenyConfig) + fakeOnlineEnabled := config.AutoScale.FakeOnline && config.AutoScale.Up && (config.InKubeCluster || config.KubeConfig != "") + + connectorConfig := server.ConnectorConfig{ + SendProxyProto: config.UseProxyProtocol, + ReceiveProxyProto: config.ReceiveProxyProtocol, + TrustedProxyNets: trustedIpNets, + RecordLogins: config.RecordLogins, + AutoScaleUpAllowDenyConfig: autoScaleAllowDenyConfig, + AutoScaleUp: config.AutoScale.Up, + FakeOnline: fakeOnlineEnabled, + FakeOnlineMOTD: config.AutoScale.FakeOnlineMOTD, + CacheStatus: config.AutoScale.CacheStatus, + } + + connector := server.NewConnector(metricsBuilder.BuildConnectorMetrics(), connectorConfig) clientFilter, err := server.NewClientFilter(config.ClientsToAllow, config.ClientsToDeny) if err != nil { @@ -184,6 +201,15 @@ func main() { server.NewWebhookNotifier(config.Webhook.Url, config.Webhook.RequireUser)) } + var cacheInterval time.Duration + 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.AutoScale.CacheStatusInterval).Debug("Using cache status interval") + } + if config.NgrokToken != "" { connector.UseNgrok(config.NgrokToken) } @@ -240,6 +266,15 @@ func main() { logrus.WithError(err).Fatal("Unable to start metrics reporter") } + 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("Updating status cache with mappings") + 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 6b67d8d..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 @@ -50,6 +51,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..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,8 +178,11 @@ 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= +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= @@ -171,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= @@ -181,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= @@ -199,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= @@ -224,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= @@ -246,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= @@ -265,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/types.go b/mcproto/types.go index 9ff0f2f..172107f 100644 --- a/mcproto/types.go +++ b/mcproto/types.go @@ -2,6 +2,7 @@ package mcproto import ( "fmt" + "github.com/google/uuid" ) @@ -84,3 +85,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 []StatusPlayerEntry `json:"sample,omitempty"` +} + +type StatusPlayerEntry struct { + Name string `json:"name"` + ID string `json:"id"` +} + +type StatusText struct { + Text string `json:"text"` +} diff --git a/mcproto/write.go b/mcproto/write.go new file mode 100644 index 0000000..1d5d8b6 --- /dev/null +++ b/mcproto/write.go @@ -0,0 +1,41 @@ +package mcproto + +import ( + "encoding/json" + "io" +) + +func WriteStatusResponse(w io.Writer, status *StatusResponse) error { + data, err := json.Marshal(status) + 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 +} 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/cache.go b/server/cache.go new file mode 100644 index 0000000..ae1c4d4 --- /dev/null +++ b/server/cache.go @@ -0,0 +1,115 @@ +package server + +import ( + "net" + "strconv" + "sync" + "time" + + "github.com/itzg/mc-router/mcproto" + "github.com/sirupsen/logrus" + + mcpinger "github.com/Raqbit/mc-pinger" +) + +type StatusCache struct { + mu sync.RWMutex + cache map[string]*mcproto.StatusResponse // key: serverAddress +} + +func NewStatusCache() *StatusCache { + return &StatusCache{ + cache: make(map[string]*mcproto.StatusResponse), + } +} + +func (sc *StatusCache) Get(serverAddress string) (*mcproto.StatusResponse, bool) { + sc.mu.RLock() + defer sc.mu.RUnlock() + status, ok := sc.cache[serverAddress] + if !ok { + return nil, false + } + return status, true +} + +func (sc *StatusCache) Set(serverAddress string, status *mcproto.StatusResponse) { + 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() { + logrus. + WithField("serverAddress", serverAddress). + WithField("backendAddress", backendAddress). + Debug("Updating status cache") + + 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(backendHost string) (*mcproto.StatusResponse, error) { + address, port, splitErr := net.SplitHostPort(backendHost) + if splitErr != nil { + logrus. + WithError(splitErr). + WithField("backend", backendHost). + Error("Failed to split server address") + return nil, splitErr + } + + portInt, atoiErr := strconv.Atoi(port) + if atoiErr != nil { + logrus. + WithError(atoiErr). + WithField("serverAddress", backendHost). + 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("backend", backendHost). + Error("Failed to ping backend server") + return nil, err + } + + 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{}}, + }, nil +} diff --git a/server/connector.go b/server/connector.go index d805462..cbf2b12 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,11 +21,15 @@ import ( "github.com/itzg/mc-router/mcproto" "github.com/juju/ratelimit" "github.com/pires/go-proxyproto" + "github.com/sethvargo/go-retry" "github.com/sirupsen/logrus" ) const ( - handshakeTimeout = 5 * time.Second + handshakeTimeout = 5 * time.Second + backendTimeout = 30 * time.Second + backendRetryInterval = 3 * time.Second + backendStatusTimeout = 1 * time.Second ) var noDeadline time.Time @@ -108,34 +113,42 @@ 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, - sendProxyProto: sendProxyProto, - 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(), } } +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 - sendProxyProto bool - receiveProxyProto bool - recordLogins bool - trustedProxyNets []*net.IPNet - - activeConnections int32 - serverMetrics *ServerMetrics - connectionsCond *sync.Cond - ngrokToken string - clientFilter *ClientFilter - autoScaleUpAllowDenyConfig *AllowDenyConfig + state mcproto.State + metrics *ConnectorMetrics + + activeConnections int32 + serverMetrics *ServerMetrics + connectionsCond *sync.Cond + ngrokToken string + clientFilter *ClientFilter + + config ConnectorConfig + + StatusCache *StatusCache connectionNotifier ConnectionNotifier } @@ -180,7 +193,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(), @@ -194,7 +207,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") @@ -409,7 +422,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()). @@ -434,8 +447,8 @@ func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net. c.cleanupBackendConnection(ctx, clientAddr, serverAddress, playerInfo, backendHostPort, cleanupMetrics, cleanupCheckScaleDown) }() - if waker != nil && nextState > mcproto.StateStatus { - serverAllowsPlayer := c.autoScaleUpAllowDenyConfig.ServerAllowsPlayer(serverAddress, playerInfo) + if c.config.AutoScaleUp && waker != nil && nextState > mcproto.StateStatus { + serverAllowsPlayer := c.config.AutoScaleUpAllowDenyConfig.ServerAllowsPlayer(serverAddress, playerInfo) logrus. WithField("client", clientAddr). WithField("server", serverAddress). @@ -479,7 +492,10 @@ func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net. WithField("player", playerInfo). Info("Connecting to backend") - backendConn, err := net.Dial("tcp", backendHostPort) + // 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) + + // Failed to connect to the backend if err != nil { logrus. WithError(err). @@ -497,14 +513,20 @@ func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net. } } - 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") + } - 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 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 } c.metrics.ConnectionsBackend.With("host", resolvedHost).Add(1) @@ -517,7 +539,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). @@ -540,7 +562,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 @@ -625,6 +647,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) 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()