Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .web/docs/guide/lite.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,31 @@ config:
backend: [ 10.0.0.2:25566 ]
```

## Maximum Players Limit

You can limit the total number of players that can connect to your Gate Lite instance by setting the `maxPlayers` option:

```yaml
config:
lite:
enabled: true
maxPlayers: 100 # Limit to 100 concurrent players // [!code ++]
maxPlayersMessage: "§cThis Node is full.Please try another node." # Custom rejection message // [!code ++]
routes:
- host: abc.example.com
backend: 10.0.0.3:25568
```

When the maximum player limit is reached, any new connection attempts will be rejected with a custom message indicating that the server is full. For example:

```
§cThis Node is full.Please try another node.
```

You can customize this message by setting the `maxPlayersMessage` configuration option. The default message is in Chinese and translates to "This node has too many players, please use another node".

Setting `maxPlayers` to `0` (the default) means there is no limit on the number of concurrent players.

## Ping Response Caching

Players send server list ping requests to Gate Lite to display the motd (message of the day).
Expand Down
7 changes: 7 additions & 0 deletions config-lite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ config:
# If enabled, the proxy will act as a lightweight reverse proxy to support new types of deployment architecture.
# Default: false
enabled: true
# Maximum number of players to accept, 0 means unlimited.
# When this limit is reached, new connections will be rejected.
# Default: 0
maxPlayers: 0
# Custom message to show when the maximum player limit is reached.
# Default: §cThis Node is full.Please try another node.
maxPlayersMessage: "§cThis Node is full.Please try another node."
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should use our text config component like other settings do to support the different legacy and modern text types.

# The routes that the proxy redirects player connections to based on matching the virtual host address.
# The routes are matched in order of appearance.
# Examples:
Expand Down
12 changes: 8 additions & 4 deletions pkg/edition/java/lite/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,19 @@ import (

// DefaultConfig is the default configuration for Lite mode.
var DefaultConfig = Config{
Enabled: false,
Routes: []Route{},
Enabled: false,
Routes: []Route{},
MaxPlayers: 0, // 0 means unlimited
MaxPlayersMessage: "§cThis Node is full.Please try another node.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use text component directly. also don't call it Node, it's a proxy

}

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"`
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
Routes []Route `yaml:"routes,omitempty" json:"routes,omitempty"`
MaxPlayers int `yaml:"maxPlayers,omitempty" json:"maxPlayers,omitempty"` // Maximum number of players to accept, 0 means unlimited
MaxPlayersMessage string `yaml:"maxPlayersMessage,omitempty" json:"maxPlayersMessage,omitempty"` // Message to show when reaching max players limit
Comment on lines +29 to +30
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can put the player specific settings into a sub-struct Players struct { MaxPlayers, MaxMessage }

}
Route struct {
Host configutil.SingleOrMulti[string] `json:"host,omitempty" yaml:"host,omitempty"`
Expand Down
28 changes: 28 additions & 0 deletions pkg/edition/java/lite/counter.go
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need this file. Just use atomic.Int64.

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package lite

import (
"sync/atomic"
)

// ConnectionCounter tracks the number of active connections in LiteMode
var ConnectionCounter = &counter{}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not put global state like this. A process could run many Gate instances at once, they must have their own counter


// counter is a thread-safe counter for connections
type counter struct {
count int32
}

// Increment increments the counter by 1 and returns the new value
func (c *counter) Increment() int32 {
return atomic.AddInt32(&c.count, 1)
}

// Decrement decrements the counter by 1 and returns the new value
func (c *counter) Decrement() int32 {
return atomic.AddInt32(&c.count, -1)
}

// Count returns the current count
func (c *counter) Count() int32 {
return atomic.LoadInt32(&c.count)
}
44 changes: 44 additions & 0 deletions pkg/edition/java/lite/forward.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (

"github.com/go-logr/logr"
"github.com/jellydator/ttlcache/v3"
"go.minekube.com/common/minecraft/color"
"go.minekube.com/common/minecraft/component"
"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"
Expand Down Expand Up @@ -43,6 +45,35 @@ func Forward(
return
}

// Check maximum connections limit if it's set (>0)
cfg := getConfig()
if cfg != nil && cfg.MaxPlayers > 0 {
currentCount := ConnectionCounter.Count()
if int(currentCount) >= cfg.MaxPlayers {
log.Info("connection rejected due to maximum player limit",
"current", currentCount,
"max", cfg.MaxPlayers)

// Use the configured message
message := cfg.MaxPlayersMessage
if message == "" {
message = "§cThis Node is full.Please try another node."
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stay DRY

}

// Disconnect and display custom message
reason := &component.Text{
Content: message,
S: component.Style{Color: color.Red},
}
_ = netmc.CloseWith(client, packet.NewDisconnect(reason, client.Protocol(), client.State().State))
return
}
Comment on lines +49 to +70
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a lot of code, let's put it into a helper function

}

// Increment connection counter
ConnectionCounter.Increment()
defer ConnectionCounter.Decrement()

// 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)
Expand All @@ -62,6 +93,19 @@ func Forward(
pipe(log, src, dst)
}

// reference to the current lite mode config
var currentConfig *config.Config

// SetConfig sets the current lite mode config
func SetConfig(cfg *config.Config) {
currentConfig = cfg
}

// getConfig returns the current lite mode config
func getConfig() *config.Config {
return currentConfig
}

Comment on lines +96 to +108
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a cheeky hack to pass state. It should be handled differently.
I don't want this :)

// errAllBackendsFailed is returned when all backends failed to dial.
var errAllBackendsFailed = errors.New("all backends failed")

Expand Down
2 changes: 2 additions & 0 deletions pkg/edition/java/proxy/session_client_handshake.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ func (h *handshakeSessionHandler) handleHandshake(handshake *packet.Handshake, p
h.conn.SetState(nextState)
dialTimeout := time.Duration(h.config().ConnectionTimeout)
if nextState == state.Login {
// Set the current lite mode config
lite.SetConfig(&h.config().Lite)
// Lite mode enabled, pipe the connection.
lite.Forward(dialTimeout, h.config().Lite.Routes, h.log, h.conn, handshake, pc)
return
Expand Down
Loading