Add option for scale-up allow/deny lists for servers (#397)

This commit is contained in:
Samuel McBroom
2025-04-26 08:57:33 -07:00
committed by GitHub
parent cc5d77e4c8
commit da52e7096f
6 changed files with 450 additions and 17 deletions
+84
View File
@@ -0,0 +1,84 @@
package server
import (
"encoding/json"
"github.com/google/uuid"
"os"
)
type AllowDenyLists struct {
Allowlist []PlayerInfo
Denylist []PlayerInfo
}
type AllowDenyConfig struct {
Global AllowDenyLists
Servers map[string]AllowDenyLists
}
func ParseAllowDenyConfig(allowDenyListPath string) (*AllowDenyConfig, error) {
allowDenyConfig := AllowDenyConfig{}
data, err := os.ReadFile(allowDenyListPath)
if err != nil {
return nil, err
}
err = json.Unmarshal(data, &allowDenyConfig)
if err != nil {
return nil, err
}
return &allowDenyConfig, nil
}
func entryMatchesPlayer(entry *PlayerInfo, userInfo *PlayerInfo) bool {
// User has added an "empty" entry
// This should never match player info
if entry.Name == "" && entry.Uuid == uuid.Nil {
return false
}
if entry.Name != "" && entry.Uuid != uuid.Nil {
return *entry == *userInfo
}
if entry.Uuid != uuid.Nil {
return entry.Uuid == userInfo.Uuid
}
return entry.Name == userInfo.Name
}
func (allowDenyConfig *AllowDenyConfig) ServerAllowsPlayer(serverAddress string, userInfo *PlayerInfo) bool {
if allowDenyConfig == nil {
return true
}
allowlist := allowDenyConfig.Global.Allowlist
denylist := allowDenyConfig.Global.Denylist
serverAllowDenyConfig, ok := allowDenyConfig.Servers[serverAddress]
// Merges global allow/deny lists with server-specific allow/deny lists if provided
if ok {
allowlist = append(allowlist, serverAllowDenyConfig.Allowlist...)
denylist = append(denylist, serverAllowDenyConfig.Denylist...)
}
// If the allowlist is not empty, the player must have an entry or they will be denied
// If the allowlist is empty, then the denylist is checked
// If the allowlist is empty and the player was not in the denylist, then they are allowed
for _, allowedPlayer := range allowlist {
if entryMatchesPlayer(&allowedPlayer, userInfo) {
return true
}
}
if len(allowlist) > 0 {
return false
}
for _, deniedPlayer := range denylist {
if entryMatchesPlayer(&deniedPlayer, userInfo) {
return false
}
}
return true
}
+230
View File
@@ -0,0 +1,230 @@
package server
import (
"github.com/google/uuid"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_allowDenyConfig_ServerAllowsPlayer(t *testing.T) {
type args struct {
serverAddress string
userInfo *PlayerInfo
}
validUserInfo := &PlayerInfo{
Name: "player_name",
Uuid: uuid.MustParse("53036a8f-cbc8-4074-bbc5-98e5e19b0b14"),
}
otherUserInfo := &PlayerInfo{
Name: "other_player",
Uuid: uuid.MustParse("0d51a0ca-f498-44bf-813f-635c18594b8c"),
}
tests := []struct {
name string
allowDenyConfig *AllowDenyConfig
args args
want bool
}{
{
name: "nil config",
allowDenyConfig: nil,
args: args{
serverAddress: "server.my.domain",
userInfo: validUserInfo,
},
want: true,
},
{
name: "empty config",
allowDenyConfig: &AllowDenyConfig{},
args: args{
serverAddress: "server.my.domain",
userInfo: validUserInfo,
},
want: true,
},
{
name: "impossible global allowlist",
allowDenyConfig: &AllowDenyConfig{
Global: AllowDenyLists{
Allowlist: []PlayerInfo{
PlayerInfo{
Name: "",
Uuid: uuid.Nil,
},
},
},
},
args: args{
serverAddress: "server.my.domain",
userInfo: validUserInfo,
},
want: false,
},
{
name: "player allowed globally",
allowDenyConfig: &AllowDenyConfig{
Global: AllowDenyLists{
Allowlist: []PlayerInfo{
*validUserInfo,
},
},
},
args: args{
serverAddress: "server.my.domain",
userInfo: validUserInfo,
},
want: true,
},
{
name: "player not in allowlist",
allowDenyConfig: &AllowDenyConfig{
Global: AllowDenyLists{
Allowlist: []PlayerInfo{
*otherUserInfo,
},
},
},
args: args{
serverAddress: "server.my.domain",
userInfo: validUserInfo,
},
want: false,
},
{
name: "player denied globally",
allowDenyConfig: &AllowDenyConfig{
Global: AllowDenyLists{
Denylist: []PlayerInfo{
*validUserInfo,
},
},
},
args: args{
serverAddress: "server.my.domain",
userInfo: validUserInfo,
},
want: false,
},
{
name: "player allowed and denied globally",
allowDenyConfig: &AllowDenyConfig{
Global: AllowDenyLists{
Allowlist: []PlayerInfo{
*validUserInfo,
},
Denylist: []PlayerInfo{
*validUserInfo,
},
},
},
args: args{
serverAddress: "server.my.domain",
userInfo: validUserInfo,
},
want: true,
},
{
name: "player allowed on server",
allowDenyConfig: &AllowDenyConfig{
Servers: map[string]AllowDenyLists{
"server.my.domain": AllowDenyLists{
Allowlist: []PlayerInfo{
*validUserInfo,
},
},
},
},
args: args{
serverAddress: "server.my.domain",
userInfo: validUserInfo,
},
want: true,
},
{
name: "player not allowed on server",
allowDenyConfig: &AllowDenyConfig{
Servers: map[string]AllowDenyLists{
"server.my.domain": AllowDenyLists{
Allowlist: []PlayerInfo{
*otherUserInfo,
},
},
},
},
args: args{
serverAddress: "server.my.domain",
userInfo: validUserInfo,
},
want: false,
},
{
name: "player denied on server",
allowDenyConfig: &AllowDenyConfig{
Servers: map[string]AllowDenyLists{
"server.my.domain": AllowDenyLists{
Denylist: []PlayerInfo{
*validUserInfo,
},
},
},
},
args: args{
serverAddress: "server.my.domain",
userInfo: validUserInfo,
},
want: false,
},
{
name: "player allowed globally but denied on server",
allowDenyConfig: &AllowDenyConfig{
Global: AllowDenyLists{
Allowlist: []PlayerInfo{
*validUserInfo,
},
},
Servers: map[string]AllowDenyLists{
"server.my.domain": AllowDenyLists{
Denylist: []PlayerInfo{
*validUserInfo,
},
},
},
},
args: args{
serverAddress: "server.my.domain",
userInfo: validUserInfo,
},
want: true,
},
{
name: "player denied globally but allowed on server",
allowDenyConfig: &AllowDenyConfig{
Global: AllowDenyLists{
Denylist: []PlayerInfo{
*validUserInfo,
},
},
Servers: map[string]AllowDenyLists{
"server.my.domain": AllowDenyLists{
Allowlist: []PlayerInfo{
*validUserInfo,
},
},
},
},
args: args{
serverAddress: "server.my.domain",
userInfo: validUserInfo,
},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
allowed := tt.allowDenyConfig.ServerAllowsPlayer(tt.args.serverAddress, tt.args.userInfo)
assert.Equal(t, tt.want, allowed)
})
}
}
+26 -15
View File
@@ -59,14 +59,15 @@ type PlayerInfo struct {
Uuid uuid.UUID `json:"uuid"`
}
func NewConnector(metrics *ConnectorMetrics, sendProxyProto bool, receiveProxyProto bool, trustedProxyNets []*net.IPNet, recordLogins bool) *Connector {
func NewConnector(metrics *ConnectorMetrics, sendProxyProto bool, receiveProxyProto bool, trustedProxyNets []*net.IPNet, recordLogins bool, autoScaleUpAllowDenyConfig *AllowDenyConfig) *Connector {
return &Connector{
metrics: metrics,
sendProxyProto: sendProxyProto,
connectionsCond: sync.NewCond(&sync.Mutex{}),
receiveProxyProto: receiveProxyProto,
trustedProxyNets: trustedProxyNets,
recordLogins: recordLogins,
metrics: metrics,
sendProxyProto: sendProxyProto,
connectionsCond: sync.NewCond(&sync.Mutex{}),
receiveProxyProto: receiveProxyProto,
trustedProxyNets: trustedProxyNets,
recordLogins: recordLogins,
autoScaleUpAllowDenyConfig: autoScaleUpAllowDenyConfig,
}
}
@@ -78,10 +79,11 @@ type Connector struct {
recordLogins bool
trustedProxyNets []*net.IPNet
activeConnections int32
connectionsCond *sync.Cond
ngrokToken string
clientFilter *ClientFilter
activeConnections int32
connectionsCond *sync.Cond
ngrokToken string
clientFilter *ClientFilter
autoScaleUpAllowDenyConfig *AllowDenyConfig
connectionNotifier ConnectionNotifier
}
@@ -335,10 +337,19 @@ func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net.
backendHostPort, resolvedHost, waker := Routes.FindBackendForServerAddress(ctx, serverAddress)
if waker != nil && nextState > mcproto.StateStatus {
if err := waker(ctx); err != nil {
logrus.WithFields(logrus.Fields{"serverAddress": serverAddress}).WithError(err).Error("failed to wake up backend")
c.metrics.Errors.With("type", "wakeup_failed").Add(1)
return
serverAllowsPlayer := c.autoScaleUpAllowDenyConfig.ServerAllowsPlayer(serverAddress, userInfo)
logrus.
WithField("client", clientAddr).
WithField("server", serverAddress).
WithField("userInfo", userInfo).
WithField("serverAllowsPlayer", serverAllowsPlayer).
Debug("checked if player is allowed to wake up the server")
if serverAllowsPlayer {
if err := waker(ctx); err != nil {
logrus.WithFields(logrus.Fields{"serverAddress": serverAddress}).WithError(err).Error("failed to wake up backend")
c.metrics.Errors.With("type", "wakeup_failed").Add(1)
return
}
}
}