From 7e1437654c031340f6f4488f3dccb6b95c2e7f8a Mon Sep 17 00:00:00 2001 From: okeanosthedev <66648881+okeanosthedev@users.noreply.github.com> Date: Tue, 3 Dec 2024 11:46:34 +0300 Subject: [PATCH 01/27] Update config.go --- pkg/edition/java/lite/config/config.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pkg/edition/java/lite/config/config.go b/pkg/edition/java/lite/config/config.go index 1697e848..c275ad64 100644 --- a/pkg/edition/java/lite/config/config.go +++ b/pkg/edition/java/lite/config/config.go @@ -28,6 +28,7 @@ type ( Route struct { Host configutil.SingleOrMulti[string] `json:"host,omitempty" yaml:"host,omitempty"` Backend configutil.SingleOrMulti[string] `json:"backend,omitempty" yaml:"backend,omitempty"` + Strategy string `json:"strategy,omitempty" yaml:"strategy,omitempty"` CachePingTTL configutil.Duration `json:"cachePingTTL,omitempty" yaml:"cachePingTTL,omitempty"` // 0 = default, < 0 = disabled Fallback *Status `json:"fallback,omitempty" yaml:"fallback,omitempty"` // nil = disabled ProxyProtocol bool `json:"proxyProtocol,omitempty" yaml:"proxyProtocol,omitempty"` @@ -69,6 +70,13 @@ func (r *Route) CachePingEnabled() bool { return r.GetCachePingTTL() > 0 } // GetTCPShieldRealIP returns the configured TCPShieldRealIP or deprecated RealIP value. func (r *Route) GetTCPShieldRealIP() bool { return r.TCPShieldRealIP || r.RealIP } +var allowedStrategies = map[string]bool{ + "random": true, + "round-robin": true, + "least connections": true, + "lowest latency": true, +} + func (c Config) Validate() (warns []error, errs []error) { e := func(m string, args ...any) { errs = append(errs, fmt.Errorf(m, args...)) } @@ -84,6 +92,9 @@ func (c Config) Validate() (warns []error, errs []error) { if len(ep.Backend) == 0 { e("Route %d: no backend configured", i) } + if _, ok := allowedStrategies[ep.Strategy]; !ok && ep.Strategy != "" { + e("Route %d: invalid strategy '%s'", i, ep.Strategy) + } for i, addr := range ep.Backend { _, err := netutil.Parse(addr, "tcp") if err != nil { From c218dbe1b4713d4bf265a1640f36da339386f034 Mon Sep 17 00:00:00 2001 From: okeanosthedev <66648881+okeanosthedev@users.noreply.github.com> Date: Tue, 3 Dec 2024 11:47:54 +0300 Subject: [PATCH 02/27] Update forward.go --- pkg/edition/java/lite/forward.go | 105 +++++++++++++++++++++++++++---- 1 file changed, 92 insertions(+), 13 deletions(-) diff --git a/pkg/edition/java/lite/forward.go b/pkg/edition/java/lite/forward.go index c9e7a693..76ab5d67 100644 --- a/pkg/edition/java/lite/forward.go +++ b/pkg/edition/java/lite/forward.go @@ -7,8 +7,11 @@ import ( "errors" "fmt" "io" + "math" + "math/rand" "net" "strings" + "sync" "time" "github.com/go-logr/logr" @@ -157,27 +160,103 @@ func findRoute( tryBackends := route.Backend.Copy() nextBackend = func() (string, logr.Logger, bool) { + switch route.Strategy { + case "random": + return randomNextBackend(tryBackends)() + case "round-robin": + return roundRobinNextBackend(host, tryBackends)() + case "least connections": + return leastConnectionsNextBackend(tryBackends)() + case "lowest latency": + return lowestLatencyNextBackend(tryBackends)() + default: + // Default to random strategy + return randomNextBackend(tryBackends)() + } + } + + return log, src, route, nextBackend, nil +} + +func randomNextBackend(tryBackends []string) nextBackendFunc { + return func() (string, bool) { if len(tryBackends) == 0 { - return "", log, false + return "", false } - // Pop first backend - backend := tryBackends[0] - tryBackends = tryBackends[1:] + rand.Seed(time.Now().UnixNano()) + randIndex := rand.Intn(len(tryBackends)) + return tryBackends[randIndex], true + } +} - dstAddr, err := netutil.Parse(backend, src.RemoteAddr().Network()) - if err != nil { - log.Info("failed to parse backend address", "wrongBackendAddr", backend, "error", err) - return "", log, false +var roundRobinIndex = make(map[string]int) + +func roundRobinNextBackend(routeHost string, tryBackends []string) nextBackendFunc { + return func() (string, bool) { + if len(tryBackends) == 0 { + return "", false + } + index := roundRobinIndex[routeHost] % len(tryBackends) + backend := tryBackends[index] + roundRobinIndex[routeHost]++ + return backend, true + } +} + +var connectionCounts = make(map[string]int) +var connectionCountsMutex sync.Mutex + +func leastConnectionsNextBackend(tryBackends []string) nextBackendFunc { + return func() (string, bool) { + if len(tryBackends) == 0 { + return "", false } - backendAddr := dstAddr.String() - if _, port := netutil.HostPort(dstAddr); port == 0 { - backendAddr = net.JoinHostPort(dstAddr.String(), "25565") + connectionCountsMutex.Lock() + defer connectionCountsMutex.Unlock() + least := tryBackends[0] + for _, backend := range tryBackends { + if connectionCounts[backend] < connectionCounts[least] { + least = backend + } } + connectionCounts[least]++ + return least, true + } +} + +var latencyCache = ttlcache.New[string, time.Duration]() - return backendAddr, log.WithValues("backendAddr", backendAddr), true +func lowestLatencyNextBackend(tryBackends []string) nextBackendFunc { + return func() (string, bool) { + if len(tryBackends) == 0 { + return "", false + } + var lowestBackend string + var lowestLatency time.Duration + for _, backend := range tryBackends { + latencyItem := latencyCache.Get(backend) + if latencyItem == nil { + latency := measureLatency(backend) + latencyCache.Set(backend, latency, ttlcache.TTL(time.Minute)) + latencyItem = latencyCache.Get(backend) + } + if latencyItem != nil && (lowestLatency == 0 || latencyItem.Value() < lowestLatency) { + lowestBackend = backend + lowestLatency = latencyItem.Value() + } + } + return lowestBackend, true } +} - return log, src, route, nextBackend, nil +func measureLatency(backend string) time.Duration { + start := time.Now() + conn, err := net.DialTimeout("tcp", backend, time.Second*5) + if err != nil { + return time.Duration(math.MaxInt64) // Return a very high latency if connection fails + } + conn.Close() + return time.Since(start) } func dialRoute( From 6e33a797beab8b84b323138f4741caf4966e8a84 Mon Sep 17 00:00:00 2001 From: okeanosthedev <66648881+okeanosthedev@users.noreply.github.com> Date: Tue, 3 Dec 2024 11:50:24 +0300 Subject: [PATCH 03/27] Update match.go From df31c63b39dece1720a78bbb221977cb5ed715da Mon Sep 17 00:00:00 2001 From: okeanosthedev <66648881+okeanosthedev@users.noreply.github.com> Date: Tue, 3 Dec 2024 11:50:41 +0300 Subject: [PATCH 04/27] Update match_test.go From 923f5cf0951417a7879c95009171c72f9ad9e9f4 Mon Sep 17 00:00:00 2001 From: okeanosthedev <66648881+okeanosthedev@users.noreply.github.com> Date: Tue, 3 Dec 2024 11:50:55 +0300 Subject: [PATCH 05/27] Update util.go From 04e758ad932121a41d12771a01b1760da0b05d68 Mon Sep 17 00:00:00 2001 From: okeanosthedev <66648881+okeanosthedev@users.noreply.github.com> Date: Tue, 3 Dec 2024 12:45:02 +0300 Subject: [PATCH 06/27] Update config.go --- pkg/edition/java/config/config.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/edition/java/config/config.go b/pkg/edition/java/config/config.go index 0e25632b..fca2bfd0 100644 --- a/pkg/edition/java/config/config.go +++ b/pkg/edition/java/config/config.go @@ -27,7 +27,7 @@ var DefaultConfig = Config{ ShowMaxPlayers: 1000, Motd: defaultMotd(), // Contains Gate's icon - Favicon: "data:image/png;base64,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", + Favicon: "", LogPingRequests: false, }, Query: Query{ @@ -76,12 +76,13 @@ var DefaultConfig = Config{ func defaultMotd() *configutil.TextComponent { return text("§bA Gate Proxy\n§bVisit ➞ §fgithub.com/minekube/gate") } + func defaultShutdownReason() *configutil.TextComponent { return text("§cGate proxy is shutting down...\nPlease reconnect in a moment!") } // Config is the configuration of the proxy. -type Config struct { // TODO use https://github.com/projectdiscovery/yamldoc-go for generating output yaml and markdown for the docs +type Config struct { Bind string `yaml:"bind"` // The address to listen for connections. OnlineMode bool `yaml:"onlineMode,omitempty" json:"onlineMode,omitempty"` // Whether to enable online mode. From 35a570379b0fbbdbec6de0e36ab141d8b9d700ad Mon Sep 17 00:00:00 2001 From: okeanosthedev <66648881+okeanosthedev@users.noreply.github.com> Date: Tue, 3 Dec 2024 12:47:08 +0300 Subject: [PATCH 07/27] Update forward.go --- pkg/edition/java/lite/forward.go | 100 +++++++++++++++---------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/pkg/edition/java/lite/forward.go b/pkg/edition/java/lite/forward.go index 76ab5d67..64743cb4 100644 --- a/pkg/edition/java/lite/forward.go +++ b/pkg/edition/java/lite/forward.go @@ -31,59 +31,59 @@ import ( // Forward forwards a client connection to a matching backend route. func Forward( - dialTimeout time.Duration, - routes []config.Route, - log logr.Logger, - client netmc.MinecraftConn, - handshake *packet.Handshake, - pc *proto.PacketContext, + dialTimeout time.Duration, + routes []config.Route, + log logr.Logger, + client netmc.MinecraftConn, + handshake *packet.Handshake, + pc *proto.PacketContext, ) { - defer func() { _ = client.Close() }() - - log, src, route, nextBackend, err := findRoute(routes, log, client, handshake) - if err != nil { - errs.V(log, err).Info("failed to find route", "error", err) - return - } - - // Find a backend to dial successfully. - log, dst, err := tryBackends(nextBackend, func(log logr.Logger, backendAddr string) (logr.Logger, net.Conn, error) { - conn, err := dialRoute(client.Context(), dialTimeout, src.RemoteAddr(), route, backendAddr, handshake, pc, false) - return log, conn, err - }) - if err != nil { - return - } - defer func() { _ = dst.Close() }() - - if err = emptyReadBuff(client, dst); err != nil { - errs.V(log, err).Info("failed to empty client buffer", "error", err) - return - } - - log.Info("forwarding connection", "backendAddr", netutil.Host(dst.RemoteAddr())) - pipe(log, src, dst) + defer func() { _ = client.Close() }() + + log, src, route, nextBackend, err := findRoute(routes, log, client, handshake) + if err != nil { + errs.V(log, err).Info("failed to find route", "error", err) + return + } + + // Find a backend to dial successfully. + log, dst, err := tryBackends(log, nextBackend, func(log logr.Logger, backendAddr string) (logr.Logger, net.Conn, error) { + conn, err := dialRoute(client.Context(), dialTimeout, src.RemoteAddr(), route, backendAddr, handshake, pc, false) + return log, conn, err + }) + if err != nil { + return + } + defer func() { _ = dst.Close() }() + + if err = emptyReadBuff(client, dst); err != nil { + errs.V(log, err).Info("failed to empty client buffer", "error", err) + return + } + + log.Info("forwarding connection", "backendAddr", netutil.Host(dst.RemoteAddr())) + pipe(log, src, dst) } // errAllBackendsFailed is returned when all backends failed to dial. var errAllBackendsFailed = errors.New("all backends failed") // tryBackends tries backends until one succeeds or all fail. -func tryBackends[T any](next nextBackendFunc, try func(log logr.Logger, backendAddr string) (logr.Logger, T, error)) (logr.Logger, T, error) { - for { - backendAddr, log, ok := next() - if !ok { - var zero T - return log, zero, errAllBackendsFailed - } - - log, t, err := try(log, backendAddr) - if err != nil { - errs.V(log, err).Info("failed to try backend", "error", err) - continue - } - return log, t, nil - } +func tryBackends[T any](log logr.Logger, next nextBackendFunc, try func(log logr.Logger, backendAddr string) (logr.Logger, T, error)) (logr.Logger, T, error) { + for { + backendAddr, ok := next() + if !ok { + var zero T + return log, zero, errAllBackendsFailed + } + + log, t, err := try(log, backendAddr) + if err != nil { + errs.V(log, err).Info("failed to try backend", "error", err) + continue + } + return log, t, nil + } } func emptyReadBuff(src netmc.MinecraftConn, dst net.Conn) error { @@ -121,7 +121,7 @@ func pipe(log logr.Logger, src, dst net.Conn) { } } -type nextBackendFunc func() (backendAddr string, log logr.Logger, ok bool) +type nextBackendFunc func() (backendAddr string, ok bool) func findRoute( routes []config.Route, @@ -159,7 +159,7 @@ func findRoute( } tryBackends := route.Backend.Copy() - nextBackend = func() (string, logr.Logger, bool) { + nextBackend = func() (string, bool) { switch route.Strategy { case "random": return randomNextBackend(tryBackends)() @@ -237,7 +237,7 @@ func lowestLatencyNextBackend(tryBackends []string) nextBackendFunc { latencyItem := latencyCache.Get(backend) if latencyItem == nil { latency := measureLatency(backend) - latencyCache.Set(backend, latency, ttlcache.TTL(time.Minute)) + latencyCache.Set(backend, latency, time.Minute) latencyItem = latencyCache.Get(backend) } if latencyItem != nil && (lowestLatency == 0 || latencyItem.Value() < lowestLatency) { @@ -358,7 +358,7 @@ func ResolveStatusResponse( return log, nil, err } - log, res, err := tryBackends(nextBackend, func(log logr.Logger, backendAddr string) (logr.Logger, *packet.StatusResponse, error) { + log, res, err := tryBackends(log, nextBackend, func(log logr.Logger, backendAddr string) (logr.Logger, *packet.StatusResponse, error) { return resolveStatusResponse(src, dialTimeout, backendAddr, route, log, client, handshake, handshakeCtx, statusRequestCtx) }) if err != nil && route.Fallback != nil { From 9b5a64334b1fe6e43447532ebd67b6c6d47e39bd Mon Sep 17 00:00:00 2001 From: okeanosthedev <66648881+okeanosthedev@users.noreply.github.com> Date: Tue, 3 Dec 2024 12:53:40 +0300 Subject: [PATCH 08/27] Update forward.go --- pkg/edition/java/lite/forward.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/edition/java/lite/forward.go b/pkg/edition/java/lite/forward.go index 64743cb4..a4169670 100644 --- a/pkg/edition/java/lite/forward.go +++ b/pkg/edition/java/lite/forward.go @@ -165,9 +165,9 @@ func findRoute( return randomNextBackend(tryBackends)() case "round-robin": return roundRobinNextBackend(host, tryBackends)() - case "least connections": + case "least-connections": return leastConnectionsNextBackend(tryBackends)() - case "lowest latency": + case "lowest-latency": return lowestLatencyNextBackend(tryBackends)() default: // Default to random strategy From 74edd1cdcbcfccddbdf7160304fc86d7d0c4d53e Mon Sep 17 00:00:00 2001 From: okeanosthedev <66648881+okeanosthedev@users.noreply.github.com> Date: Tue, 3 Dec 2024 12:57:49 +0300 Subject: [PATCH 09/27] Update config.go --- pkg/edition/java/lite/config/config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/edition/java/lite/config/config.go b/pkg/edition/java/lite/config/config.go index c275ad64..146c45e1 100644 --- a/pkg/edition/java/lite/config/config.go +++ b/pkg/edition/java/lite/config/config.go @@ -73,8 +73,8 @@ func (r *Route) GetTCPShieldRealIP() bool { return r.TCPShieldRealIP || r.RealIP var allowedStrategies = map[string]bool{ "random": true, "round-robin": true, - "least connections": true, - "lowest latency": true, + "least-connections": true, + "lowest-latency": true, } func (c Config) Validate() (warns []error, errs []error) { From f101e490e78b0c6623f3ccc0ff9678e12d875db0 Mon Sep 17 00:00:00 2001 From: okeanosthedev <66648881+okeanosthedev@users.noreply.github.com> Date: Tue, 3 Dec 2024 12:59:32 +0300 Subject: [PATCH 10/27] Update config.go --- pkg/edition/java/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/edition/java/config/config.go b/pkg/edition/java/config/config.go index fca2bfd0..af53345e 100644 --- a/pkg/edition/java/config/config.go +++ b/pkg/edition/java/config/config.go @@ -27,7 +27,7 @@ var DefaultConfig = Config{ ShowMaxPlayers: 1000, Motd: defaultMotd(), // Contains Gate's icon - Favicon: "", + Favicon: "data:image/png;base64,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", LogPingRequests: false, }, Query: Query{ From d2f1a56f9f7c1de2fee72bf6590f77fdf46d38a0 Mon Sep 17 00:00:00 2001 From: okeanosthedev <66648881+okeanosthedev@users.noreply.github.com> Date: Tue, 3 Dec 2024 13:00:30 +0300 Subject: [PATCH 11/27] Update config.go --- pkg/edition/java/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/edition/java/config/config.go b/pkg/edition/java/config/config.go index af53345e..94c7c0d8 100644 --- a/pkg/edition/java/config/config.go +++ b/pkg/edition/java/config/config.go @@ -82,7 +82,7 @@ func defaultShutdownReason() *configutil.TextComponent { } // Config is the configuration of the proxy. -type Config struct { +type Config struct { // TODO use https://github.com/projectdiscovery/yamldoc-go for generating output yaml and markdown for the docs Bind string `yaml:"bind"` // The address to listen for connections. OnlineMode bool `yaml:"onlineMode,omitempty" json:"onlineMode,omitempty"` // Whether to enable online mode. From 76deed8fc9dc331db82686f508a4eda7a9de2cd8 Mon Sep 17 00:00:00 2001 From: okeanosthedev <66648881+okeanosthedev@users.noreply.github.com> Date: Tue, 3 Dec 2024 13:09:30 +0300 Subject: [PATCH 12/27] Update config.go From 981fdfb57f51e1002b02345a53e8898af9803ba2 Mon Sep 17 00:00:00 2001 From: okeanosthedev <66648881+okeanosthedev@users.noreply.github.com> Date: Tue, 3 Dec 2024 13:10:04 +0300 Subject: [PATCH 13/27] Update config.go --- pkg/edition/java/lite/config/config.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/edition/java/lite/config/config.go b/pkg/edition/java/lite/config/config.go index 146c45e1..df37584b 100644 --- a/pkg/edition/java/lite/config/config.go +++ b/pkg/edition/java/lite/config/config.go @@ -28,7 +28,7 @@ type ( Route struct { Host configutil.SingleOrMulti[string] `json:"host,omitempty" yaml:"host,omitempty"` Backend configutil.SingleOrMulti[string] `json:"backend,omitempty" yaml:"backend,omitempty"` - Strategy string `json:"strategy,omitempty" yaml:"strategy,omitempty"` + Strategy string `json:"strategy,omitempty" yaml:"strategy,omitempty"` CachePingTTL configutil.Duration `json:"cachePingTTL,omitempty" yaml:"cachePingTTL,omitempty"` // 0 = default, < 0 = disabled Fallback *Status `json:"fallback,omitempty" yaml:"fallback,omitempty"` // nil = disabled ProxyProtocol bool `json:"proxyProtocol,omitempty" yaml:"proxyProtocol,omitempty"` @@ -71,10 +71,10 @@ func (r *Route) CachePingEnabled() bool { return r.GetCachePingTTL() > 0 } func (r *Route) GetTCPShieldRealIP() bool { return r.TCPShieldRealIP || r.RealIP } var allowedStrategies = map[string]bool{ - "random": true, - "round-robin": true, - "least-connections": true, - "lowest-latency": true, + "random": true, + "round-robin": true, + "least connections": true, + "lowest latency": true, } func (c Config) Validate() (warns []error, errs []error) { From 92e980d18b02bb7e0923be31017388fe2d82bf85 Mon Sep 17 00:00:00 2001 From: okeanosthedev <66648881+okeanosthedev@users.noreply.github.com> Date: Tue, 3 Dec 2024 13:10:37 +0300 Subject: [PATCH 14/27] Update config.go --- pkg/edition/java/lite/config/config.go | 563 +++++++++++++++++++++---- 1 file changed, 481 insertions(+), 82 deletions(-) diff --git a/pkg/edition/java/lite/config/config.go b/pkg/edition/java/lite/config/config.go index df37584b..48c4a8c4 100644 --- a/pkg/edition/java/lite/config/config.go +++ b/pkg/edition/java/lite/config/config.go @@ -1,120 +1,519 @@ -package config +package lite import ( + "bytes" + "context" "encoding/json" + "errors" "fmt" + "io" + "math" + "math/rand" + "net" + "strings" + "sync" "time" - "go.minekube.com/gate/pkg/edition/java/forge/modinfo" - "go.minekube.com/gate/pkg/edition/java/ping" + "github.com/go-logr/logr" + "github.com/jellydator/ttlcache/v3" + "go.minekube.com/gate/pkg/edition/java/internal/protoutil" + "go.minekube.com/gate/pkg/edition/java/lite/config" + "go.minekube.com/gate/pkg/edition/java/netmc" + "go.minekube.com/gate/pkg/edition/java/proto/codec" + "go.minekube.com/gate/pkg/edition/java/proto/packet" + "go.minekube.com/gate/pkg/edition/java/proto/state" + "go.minekube.com/gate/pkg/edition/java/proto/util" "go.minekube.com/gate/pkg/gate/proto" - "go.minekube.com/gate/pkg/util/configutil" - "go.minekube.com/gate/pkg/util/favicon" + "go.minekube.com/gate/pkg/util/errs" "go.minekube.com/gate/pkg/util/netutil" + "golang.org/x/sync/singleflight" ) -// DefaultConfig is the default configuration for Lite mode. -var DefaultConfig = Config{ - Enabled: false, - Routes: []Route{}, -} - -type ( - // Config is the configuration for Lite mode. - Config struct { - Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"` - Routes []Route `yaml:"routes,omitempty" json:"routes,omitempty"` - } - Route struct { - Host configutil.SingleOrMulti[string] `json:"host,omitempty" yaml:"host,omitempty"` - Backend configutil.SingleOrMulti[string] `json:"backend,omitempty" yaml:"backend,omitempty"` - Strategy string `json:"strategy,omitempty" yaml:"strategy,omitempty"` - CachePingTTL configutil.Duration `json:"cachePingTTL,omitempty" yaml:"cachePingTTL,omitempty"` // 0 = default, < 0 = disabled - Fallback *Status `json:"fallback,omitempty" yaml:"fallback,omitempty"` // nil = disabled - ProxyProtocol bool `json:"proxyProtocol,omitempty" yaml:"proxyProtocol,omitempty"` - // Deprecated: use TCPShieldRealIP instead. - RealIP bool `json:"realIP,omitempty" yaml:"realIP,omitempty"` - TCPShieldRealIP bool `json:"tcpShieldRealIP,omitempty" yaml:"tcpShieldRealIP,omitempty"` - ModifyVirtualHost bool `json:"modifyVirtualHost,omitempty" yaml:"modifyVirtualHost,omitempty"` - } - Status struct { - MOTD *configutil.TextComponent `yaml:"motd,omitempty" json:"motd,omitempty"` - Version ping.Version `yaml:"version,omitempty" json:"version,omitempty"` - Favicon favicon.Favicon `yaml:"favicon,omitempty" json:"favicon,omitempty"` - ModInfo modinfo.ModInfo `yaml:"modInfo,omitempty" json:"modInfo,omitempty"` +// Forward forwards a client connection to a matching backend route. +func Forward( + dialTimeout time.Duration, + routes []config.Route, + log logr.Logger, + client netmc.MinecraftConn, + handshake *packet.Handshake, + pc *proto.PacketContext, +) { + defer func() { _ = client.Close() }() + + log, src, route, nextBackend, err := findRoute(routes, log, client, handshake) + if err != nil { + errs.V(log, err).Info("failed to find route", "error", err) + return } -) -// Response returns the configured status response. -func (s *Status) Response(proto.Protocol) (*ping.ServerPing, error) { - return &ping.ServerPing{ - Version: s.Version, - Description: s.MOTD.T(), - Favicon: s.Favicon, - ModInfo: &s.ModInfo, - }, nil + // Find a backend to dial successfully. + log, dst, err := tryBackends(log, nextBackend, func(log logr.Logger, backendAddr string) (logr.Logger, net.Conn, error) { + conn, err := dialRoute(client.Context(), dialTimeout, src.RemoteAddr(), route, backendAddr, handshake, pc, false) + return log, conn, err + }) + if err != nil { + return + } + defer func() { _ = dst.Close() }() + + if err = emptyReadBuff(client, dst); err != nil { + errs.V(log, err).Info("failed to empty client buffer", "error", err) + return + } + + log.Info("forwarding connection", "backendAddr", netutil.Host(dst.RemoteAddr())) + pipe(log, src, dst) } -// GetCachePingTTL returns the configured ping cache TTL or a default duration if not set. -func (r *Route) GetCachePingTTL() time.Duration { - const defaultTTL = time.Second * 10 - if r.CachePingTTL == 0 { - return defaultTTL +// errAllBackendsFailed is returned when all backends failed to dial. +var errAllBackendsFailed = errors.New("all backends failed") + +// tryBackends tries backends until one succeeds or all fail. +func tryBackends[T any](log logr.Logger, next nextBackendFunc, try func(log logr.Logger, backendAddr string) (logr.Logger, T, error)) (logr.Logger, T, error) { + for { + backendAddr, ok := next() + if !ok { + var zero T + return log, zero, errAllBackendsFailed + } + + log, t, err := try(log, backendAddr) + if err != nil { + errs.V(log, err).Info("failed to try backend", "error", err) + continue + } + return log, t, nil } - return time.Duration(r.CachePingTTL) } -// CachePingEnabled returns true if the route has a ping cache enabled. -func (r *Route) CachePingEnabled() bool { return r.GetCachePingTTL() > 0 } +func emptyReadBuff(src netmc.MinecraftConn, dst net.Conn) error { + buf, ok := src.(interface{ ReadBuffered() ([]byte, error) }) + if ok { + b, err := buf.ReadBuffered() + if err != nil { + return fmt.Errorf("failed to read buffered bytes: %w", err) + } + if len(b) != 0 { + _, err = dst.Write(b) + if err != nil { + return fmt.Errorf("failed to write buffered bytes: %w", err) + } + } + } + return nil +} -// GetTCPShieldRealIP returns the configured TCPShieldRealIP or deprecated RealIP value. -func (r *Route) GetTCPShieldRealIP() bool { return r.TCPShieldRealIP || r.RealIP } +func pipe(log logr.Logger, src, dst net.Conn) { + // disable deadlines + var zero time.Time + _ = src.SetDeadline(zero) + _ = dst.SetDeadline(zero) -var allowedStrategies = map[string]bool{ - "random": true, - "round-robin": true, - "least connections": true, - "lowest latency": true, + go func() { + i, err := io.Copy(src, dst) + if log.Enabled() { + log.V(1).Info("done copying backend -> client", "bytes", i, "error", err) + } + }() + i, err := io.Copy(dst, src) + if log.Enabled() { + log.V(1).Info("done copying client -> backend", "bytes", i, "error", err) + } } -func (c Config) Validate() (warns []error, errs []error) { - e := func(m string, args ...any) { errs = append(errs, fmt.Errorf(m, args...)) } +type nextBackendFunc func() (backendAddr string, ok bool) - if len(c.Routes) == 0 { - e("No routes configured") - return +func findRoute( + routes []config.Route, + log logr.Logger, + client netmc.MinecraftConn, + handshake *packet.Handshake, +) ( + newLog logr.Logger, + src net.Conn, + route *config.Route, + nextBackend nextBackendFunc, + err error, +) { + srcConn, ok := netmc.Assert[interface{ Conn() net.Conn }](client) + if !ok { + return log, src, nil, nil, errors.New("failed to assert connection as net.Conn") } + src = srcConn.Conn() - for i, ep := range c.Routes { - if len(ep.Host) == 0 { - e("Route %d: no host configured", i) + clearedHost := ClearVirtualHost(handshake.ServerAddress) + log = log.WithName("lite").WithValues( + "clientAddr", netutil.Host(src.RemoteAddr()), + "virtualHost", clearedHost, + "protocol", proto.Protocol(handshake.ProtocolVersion).String(), + ) + + host, route := FindRoute(clearedHost, routes...) + if route == nil { + return log.V(1), src, nil, nil, fmt.Errorf("no route configured for host %s", clearedHost) + } + log = log.WithValues("route", host) + + if len(route.Backend) == 0 { + return log, src, route, nil, errors.New("no backend configured for route") + } + + tryBackends := route.Backend.Copy() + nextBackend = func() (string, bool) { + switch route.Strategy { + case "random": + return randomNextBackend(tryBackends)() + case "round-robin": + return roundRobinNextBackend(host, tryBackends)() + case "least connections": + return leastConnectionsNextBackend(tryBackends)() + case "lowest latency": + return lowestLatencyNextBackend(tryBackends)() + default: + // Default to random strategy + return randomNextBackend(tryBackends)() } - if len(ep.Backend) == 0 { - e("Route %d: no backend configured", i) + } + + return log, src, route, nextBackend, nil +} + +func randomNextBackend(tryBackends []string) nextBackendFunc { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + return func() (string, bool) { + if len(tryBackends) == 0 { + return "", false } - if _, ok := allowedStrategies[ep.Strategy]; !ok && ep.Strategy != "" { - e("Route %d: invalid strategy '%s'", i, ep.Strategy) + randIndex := r.Intn(len(tryBackends)) + return tryBackends[randIndex], true + } +} + +var roundRobinIndex = make(map[string]int) + +func roundRobinNextBackend(routeHost string, tryBackends []string) nextBackendFunc { + return func() (string, bool) { + if len(tryBackends) == 0 { + return "", false } - for i, addr := range ep.Backend { - _, err := netutil.Parse(addr, "tcp") - if err != nil { - e("Route %d: backend %d: failed to parse address: %w", i, err) + index := roundRobinIndex[routeHost] % len(tryBackends) + backend := tryBackends[index] + roundRobinIndex[routeHost]++ + return backend, true + } +} + +var connectionCounts = make(map[string]int) +var connectionCountsMutex sync.Mutex + +func leastConnectionsNextBackend(tryBackends []string) nextBackendFunc { + return func() (string, bool) { + if len(tryBackends) == 0 { + return "", false + } + connectionCountsMutex.Lock() + defer connectionCountsMutex.Unlock() + least := tryBackends[0] + for _, backend := range tryBackends { + if connectionCounts[backend] < connectionCounts[least] { + least = backend } } + connectionCounts[least]++ + return least, true + } +} + +var latencyCache = ttlcache.New[string, time.Duration]() + +func lowestLatencyNextBackend(tryBackends []string) nextBackendFunc { + return func() (string, bool) { + if len(tryBackends) == 0 { + return "", false + } + var lowestBackend string + var lowestLatency time.Duration + for _, backend := range tryBackends { + latencyItem := latencyCache.Get(backend) + if latencyItem == nil { + latency := measureLatency(backend) + latencyCache.Set(backend, latency, time.Minute) + latencyItem = latencyCache.Get(backend) + } + if latencyItem != nil && (lowestLatency == 0 || latencyItem.Value() < lowestLatency) { + lowestBackend = backend + lowestLatency = latencyItem.Value() + } + } + return lowestBackend, true + } +} + +func measureLatency(backend string) time.Duration { + start := time.Now() + conn, err := net.DialTimeout("tcp", backend, time.Second*5) + if err != nil { + return time.Duration(math.MaxInt64) // Return a very high latency if connection fails + } + conn.Close() + return time.Since(start) +} + +func dialRoute( + ctx context.Context, + dialTimeout time.Duration, + srcAddr net.Addr, + route *config.Route, + backendAddr string, + handshake *packet.Handshake, + handshakeCtx *proto.PacketContext, + forceUpdatePacketContext bool, +) (dst net.Conn, err error) { + dialCtx, cancel := context.WithTimeout(ctx, dialTimeout) + defer cancel() + + var dialer net.Dialer + dst, err = dialer.DialContext(dialCtx, "tcp", backendAddr) + if err != nil { + v := 0 + if dialCtx.Err() != nil { + v++ + } + return nil, &errs.VerbosityError{ + Verbosity: v, + Err: fmt.Errorf("failed to connect to backend %s: %w", backendAddr, err), + } + } + dstConn := dst + defer func() { + if err != nil { + _ = dstConn.Close() + } + }() + + if route.ProxyProtocol { + header := protoutil.ProxyHeader(srcAddr, dst.RemoteAddr()) + if _, err = header.WriteTo(dst); err != nil { + return dst, fmt.Errorf("failed to write proxy protocol header to backend: %w", err) + } + } + + if route.ModifyVirtualHost { + clearedHost := ClearVirtualHost(handshake.ServerAddress) + backendHost := netutil.HostStr(backendAddr) + if !strings.EqualFold(clearedHost, backendHost) { + // Modify the handshake packet to use the backend host as virtual host. + handshake.ServerAddress = strings.ReplaceAll(handshake.ServerAddress, clearedHost, backendHost) + forceUpdatePacketContext = true + } + } + if route.GetTCPShieldRealIP() && IsTCPShieldRealIP(handshake.ServerAddress) { + // Modify the handshake packet to use TCPShieldRealIP of the client. + handshake.ServerAddress = TCPShieldRealIP(handshake.ServerAddress, srcAddr) + forceUpdatePacketContext = true + } + if forceUpdatePacketContext { + update(handshakeCtx, handshake) } - return + // Forward handshake packet as is. + if err = writePacket(dst, handshakeCtx); err != nil { + return dst, fmt.Errorf("failed to write handshake packet to backend: %w", err) + } + + return dst, nil } -// Equal returns true if the Routes are equal. -func (r *Route) Equal(other *Route) bool { - j, err := json.Marshal(r) +func writePacket(dst net.Conn, pc *proto.PacketContext) error { + err := util.WriteVarInt(dst, len(pc.Payload)) + if err != nil { + return fmt.Errorf("failed to write packet length: %w", err) + } + _, err = dst.Write(pc.Payload) if err != nil { - return false + return fmt.Errorf("failed to write packet payload: %w", err) } - o, err := json.Marshal(other) + return nil +} + +func update(pc *proto.PacketContext, h *packet.Handshake) { + payload := new(bytes.Buffer) + _ = util.WriteVarInt(payload, int(pc.PacketID)) + _ = h.Encode(pc, payload) + pc.Payload = payload.Bytes() +} + +// ResolveStatusResponse resolves the status response for the matching route and caches it for a short time. +func ResolveStatusResponse( + dialTimeout time.Duration, + routes []config.Route, + log logr.Logger, + client netmc.MinecraftConn, + handshake *packet.Handshake, + handshakeCtx *proto.PacketContext, + statusRequestCtx *proto.PacketContext, +) (logr.Logger, *packet.StatusResponse, error) { + log, src, route, nextBackend, err := findRoute(routes, log, client, handshake) if err != nil { - return false + return log, nil, err } - return string(j) == string(o) + + log, res, err := tryBackends(log, nextBackend, func(log logr.Logger, backendAddr string) (logr.Logger, *packet.StatusResponse, error) { + return resolveStatusResponse(src, dialTimeout, backendAddr, route, log, client, handshake, handshakeCtx, statusRequestCtx) + }) + if err != nil && route.Fallback != nil { + log.Info("failed to resolve status response, will use fallback status response", "error", err) + + // Fallback status response if configured + fallbackPong, err := route.Fallback.Response(handshakeCtx.Protocol) + if err != nil { + log.Info("failed to get fallback status response", "error", err) + } + if fallbackPong != nil { + status, err2 := json.Marshal(fallbackPong) + if err2 != nil { + return log, nil, fmt.Errorf("%w: failed to marshal fallback status response: %w", err, err2) + } + if log.V(1).Enabled() { + log.V(1).Info("using fallback status response", "status", string(status)) + } + return log, &packet.StatusResponse{Status: string(status)}, nil + } + } + return log, res, err +} + +var ( + pingCache = ttlcache.New[pingKey, *pingResult]() + sfg = new(singleflight.Group) +) + +// ResetPingCache resets the ping cache. +func ResetPingCache() { + pingCache.DeleteAll() + compiledRegexCache.DeleteAll() +} + +func init() { + go pingCache.Start() // start ttl eviction once +} + +type pingKey struct { + backendAddr string + protocol proto.Protocol +} + +type pingResult struct { + res *packet.StatusResponse + err error +} + +func resolveStatusResponse( + src net.Conn, + dialTimeout time.Duration, + backendAddr string, + route *config.Route, + log logr.Logger, + client netmc.MinecraftConn, + handshake *packet.Handshake, + handshakeCtx *proto.PacketContext, + statusRequestCtx *proto.PacketContext, +) (logr.Logger, *packet.StatusResponse, error) { + key := pingKey{backendAddr, proto.Protocol(handshake.ProtocolVersion)} + + // fast path: use cache without loader + if route.CachePingEnabled() { + item := pingCache.Get(key) + if item != nil { + log.V(1).Info("returning cached status result") + val := item.Value() + return log, val.res, val.err + } + } + + // slow path: load cache, block many requests to same route + // + // resolve ping of remote backend, cache and return it. + // if more ping requests arrive at slow path for the same route + // the ping result of the first original request is returned to + // ensure a single connection per route for fetching the status + // while allowing many ping requests + + load := func(ctx context.Context) (*packet.StatusResponse, error) { + log.V(1).Info("resolving status") + + ctx = logr.NewContext(ctx, log) + dst, err := dialRoute(ctx, dialTimeout, src.RemoteAddr(), route, backendAddr, handshake, handshakeCtx, route.CachePingEnabled()) + if err != nil { + return nil, fmt.Errorf("failed to dial route: %w", err) + } + defer func() { _ = dst.Close() }() + + log = log.WithValues("backendAddr", netutil.Host(dst.RemoteAddr())) + return fetchStatus(log, dst, handshakeCtx.Protocol, statusRequestCtx) + } + + if !route.CachePingEnabled() { + res, err := load(client.Context()) + return log, res, err + } + + opt := withLoader(sfg, route.GetCachePingTTL(), func(key pingKey) *pingResult { + res, err := load(context.Background()) + return &pingResult{res: res, err: err} + }) + + resultChan := make(chan *pingResult, 1) + go func() { resultChan <- pingCache.Get(key, opt).Value() }() + + select { + case result := <-resultChan: + return log, result.res, result.err + case <-client.Context().Done(): + return log, nil, &errs.VerbosityError{ + Err: context.Cause(client.Context()), + Verbosity: 1, + } + } +} + +func fetchStatus( + log logr.Logger, + conn net.Conn, + protocol proto.Protocol, + statusRequestCtx *proto.PacketContext, +) (*packet.StatusResponse, error) { + if err := writePacket(conn, statusRequestCtx); err != nil { + return nil, fmt.Errorf("failed to write status request packet to backend: %w", err) + } + + dec := codec.NewDecoder(conn, proto.ClientBound, log.V(2)) + dec.SetProtocol(protocol) + dec.SetState(state.Status) + + pongCtx, err := dec.Decode() + if err != nil { + return nil, fmt.Errorf("failed to decode status response: %w", err) + } + + res, ok := pongCtx.Packet.(*packet.StatusResponse) + if !ok { + return nil, fmt.Errorf("received unexpected response: %s, expected %T", pongCtx, res) + } + + return res, nil +} + +// withLoader returns a ttlcache option that uses the given load function to load a value for a key +// if it is not already cached. +func withLoader[K comparable, V any](group *singleflight.Group, ttl time.Duration, load func(key K) V) ttlcache.Option[K, V] { + loader := ttlcache.LoaderFunc[K, V]( + func(c *ttlcache.Cache[K, V], key K) *ttlcache.Item[K, V] { + v := load(key) + return c.Set(key, v, ttl) + }, + ) + return ttlcache.WithLoader[K, V]( + ttlcache.NewSuppressedLoader[K, V](loader, group), + ) } From 422576232c6e3994b8751698fe99e0176507bf7a Mon Sep 17 00:00:00 2001 From: okeanosthedev <66648881+okeanosthedev@users.noreply.github.com> Date: Tue, 3 Dec 2024 13:17:48 +0300 Subject: [PATCH 15/27] Update config.go --- pkg/edition/java/lite/config/config.go | 563 ++++--------------------- 1 file changed, 82 insertions(+), 481 deletions(-) diff --git a/pkg/edition/java/lite/config/config.go b/pkg/edition/java/lite/config/config.go index 48c4a8c4..df37584b 100644 --- a/pkg/edition/java/lite/config/config.go +++ b/pkg/edition/java/lite/config/config.go @@ -1,519 +1,120 @@ -package lite +package config import ( - "bytes" - "context" "encoding/json" - "errors" "fmt" - "io" - "math" - "math/rand" - "net" - "strings" - "sync" "time" - "github.com/go-logr/logr" - "github.com/jellydator/ttlcache/v3" - "go.minekube.com/gate/pkg/edition/java/internal/protoutil" - "go.minekube.com/gate/pkg/edition/java/lite/config" - "go.minekube.com/gate/pkg/edition/java/netmc" - "go.minekube.com/gate/pkg/edition/java/proto/codec" - "go.minekube.com/gate/pkg/edition/java/proto/packet" - "go.minekube.com/gate/pkg/edition/java/proto/state" - "go.minekube.com/gate/pkg/edition/java/proto/util" + "go.minekube.com/gate/pkg/edition/java/forge/modinfo" + "go.minekube.com/gate/pkg/edition/java/ping" "go.minekube.com/gate/pkg/gate/proto" - "go.minekube.com/gate/pkg/util/errs" + "go.minekube.com/gate/pkg/util/configutil" + "go.minekube.com/gate/pkg/util/favicon" "go.minekube.com/gate/pkg/util/netutil" - "golang.org/x/sync/singleflight" ) -// Forward forwards a client connection to a matching backend route. -func Forward( - dialTimeout time.Duration, - routes []config.Route, - log logr.Logger, - client netmc.MinecraftConn, - handshake *packet.Handshake, - pc *proto.PacketContext, -) { - defer func() { _ = client.Close() }() - - log, src, route, nextBackend, err := findRoute(routes, log, client, handshake) - if err != nil { - errs.V(log, err).Info("failed to find route", "error", err) - return - } - - // Find a backend to dial successfully. - log, dst, err := tryBackends(log, nextBackend, func(log logr.Logger, backendAddr string) (logr.Logger, net.Conn, error) { - conn, err := dialRoute(client.Context(), dialTimeout, src.RemoteAddr(), route, backendAddr, handshake, pc, false) - return log, conn, err - }) - if err != nil { - return - } - defer func() { _ = dst.Close() }() - - if err = emptyReadBuff(client, dst); err != nil { - errs.V(log, err).Info("failed to empty client buffer", "error", err) - return +// DefaultConfig is the default configuration for Lite mode. +var DefaultConfig = Config{ + Enabled: false, + Routes: []Route{}, +} + +type ( + // Config is the configuration for Lite mode. + Config struct { + Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"` + Routes []Route `yaml:"routes,omitempty" json:"routes,omitempty"` + } + Route struct { + Host configutil.SingleOrMulti[string] `json:"host,omitempty" yaml:"host,omitempty"` + Backend configutil.SingleOrMulti[string] `json:"backend,omitempty" yaml:"backend,omitempty"` + Strategy string `json:"strategy,omitempty" yaml:"strategy,omitempty"` + CachePingTTL configutil.Duration `json:"cachePingTTL,omitempty" yaml:"cachePingTTL,omitempty"` // 0 = default, < 0 = disabled + Fallback *Status `json:"fallback,omitempty" yaml:"fallback,omitempty"` // nil = disabled + ProxyProtocol bool `json:"proxyProtocol,omitempty" yaml:"proxyProtocol,omitempty"` + // Deprecated: use TCPShieldRealIP instead. + RealIP bool `json:"realIP,omitempty" yaml:"realIP,omitempty"` + TCPShieldRealIP bool `json:"tcpShieldRealIP,omitempty" yaml:"tcpShieldRealIP,omitempty"` + ModifyVirtualHost bool `json:"modifyVirtualHost,omitempty" yaml:"modifyVirtualHost,omitempty"` + } + Status struct { + MOTD *configutil.TextComponent `yaml:"motd,omitempty" json:"motd,omitempty"` + Version ping.Version `yaml:"version,omitempty" json:"version,omitempty"` + Favicon favicon.Favicon `yaml:"favicon,omitempty" json:"favicon,omitempty"` + ModInfo modinfo.ModInfo `yaml:"modInfo,omitempty" json:"modInfo,omitempty"` } +) - log.Info("forwarding connection", "backendAddr", netutil.Host(dst.RemoteAddr())) - pipe(log, src, dst) +// Response returns the configured status response. +func (s *Status) Response(proto.Protocol) (*ping.ServerPing, error) { + return &ping.ServerPing{ + Version: s.Version, + Description: s.MOTD.T(), + Favicon: s.Favicon, + ModInfo: &s.ModInfo, + }, nil } -// errAllBackendsFailed is returned when all backends failed to dial. -var errAllBackendsFailed = errors.New("all backends failed") - -// tryBackends tries backends until one succeeds or all fail. -func tryBackends[T any](log logr.Logger, next nextBackendFunc, try func(log logr.Logger, backendAddr string) (logr.Logger, T, error)) (logr.Logger, T, error) { - for { - backendAddr, ok := next() - if !ok { - var zero T - return log, zero, errAllBackendsFailed - } - - log, t, err := try(log, backendAddr) - if err != nil { - errs.V(log, err).Info("failed to try backend", "error", err) - continue - } - return log, t, nil +// GetCachePingTTL returns the configured ping cache TTL or a default duration if not set. +func (r *Route) GetCachePingTTL() time.Duration { + const defaultTTL = time.Second * 10 + if r.CachePingTTL == 0 { + return defaultTTL } + return time.Duration(r.CachePingTTL) } -func emptyReadBuff(src netmc.MinecraftConn, dst net.Conn) error { - buf, ok := src.(interface{ ReadBuffered() ([]byte, error) }) - if ok { - b, err := buf.ReadBuffered() - if err != nil { - return fmt.Errorf("failed to read buffered bytes: %w", err) - } - if len(b) != 0 { - _, err = dst.Write(b) - if err != nil { - return fmt.Errorf("failed to write buffered bytes: %w", err) - } - } - } - return nil -} +// CachePingEnabled returns true if the route has a ping cache enabled. +func (r *Route) CachePingEnabled() bool { return r.GetCachePingTTL() > 0 } -func pipe(log logr.Logger, src, dst net.Conn) { - // disable deadlines - var zero time.Time - _ = src.SetDeadline(zero) - _ = dst.SetDeadline(zero) +// GetTCPShieldRealIP returns the configured TCPShieldRealIP or deprecated RealIP value. +func (r *Route) GetTCPShieldRealIP() bool { return r.TCPShieldRealIP || r.RealIP } - go func() { - i, err := io.Copy(src, dst) - if log.Enabled() { - log.V(1).Info("done copying backend -> client", "bytes", i, "error", err) - } - }() - i, err := io.Copy(dst, src) - if log.Enabled() { - log.V(1).Info("done copying client -> backend", "bytes", i, "error", err) - } +var allowedStrategies = map[string]bool{ + "random": true, + "round-robin": true, + "least connections": true, + "lowest latency": true, } -type nextBackendFunc func() (backendAddr string, ok bool) - -func findRoute( - routes []config.Route, - log logr.Logger, - client netmc.MinecraftConn, - handshake *packet.Handshake, -) ( - newLog logr.Logger, - src net.Conn, - route *config.Route, - nextBackend nextBackendFunc, - err error, -) { - srcConn, ok := netmc.Assert[interface{ Conn() net.Conn }](client) - if !ok { - return log, src, nil, nil, errors.New("failed to assert connection as net.Conn") - } - src = srcConn.Conn() - - clearedHost := ClearVirtualHost(handshake.ServerAddress) - log = log.WithName("lite").WithValues( - "clientAddr", netutil.Host(src.RemoteAddr()), - "virtualHost", clearedHost, - "protocol", proto.Protocol(handshake.ProtocolVersion).String(), - ) - - host, route := FindRoute(clearedHost, routes...) - if route == nil { - return log.V(1), src, nil, nil, fmt.Errorf("no route configured for host %s", clearedHost) - } - log = log.WithValues("route", host) +func (c Config) Validate() (warns []error, errs []error) { + e := func(m string, args ...any) { errs = append(errs, fmt.Errorf(m, args...)) } - if len(route.Backend) == 0 { - return log, src, route, nil, errors.New("no backend configured for route") - } - - tryBackends := route.Backend.Copy() - nextBackend = func() (string, bool) { - switch route.Strategy { - case "random": - return randomNextBackend(tryBackends)() - case "round-robin": - return roundRobinNextBackend(host, tryBackends)() - case "least connections": - return leastConnectionsNextBackend(tryBackends)() - case "lowest latency": - return lowestLatencyNextBackend(tryBackends)() - default: - // Default to random strategy - return randomNextBackend(tryBackends)() - } - } - - return log, src, route, nextBackend, nil -} - -func randomNextBackend(tryBackends []string) nextBackendFunc { - r := rand.New(rand.NewSource(time.Now().UnixNano())) - return func() (string, bool) { - if len(tryBackends) == 0 { - return "", false - } - randIndex := r.Intn(len(tryBackends)) - return tryBackends[randIndex], true + if len(c.Routes) == 0 { + e("No routes configured") + return } -} -var roundRobinIndex = make(map[string]int) - -func roundRobinNextBackend(routeHost string, tryBackends []string) nextBackendFunc { - return func() (string, bool) { - if len(tryBackends) == 0 { - return "", false + for i, ep := range c.Routes { + if len(ep.Host) == 0 { + e("Route %d: no host configured", i) } - index := roundRobinIndex[routeHost] % len(tryBackends) - backend := tryBackends[index] - roundRobinIndex[routeHost]++ - return backend, true - } -} - -var connectionCounts = make(map[string]int) -var connectionCountsMutex sync.Mutex - -func leastConnectionsNextBackend(tryBackends []string) nextBackendFunc { - return func() (string, bool) { - if len(tryBackends) == 0 { - return "", false + if len(ep.Backend) == 0 { + e("Route %d: no backend configured", i) } - connectionCountsMutex.Lock() - defer connectionCountsMutex.Unlock() - least := tryBackends[0] - for _, backend := range tryBackends { - if connectionCounts[backend] < connectionCounts[least] { - least = backend - } + if _, ok := allowedStrategies[ep.Strategy]; !ok && ep.Strategy != "" { + e("Route %d: invalid strategy '%s'", i, ep.Strategy) } - connectionCounts[least]++ - return least, true - } -} - -var latencyCache = ttlcache.New[string, time.Duration]() - -func lowestLatencyNextBackend(tryBackends []string) nextBackendFunc { - return func() (string, bool) { - if len(tryBackends) == 0 { - return "", false - } - var lowestBackend string - var lowestLatency time.Duration - for _, backend := range tryBackends { - latencyItem := latencyCache.Get(backend) - if latencyItem == nil { - latency := measureLatency(backend) - latencyCache.Set(backend, latency, time.Minute) - latencyItem = latencyCache.Get(backend) - } - if latencyItem != nil && (lowestLatency == 0 || latencyItem.Value() < lowestLatency) { - lowestBackend = backend - lowestLatency = latencyItem.Value() + for i, addr := range ep.Backend { + _, err := netutil.Parse(addr, "tcp") + if err != nil { + e("Route %d: backend %d: failed to parse address: %w", i, err) } } - return lowestBackend, true - } -} - -func measureLatency(backend string) time.Duration { - start := time.Now() - conn, err := net.DialTimeout("tcp", backend, time.Second*5) - if err != nil { - return time.Duration(math.MaxInt64) // Return a very high latency if connection fails - } - conn.Close() - return time.Since(start) -} - -func dialRoute( - ctx context.Context, - dialTimeout time.Duration, - srcAddr net.Addr, - route *config.Route, - backendAddr string, - handshake *packet.Handshake, - handshakeCtx *proto.PacketContext, - forceUpdatePacketContext bool, -) (dst net.Conn, err error) { - dialCtx, cancel := context.WithTimeout(ctx, dialTimeout) - defer cancel() - - var dialer net.Dialer - dst, err = dialer.DialContext(dialCtx, "tcp", backendAddr) - if err != nil { - v := 0 - if dialCtx.Err() != nil { - v++ - } - return nil, &errs.VerbosityError{ - Verbosity: v, - Err: fmt.Errorf("failed to connect to backend %s: %w", backendAddr, err), - } - } - dstConn := dst - defer func() { - if err != nil { - _ = dstConn.Close() - } - }() - - if route.ProxyProtocol { - header := protoutil.ProxyHeader(srcAddr, dst.RemoteAddr()) - if _, err = header.WriteTo(dst); err != nil { - return dst, fmt.Errorf("failed to write proxy protocol header to backend: %w", err) - } - } - - if route.ModifyVirtualHost { - clearedHost := ClearVirtualHost(handshake.ServerAddress) - backendHost := netutil.HostStr(backendAddr) - if !strings.EqualFold(clearedHost, backendHost) { - // Modify the handshake packet to use the backend host as virtual host. - handshake.ServerAddress = strings.ReplaceAll(handshake.ServerAddress, clearedHost, backendHost) - forceUpdatePacketContext = true - } - } - if route.GetTCPShieldRealIP() && IsTCPShieldRealIP(handshake.ServerAddress) { - // Modify the handshake packet to use TCPShieldRealIP of the client. - handshake.ServerAddress = TCPShieldRealIP(handshake.ServerAddress, srcAddr) - forceUpdatePacketContext = true - } - if forceUpdatePacketContext { - update(handshakeCtx, handshake) } - // Forward handshake packet as is. - if err = writePacket(dst, handshakeCtx); err != nil { - return dst, fmt.Errorf("failed to write handshake packet to backend: %w", err) - } - - return dst, nil + return } -func writePacket(dst net.Conn, pc *proto.PacketContext) error { - err := util.WriteVarInt(dst, len(pc.Payload)) - if err != nil { - return fmt.Errorf("failed to write packet length: %w", err) - } - _, err = dst.Write(pc.Payload) +// Equal returns true if the Routes are equal. +func (r *Route) Equal(other *Route) bool { + j, err := json.Marshal(r) if err != nil { - return fmt.Errorf("failed to write packet payload: %w", err) + return false } - return nil -} - -func update(pc *proto.PacketContext, h *packet.Handshake) { - payload := new(bytes.Buffer) - _ = util.WriteVarInt(payload, int(pc.PacketID)) - _ = h.Encode(pc, payload) - pc.Payload = payload.Bytes() -} - -// ResolveStatusResponse resolves the status response for the matching route and caches it for a short time. -func ResolveStatusResponse( - dialTimeout time.Duration, - routes []config.Route, - log logr.Logger, - client netmc.MinecraftConn, - handshake *packet.Handshake, - handshakeCtx *proto.PacketContext, - statusRequestCtx *proto.PacketContext, -) (logr.Logger, *packet.StatusResponse, error) { - log, src, route, nextBackend, err := findRoute(routes, log, client, handshake) + o, err := json.Marshal(other) if err != nil { - return log, nil, err + return false } - - log, res, err := tryBackends(log, nextBackend, func(log logr.Logger, backendAddr string) (logr.Logger, *packet.StatusResponse, error) { - return resolveStatusResponse(src, dialTimeout, backendAddr, route, log, client, handshake, handshakeCtx, statusRequestCtx) - }) - if err != nil && route.Fallback != nil { - log.Info("failed to resolve status response, will use fallback status response", "error", err) - - // Fallback status response if configured - fallbackPong, err := route.Fallback.Response(handshakeCtx.Protocol) - if err != nil { - log.Info("failed to get fallback status response", "error", err) - } - if fallbackPong != nil { - status, err2 := json.Marshal(fallbackPong) - if err2 != nil { - return log, nil, fmt.Errorf("%w: failed to marshal fallback status response: %w", err, err2) - } - if log.V(1).Enabled() { - log.V(1).Info("using fallback status response", "status", string(status)) - } - return log, &packet.StatusResponse{Status: string(status)}, nil - } - } - return log, res, err -} - -var ( - pingCache = ttlcache.New[pingKey, *pingResult]() - sfg = new(singleflight.Group) -) - -// ResetPingCache resets the ping cache. -func ResetPingCache() { - pingCache.DeleteAll() - compiledRegexCache.DeleteAll() -} - -func init() { - go pingCache.Start() // start ttl eviction once -} - -type pingKey struct { - backendAddr string - protocol proto.Protocol -} - -type pingResult struct { - res *packet.StatusResponse - err error -} - -func resolveStatusResponse( - src net.Conn, - dialTimeout time.Duration, - backendAddr string, - route *config.Route, - log logr.Logger, - client netmc.MinecraftConn, - handshake *packet.Handshake, - handshakeCtx *proto.PacketContext, - statusRequestCtx *proto.PacketContext, -) (logr.Logger, *packet.StatusResponse, error) { - key := pingKey{backendAddr, proto.Protocol(handshake.ProtocolVersion)} - - // fast path: use cache without loader - if route.CachePingEnabled() { - item := pingCache.Get(key) - if item != nil { - log.V(1).Info("returning cached status result") - val := item.Value() - return log, val.res, val.err - } - } - - // slow path: load cache, block many requests to same route - // - // resolve ping of remote backend, cache and return it. - // if more ping requests arrive at slow path for the same route - // the ping result of the first original request is returned to - // ensure a single connection per route for fetching the status - // while allowing many ping requests - - load := func(ctx context.Context) (*packet.StatusResponse, error) { - log.V(1).Info("resolving status") - - ctx = logr.NewContext(ctx, log) - dst, err := dialRoute(ctx, dialTimeout, src.RemoteAddr(), route, backendAddr, handshake, handshakeCtx, route.CachePingEnabled()) - if err != nil { - return nil, fmt.Errorf("failed to dial route: %w", err) - } - defer func() { _ = dst.Close() }() - - log = log.WithValues("backendAddr", netutil.Host(dst.RemoteAddr())) - return fetchStatus(log, dst, handshakeCtx.Protocol, statusRequestCtx) - } - - if !route.CachePingEnabled() { - res, err := load(client.Context()) - return log, res, err - } - - opt := withLoader(sfg, route.GetCachePingTTL(), func(key pingKey) *pingResult { - res, err := load(context.Background()) - return &pingResult{res: res, err: err} - }) - - resultChan := make(chan *pingResult, 1) - go func() { resultChan <- pingCache.Get(key, opt).Value() }() - - select { - case result := <-resultChan: - return log, result.res, result.err - case <-client.Context().Done(): - return log, nil, &errs.VerbosityError{ - Err: context.Cause(client.Context()), - Verbosity: 1, - } - } -} - -func fetchStatus( - log logr.Logger, - conn net.Conn, - protocol proto.Protocol, - statusRequestCtx *proto.PacketContext, -) (*packet.StatusResponse, error) { - if err := writePacket(conn, statusRequestCtx); err != nil { - return nil, fmt.Errorf("failed to write status request packet to backend: %w", err) - } - - dec := codec.NewDecoder(conn, proto.ClientBound, log.V(2)) - dec.SetProtocol(protocol) - dec.SetState(state.Status) - - pongCtx, err := dec.Decode() - if err != nil { - return nil, fmt.Errorf("failed to decode status response: %w", err) - } - - res, ok := pongCtx.Packet.(*packet.StatusResponse) - if !ok { - return nil, fmt.Errorf("received unexpected response: %s, expected %T", pongCtx, res) - } - - return res, nil -} - -// withLoader returns a ttlcache option that uses the given load function to load a value for a key -// if it is not already cached. -func withLoader[K comparable, V any](group *singleflight.Group, ttl time.Duration, load func(key K) V) ttlcache.Option[K, V] { - loader := ttlcache.LoaderFunc[K, V]( - func(c *ttlcache.Cache[K, V], key K) *ttlcache.Item[K, V] { - v := load(key) - return c.Set(key, v, ttl) - }, - ) - return ttlcache.WithLoader[K, V]( - ttlcache.NewSuppressedLoader[K, V](loader, group), - ) + return string(j) == string(o) } From bc228bae8c3d16828e66aa30b4e77d9ab106b1e3 Mon Sep 17 00:00:00 2001 From: okeanosthedev <66648881+okeanosthedev@users.noreply.github.com> Date: Tue, 3 Dec 2024 13:19:57 +0300 Subject: [PATCH 16/27] Update forward.go --- pkg/edition/java/lite/forward.go | 98 ++++++++++++++++---------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/pkg/edition/java/lite/forward.go b/pkg/edition/java/lite/forward.go index a4169670..48c4a8c4 100644 --- a/pkg/edition/java/lite/forward.go +++ b/pkg/edition/java/lite/forward.go @@ -31,38 +31,38 @@ import ( // Forward forwards a client connection to a matching backend route. func Forward( - dialTimeout time.Duration, - routes []config.Route, - log logr.Logger, - client netmc.MinecraftConn, - handshake *packet.Handshake, - pc *proto.PacketContext, + dialTimeout time.Duration, + routes []config.Route, + log logr.Logger, + client netmc.MinecraftConn, + handshake *packet.Handshake, + pc *proto.PacketContext, ) { - defer func() { _ = client.Close() }() - - log, src, route, nextBackend, err := findRoute(routes, log, client, handshake) - if err != nil { - errs.V(log, err).Info("failed to find route", "error", err) - return - } - - // Find a backend to dial successfully. - log, dst, err := tryBackends(log, nextBackend, func(log logr.Logger, backendAddr string) (logr.Logger, net.Conn, error) { - conn, err := dialRoute(client.Context(), dialTimeout, src.RemoteAddr(), route, backendAddr, handshake, pc, false) - return log, conn, err - }) - if err != nil { - return - } - defer func() { _ = dst.Close() }() - - if err = emptyReadBuff(client, dst); err != nil { - errs.V(log, err).Info("failed to empty client buffer", "error", err) - return - } - - log.Info("forwarding connection", "backendAddr", netutil.Host(dst.RemoteAddr())) - pipe(log, src, dst) + defer func() { _ = client.Close() }() + + log, src, route, nextBackend, err := findRoute(routes, log, client, handshake) + if err != nil { + errs.V(log, err).Info("failed to find route", "error", err) + return + } + + // Find a backend to dial successfully. + log, dst, err := tryBackends(log, nextBackend, func(log logr.Logger, backendAddr string) (logr.Logger, net.Conn, error) { + conn, err := dialRoute(client.Context(), dialTimeout, src.RemoteAddr(), route, backendAddr, handshake, pc, false) + return log, conn, err + }) + if err != nil { + return + } + defer func() { _ = dst.Close() }() + + if err = emptyReadBuff(client, dst); err != nil { + errs.V(log, err).Info("failed to empty client buffer", "error", err) + return + } + + log.Info("forwarding connection", "backendAddr", netutil.Host(dst.RemoteAddr())) + pipe(log, src, dst) } // errAllBackendsFailed is returned when all backends failed to dial. @@ -70,20 +70,20 @@ var errAllBackendsFailed = errors.New("all backends failed") // tryBackends tries backends until one succeeds or all fail. func tryBackends[T any](log logr.Logger, next nextBackendFunc, try func(log logr.Logger, backendAddr string) (logr.Logger, T, error)) (logr.Logger, T, error) { - for { - backendAddr, ok := next() - if !ok { - var zero T - return log, zero, errAllBackendsFailed - } - - log, t, err := try(log, backendAddr) - if err != nil { - errs.V(log, err).Info("failed to try backend", "error", err) - continue - } - return log, t, nil - } + for { + backendAddr, ok := next() + if !ok { + var zero T + return log, zero, errAllBackendsFailed + } + + log, t, err := try(log, backendAddr) + if err != nil { + errs.V(log, err).Info("failed to try backend", "error", err) + continue + } + return log, t, nil + } } func emptyReadBuff(src netmc.MinecraftConn, dst net.Conn) error { @@ -165,9 +165,9 @@ func findRoute( return randomNextBackend(tryBackends)() case "round-robin": return roundRobinNextBackend(host, tryBackends)() - case "least-connections": + case "least connections": return leastConnectionsNextBackend(tryBackends)() - case "lowest-latency": + case "lowest latency": return lowestLatencyNextBackend(tryBackends)() default: // Default to random strategy @@ -179,12 +179,12 @@ func findRoute( } func randomNextBackend(tryBackends []string) nextBackendFunc { + r := rand.New(rand.NewSource(time.Now().UnixNano())) return func() (string, bool) { if len(tryBackends) == 0 { return "", false } - rand.Seed(time.Now().UnixNano()) - randIndex := rand.Intn(len(tryBackends)) + randIndex := r.Intn(len(tryBackends)) return tryBackends[randIndex], true } } From 2a577271cdb6e4c25500a73e26f0409f0e7a5b51 Mon Sep 17 00:00:00 2001 From: okeanosthedev <66648881+okeanosthedev@users.noreply.github.com> Date: Tue, 3 Dec 2024 21:50:46 +0300 Subject: [PATCH 17/27] Update config.go --- pkg/edition/java/lite/config/config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/edition/java/lite/config/config.go b/pkg/edition/java/lite/config/config.go index df37584b..982af0e0 100644 --- a/pkg/edition/java/lite/config/config.go +++ b/pkg/edition/java/lite/config/config.go @@ -73,8 +73,8 @@ func (r *Route) GetTCPShieldRealIP() bool { return r.TCPShieldRealIP || r.RealIP var allowedStrategies = map[string]bool{ "random": true, "round-robin": true, - "least connections": true, - "lowest latency": true, + "least-connections": true, + "lowest-latency": true, } func (c Config) Validate() (warns []error, errs []error) { From ff9f73d8390df42727f9510b42093286a66016cc Mon Sep 17 00:00:00 2001 From: okeanosthedev <66648881+okeanosthedev@users.noreply.github.com> Date: Tue, 3 Dec 2024 21:51:05 +0300 Subject: [PATCH 18/27] Update forward.go --- pkg/edition/java/lite/forward.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/edition/java/lite/forward.go b/pkg/edition/java/lite/forward.go index 48c4a8c4..b9054799 100644 --- a/pkg/edition/java/lite/forward.go +++ b/pkg/edition/java/lite/forward.go @@ -165,9 +165,9 @@ func findRoute( return randomNextBackend(tryBackends)() case "round-robin": return roundRobinNextBackend(host, tryBackends)() - case "least connections": + case "least-connections": return leastConnectionsNextBackend(tryBackends)() - case "lowest latency": + case "lowest-latency": return lowestLatencyNextBackend(tryBackends)() default: // Default to random strategy From 2cf7c92af7548415fe599ccb445f2d7205f779a4 Mon Sep 17 00:00:00 2001 From: okeanosthedev <66648881+okeanosthedev@users.noreply.github.com> Date: Wed, 4 Dec 2024 12:20:29 +0300 Subject: [PATCH 19/27] Update forward.go --- pkg/edition/java/lite/forward.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/edition/java/lite/forward.go b/pkg/edition/java/lite/forward.go index b9054799..f7f1f0e1 100644 --- a/pkg/edition/java/lite/forward.go +++ b/pkg/edition/java/lite/forward.go @@ -61,7 +61,15 @@ func Forward( return } - log.Info("forwarding connection", "backendAddr", netutil.Host(dst.RemoteAddr())) + // Extract the backendAddr with the port and the IP separately + backendAddrWithPort := dst.RemoteAddr().String() + backendIP, _, _ := net.SplitHostPort(backendAddrWithPort) + + // Include the strategy name in the log + strategyName := route.Strategy + + log.Info("forwarding connection", "clientAddr", netutil.Host(src.RemoteAddr()), "virtualHost", ClearVirtualHost(handshake.ServerAddress), "protocol", proto.Protocol(handshake.ProtocolVersion).String(), "route", route.Host, "backendAddr", backendAddrWithPort, "backendAddr", backendIP, "strategy", strategyName) + pipe(log, src, dst) } From 58ddd018c7bb6b8557c55b29913c37e36d2d432c Mon Sep 17 00:00:00 2001 From: okeanosthedev <66648881+okeanosthedev@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:16:39 +0300 Subject: [PATCH 20/27] Revert "Merge branch 'minekube:master' into master" This reverts commit 94b87a27ec613480f1148356f3b08803ccf8e011, reversing changes made to 2cf7c92af7548415fe599ccb445f2d7205f779a4. --- .examples/extend/simple-proxy/go.mod | 14 ++++---- .examples/extend/simple-proxy/go.sum | 12 ------- .web/docs/developers/api/java/pom.xml | 10 +++--- .../developers/api/kotlin/build.gradle.kts | 4 +-- .web/docs/guide/install/docker.md | 15 +------- Dockerfile | 2 +- go.mod | 14 ++++---- go.sum | 26 +++++++------- .../proto/packet/tablist/playerinfo/upsert.go | 19 ++--------- pkg/edition/java/proto/state/register.go | 1 - pkg/edition/java/proto/version/version.go | 3 +- pkg/edition/java/proxy/tablist/tablist.go | 10 +----- pkg/internal/api/service.go | 4 +-- pkg/internal/tablist/entry.go | 34 +------------------ pkg/internal/tablist/tablist.go | 19 +---------- pkg/util/connectutil/config/auth.go | 7 +--- 16 files changed, 46 insertions(+), 148 deletions(-) diff --git a/.examples/extend/simple-proxy/go.mod b/.examples/extend/simple-proxy/go.mod index 08b2e00e..22429b76 100644 --- a/.examples/extend/simple-proxy/go.mod +++ b/.examples/extend/simple-proxy/go.mod @@ -7,8 +7,8 @@ replace go.minekube.com/gate => ../../../ require ( github.com/robinbraemer/event v0.1.1 go.minekube.com/brigodier v0.0.1 - go.minekube.com/common v0.0.6 - go.minekube.com/gate v0.46.0 + go.minekube.com/common v0.0.5 + go.minekube.com/gate v0.43.0 ) require ( @@ -57,13 +57,13 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect - golang.org/x/net v0.32.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/net v0.31.0 // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.20.0 // indirect golang.org/x/time v0.8.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f // indirect - google.golang.org/grpc v1.68.1 // indirect + google.golang.org/grpc v1.68.0 // indirect google.golang.org/protobuf v1.35.2 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/.examples/extend/simple-proxy/go.sum b/.examples/extend/simple-proxy/go.sum index c914f8e8..d87c6f67 100644 --- a/.examples/extend/simple-proxy/go.sum +++ b/.examples/extend/simple-proxy/go.sum @@ -273,8 +273,6 @@ go.minekube.com/brigodier v0.0.1 h1:v5x+fZNefM24JIi+fYQjQcjZ8rwJbfRSpnnpw4b/x6k= go.minekube.com/brigodier v0.0.1/go.mod h1:WJf/lyJVTId/phiY6phPW6++qkTjCQ72rbOWqo4XIqc= go.minekube.com/common v0.0.5 h1:h9EqMI3drSewTroBssy/eQniIP+Itirtj+av2PxyoP4= go.minekube.com/common v0.0.5/go.mod h1:PCdSdTInlQv6ggDIbVjLFs7ehSRP4i9KqYsLAeeNUYU= -go.minekube.com/common v0.0.6 h1:XA4mcgDG13hQEBcY2JdmK0Ca2mx2jOZ9M8pflZ85dkE= -go.minekube.com/common v0.0.6/go.mod h1:RTT2cwrMS+hwGAjJOt06bWtbKx04MuiF0tScyvGeAZo= go.minekube.com/connect v0.6.0 h1:44hY7AClb1rWgpga6NdMGTJU6FpUpK05y7dGLqX09OI= go.minekube.com/connect v0.6.0/go.mod h1:6PKiMQqV/k5uZIpXWJzhXBWoCqwpaWSXZv0uFMh+AF0= go.minekube.com/connect v0.6.1 h1:kSb7W/FCVkH09agGDliOs5UtlOHJdC4b43s517rgsc0= @@ -325,8 +323,6 @@ golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= -golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= -golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -346,8 +342,6 @@ golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -366,8 +360,6 @@ golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= @@ -384,8 +376,6 @@ golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= @@ -453,8 +443,6 @@ google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= -google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= -google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= diff --git a/.web/docs/developers/api/java/pom.xml b/.web/docs/developers/api/java/pom.xml index 6525c4c9..182887af 100644 --- a/.web/docs/developers/api/java/pom.xml +++ b/.web/docs/developers/api/java/pom.xml @@ -33,7 +33,7 @@ build.buf.gen minekube_gate_protocolbuffers_java - 29.1.0.1.20241118150055.50fffb007499 + 28.3.0.2.20241118150055.50fffb007499 build.buf.gen @@ -44,23 +44,23 @@ io.grpc grpc-netty-shaded - 1.69.0 + 1.68.2 io.grpc grpc-protobuf - 1.69.0 + 1.68.2 io.grpc grpc-stub - 1.69.0 + 1.68.2 com.google.protobuf protobuf-java - 4.29.1 + 4.29.0 \ No newline at end of file diff --git a/.web/docs/developers/api/kotlin/build.gradle.kts b/.web/docs/developers/api/kotlin/build.gradle.kts index 83b8498e..4fa29c14 100644 --- a/.web/docs/developers/api/kotlin/build.gradle.kts +++ b/.web/docs/developers/api/kotlin/build.gradle.kts @@ -11,10 +11,10 @@ repositories { } } -val grpcVersion = "1.69.0" +val grpcVersion = "1.68.2" val grpcKotlinVersion = "1.4.1" val connectVersion = "0.7.1" -val protobufVersion = "4.29.1" +val protobufVersion = "4.29.0" dependencies { // Kotlin diff --git a/.web/docs/guide/install/docker.md b/.web/docs/guide/install/docker.md index 364cc4da..57fa9868 100644 --- a/.web/docs/guide/install/docker.md +++ b/.web/docs/guide/install/docker.md @@ -37,19 +37,6 @@ This command will pull and run the latest Gate image. ### Mounting a config file and Minekube Connect token -#### Using an environment variable for the token - -```sh{3} console -docker run -it --rm \ - -v PATH-TO-CONFIG/config.yml:/config.yml \ - -e CONNECT_TOKEN=YOUR-TOKEN \ - ghcr.io/minekube/gate:latest -``` - -**Note:** The `CONNECT_TOKEN` environment variable takes precedence over the `connect.json` file, so if both are provided, the token from the environment variable will be used instead. - -#### Using a volume for the token - ```sh{3} console docker run -it --rm \ -v PATH-TO-CONFIG/config.yml:/config.yml \ @@ -57,7 +44,7 @@ docker run -it --rm \ ghcr.io/minekube/gate:latest ``` -A [Minekube Connect](https://connect.minekube.com/) token json file can be automatically generated by running Gate with `connect.enable: true` in the config. +A [Minekube Connect](https://connect.minekube.com/) token json file can be automatically generated by running Gate with `connect.enable: true` in the config. ```json connect.json {"token":"YOUR-TOKEN"} diff --git a/Dockerfile b/Dockerfile index 82de6d9b..9877daa3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=$BUILDPLATFORM golang:1.23.4 AS build +FROM --platform=$BUILDPLATFORM golang:1.23.3 AS build WORKDIR /workspace # Copy the Go Modules manifests diff --git a/go.mod b/go.mod index 59aa47c1..f3a7f0d1 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module go.minekube.com/gate go 1.23.2 -toolchain go1.23.4 +toolchain go1.23.3 require ( connectrpc.com/connect v1.17.0 @@ -31,16 +31,16 @@ require ( github.com/urfave/cli/v2 v2.27.5 github.com/zyedidia/generic v1.2.1 go.minekube.com/brigodier v0.0.1 - go.minekube.com/common v0.0.6 + go.minekube.com/common v0.0.5 go.minekube.com/connect v0.6.2 go.uber.org/atomic v1.11.0 go.uber.org/zap v1.27.0 golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f - golang.org/x/net v0.32.0 - golang.org/x/sync v0.10.0 - golang.org/x/text v0.21.0 + golang.org/x/net v0.31.0 + golang.org/x/sync v0.9.0 + golang.org/x/text v0.20.0 golang.org/x/time v0.8.0 - google.golang.org/grpc v1.68.1 + google.golang.org/grpc v1.68.0 google.golang.org/protobuf v1.35.2 gopkg.in/yaml.v3 v3.0.1 ) @@ -74,7 +74,7 @@ require ( github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/image v0.18.0 // indirect - golang.org/x/sys v0.28.0 // indirect + golang.org/x/sys v0.27.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f // indirect gopkg.in/ini.v1 v1.67.0 // indirect ) diff --git a/go.sum b/go.sum index 96a9f958..553378c9 100644 --- a/go.sum +++ b/go.sum @@ -140,6 +140,8 @@ github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/robinbraemer/event v0.0.1 h1:2499Bm1c13+//IZyAQpjoTg4vQ+dndE8trxo1aUxWdI= +github.com/robinbraemer/event v0.0.1/go.mod h1:fKkjL2UbPajNcxc4oWYyRCcUalss0YtPxwMtZTuNo8o= github.com/robinbraemer/event v0.1.1 h1:1T7GturBzxsa8UUe/r3EmW9aHLErKBggfn43up5hOUA= github.com/robinbraemer/event v0.1.1/go.mod h1:fKkjL2UbPajNcxc4oWYyRCcUalss0YtPxwMtZTuNo8o= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= @@ -198,6 +200,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= @@ -217,8 +221,6 @@ go.minekube.com/brigodier v0.0.1 h1:v5x+fZNefM24JIi+fYQjQcjZ8rwJbfRSpnnpw4b/x6k= go.minekube.com/brigodier v0.0.1/go.mod h1:WJf/lyJVTId/phiY6phPW6++qkTjCQ72rbOWqo4XIqc= go.minekube.com/common v0.0.5 h1:h9EqMI3drSewTroBssy/eQniIP+Itirtj+av2PxyoP4= go.minekube.com/common v0.0.5/go.mod h1:PCdSdTInlQv6ggDIbVjLFs7ehSRP4i9KqYsLAeeNUYU= -go.minekube.com/common v0.0.6 h1:XA4mcgDG13hQEBcY2JdmK0Ca2mx2jOZ9M8pflZ85dkE= -go.minekube.com/common v0.0.6/go.mod h1:RTT2cwrMS+hwGAjJOt06bWtbKx04MuiF0tScyvGeAZo= go.minekube.com/connect v0.6.2 h1:RajPc7YgqygcOviV2g4xetL3J0Wzi8b/lsYXUzIltxE= go.minekube.com/connect v0.6.2/go.mod h1:28k11I4RyzUfAL3AkOXt3atzjeOFAC8eqbCMwsYKAb0= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= @@ -252,8 +254,8 @@ golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= -golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -263,19 +265,19 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= @@ -302,8 +304,8 @@ google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmE google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= -google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= +google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= +google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/edition/java/proto/packet/tablist/playerinfo/upsert.go b/pkg/edition/java/proto/packet/tablist/playerinfo/upsert.go index 63725385..5df4c17f 100644 --- a/pkg/edition/java/proto/packet/tablist/playerinfo/upsert.go +++ b/pkg/edition/java/proto/packet/tablist/playerinfo/upsert.go @@ -23,7 +23,6 @@ type ( Latency int // in milliseconds GameMode int DisplayName *chat.ComponentHolder // nil-able - ShowHat bool ListOrder int RemoteChatSession *chat.RemoteChatSession // nil-able } @@ -64,15 +63,14 @@ func ContainsAction(actions []UpsertAction, action UpsertAction) bool { } func (u *Upsert) Decode(c *proto.PacketContext, rd io.Reader) (err error) { - z := len(UpsertActions) + 1 - bytes := make([]byte, -mathutil.FloorDiv(-len(UpsertActions), z)) + bytes := make([]byte, -mathutil.FloorDiv(-len(UpsertActions), 8)) if _, err = io.ReadFull(rd, bytes); err != nil { return err } u.ActionSet = nil for i, action := range UpsertActions { - if bytes[i/z]&(1< Date: Mon, 16 Dec 2024 02:16:53 +0300 Subject: [PATCH 21/27] Addition of blacklist and max connection. Example console adding of blacklist: go run cmd/blacklist/main.go --remove || --add 127.0.0.1 and --list --- cmd/blacklist/main.go | 72 ++++++ ip_blacklist.json | 0 pkg/edition/java/lite/blacklist/blacklist.go | 147 +++++++++++ .../java/lite/blacklist/route_blacklist.go | 117 +++++++++ pkg/edition/java/lite/config/config.go | 23 +- pkg/edition/java/lite/forward.go | 228 ++++++++++++++++-- 6 files changed, 555 insertions(+), 32 deletions(-) create mode 100644 cmd/blacklist/main.go create mode 100644 ip_blacklist.json create mode 100644 pkg/edition/java/lite/blacklist/blacklist.go create mode 100644 pkg/edition/java/lite/blacklist/route_blacklist.go diff --git a/cmd/blacklist/main.go b/cmd/blacklist/main.go new file mode 100644 index 00000000..c62e3575 --- /dev/null +++ b/cmd/blacklist/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "flag" + "fmt" + "os" + + "go.minekube.com/gate/pkg/edition/java/lite/blacklist" +) + +func main() { + blacklistFile := flag.String("file", "ip_blacklist.json", "Path to the global blacklist JSON file") + routeBlacklistFile := flag.String("route-file", "route_blacklist.json", "Path to the route blacklist JSON file") + add := flag.String("add", "", "IP address to add to the blacklist") + remove := flag.String("remove", "", "IP address to remove from the blacklist") + route := flag.String("route", "", "Route for adding/removing IP (if not specified, uses global blacklist)") + list := flag.Bool("list", false, "List all blacklisted IPs") + flag.Parse() + + bl, err := blacklist.NewBlacklist(*blacklistFile) + if err != nil { + fmt.Printf("Error initializing global blacklist: %v\n", err) + os.Exit(1) + } + + rbl, err := blacklist.NewRouteBlacklist(*routeBlacklistFile) + if err != nil { + fmt.Printf("Error initializing route blacklist: %v\n", err) + os.Exit(1) + } + + if *add != "" { + if *route != "" { + err = rbl.Add(*route, *add) + } else { + err = bl.Add(*add) + } + if err != nil { + fmt.Printf("Error adding IP to blacklist: %v\n", err) + os.Exit(1) + } + fmt.Printf("Added %s to the blacklist\n", *add) + } + + if *remove != "" { + if *route != "" { + err = rbl.Remove(*route, *remove) + } else { + err = bl.Remove(*remove) + } + if err != nil { + fmt.Printf("Error removing IP from blacklist: %v\n", err) + os.Exit(1) + } + fmt.Printf("Removed %s from the blacklist\n", *remove) + } + + if *list { + fmt.Println("Global Blacklisted IPs:") + for _, ip := range bl.GetIPs() { + fmt.Println(ip) + } + fmt.Println("\nRoute Blacklisted IPs:") + for route, ips := range rbl.Blacklists { + fmt.Printf("Route: %s\n", route) + for _, ip := range ips { + fmt.Printf(" %s\n", ip) + } + } + } +} + diff --git a/ip_blacklist.json b/ip_blacklist.json new file mode 100644 index 00000000..e69de29b diff --git a/pkg/edition/java/lite/blacklist/blacklist.go b/pkg/edition/java/lite/blacklist/blacklist.go new file mode 100644 index 00000000..95d88ce9 --- /dev/null +++ b/pkg/edition/java/lite/blacklist/blacklist.go @@ -0,0 +1,147 @@ +package blacklist + +import ( + "encoding/json" + "fmt" + "net" + "os" + "sync" +) + +type Blacklist struct { + IPs []string `json:"ips"` + mu sync.RWMutex + file string +} + +func NewBlacklist(file string) (*Blacklist, error) { + bl := &Blacklist{ + IPs: []string{}, + file: file, + } + err := bl.Load() + if err != nil { + if os.IsNotExist(err) { + // If the file doesn't exist, create it with an empty blacklist + return bl, bl.Save() + } + return nil, err + } + return bl, nil +} + +func (bl *Blacklist) Load() error { + bl.mu.Lock() + defer bl.mu.Unlock() + + data, err := os.ReadFile(bl.file) + if err != nil { + return err + } + + var temp struct { + IPs []string `json:"ips"` + } + + err = json.Unmarshal(data, &temp) + if err != nil { + return err + } + + bl.IPs = temp.IPs + return nil +} + +func (bl *Blacklist) Save() error { + bl.mu.Lock() + defer bl.mu.Unlock() + + temp := struct { + IPs []string `json:"ips"` + }{ + IPs: bl.IPs, + } + + data, err := json.MarshalIndent(temp, "", " ") + if err != nil { + return err + } + + return os.WriteFile(bl.file, data, 0644) +} + +func (bl *Blacklist) Add(ip string) error { + if net.ParseIP(ip) == nil { + return fmt.Errorf("invalid IP address: %s", ip) + } + + bl.mu.Lock() + defer bl.mu.Unlock() + + for _, existingIP := range bl.IPs { + if existingIP == ip { + return nil // IP already in the list + } + } + + bl.IPs = append(bl.IPs, ip) + + temp := struct { + IPs []string `json:"ips"` + }{ + IPs: bl.IPs, + } + + data, err := json.MarshalIndent(temp, "", " ") + if err != nil { + return err + } + + return os.WriteFile(bl.file, data, 0644) +} + +func (bl *Blacklist) Remove(ip string) error { + bl.mu.Lock() + defer bl.mu.Unlock() + + for i, existingIP := range bl.IPs { + if existingIP == ip { + bl.IPs = append(bl.IPs[:i], bl.IPs[i+1:]...) + + temp := struct { + IPs []string `json:"ips"` + }{ + IPs: bl.IPs, + } + + data, err := json.MarshalIndent(temp, "", " ") + if err != nil { + return err + } + + return os.WriteFile(bl.file, data, 0644) + } + } + + return nil // IP not found in the list +} + +func (bl *Blacklist) Contains(ip string) bool { + bl.mu.RLock() + defer bl.mu.RUnlock() + + for _, existingIP := range bl.IPs { + if existingIP == ip { + return true + } + } + return false +} + +func (bl *Blacklist) GetIPs() []string { + bl.mu.RLock() + defer bl.mu.RUnlock() + + return append([]string{}, bl.IPs...) +} + diff --git a/pkg/edition/java/lite/blacklist/route_blacklist.go b/pkg/edition/java/lite/blacklist/route_blacklist.go new file mode 100644 index 00000000..f3ffd69c --- /dev/null +++ b/pkg/edition/java/lite/blacklist/route_blacklist.go @@ -0,0 +1,117 @@ +package blacklist + +import ( + "encoding/json" + "fmt" + "net" + "os" + "sync" +) + +type RouteBlacklist struct { + Blacklists map[string][]string `json:"blacklists"` + mu sync.RWMutex + file string +} + +func NewRouteBlacklist(file string) (*RouteBlacklist, error) { + rb := &RouteBlacklist{ + Blacklists: make(map[string][]string), + file: file, + } + err := rb.Load() + if err != nil { + if os.IsNotExist(err) { + return rb, rb.Save() + } + return nil, err + } + return rb, nil +} + +func (rb *RouteBlacklist) Load() error { + rb.mu.Lock() + defer rb.mu.Unlock() + + data, err := os.ReadFile(rb.file) + if err != nil { + return err + } + + return json.Unmarshal(data, &rb.Blacklists) +} + +func (rb *RouteBlacklist) Save() error { + rb.mu.Lock() + defer rb.mu.Unlock() + + data, err := json.MarshalIndent(rb.Blacklists, "", " ") + if err != nil { + return err + } + + return os.WriteFile(rb.file, data, 0644) +} + +func (rb *RouteBlacklist) Add(route, ip string) error { + if net.ParseIP(ip) == nil { + return fmt.Errorf("invalid IP address: %s", ip) + } + + rb.mu.Lock() + defer rb.mu.Unlock() + + if _, ok := rb.Blacklists[route]; !ok { + rb.Blacklists[route] = []string{} + } + + for _, existingIP := range rb.Blacklists[route] { + if existingIP == ip { + return nil // IP already in the list + } + } + + rb.Blacklists[route] = append(rb.Blacklists[route], ip) + return rb.Save() +} + +func (rb *RouteBlacklist) Remove(route, ip string) error { + rb.mu.Lock() + defer rb.mu.Unlock() + + if ips, ok := rb.Blacklists[route]; ok { + for i, existingIP := range ips { + if existingIP == ip { + rb.Blacklists[route] = append(ips[:i], ips[i+1:]...) + return rb.Save() + } + } + } + + return nil // IP not found in the list +} + +func (rb *RouteBlacklist) Contains(route, ip string) bool { + rb.mu.RLock() + defer rb.mu.RUnlock() + + if ips, ok := rb.Blacklists[route]; ok { + for _, existingIP := range ips { + if existingIP == ip { + return true + } + } + } + return false +} + +func (rb *RouteBlacklist) GetIPs(route string) []string { + rb.mu.RLock() + defer rb.mu.RUnlock() + + if ips, ok := rb.Blacklists[route]; ok { + return append([]string{}, ips...) + } + return []string{} +} + diff --git a/pkg/edition/java/lite/config/config.go b/pkg/edition/java/lite/config/config.go index 982af0e0..ba391a9d 100644 --- a/pkg/edition/java/lite/config/config.go +++ b/pkg/edition/java/lite/config/config.go @@ -28,14 +28,16 @@ type ( Route struct { Host configutil.SingleOrMulti[string] `json:"host,omitempty" yaml:"host,omitempty"` Backend configutil.SingleOrMulti[string] `json:"backend,omitempty" yaml:"backend,omitempty"` - Strategy string `json:"strategy,omitempty" yaml:"strategy,omitempty"` CachePingTTL configutil.Duration `json:"cachePingTTL,omitempty" yaml:"cachePingTTL,omitempty"` // 0 = default, < 0 = disabled Fallback *Status `json:"fallback,omitempty" yaml:"fallback,omitempty"` // nil = disabled ProxyProtocol bool `json:"proxyProtocol,omitempty" yaml:"proxyProtocol,omitempty"` // Deprecated: use TCPShieldRealIP instead. - RealIP bool `json:"realIP,omitempty" yaml:"realIP,omitempty"` - TCPShieldRealIP bool `json:"tcpShieldRealIP,omitempty" yaml:"tcpShieldRealIP,omitempty"` - ModifyVirtualHost bool `json:"modifyVirtualHost,omitempty" yaml:"modifyVirtualHost,omitempty"` + RealIP bool `json:"realIP,omitempty" yaml:"realIP,omitempty"` + TCPShieldRealIP bool `json:"tcpShieldRealIP,omitempty" yaml:"tcpShieldRealIP,omitempty"` + ModifyVirtualHost bool `json:"modifyVirtualHost,omitempty" yaml:"modifyVirtualHost,omitempty"` + MaxConnections int `json:"max-connections,omitempty" yaml:"max-connections,omitempty"` // New field for max connections + Strategy string `json:"strategy,omitempty" yaml:"strategy,omitempty"` // New field for strategy + Blacklist []string `json:"blacklist,omitempty" yaml:"blacklist,omitempty"` } Status struct { MOTD *configutil.TextComponent `yaml:"motd,omitempty" json:"motd,omitempty"` @@ -46,7 +48,7 @@ type ( ) // Response returns the configured status response. -func (s *Status) Response(proto.Protocol) (*ping.ServerPing, error) { +func (s *Status) Response(protocol proto.Protocol, maxConnections int, onlinePlayers int) (*ping.ServerPing, error) { return &ping.ServerPing{ Version: s.Version, Description: s.MOTD.T(), @@ -70,13 +72,6 @@ func (r *Route) CachePingEnabled() bool { return r.GetCachePingTTL() > 0 } // GetTCPShieldRealIP returns the configured TCPShieldRealIP or deprecated RealIP value. func (r *Route) GetTCPShieldRealIP() bool { return r.TCPShieldRealIP || r.RealIP } -var allowedStrategies = map[string]bool{ - "random": true, - "round-robin": true, - "least-connections": true, - "lowest-latency": true, -} - func (c Config) Validate() (warns []error, errs []error) { e := func(m string, args ...any) { errs = append(errs, fmt.Errorf(m, args...)) } @@ -92,9 +87,6 @@ func (c Config) Validate() (warns []error, errs []error) { if len(ep.Backend) == 0 { e("Route %d: no backend configured", i) } - if _, ok := allowedStrategies[ep.Strategy]; !ok && ep.Strategy != "" { - e("Route %d: invalid strategy '%s'", i, ep.Strategy) - } for i, addr := range ep.Backend { _, err := netutil.Parse(addr, "tcp") if err != nil { @@ -118,3 +110,4 @@ func (r *Route) Equal(other *Route) bool { } return string(j) == string(o) } + diff --git a/pkg/edition/java/lite/forward.go b/pkg/edition/java/lite/forward.go index f7f1f0e1..df7b7cff 100644 --- a/pkg/edition/java/lite/forward.go +++ b/pkg/edition/java/lite/forward.go @@ -14,9 +14,11 @@ import ( "sync" "time" + "github.com/fsnotify/fsnotify" "github.com/go-logr/logr" "github.com/jellydator/ttlcache/v3" "go.minekube.com/gate/pkg/edition/java/internal/protoutil" + "go.minekube.com/gate/pkg/edition/java/lite/blacklist" "go.minekube.com/gate/pkg/edition/java/lite/config" "go.minekube.com/gate/pkg/edition/java/netmc" "go.minekube.com/gate/pkg/edition/java/proto/codec" @@ -29,6 +31,135 @@ import ( "golang.org/x/sync/singleflight" ) +var ( + globalBlacklist *blacklist.Blacklist + routeBlacklist *blacklist.RouteBlacklist + connectionCountManager *ConnectionCountManager + logger logr.Logger + watcher *fsnotify.Watcher +) + +// ConnectionCountManager manages connection counts for each route +type ConnectionCountManager struct { + counts map[string]int + mu sync.Mutex +} + +// NewConnectionCountManager creates a new ConnectionCountManager +func NewConnectionCountManager() *ConnectionCountManager { + return &ConnectionCountManager{ + counts: make(map[string]int), + } +} + +// Increment increases the connection count for a route +func (cm *ConnectionCountManager) Increment(routeKey string) { + cm.mu.Lock() + defer cm.mu.Unlock() + cm.counts[routeKey]++ +} + +// Decrement decreases the connection count for a route +func (cm *ConnectionCountManager) Decrement(routeKey string) { + cm.mu.Lock() + defer cm.mu.Unlock() + if cm.counts[routeKey] > 0 { + cm.counts[routeKey]-- + } +} + +// GetCount returns the current connection count for a route +func (cm *ConnectionCountManager) GetCount(routeKey string) int { + cm.mu.Lock() + defer cm.mu.Unlock() + return cm.counts[routeKey] +} + +// Add this helper function at the top level +func isIPBlacklisted(ip string, route *config.Route) bool { + if globalBlacklist != nil && globalBlacklist.Contains(ip) { + return true + } + if routeBlacklist != nil && routeBlacklist.Contains(route.Host[0], ip) { + return true + } + for _, blacklistedIP := range route.Blacklist { + if ip == blacklistedIP { + return true + } + } + return false +} + +// InitBlacklist initializes the global blacklist and sets up a file watcher +func InitBlacklist(globalBlacklistPath, routeBlacklistPath string) error { + var err error + globalBlacklist, err = blacklist.NewBlacklist(globalBlacklistPath) + if err != nil { + return fmt.Errorf("failed to initialize global blacklist: %w", err) + } + + routeBlacklist, err = blacklist.NewRouteBlacklist(routeBlacklistPath) + if err != nil { + return fmt.Errorf("failed to initialize route blacklist: %w", err) + } + + watcher, err = fsnotify.NewWatcher() + if err != nil { + return fmt.Errorf("failed to create file watcher: %w", err) + } + + err = watcher.Add(globalBlacklistPath) + if err != nil { + return fmt.Errorf("failed to add global blacklist file to watcher: %w", err) + } + + err = watcher.Add(routeBlacklistPath) + if err != nil { + return fmt.Errorf("failed to add route blacklist file to watcher: %w", err) + } + + go watchBlacklistFiles(globalBlacklistPath, routeBlacklistPath) + + return nil +} + +func watchBlacklistFiles(globalBlacklistPath, routeBlacklistPath string) { + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + if event.Op&fsnotify.Write == fsnotify.Write { + switch event.Name { + case globalBlacklistPath: + logger.Info("Global blacklist file modified, reloading...") + err := globalBlacklist.Load() + if err != nil { + logger.Error(err, "Failed to reload global blacklist") + } else { + logger.Info("Global blacklist reloaded successfully") + } + case routeBlacklistPath: + logger.Info("Route blacklist file modified, reloading...") + err := routeBlacklist.Load() + if err != nil { + logger.Error(err, "Failed to reload route blacklist") + } else { + logger.Info("Route blacklist reloaded successfully") + } + } + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + logger.Error(err, "Error watching blacklist files") + } + } +} + // Forward forwards a client connection to a matching backend route. func Forward( dialTimeout time.Duration, @@ -46,14 +177,50 @@ func Forward( return } + // Get client IP + clientIP, _, err := net.SplitHostPort(src.RemoteAddr().String()) + if err != nil { + log.Error(err, "failed to parse client IP address") + return + } + + // Check if the IP is blacklisted + if isIPBlacklisted(clientIP, route) { + log.Info("connection denied due to blacklisted IP", + "clientIP", clientIP, + "route", route.Host[0]) + return + } + + // Check if the route has reached its max connections + routeKey := route.Host[0] // Use first host as the key + if route.MaxConnections > 0 { + currentCount := connectionCountManager.GetCount(routeKey) + if currentCount >= route.MaxConnections { + log.Info("connection denied due to max connections reached", + "route", routeKey, + "maxConnections", route.MaxConnections, + "currentConnections", currentCount) + return + } + } + // Find a backend to dial successfully. log, dst, err := tryBackends(log, nextBackend, func(log logr.Logger, backendAddr string) (logr.Logger, net.Conn, error) { conn, err := dialRoute(client.Context(), dialTimeout, src.RemoteAddr(), route, backendAddr, handshake, pc, false) return log, conn, err }) if err != nil { + log.Error(err, "Failed to connect to any backend") return } + + // Increment the connection count only after successful connection + if route.MaxConnections > 0 { + connectionCountManager.Increment(routeKey) + defer connectionCountManager.Decrement(routeKey) + } + defer func() { _ = dst.Close() }() if err = emptyReadBuff(client, dst); err != nil { @@ -68,7 +235,15 @@ func Forward( // Include the strategy name in the log strategyName := route.Strategy - log.Info("forwarding connection", "clientAddr", netutil.Host(src.RemoteAddr()), "virtualHost", ClearVirtualHost(handshake.ServerAddress), "protocol", proto.Protocol(handshake.ProtocolVersion).String(), "route", route.Host, "backendAddr", backendAddrWithPort, "backendAddr", backendIP, "strategy", strategyName) + log.Info("forwarding connection", + "clientAddr", netutil.Host(src.RemoteAddr()), + "virtualHost", ClearVirtualHost(handshake.ServerAddress), + "protocol", proto.Protocol(handshake.ProtocolVersion).String(), + "route", routeKey, + "backendAddr", backendAddrWithPort, + "backendIP", backendIP, + "strategy", strategyName, + "currentConnections", connectionCountManager.GetCount(routeKey)) pipe(log, src, dst) } @@ -78,19 +253,23 @@ var errAllBackendsFailed = errors.New("all backends failed") // tryBackends tries backends until one succeeds or all fail. func tryBackends[T any](log logr.Logger, next nextBackendFunc, try func(log logr.Logger, backendAddr string) (logr.Logger, T, error)) (logr.Logger, T, error) { + var lastErr error for { backendAddr, ok := next() if !ok { var zero T + if lastErr != nil { + return log, zero, fmt.Errorf("all backends failed, last error: %w", lastErr) + } return log, zero, errAllBackendsFailed } log, t, err := try(log, backendAddr) - if err != nil { - errs.V(log, err).Info("failed to try backend", "error", err) - continue + if err == nil { + return log, t, nil } - return log, t, nil + lastErr = err + log.V(1).Info("Backend connection attempt failed", "backendAddr", backendAddr, "error", err) } } @@ -173,9 +352,9 @@ func findRoute( return randomNextBackend(tryBackends)() case "round-robin": return roundRobinNextBackend(host, tryBackends)() - case "least-connections": + case "least connections": return leastConnectionsNextBackend(tryBackends)() - case "lowest-latency": + case "lowest latency": return lowestLatencyNextBackend(tryBackends)() default: // Default to random strategy @@ -211,24 +390,23 @@ func roundRobinNextBackend(routeHost string, tryBackends []string) nextBackendFu } } -var connectionCounts = make(map[string]int) -var connectionCountsMutex sync.Mutex func leastConnectionsNextBackend(tryBackends []string) nextBackendFunc { return func() (string, bool) { if len(tryBackends) == 0 { return "", false } - connectionCountsMutex.Lock() - defer connectionCountsMutex.Unlock() - least := tryBackends[0] + minConnections := math.MaxInt32 + var minBackend string for _, backend := range tryBackends { - if connectionCounts[backend] < connectionCounts[least] { - least = backend + count := connectionCountManager.GetCount(backend) + if count < minConnections { + minConnections = count + minBackend = backend } } - connectionCounts[least]++ - return least, true + connectionCountManager.Increment(minBackend) + return minBackend, true } } @@ -372,8 +550,9 @@ func ResolveStatusResponse( if err != nil && route.Fallback != nil { log.Info("failed to resolve status response, will use fallback status response", "error", err) + onlinePlayers := connectionCountManager.GetCount(route.Host[0]) // Fallback status response if configured - fallbackPong, err := route.Fallback.Response(handshakeCtx.Protocol) + fallbackPong, err := route.Fallback.Response(handshakeCtx.Protocol, route.MaxConnections, onlinePlayers) if err != nil { log.Info("failed to get fallback status response", "error", err) } @@ -403,7 +582,16 @@ func ResetPingCache() { } func init() { + // Initialize the connection count manager + connectionCountManager = NewConnectionCountManager() + go pingCache.Start() // start ttl eviction once + + // Initialize the global and route blacklists + err := InitBlacklist("./ip_blacklist.json", "./route_blacklist.json") + if err != nil { + logger.Error(err, "Failed to initialize blacklists") + } } type pingKey struct { @@ -525,3 +713,9 @@ func withLoader[K comparable, V any](group *singleflight.Group, ttl time.Duratio ttlcache.NewSuppressedLoader[K, V](loader, group), ) } + +// Initialize logger +func SetLogger(log logr.Logger) { + logger = log +} + From 61354a1862b604a9c6934388461e80687bea54e3 Mon Sep 17 00:00:00 2001 From: okeanosthedev <66648881+okeanosthedev@users.noreply.github.com> Date: Mon, 16 Dec 2024 02:18:48 +0300 Subject: [PATCH 22/27] Merge branch 'master' of https://github.com/okeanosthedev/dgate --- config-lite.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/config-lite.yml b/config-lite.yml index 4759df60..3e15e58d 100644 --- a/config-lite.yml +++ b/config-lite.yml @@ -25,7 +25,11 @@ config: # Match the virtual host address to the backend server. - host: localhost # The backend server to connect to if matched. - backend: localhost:25566 + backend: [213.133.109.143:25565] + strategy: random + max-connections: 1 + proxyProtocol: true # Use proxy protocol to connect to backend. + blacklist: [] # The optional fallback status response when all backends of this route are offline. fallback: motd: | @@ -37,10 +41,12 @@ config: # The optional favicon to show in the server list (optimal 64x64). # Accepts a path of an image file or the base64 data uri. favicon: data:image/png;base64,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 + # Can be added in the future: + # maxConnectionsMsg: "§cThe server is full. Please try again later." + # blacklistedIPMsg: "§cYour IP has been blacklisted. Please contact the server owner." # You can also use * wildcard to match any subdomain. - host: '*.example.com' backend: 172.16.0.12:25566 - proxyProtocol: true # Use proxy protocol to connect to backend. tcpShieldRealIP: true # Optionally you can also use TCPShield's RealIP protocol. # You can also match to multiple hosts to one or multiple random backends. - host: [ 127.0.0.1, localhost ] From 089844c13b0c9a209929267f016b63e33170ffbc Mon Sep 17 00:00:00 2001 From: okeanosthedev <66648881+okeanosthedev@users.noreply.github.com> Date: Mon, 16 Dec 2024 02:19:19 +0300 Subject: [PATCH 23/27] Update config-lite.yml --- config-lite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config-lite.yml b/config-lite.yml index 3e15e58d..ed9659d8 100644 --- a/config-lite.yml +++ b/config-lite.yml @@ -25,7 +25,7 @@ config: # Match the virtual host address to the backend server. - host: localhost # The backend server to connect to if matched. - backend: [213.133.109.143:25565] + backend: [213.133.0.0:25565] strategy: random max-connections: 1 proxyProtocol: true # Use proxy protocol to connect to backend. From 06fa68549c9df200bd337da725638478f094a384 Mon Sep 17 00:00:00 2001 From: okeanosthedev <66648881+okeanosthedev@users.noreply.github.com> Date: Mon, 16 Dec 2024 02:20:42 +0300 Subject: [PATCH 24/27] Update config-lite.yml --- config-lite.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config-lite.yml b/config-lite.yml index ed9659d8..08c88547 100644 --- a/config-lite.yml +++ b/config-lite.yml @@ -25,7 +25,11 @@ config: # Match the virtual host address to the backend server. - host: localhost # The backend server to connect to if matched. +<<<<<<< HEAD backend: [213.133.0.0:25565] +======= + backend: [213.133.109.143:25565] +>>>>>>> 93576459dd353f97dfb2bed6a37e33e275a11530 strategy: random max-connections: 1 proxyProtocol: true # Use proxy protocol to connect to backend. From 89eef7010d5b4cfb72349dc3d75177b6161e25b3 Mon Sep 17 00:00:00 2001 From: okeanosthedev <66648881+okeanosthedev@users.noreply.github.com> Date: Mon, 16 Dec 2024 02:20:49 +0300 Subject: [PATCH 25/27] Revert "Update config-lite.yml" This reverts commit 06fa68549c9df200bd337da725638478f094a384. --- config-lite.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/config-lite.yml b/config-lite.yml index 08c88547..ed9659d8 100644 --- a/config-lite.yml +++ b/config-lite.yml @@ -25,11 +25,7 @@ config: # Match the virtual host address to the backend server. - host: localhost # The backend server to connect to if matched. -<<<<<<< HEAD backend: [213.133.0.0:25565] -======= - backend: [213.133.109.143:25565] ->>>>>>> 93576459dd353f97dfb2bed6a37e33e275a11530 strategy: random max-connections: 1 proxyProtocol: true # Use proxy protocol to connect to backend. From f201e7479af0e7f7876989d7b45a59de9bf94181 Mon Sep 17 00:00:00 2001 From: okeanosthedev <66648881+okeanosthedev@users.noreply.github.com> Date: Mon, 16 Dec 2024 02:20:55 +0300 Subject: [PATCH 26/27] Reapply "Update config-lite.yml" This reverts commit 89eef7010d5b4cfb72349dc3d75177b6161e25b3. --- config-lite.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config-lite.yml b/config-lite.yml index ed9659d8..08c88547 100644 --- a/config-lite.yml +++ b/config-lite.yml @@ -25,7 +25,11 @@ config: # Match the virtual host address to the backend server. - host: localhost # The backend server to connect to if matched. +<<<<<<< HEAD backend: [213.133.0.0:25565] +======= + backend: [213.133.109.143:25565] +>>>>>>> 93576459dd353f97dfb2bed6a37e33e275a11530 strategy: random max-connections: 1 proxyProtocol: true # Use proxy protocol to connect to backend. From 54fa599a7d14955d5761bed98928daa75678d092 Mon Sep 17 00:00:00 2001 From: okeanosthedev <66648881+okeanosthedev@users.noreply.github.com> Date: Mon, 16 Dec 2024 08:16:58 +0300 Subject: [PATCH 27/27] PR Strategy Clean --- cmd/blacklist/main.go | 72 ------ config-lite.yml | 17 +- pkg/edition/java/lite/blacklist/blacklist.go | 147 ----------- .../java/lite/blacklist/route_blacklist.go | 117 --------- pkg/edition/java/lite/config/config.go | 25 +- pkg/edition/java/lite/forward.go | 230 ++---------------- 6 files changed, 38 insertions(+), 570 deletions(-) delete mode 100644 cmd/blacklist/main.go delete mode 100644 pkg/edition/java/lite/blacklist/blacklist.go delete mode 100644 pkg/edition/java/lite/blacklist/route_blacklist.go diff --git a/cmd/blacklist/main.go b/cmd/blacklist/main.go deleted file mode 100644 index c62e3575..00000000 --- a/cmd/blacklist/main.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "os" - - "go.minekube.com/gate/pkg/edition/java/lite/blacklist" -) - -func main() { - blacklistFile := flag.String("file", "ip_blacklist.json", "Path to the global blacklist JSON file") - routeBlacklistFile := flag.String("route-file", "route_blacklist.json", "Path to the route blacklist JSON file") - add := flag.String("add", "", "IP address to add to the blacklist") - remove := flag.String("remove", "", "IP address to remove from the blacklist") - route := flag.String("route", "", "Route for adding/removing IP (if not specified, uses global blacklist)") - list := flag.Bool("list", false, "List all blacklisted IPs") - flag.Parse() - - bl, err := blacklist.NewBlacklist(*blacklistFile) - if err != nil { - fmt.Printf("Error initializing global blacklist: %v\n", err) - os.Exit(1) - } - - rbl, err := blacklist.NewRouteBlacklist(*routeBlacklistFile) - if err != nil { - fmt.Printf("Error initializing route blacklist: %v\n", err) - os.Exit(1) - } - - if *add != "" { - if *route != "" { - err = rbl.Add(*route, *add) - } else { - err = bl.Add(*add) - } - if err != nil { - fmt.Printf("Error adding IP to blacklist: %v\n", err) - os.Exit(1) - } - fmt.Printf("Added %s to the blacklist\n", *add) - } - - if *remove != "" { - if *route != "" { - err = rbl.Remove(*route, *remove) - } else { - err = bl.Remove(*remove) - } - if err != nil { - fmt.Printf("Error removing IP from blacklist: %v\n", err) - os.Exit(1) - } - fmt.Printf("Removed %s from the blacklist\n", *remove) - } - - if *list { - fmt.Println("Global Blacklisted IPs:") - for _, ip := range bl.GetIPs() { - fmt.Println(ip) - } - fmt.Println("\nRoute Blacklisted IPs:") - for route, ips := range rbl.Blacklists { - fmt.Printf("Route: %s\n", route) - for _, ip := range ips { - fmt.Printf(" %s\n", ip) - } - } - } -} - diff --git a/config-lite.yml b/config-lite.yml index 08c88547..6a81e75a 100644 --- a/config-lite.yml +++ b/config-lite.yml @@ -25,15 +25,8 @@ config: # Match the virtual host address to the backend server. - host: localhost # The backend server to connect to if matched. -<<<<<<< HEAD - backend: [213.133.0.0:25565] -======= - backend: [213.133.109.143:25565] ->>>>>>> 93576459dd353f97dfb2bed6a37e33e275a11530 - strategy: random - max-connections: 1 - proxyProtocol: true # Use proxy protocol to connect to backend. - blacklist: [] + backend: localhost:25566 + strategy: random # round-robin, random, lowest-latency, least-connections # The optional fallback status response when all backends of this route are offline. fallback: motd: | @@ -45,12 +38,10 @@ config: # The optional favicon to show in the server list (optimal 64x64). # Accepts a path of an image file or the base64 data uri. favicon: data:image/png;base64,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 - # Can be added in the future: - # maxConnectionsMsg: "§cThe server is full. Please try again later." - # blacklistedIPMsg: "§cYour IP has been blacklisted. Please contact the server owner." # You can also use * wildcard to match any subdomain. - host: '*.example.com' backend: 172.16.0.12:25566 + proxyProtocol: true # Use proxy protocol to connect to backend. tcpShieldRealIP: true # Optionally you can also use TCPShield's RealIP protocol. # You can also match to multiple hosts to one or multiple random backends. - host: [ 127.0.0.1, localhost ] @@ -127,4 +118,4 @@ api: enabled: false # The bind address to listen for API connections. # Default: localhost:8080 - bind: localhost:8080 + bind: localhost:8080 \ No newline at end of file diff --git a/pkg/edition/java/lite/blacklist/blacklist.go b/pkg/edition/java/lite/blacklist/blacklist.go deleted file mode 100644 index 95d88ce9..00000000 --- a/pkg/edition/java/lite/blacklist/blacklist.go +++ /dev/null @@ -1,147 +0,0 @@ -package blacklist - -import ( - "encoding/json" - "fmt" - "net" - "os" - "sync" -) - -type Blacklist struct { - IPs []string `json:"ips"` - mu sync.RWMutex - file string -} - -func NewBlacklist(file string) (*Blacklist, error) { - bl := &Blacklist{ - IPs: []string{}, - file: file, - } - err := bl.Load() - if err != nil { - if os.IsNotExist(err) { - // If the file doesn't exist, create it with an empty blacklist - return bl, bl.Save() - } - return nil, err - } - return bl, nil -} - -func (bl *Blacklist) Load() error { - bl.mu.Lock() - defer bl.mu.Unlock() - - data, err := os.ReadFile(bl.file) - if err != nil { - return err - } - - var temp struct { - IPs []string `json:"ips"` - } - - err = json.Unmarshal(data, &temp) - if err != nil { - return err - } - - bl.IPs = temp.IPs - return nil -} - -func (bl *Blacklist) Save() error { - bl.mu.Lock() - defer bl.mu.Unlock() - - temp := struct { - IPs []string `json:"ips"` - }{ - IPs: bl.IPs, - } - - data, err := json.MarshalIndent(temp, "", " ") - if err != nil { - return err - } - - return os.WriteFile(bl.file, data, 0644) -} - -func (bl *Blacklist) Add(ip string) error { - if net.ParseIP(ip) == nil { - return fmt.Errorf("invalid IP address: %s", ip) - } - - bl.mu.Lock() - defer bl.mu.Unlock() - - for _, existingIP := range bl.IPs { - if existingIP == ip { - return nil // IP already in the list - } - } - - bl.IPs = append(bl.IPs, ip) - - temp := struct { - IPs []string `json:"ips"` - }{ - IPs: bl.IPs, - } - - data, err := json.MarshalIndent(temp, "", " ") - if err != nil { - return err - } - - return os.WriteFile(bl.file, data, 0644) -} - -func (bl *Blacklist) Remove(ip string) error { - bl.mu.Lock() - defer bl.mu.Unlock() - - for i, existingIP := range bl.IPs { - if existingIP == ip { - bl.IPs = append(bl.IPs[:i], bl.IPs[i+1:]...) - - temp := struct { - IPs []string `json:"ips"` - }{ - IPs: bl.IPs, - } - - data, err := json.MarshalIndent(temp, "", " ") - if err != nil { - return err - } - - return os.WriteFile(bl.file, data, 0644) - } - } - - return nil // IP not found in the list -} - -func (bl *Blacklist) Contains(ip string) bool { - bl.mu.RLock() - defer bl.mu.RUnlock() - - for _, existingIP := range bl.IPs { - if existingIP == ip { - return true - } - } - return false -} - -func (bl *Blacklist) GetIPs() []string { - bl.mu.RLock() - defer bl.mu.RUnlock() - - return append([]string{}, bl.IPs...) -} - diff --git a/pkg/edition/java/lite/blacklist/route_blacklist.go b/pkg/edition/java/lite/blacklist/route_blacklist.go deleted file mode 100644 index f3ffd69c..00000000 --- a/pkg/edition/java/lite/blacklist/route_blacklist.go +++ /dev/null @@ -1,117 +0,0 @@ -package blacklist - -import ( - "encoding/json" - "fmt" - "net" - "os" - "sync" -) - -type RouteBlacklist struct { - Blacklists map[string][]string `json:"blacklists"` - mu sync.RWMutex - file string -} - -func NewRouteBlacklist(file string) (*RouteBlacklist, error) { - rb := &RouteBlacklist{ - Blacklists: make(map[string][]string), - file: file, - } - err := rb.Load() - if err != nil { - if os.IsNotExist(err) { - return rb, rb.Save() - } - return nil, err - } - return rb, nil -} - -func (rb *RouteBlacklist) Load() error { - rb.mu.Lock() - defer rb.mu.Unlock() - - data, err := os.ReadFile(rb.file) - if err != nil { - return err - } - - return json.Unmarshal(data, &rb.Blacklists) -} - -func (rb *RouteBlacklist) Save() error { - rb.mu.Lock() - defer rb.mu.Unlock() - - data, err := json.MarshalIndent(rb.Blacklists, "", " ") - if err != nil { - return err - } - - return os.WriteFile(rb.file, data, 0644) -} - -func (rb *RouteBlacklist) Add(route, ip string) error { - if net.ParseIP(ip) == nil { - return fmt.Errorf("invalid IP address: %s", ip) - } - - rb.mu.Lock() - defer rb.mu.Unlock() - - if _, ok := rb.Blacklists[route]; !ok { - rb.Blacklists[route] = []string{} - } - - for _, existingIP := range rb.Blacklists[route] { - if existingIP == ip { - return nil // IP already in the list - } - } - - rb.Blacklists[route] = append(rb.Blacklists[route], ip) - return rb.Save() -} - -func (rb *RouteBlacklist) Remove(route, ip string) error { - rb.mu.Lock() - defer rb.mu.Unlock() - - if ips, ok := rb.Blacklists[route]; ok { - for i, existingIP := range ips { - if existingIP == ip { - rb.Blacklists[route] = append(ips[:i], ips[i+1:]...) - return rb.Save() - } - } - } - - return nil // IP not found in the list -} - -func (rb *RouteBlacklist) Contains(route, ip string) bool { - rb.mu.RLock() - defer rb.mu.RUnlock() - - if ips, ok := rb.Blacklists[route]; ok { - for _, existingIP := range ips { - if existingIP == ip { - return true - } - } - } - return false -} - -func (rb *RouteBlacklist) GetIPs(route string) []string { - rb.mu.RLock() - defer rb.mu.RUnlock() - - if ips, ok := rb.Blacklists[route]; ok { - return append([]string{}, ips...) - } - return []string{} -} - diff --git a/pkg/edition/java/lite/config/config.go b/pkg/edition/java/lite/config/config.go index ba391a9d..7ed47c61 100644 --- a/pkg/edition/java/lite/config/config.go +++ b/pkg/edition/java/lite/config/config.go @@ -28,16 +28,14 @@ type ( Route struct { Host configutil.SingleOrMulti[string] `json:"host,omitempty" yaml:"host,omitempty"` Backend configutil.SingleOrMulti[string] `json:"backend,omitempty" yaml:"backend,omitempty"` + Strategy string `json:"strategy,omitempty" yaml:"strategy,omitempty"` CachePingTTL configutil.Duration `json:"cachePingTTL,omitempty" yaml:"cachePingTTL,omitempty"` // 0 = default, < 0 = disabled Fallback *Status `json:"fallback,omitempty" yaml:"fallback,omitempty"` // nil = disabled ProxyProtocol bool `json:"proxyProtocol,omitempty" yaml:"proxyProtocol,omitempty"` // Deprecated: use TCPShieldRealIP instead. - RealIP bool `json:"realIP,omitempty" yaml:"realIP,omitempty"` - TCPShieldRealIP bool `json:"tcpShieldRealIP,omitempty" yaml:"tcpShieldRealIP,omitempty"` - ModifyVirtualHost bool `json:"modifyVirtualHost,omitempty" yaml:"modifyVirtualHost,omitempty"` - MaxConnections int `json:"max-connections,omitempty" yaml:"max-connections,omitempty"` // New field for max connections - Strategy string `json:"strategy,omitempty" yaml:"strategy,omitempty"` // New field for strategy - Blacklist []string `json:"blacklist,omitempty" yaml:"blacklist,omitempty"` + RealIP bool `json:"realIP,omitempty" yaml:"realIP,omitempty"` + TCPShieldRealIP bool `json:"tcpShieldRealIP,omitempty" yaml:"tcpShieldRealIP,omitempty"` + ModifyVirtualHost bool `json:"modifyVirtualHost,omitempty" yaml:"modifyVirtualHost,omitempty"` } Status struct { MOTD *configutil.TextComponent `yaml:"motd,omitempty" json:"motd,omitempty"` @@ -48,7 +46,7 @@ type ( ) // Response returns the configured status response. -func (s *Status) Response(protocol proto.Protocol, maxConnections int, onlinePlayers int) (*ping.ServerPing, error) { +func (s *Status) Response(proto.Protocol) (*ping.ServerPing, error) { return &ping.ServerPing{ Version: s.Version, Description: s.MOTD.T(), @@ -72,6 +70,13 @@ func (r *Route) CachePingEnabled() bool { return r.GetCachePingTTL() > 0 } // GetTCPShieldRealIP returns the configured TCPShieldRealIP or deprecated RealIP value. func (r *Route) GetTCPShieldRealIP() bool { return r.TCPShieldRealIP || r.RealIP } +var allowedStrategies = map[string]bool{ + "random": true, + "round-robin": true, + "least-connections": true, + "lowest-latency": true, +} + func (c Config) Validate() (warns []error, errs []error) { e := func(m string, args ...any) { errs = append(errs, fmt.Errorf(m, args...)) } @@ -87,6 +92,9 @@ func (c Config) Validate() (warns []error, errs []error) { if len(ep.Backend) == 0 { e("Route %d: no backend configured", i) } + if _, ok := allowedStrategies[ep.Strategy]; !ok && ep.Strategy != "" { + e("Route %d: invalid strategy '%s'", i, ep.Strategy) + } for i, addr := range ep.Backend { _, err := netutil.Parse(addr, "tcp") if err != nil { @@ -109,5 +117,4 @@ func (r *Route) Equal(other *Route) bool { return false } return string(j) == string(o) -} - +} \ No newline at end of file diff --git a/pkg/edition/java/lite/forward.go b/pkg/edition/java/lite/forward.go index df7b7cff..6c38712c 100644 --- a/pkg/edition/java/lite/forward.go +++ b/pkg/edition/java/lite/forward.go @@ -14,11 +14,9 @@ import ( "sync" "time" - "github.com/fsnotify/fsnotify" "github.com/go-logr/logr" "github.com/jellydator/ttlcache/v3" "go.minekube.com/gate/pkg/edition/java/internal/protoutil" - "go.minekube.com/gate/pkg/edition/java/lite/blacklist" "go.minekube.com/gate/pkg/edition/java/lite/config" "go.minekube.com/gate/pkg/edition/java/netmc" "go.minekube.com/gate/pkg/edition/java/proto/codec" @@ -31,135 +29,6 @@ import ( "golang.org/x/sync/singleflight" ) -var ( - globalBlacklist *blacklist.Blacklist - routeBlacklist *blacklist.RouteBlacklist - connectionCountManager *ConnectionCountManager - logger logr.Logger - watcher *fsnotify.Watcher -) - -// ConnectionCountManager manages connection counts for each route -type ConnectionCountManager struct { - counts map[string]int - mu sync.Mutex -} - -// NewConnectionCountManager creates a new ConnectionCountManager -func NewConnectionCountManager() *ConnectionCountManager { - return &ConnectionCountManager{ - counts: make(map[string]int), - } -} - -// Increment increases the connection count for a route -func (cm *ConnectionCountManager) Increment(routeKey string) { - cm.mu.Lock() - defer cm.mu.Unlock() - cm.counts[routeKey]++ -} - -// Decrement decreases the connection count for a route -func (cm *ConnectionCountManager) Decrement(routeKey string) { - cm.mu.Lock() - defer cm.mu.Unlock() - if cm.counts[routeKey] > 0 { - cm.counts[routeKey]-- - } -} - -// GetCount returns the current connection count for a route -func (cm *ConnectionCountManager) GetCount(routeKey string) int { - cm.mu.Lock() - defer cm.mu.Unlock() - return cm.counts[routeKey] -} - -// Add this helper function at the top level -func isIPBlacklisted(ip string, route *config.Route) bool { - if globalBlacklist != nil && globalBlacklist.Contains(ip) { - return true - } - if routeBlacklist != nil && routeBlacklist.Contains(route.Host[0], ip) { - return true - } - for _, blacklistedIP := range route.Blacklist { - if ip == blacklistedIP { - return true - } - } - return false -} - -// InitBlacklist initializes the global blacklist and sets up a file watcher -func InitBlacklist(globalBlacklistPath, routeBlacklistPath string) error { - var err error - globalBlacklist, err = blacklist.NewBlacklist(globalBlacklistPath) - if err != nil { - return fmt.Errorf("failed to initialize global blacklist: %w", err) - } - - routeBlacklist, err = blacklist.NewRouteBlacklist(routeBlacklistPath) - if err != nil { - return fmt.Errorf("failed to initialize route blacklist: %w", err) - } - - watcher, err = fsnotify.NewWatcher() - if err != nil { - return fmt.Errorf("failed to create file watcher: %w", err) - } - - err = watcher.Add(globalBlacklistPath) - if err != nil { - return fmt.Errorf("failed to add global blacklist file to watcher: %w", err) - } - - err = watcher.Add(routeBlacklistPath) - if err != nil { - return fmt.Errorf("failed to add route blacklist file to watcher: %w", err) - } - - go watchBlacklistFiles(globalBlacklistPath, routeBlacklistPath) - - return nil -} - -func watchBlacklistFiles(globalBlacklistPath, routeBlacklistPath string) { - for { - select { - case event, ok := <-watcher.Events: - if !ok { - return - } - if event.Op&fsnotify.Write == fsnotify.Write { - switch event.Name { - case globalBlacklistPath: - logger.Info("Global blacklist file modified, reloading...") - err := globalBlacklist.Load() - if err != nil { - logger.Error(err, "Failed to reload global blacklist") - } else { - logger.Info("Global blacklist reloaded successfully") - } - case routeBlacklistPath: - logger.Info("Route blacklist file modified, reloading...") - err := routeBlacklist.Load() - if err != nil { - logger.Error(err, "Failed to reload route blacklist") - } else { - logger.Info("Route blacklist reloaded successfully") - } - } - } - case err, ok := <-watcher.Errors: - if !ok { - return - } - logger.Error(err, "Error watching blacklist files") - } - } -} - // Forward forwards a client connection to a matching backend route. func Forward( dialTimeout time.Duration, @@ -177,50 +46,14 @@ func Forward( return } - // Get client IP - clientIP, _, err := net.SplitHostPort(src.RemoteAddr().String()) - if err != nil { - log.Error(err, "failed to parse client IP address") - return - } - - // Check if the IP is blacklisted - if isIPBlacklisted(clientIP, route) { - log.Info("connection denied due to blacklisted IP", - "clientIP", clientIP, - "route", route.Host[0]) - return - } - - // Check if the route has reached its max connections - routeKey := route.Host[0] // Use first host as the key - if route.MaxConnections > 0 { - currentCount := connectionCountManager.GetCount(routeKey) - if currentCount >= route.MaxConnections { - log.Info("connection denied due to max connections reached", - "route", routeKey, - "maxConnections", route.MaxConnections, - "currentConnections", currentCount) - return - } - } - // Find a backend to dial successfully. log, dst, err := tryBackends(log, nextBackend, func(log logr.Logger, backendAddr string) (logr.Logger, net.Conn, error) { conn, err := dialRoute(client.Context(), dialTimeout, src.RemoteAddr(), route, backendAddr, handshake, pc, false) return log, conn, err }) if err != nil { - log.Error(err, "Failed to connect to any backend") return } - - // Increment the connection count only after successful connection - if route.MaxConnections > 0 { - connectionCountManager.Increment(routeKey) - defer connectionCountManager.Decrement(routeKey) - } - defer func() { _ = dst.Close() }() if err = emptyReadBuff(client, dst); err != nil { @@ -235,15 +68,7 @@ func Forward( // Include the strategy name in the log strategyName := route.Strategy - log.Info("forwarding connection", - "clientAddr", netutil.Host(src.RemoteAddr()), - "virtualHost", ClearVirtualHost(handshake.ServerAddress), - "protocol", proto.Protocol(handshake.ProtocolVersion).String(), - "route", routeKey, - "backendAddr", backendAddrWithPort, - "backendIP", backendIP, - "strategy", strategyName, - "currentConnections", connectionCountManager.GetCount(routeKey)) + log.Info("forwarding connection", "clientAddr", netutil.Host(src.RemoteAddr()), "virtualHost", ClearVirtualHost(handshake.ServerAddress), "protocol", proto.Protocol(handshake.ProtocolVersion).String(), "route", route.Host, "backendAddr", backendAddrWithPort, "backendAddr", backendIP, "strategy", strategyName) pipe(log, src, dst) } @@ -253,23 +78,19 @@ var errAllBackendsFailed = errors.New("all backends failed") // tryBackends tries backends until one succeeds or all fail. func tryBackends[T any](log logr.Logger, next nextBackendFunc, try func(log logr.Logger, backendAddr string) (logr.Logger, T, error)) (logr.Logger, T, error) { - var lastErr error for { backendAddr, ok := next() if !ok { var zero T - if lastErr != nil { - return log, zero, fmt.Errorf("all backends failed, last error: %w", lastErr) - } return log, zero, errAllBackendsFailed } log, t, err := try(log, backendAddr) - if err == nil { - return log, t, nil + if err != nil { + errs.V(log, err).Info("failed to try backend", "error", err) + continue } - lastErr = err - log.V(1).Info("Backend connection attempt failed", "backendAddr", backendAddr, "error", err) + return log, t, nil } } @@ -352,9 +173,9 @@ func findRoute( return randomNextBackend(tryBackends)() case "round-robin": return roundRobinNextBackend(host, tryBackends)() - case "least connections": + case "least-connections": return leastConnectionsNextBackend(tryBackends)() - case "lowest latency": + case "lowest-latency": return lowestLatencyNextBackend(tryBackends)() default: // Default to random strategy @@ -390,23 +211,24 @@ func roundRobinNextBackend(routeHost string, tryBackends []string) nextBackendFu } } +var connectionCounts = make(map[string]int) +var connectionCountsMutex sync.Mutex func leastConnectionsNextBackend(tryBackends []string) nextBackendFunc { return func() (string, bool) { if len(tryBackends) == 0 { return "", false } - minConnections := math.MaxInt32 - var minBackend string + connectionCountsMutex.Lock() + defer connectionCountsMutex.Unlock() + least := tryBackends[0] for _, backend := range tryBackends { - count := connectionCountManager.GetCount(backend) - if count < minConnections { - minConnections = count - minBackend = backend + if connectionCounts[backend] < connectionCounts[least] { + least = backend } } - connectionCountManager.Increment(minBackend) - return minBackend, true + connectionCounts[least]++ + return least, true } } @@ -550,9 +372,8 @@ func ResolveStatusResponse( if err != nil && route.Fallback != nil { log.Info("failed to resolve status response, will use fallback status response", "error", err) - onlinePlayers := connectionCountManager.GetCount(route.Host[0]) // Fallback status response if configured - fallbackPong, err := route.Fallback.Response(handshakeCtx.Protocol, route.MaxConnections, onlinePlayers) + fallbackPong, err := route.Fallback.Response(handshakeCtx.Protocol) if err != nil { log.Info("failed to get fallback status response", "error", err) } @@ -582,16 +403,7 @@ func ResetPingCache() { } func init() { - // Initialize the connection count manager - connectionCountManager = NewConnectionCountManager() - go pingCache.Start() // start ttl eviction once - - // Initialize the global and route blacklists - err := InitBlacklist("./ip_blacklist.json", "./route_blacklist.json") - if err != nil { - logger.Error(err, "Failed to initialize blacklists") - } } type pingKey struct { @@ -712,10 +524,4 @@ func withLoader[K comparable, V any](group *singleflight.Group, ttl time.Duratio return ttlcache.WithLoader[K, V]( ttlcache.NewSuppressedLoader[K, V](loader, group), ) -} - -// Initialize logger -func SetLogger(log logr.Logger) { - logger = log -} - +} \ No newline at end of file