Refactored server setup and run out of main (#425)
This commit is contained in:
+34
-220
@@ -3,70 +3,14 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"runtime/pprof"
|
|
||||||
"strconv"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/itzg/go-flagsfiller"
|
"github.com/itzg/go-flagsfiller"
|
||||||
"github.com/itzg/mc-router/server"
|
"github.com/itzg/mc-router/server"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WebhookConfig struct {
|
|
||||||
Url string `usage:"If set, a POST request that contains connection status notifications will be sent to this HTTP address"`
|
|
||||||
RequireUser bool `default:"false" usage:"Indicates if the webhook will only be called if a user is connecting rather than just server list/ping"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RoutesConfig struct {
|
|
||||||
Config string `usage:"Name or full [path] to routes config file"`
|
|
||||||
ConfigWatch bool `usage:"Watch for config file changes"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Port int `default:"25565" usage:"The [port] bound to listen for Minecraft client connections"`
|
|
||||||
Default string `usage:"host:port of a default Minecraft server to use when mapping not found"`
|
|
||||||
Mapping map[string]string `usage:"Comma or newline delimited or repeated mappings of externalHostname=host:port"`
|
|
||||||
ApiBinding string `usage:"The [host:port] bound for servicing API requests"`
|
|
||||||
Version bool `usage:"Output version and exit"`
|
|
||||||
CpuProfile string `usage:"Enables CPU profiling and writes to given path"`
|
|
||||||
Debug bool `usage:"Enable debug logs"`
|
|
||||||
ConnectionRateLimit int `default:"1" usage:"Max number of connections to allow per second"`
|
|
||||||
InKubeCluster bool `usage:"Use in-cluster Kubernetes config"`
|
|
||||||
KubeConfig string `usage:"The path to a Kubernetes configuration file"`
|
|
||||||
InDocker bool `usage:"Use Docker service discovery"`
|
|
||||||
InDockerSwarm bool `usage:"Use Docker Swarm service discovery"`
|
|
||||||
DockerSocket string `default:"unix:///var/run/docker.sock" usage:"Path to Docker socket to use"`
|
|
||||||
DockerTimeout int `default:"0" usage:"Timeout configuration in seconds for the Docker integrations"`
|
|
||||||
DockerRefreshInterval int `default:"15" usage:"Refresh interval in seconds for the Docker integrations"`
|
|
||||||
MetricsBackend string `default:"discard" usage:"Backend to use for metrics exposure/publishing: discard,expvar,influxdb,prometheus"`
|
|
||||||
MetricsBackendConfig server.MetricsBackendConfig
|
|
||||||
UseProxyProtocol bool `default:"false" usage:"Send PROXY protocol to backend servers"`
|
|
||||||
ReceiveProxyProtocol bool `default:"false" usage:"Receive PROXY protocol from backend servers, by default trusts every proxy header that it receives, combine with -trusted-proxies to specify a list of trusted proxies"`
|
|
||||||
TrustedProxies []string `usage:"Comma delimited list of CIDR notation IP blocks to trust when receiving PROXY protocol"`
|
|
||||||
RecordLogins bool `default:"false" usage:"Log and generate metrics on player logins. Metrics only supported with influxdb or prometheus backend"`
|
|
||||||
Routes RoutesConfig
|
|
||||||
NgrokToken string `usage:"If set, an ngrok tunnel will be established. It is HIGHLY recommended to pass as an environment variable."`
|
|
||||||
AutoScale AutoScale
|
|
||||||
|
|
||||||
ClientsToAllow []string `usage:"Zero or more client IP addresses or CIDRs to allow. Takes precedence over deny."`
|
|
||||||
ClientsToDeny []string `usage:"Zero or more client IP addresses or CIDRs to deny. Ignored if any configured to allow"`
|
|
||||||
|
|
||||||
SimplifySRV bool `default:"false" usage:"Simplify fully qualified SRV records for mapping"`
|
|
||||||
|
|
||||||
Webhook WebhookConfig `usage:"Webhook configuration"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
version = "dev"
|
version = "dev"
|
||||||
commit = "none"
|
commit = "none"
|
||||||
@@ -77,190 +21,60 @@ func showVersion() {
|
|||||||
fmt.Printf("%v, commit %v, built at %v", version, commit, date)
|
fmt.Printf("%v, commit %v, built at %v", version, commit, date)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CliConfig struct {
|
||||||
|
Version bool `usage:"Output version and exit"`
|
||||||
|
Debug bool `usage:"Enable debug logs"`
|
||||||
|
|
||||||
|
ServerConfig server.Config `flatten:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var config Config
|
var cliConfig CliConfig
|
||||||
err := flagsfiller.Parse(&config, flagsfiller.WithEnv(""))
|
err := flagsfiller.Parse(&cliConfig, flagsfiller.WithEnv(""))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Version {
|
if cliConfig.Version {
|
||||||
showVersion()
|
showVersion()
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Debug {
|
if cliConfig.Debug {
|
||||||
logrus.SetLevel(logrus.DebugLevel)
|
logrus.SetLevel(logrus.DebugLevel)
|
||||||
logrus.Debug("Debug logs enabled")
|
logrus.Debug("Debug logs enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.CpuProfile != "" {
|
|
||||||
cpuProfileFile, err := os.Create(config.CpuProfile)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Fatal("trying to create cpu profile file")
|
|
||||||
}
|
|
||||||
//goland:noinspection GoUnhandledErrorResult
|
|
||||||
defer cpuProfileFile.Close()
|
|
||||||
|
|
||||||
logrus.WithField("file", config.CpuProfile).Info("Starting cpu profiling")
|
|
||||||
err = pprof.StartCPUProfile(cpuProfileFile)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Fatal("trying to start cpu profile")
|
|
||||||
}
|
|
||||||
defer pprof.StopCPUProfile()
|
|
||||||
}
|
|
||||||
|
|
||||||
var autoScaleAllowDenyConfig *server.AllowDenyConfig = nil
|
|
||||||
if config.AutoScale.AllowDeny != "" {
|
|
||||||
autoScaleAllowDenyConfig, err = server.ParseAllowDenyConfig(config.AutoScale.AllowDeny)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Fatal("trying to parse autoscale up allow-deny-list file")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
metricsBuilder := server.NewMetricsBuilder(config.MetricsBackend, &config.MetricsBackendConfig)
|
signals := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||||
|
|
||||||
downScalerEnabled := config.AutoScale.Down && (config.InKubeCluster || config.KubeConfig != "")
|
s, err := server.NewServer(ctx, &cliConfig.ServerConfig)
|
||||||
downScalerDelay, err := time.ParseDuration(config.AutoScale.DownAfter)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Fatal("Unable to parse auto scale down after duration")
|
logrus.WithError(err).Fatal("Could not setup server")
|
||||||
}
|
|
||||||
// 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, syscall.SIGHUP)
|
|
||||||
|
|
||||||
if config.Routes.Config != "" {
|
|
||||||
err := server.RoutesConfigLoader.Load(config.Routes.Config)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Fatal("Unable to load routes from config file")
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.Routes.ConfigWatch {
|
|
||||||
err := server.RoutesConfigLoader.WatchForChanges(ctx)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Fatal("Unable to watch for changes")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.Routes.RegisterAll(config.Mapping)
|
go s.Run()
|
||||||
if config.Default != "" {
|
|
||||||
server.Routes.SetDefaultRoute(config.Default)
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.ConnectionRateLimit < 1 {
|
|
||||||
config.ConnectionRateLimit = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
trustedIpNets := make([]*net.IPNet, 0)
|
|
||||||
for _, ip := range config.TrustedProxies {
|
|
||||||
_, ipNet, err := net.ParseCIDR(ip)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Fatal("Unable to parse trusted proxy CIDR block")
|
|
||||||
}
|
|
||||||
trustedIpNets = append(trustedIpNets, ipNet)
|
|
||||||
}
|
|
||||||
|
|
||||||
connector := server.NewConnector(metricsBuilder.BuildConnectorMetrics(), config.UseProxyProtocol, config.ReceiveProxyProtocol, trustedIpNets, config.RecordLogins, autoScaleAllowDenyConfig)
|
|
||||||
|
|
||||||
clientFilter, err := server.NewClientFilter(config.ClientsToAllow, config.ClientsToDeny)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Fatal("Unable to create client filter")
|
|
||||||
}
|
|
||||||
connector.SetClientFilter(clientFilter)
|
|
||||||
|
|
||||||
if config.Webhook.Url != "" {
|
|
||||||
logrus.
|
|
||||||
WithField("url", config.Webhook.Url).
|
|
||||||
WithField("require-user", config.Webhook.RequireUser).
|
|
||||||
Info("Using webhook for connection status notifications")
|
|
||||||
connector.SetConnectionNotifier(
|
|
||||||
server.NewWebhookNotifier(config.Webhook.Url, config.Webhook.RequireUser))
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.NgrokToken != "" {
|
|
||||||
connector.UseNgrok(config.NgrokToken)
|
|
||||||
}
|
|
||||||
err = connector.StartAcceptingConnections(ctx,
|
|
||||||
net.JoinHostPort("", strconv.Itoa(config.Port)),
|
|
||||||
config.ConnectionRateLimit,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.ApiBinding != "" {
|
|
||||||
server.StartApiServer(config.ApiBinding)
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.InKubeCluster {
|
|
||||||
err = server.K8sWatcher.StartInCluster(config.AutoScale.Up, config.AutoScale.Down)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Fatal("Unable to start k8s integration")
|
|
||||||
} else {
|
|
||||||
defer server.K8sWatcher.Stop()
|
|
||||||
}
|
|
||||||
} else if config.KubeConfig != "" {
|
|
||||||
err := server.K8sWatcher.StartWithConfig(config.KubeConfig, config.AutoScale.Up, config.AutoScale.Down)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Fatal("Unable to start k8s integration")
|
|
||||||
} else {
|
|
||||||
defer server.K8sWatcher.Stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.InDocker {
|
|
||||||
err = server.DockerWatcher.Start(config.DockerSocket, config.DockerTimeout, config.DockerRefreshInterval, config.AutoScale.Up, config.AutoScale.Down)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Fatal("Unable to start docker integration")
|
|
||||||
} else {
|
|
||||||
defer server.DockerWatcher.Stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.InDockerSwarm {
|
|
||||||
err = server.DockerSwarmWatcher.Start(config.DockerSocket, config.DockerTimeout, config.DockerRefreshInterval, config.AutoScale.Up, config.AutoScale.Down)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Fatal("Unable to start docker swarm integration")
|
|
||||||
} else {
|
|
||||||
defer server.DockerSwarmWatcher.Stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
server.Routes.SimplifySRV(config.SimplifySRV)
|
|
||||||
|
|
||||||
err = metricsBuilder.Start(ctx)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Fatal("Unable to start metrics reporter")
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle signals
|
|
||||||
for {
|
for {
|
||||||
sig := <-c
|
select {
|
||||||
switch sig {
|
case <-s.Done():
|
||||||
case syscall.SIGHUP:
|
|
||||||
if config.Routes.Config != "" {
|
|
||||||
logrus.Info("Received SIGHUP, reloading routes config...")
|
|
||||||
if err := server.RoutesConfigLoader.Reload(); err != nil {
|
|
||||||
logrus.
|
|
||||||
WithError(err).
|
|
||||||
WithField("routesConfig", config.Routes.Config).
|
|
||||||
Error("Could not re-read the routes config file")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case syscall.SIGINT, syscall.SIGTERM:
|
|
||||||
logrus.WithField("signal", sig).Info("Stopping. Waiting for connections to complete...")
|
|
||||||
signal.Stop(c)
|
|
||||||
connector.WaitForConnections()
|
|
||||||
logrus.Info("Stopped")
|
|
||||||
return
|
return
|
||||||
default:
|
|
||||||
logrus.WithField("signal", sig).Warn("Received unexpected signal")
|
case sig := <-signals:
|
||||||
|
switch sig {
|
||||||
|
case syscall.SIGHUP:
|
||||||
|
s.ReloadConfig()
|
||||||
|
|
||||||
|
case syscall.SIGINT, syscall.SIGTERM:
|
||||||
|
cancel()
|
||||||
|
// but wait for the server to be done
|
||||||
|
|
||||||
|
default:
|
||||||
|
logrus.WithField("signal", sig).Warn("Received unexpected signal")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ require (
|
|||||||
github.com/go-kit/kit v0.13.0
|
github.com/go-kit/kit v0.13.0
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c
|
github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c
|
||||||
github.com/itzg/go-flagsfiller v1.15.2
|
github.com/itzg/go-flagsfiller v1.16.0
|
||||||
github.com/juju/ratelimit v1.0.2
|
github.com/juju/ratelimit v1.0.2
|
||||||
github.com/pires/go-proxyproto v0.8.1
|
github.com/pires/go-proxyproto v0.8.1
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
@@ -80,7 +80,7 @@ require (
|
|||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/VividCortex/gohistogram v1.0.0 // indirect
|
github.com/VividCortex/gohistogram v1.0.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/docker/docker v28.3.0+incompatible
|
github.com/docker/docker v28.3.1+incompatible
|
||||||
github.com/docker/go-connections v0.5.0 // indirect
|
github.com/docker/go-connections v0.5.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/go-kit/log v0.2.1 // indirect
|
github.com/go-kit/log v0.2.1 // indirect
|
||||||
@@ -98,7 +98,7 @@ require (
|
|||||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/spf13/pflag v1.0.6 // indirect
|
github.com/spf13/pflag v1.0.6 // indirect
|
||||||
golang.org/x/net v0.40.0 // indirect
|
golang.org/x/net v0.41.0 // indirect
|
||||||
golang.org/x/oauth2 v0.30.0 // indirect
|
golang.org/x/oauth2 v0.30.0 // indirect
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
golang.org/x/term v0.32.0 // indirect
|
golang.org/x/term v0.32.0 // indirect
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
|||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/docker/docker v28.3.0+incompatible h1:ffS62aKWupCWdvcee7nBU9fhnmknOqDPaJAMtfK0ImQ=
|
github.com/docker/docker v28.3.0+incompatible h1:ffS62aKWupCWdvcee7nBU9fhnmknOqDPaJAMtfK0ImQ=
|
||||||
github.com/docker/docker v28.3.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
github.com/docker/docker v28.3.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
|
github.com/docker/docker v28.3.1+incompatible h1:20+BmuA9FXlCX4ByQ0vYJcUEnOmRM6XljDnFWR+jCyY=
|
||||||
|
github.com/docker/docker v28.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
@@ -89,6 +91,8 @@ github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSH
|
|||||||
github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
|
github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
|
||||||
github.com/itzg/go-flagsfiller v1.15.2 h1:DvhhOKuqzawoa6C/3Q/y8pFbdO5PBdgnIvz84ZGkY0g=
|
github.com/itzg/go-flagsfiller v1.15.2 h1:DvhhOKuqzawoa6C/3Q/y8pFbdO5PBdgnIvz84ZGkY0g=
|
||||||
github.com/itzg/go-flagsfiller v1.15.2/go.mod h1:XmllPPi99O7vXTG9wa/Hzmhnkv6BXBF1W57ifbQTVs4=
|
github.com/itzg/go-flagsfiller v1.15.2/go.mod h1:XmllPPi99O7vXTG9wa/Hzmhnkv6BXBF1W57ifbQTVs4=
|
||||||
|
github.com/itzg/go-flagsfiller v1.16.0 h1:YNwjLzFIeFzZpctT2RiN8T5qxiGrCX33bGSwtN6OSAA=
|
||||||
|
github.com/itzg/go-flagsfiller v1.16.0/go.mod h1:XmllPPi99O7vXTG9wa/Hzmhnkv6BXBF1W57ifbQTVs4=
|
||||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
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/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
|
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
|
||||||
@@ -205,6 +209,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
|||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||||
|
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
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.3.0/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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
@@ -214,6 +219,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
|
|||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||||
|
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||||
|
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
type WebhookConfig struct {
|
||||||
|
Url string `usage:"If set, a POST request that contains connection status notifications will be sent to this HTTP address"`
|
||||||
|
RequireUser bool `default:"false" usage:"Indicates if the webhook will only be called if a user is connecting rather than just server list/ping"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RoutesConfig struct {
|
||||||
|
Config string `usage:"Name or full [path] to routes config file"`
|
||||||
|
ConfigWatch bool `usage:"Watch for config file changes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Port int `default:"25565" usage:"The [port] bound to listen for Minecraft client connections"`
|
||||||
|
Default string `usage:"host:port of a default Minecraft server to use when mapping not found"`
|
||||||
|
Mapping map[string]string `usage:"Comma or newline delimited or repeated mappings of externalHostname=host:port"`
|
||||||
|
ApiBinding string `usage:"The [host:port] bound for servicing API requests"`
|
||||||
|
CpuProfile string `usage:"Enables CPU profiling and writes to given path"`
|
||||||
|
ConnectionRateLimit int `default:"1" usage:"Max number of connections to allow per second"`
|
||||||
|
InKubeCluster bool `usage:"Use in-cluster Kubernetes config"`
|
||||||
|
KubeConfig string `usage:"The path to a Kubernetes configuration file"`
|
||||||
|
InDocker bool `usage:"Use Docker service discovery"`
|
||||||
|
InDockerSwarm bool `usage:"Use Docker Swarm service discovery"`
|
||||||
|
DockerSocket string `default:"unix:///var/run/docker.sock" usage:"Path to Docker socket to use"`
|
||||||
|
DockerTimeout int `default:"0" usage:"Timeout configuration in seconds for the Docker integrations"`
|
||||||
|
DockerRefreshInterval int `default:"15" usage:"Refresh interval in seconds for the Docker integrations"`
|
||||||
|
MetricsBackend string `default:"discard" usage:"Backend to use for metrics exposure/publishing: discard,expvar,influxdb,prometheus"`
|
||||||
|
MetricsBackendConfig MetricsBackendConfig
|
||||||
|
UseProxyProtocol bool `default:"false" usage:"Send PROXY protocol to backend servers"`
|
||||||
|
ReceiveProxyProtocol bool `default:"false" usage:"Receive PROXY protocol from backend servers, by default trusts every proxy header that it receives, combine with -trusted-proxies to specify a list of trusted proxies"`
|
||||||
|
TrustedProxies []string `usage:"Comma delimited list of CIDR notation IP blocks to trust when receiving PROXY protocol"`
|
||||||
|
RecordLogins bool `default:"false" usage:"Log and generate metrics on player logins. Metrics only supported with influxdb or prometheus backend"`
|
||||||
|
Routes RoutesConfig
|
||||||
|
NgrokToken string `usage:"If set, an ngrok tunnel will be established. It is HIGHLY recommended to pass as an environment variable."`
|
||||||
|
AutoScale AutoScale
|
||||||
|
|
||||||
|
ClientsToAllow []string `usage:"Zero or more client IP addresses or CIDRs to allow. Takes precedence over deny."`
|
||||||
|
ClientsToDeny []string `usage:"Zero or more client IP addresses or CIDRs to deny. Ignored if any configured to allow"`
|
||||||
|
|
||||||
|
SimplifySRV bool `default:"false" usage:"Simplify fully qualified SRV records for mapping"`
|
||||||
|
|
||||||
|
Webhook WebhookConfig `usage:"Webhook configuration"`
|
||||||
|
}
|
||||||
@@ -49,6 +49,10 @@ func (r *routesConfigLoader) Load(routesConfigFileName string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *routesConfigLoader) Reload() error {
|
func (r *routesConfigLoader) Reload() error {
|
||||||
|
if !r.isEnabled() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
config, readErr := r.readFile()
|
config, readErr := r.readFile()
|
||||||
|
|
||||||
if readErr != nil {
|
if readErr != nil {
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"runtime/pprof"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
ctx context.Context
|
||||||
|
config *Config
|
||||||
|
connector *Connector
|
||||||
|
reloadConfigChan chan struct{}
|
||||||
|
doneChan chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer(ctx context.Context, config *Config) (*Server, error) {
|
||||||
|
if config.CpuProfile != "" {
|
||||||
|
cpuProfileFile, err := os.Create(config.CpuProfile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not create cpu profile file: %w", err)
|
||||||
|
}
|
||||||
|
//goland:noinspection GoUnhandledErrorResult
|
||||||
|
defer cpuProfileFile.Close()
|
||||||
|
|
||||||
|
logrus.WithField("file", config.CpuProfile).Info("Starting cpu profiling")
|
||||||
|
err = pprof.StartCPUProfile(cpuProfileFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not start cpu profile: %w", err)
|
||||||
|
}
|
||||||
|
defer pprof.StopCPUProfile()
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
var autoScaleAllowDenyConfig *AllowDenyConfig = nil
|
||||||
|
if config.AutoScale.AllowDeny != "" {
|
||||||
|
autoScaleAllowDenyConfig, err = ParseAllowDenyConfig(config.AutoScale.AllowDeny)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not parse autoscale allow-deny-list: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
metricsBuilder := NewMetricsBuilder(config.MetricsBackend, &config.MetricsBackendConfig)
|
||||||
|
|
||||||
|
downScalerEnabled := config.AutoScale.Down && (config.InKubeCluster || config.KubeConfig != "")
|
||||||
|
downScalerDelay, err := time.ParseDuration(config.AutoScale.DownAfter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not parse auto-scale-down-after duration: %w", err)
|
||||||
|
}
|
||||||
|
// Only one instance should be created
|
||||||
|
DownScaler = NewDownScaler(ctx, downScalerEnabled, downScalerDelay)
|
||||||
|
|
||||||
|
if config.Routes.Config != "" {
|
||||||
|
err := RoutesConfigLoader.Load(config.Routes.Config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not load routes config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Routes.ConfigWatch {
|
||||||
|
err := RoutesConfigLoader.WatchForChanges(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not watch for changes to routes config file: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Routes.RegisterAll(config.Mapping)
|
||||||
|
if config.Default != "" {
|
||||||
|
Routes.SetDefaultRoute(config.Default)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.ConnectionRateLimit < 1 {
|
||||||
|
config.ConnectionRateLimit = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
trustedIpNets := make([]*net.IPNet, 0)
|
||||||
|
for _, ip := range config.TrustedProxies {
|
||||||
|
_, ipNet, err := net.ParseCIDR(ip)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not parse trusted proxy CIDR block: %w", err)
|
||||||
|
}
|
||||||
|
trustedIpNets = append(trustedIpNets, ipNet)
|
||||||
|
}
|
||||||
|
|
||||||
|
connector := NewConnector(metricsBuilder.BuildConnectorMetrics(), config.UseProxyProtocol, config.ReceiveProxyProtocol, trustedIpNets, config.RecordLogins, autoScaleAllowDenyConfig)
|
||||||
|
|
||||||
|
clientFilter, err := NewClientFilter(config.ClientsToAllow, config.ClientsToDeny)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not create client filter: %w", err)
|
||||||
|
}
|
||||||
|
connector.SetClientFilter(clientFilter)
|
||||||
|
|
||||||
|
if config.Webhook.Url != "" {
|
||||||
|
logrus.WithField("url", config.Webhook.Url).
|
||||||
|
WithField("require-user", config.Webhook.RequireUser).
|
||||||
|
Info("Using webhook for connection status notifications")
|
||||||
|
connector.SetConnectionNotifier(
|
||||||
|
NewWebhookNotifier(config.Webhook.Url, config.Webhook.RequireUser))
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.NgrokToken != "" {
|
||||||
|
connector.UseNgrok(config.NgrokToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.ApiBinding != "" {
|
||||||
|
StartApiServer(config.ApiBinding)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.InKubeCluster {
|
||||||
|
err = K8sWatcher.StartInCluster(config.AutoScale.Up, config.AutoScale.Down)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not start in-cluster k8s integration: %w", err)
|
||||||
|
} else {
|
||||||
|
defer K8sWatcher.Stop()
|
||||||
|
}
|
||||||
|
} else if config.KubeConfig != "" {
|
||||||
|
err := K8sWatcher.StartWithConfig(config.KubeConfig, config.AutoScale.Up, config.AutoScale.Down)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not start k8s integration with kube config: %w", err)
|
||||||
|
} else {
|
||||||
|
defer K8sWatcher.Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.InDocker {
|
||||||
|
err = DockerWatcher.Start(config.DockerSocket, config.DockerTimeout, config.DockerRefreshInterval, config.AutoScale.Up, config.AutoScale.Down)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not start docker integration: %w", err)
|
||||||
|
} else {
|
||||||
|
defer DockerWatcher.Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.InDockerSwarm {
|
||||||
|
err = DockerSwarmWatcher.Start(config.DockerSocket, config.DockerTimeout, config.DockerRefreshInterval, config.AutoScale.Up, config.AutoScale.Down)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not start docker swarm integration: %w", err)
|
||||||
|
} else {
|
||||||
|
defer DockerSwarmWatcher.Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Routes.SimplifySRV(config.SimplifySRV)
|
||||||
|
|
||||||
|
err = metricsBuilder.Start(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not start metrics reporter: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Server{
|
||||||
|
ctx: ctx,
|
||||||
|
config: config,
|
||||||
|
connector: connector,
|
||||||
|
reloadConfigChan: make(chan struct{}),
|
||||||
|
doneChan: make(chan struct{}),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Done provides a channel notified when the server has closed all connections, etc
|
||||||
|
func (s *Server) Done() <-chan struct{} {
|
||||||
|
return s.doneChan
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) notifyDone() {
|
||||||
|
s.doneChan <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReloadConfig indicates that an external request, such as a SIGHUP,
|
||||||
|
// is requesting the routes config file to be reloaded, if enabled
|
||||||
|
func (s *Server) ReloadConfig() {
|
||||||
|
s.reloadConfigChan <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run will run the server until the context is done or a fatal error occurs, so this should be
|
||||||
|
// in a go routine.
|
||||||
|
func (s *Server) Run() {
|
||||||
|
err := s.connector.StartAcceptingConnections(s.ctx,
|
||||||
|
net.JoinHostPort("", strconv.Itoa(s.config.Port)),
|
||||||
|
s.config.ConnectionRateLimit,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Error("Could not start accepting connections")
|
||||||
|
s.notifyDone()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-s.reloadConfigChan:
|
||||||
|
if err := RoutesConfigLoader.Reload(); err != nil {
|
||||||
|
logrus.WithError(err).
|
||||||
|
Error("Could not re-read the routes config file")
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-s.ctx.Done():
|
||||||
|
logrus.Info("Stopping. Waiting for connections to complete...")
|
||||||
|
s.connector.WaitForConnections()
|
||||||
|
logrus.Info("Stopped")
|
||||||
|
s.notifyDone()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user