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,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH5AgJCgs6JBZy0AAAB+lJREFUeNrtmGuMXVUZht/LOvcz7diWklbJaKGCWKXFUiiBQjAEhRSLSCEqIKAiogRTmiriBeSiUUhjMCZe2qQxNE0EUZSCidAgYMBSLBJCDC1taAg4xaq1cz/788feZzqgocCoP2Q/f87JOXvvfOtd73dZGygpKSkpKSkpKSkpKSkpKSkpKSkpKXnzwNd7w+ULD0NEBtmQBFqQBNuICFRYwzc3bf7/E+Cyo+cgAIiEbESWJdl1WSQ5VGvWRwnk/0XgpnvfmAhrv3h+HhgFFeJCBCLw8Wt//B8XIB3ogs8umJMHAOCQvtnYtfP5eRGxVNaxMmdKgqzdWSfbIuuuocHBLa2ednx16WJcd9fvXn9EAQSQyGgBwUCMMbAvAvHfcIAOaBHnl0REY9fO56+itFHSjbI/JHuxrMWyl8r6mq27m+3WKpJ1kvjG2UtevyUtyDqK0t2UNklaLalu63+fAp9bNBdZFogsq0j6OsVVkix7L8WNkh6VLVuLlXyapKbsMUkrR4aGV9caNbRnTkWKPC0kgdpv7e7n2GgH7ant8WgiYomoX8uqUXoIwKm1Rn0wJQMAGu0mBv8xhEZPAxIhOX9W8byR4VHUGjUcf86qyaVApVrB6MgYIC4leaUsS/oLySsprQcwBgRkVW1fIfsGWVVJn29Naf8CwPYUedAjQ8OsNxuHApiHwAwAIwB2AtjaaNX/CgQAiuQM27NIhixQqqaU+mTtA9AfEUMA0JzSRERMB3BUIPoCgQjsiIg/NHuaewDg0TtvxqJlK964A6447nAE0CLwM0mnygbFGzujY19WMhD5bjgZTu6hdLekE4qduDAi1jWntIEseimuIHmBrLfJVuGAAdmbZV1t6yGnNI3kBkkLaE2zRNnDsnbLGiR1EcUHK5WKlbyc5BdkvUdSPXeAB21tln3D6PDIvdV6DQBeVYRXdYDyvHsXyYW5dd1PYoNURafTwS2/fRIAsPKU+eg96C17EVhNcavskPgCQCDCtK4RuaLY0WdlPWW7T9a7JS+RdYukpbJHip3PcoHctXUmKyMZkuBK+jTJb8tqSeqn9ICthuyFsk4kubZar50P4DeTSgHZAHAEyd6i7z9LYgcA9M6chuuXnwzLoPLWiIjbKd7ezfV8B+JIkp+gVNzPj0jaKvsQ2xtkLZI1n+TRo8Mj99QatY85+WRJ62TXJW0leb6kfZ2xzp/rzcZ8ktcUi98B4hJZD1KqyLqU5E0AZgH4EoDfA/j7GxbAuc0PpshCgJcoDiHGOxIDMZfgdACh5InFbRuJflA1kffJNsVNtXptS7GzOyJii6RFsqqkZjsZ1XqtX/aLJLPiOcOSnsuybLA1tQ2S51CcXQi/RvZ9TgaBEZA/BHAugEUAjgMwH8ADkxAgARGaULlJERGAJESWWdK1kpZJHJMUyncaFD+TZdltTumxl17oP3f2nEN6Jc0DeSmIWQAato/tVm5KjbzwVos5iONiklSlVoHtOsljKMFWJvswWSsmtHMCqBffWwCOnJwANgKxp7soWdNJNYAYcTKyTLBlSklWWK7ISnnQqgDAvMXz4pmt2z4gcRWlYwrrTsxvOC+uRBSuA0AS+8UhUqUC2XWK04tYJOmCA6R476RqgJMRgW0SB2Q1Zb+dZB+JJyQByDqUrpP0fVGZk1fSOsO5AJCNbX/cfoKsNZJmSRqguE7SJol7bH9K1umyUaz/XwSgVIzfgpMySmOSQHIUwG1FK504JXVQ9NQD7f5rLILxlKRtebvxQbLOlvWELIyOjIbkJ11JqNaqMxAxOz8cGRLzRUgflTSrsPIaklcC6NgJqZJOG0+viQIExgsrRZAqnuUBWbuKHBeAXwL46cSeHnnezwCQAXh6UqNwqiTUW80XndJ6O3X7/WVO6RxSmjKtF+3eNqq16hSSVyt5vu3udXjh2V1w8ludjPz3tKNSq3ZaU3tQrVdnyn6fnf+n4h6Nf0/dexq2VKlVIWsMwKauQQGcPSHnEcA7AawHcA+ANQAOnpQDsixDZBlk/Uj2SbZOk3WQ5B/IOhOIxyNQp3iKpJOK9naErDpJ9B15GGxvL4oWZF+i5L0A/kZruaT3jhc6KSGimwIDpMaK8fZwSStJPgLgfgB3ALgAwEIAZwEYALARQA+ACwEcUYR/RwB/4mTOAgBw43nvR6okSJoj61uyz7RV3T/Pu7uAp2ytln2LrDapiyWudUoLZK2XfbgnnAEo7bb9sKwzC6tfjyy+Um81EMAMST+XdXxeawAAzwM4EcB2AMcDuBXAgn8T8kjhgpUA+ic1CeaFMN89kNudfBGlU2V9UPZcWQ1JeyU9RvInTmk3xdmSWpSeQAC2Hpd9nqxPyjpKsmTtJLlByS+KfFqSKW4OZHAlgcBuAJdTugTAoUWcOwDsLcJ6GMAyAMsLUWYCGAWwragLGwtnYNIOAICbLz4DKe0/0R13+hI8fv+jDVlJ0miWZUOykZLHT3sUQRAUUa3X8OGrvotf3bqyRYlOaSCyLJvoIjIPpd5uoG/uO/DcMzu743i1iHOsk3U6BMffPnXPbEUdyCJigOTL3htM6jD0Sr53+VmQ07gQ432aQiBQq9cAomhdQgBo9bQg7x+eim6AyDJUm00gAikRlLCvswc9noZqT6uozvGyELvvRI5ddhUeufM7ebcgX/k+BXwNCy8pKSkpKSkpKSkpKSkpKSkpKSkpKXkz8k8RHxEbZN/8lgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMC0wOC0wOVQxMDoxMTo0MyswMDowMN6nNEYAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjAtMDgtMDlUMTA6MTE6NDMrMDA6MDCv+oz6AAAAAElFTkSuQmCC",
+ 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