Skip to content

Commit da52e70

Browse files
authored
Add option for scale-up allow/deny lists for servers (#397)
1 parent cc5d77e commit da52e70

File tree

6 files changed

+450
-17
lines changed

6 files changed

+450
-17
lines changed

README.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ Routes Minecraft client connections to backend servers based upon the requested
7979
-webhook-url string
8080
If set, a POST request that contains connection status notifications will be sent to this HTTP address (env WEBHOOK_URL)
8181
-record-logins
82-
Log and generate metrics on player logins. Metrics only supported with influxdb or prometheus backend
82+
Log and generate metrics on player logins. Metrics only supported with influxdb or prometheus backend (env RECORD_LOGINS)
83+
-auto-scale-up-allow-deny string
84+
Path to config for server allowlists and denylists. If -auto-scale-up is enabled and a global/server entry is specified, only players allowed to connect to the server will be able to trigger a scale up (env AUTO_SCALE_UP_ALLOW_DENY)
8385
```
8486

8587
## Docker Multi-Architecture Image
@@ -170,6 +172,41 @@ The following shows a JSON file for routes config, where `default-server` can al
170172
}
171173
```
172174

175+
## Auto Scale Up Allow/Deny List
176+
177+
The allow/deny list configuration allows limiting which players can scale up servers when using the `-auto-scale-up` option or the `AUTO_SCALE_UP` env variable. Global allow/deny lists can be configured that apply to all backend servers, but server-specific lists can be added as well. There are a few important things to note about the configuration:
178+
- The `mc-router` process will not automatically pick up changes to the config. If updates to the config are made, the router must be restarted.
179+
- Allowlists always take priority over denylists. This means if a player is included in a sever-specific allowlist and the global denylist, the player will still be considered allowed on that server. If a player is listed in both a global allowlist and denylist, the denylist entry will be ignored.
180+
- Player entries only require a `uuid` or `name`. Both will be checked if specified, but otherwise a `uuid` will take priority over a `name`.
181+
182+
An example configuration might look something like:
183+
184+
```json
185+
{
186+
"global": {
187+
"denylist": [
188+
{"uuid": "<some player's uuid>", "name": "<some player's name>"}
189+
]
190+
},
191+
"servers": {
192+
"my.server.domain": {
193+
"allowlist": [
194+
{"uuid": "<some player's uuid>"}
195+
]
196+
},
197+
"my.other-server.domain": {
198+
"denylist": [
199+
{"uuid": "<some player's uuid>"}
200+
]
201+
}
202+
}
203+
}
204+
```
205+
206+
In the example, players in the `my.server.domain` allowlist will be able to scale up `my.server.domain`. Players in the global denylist and the `my.other-server.domain` denylist will **not** be able to scale up `my.other-server.domain`. Any servers not listed in the config will also be affected by the global allowlist. Note that if a global allowlist is specified, no denylists will have any effect as that global allowlist will affect all servers.
207+
208+
For more information on the allow/deny list configuration, see the [json schema](docs/allow-deny-list.schema.json).
209+
173210
## Kubernetes Usage
174211

175212
### Using Kubernetes Service auto-discovery

cmd/mc-router/main.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ type Config struct {
5858
MetricsBackendConfig MetricsBackendConfig
5959
RoutesConfig string `usage:"Name or full path to routes config file"`
6060
NgrokToken string `usage:"If set, an ngrok tunnel will be established. It is HIGHLY recommended to pass as an environment variable."`
61+
AutoScaleUpAllowDeny string `usage:"Path to config for server allowlists and denylists. If -auto-scale-up is enabled and a global/server entry is specified, only players allowed to connect to the server will be able to trigger a scale up"`
6162

6263
ClientsToAllow []string `usage:"Zero or more client IP addresses or CIDRs to allow. Takes precedence over deny."`
6364
ClientsToDeny []string `usage:"Zero or more client IP addresses or CIDRs to deny. Ignored if any configured to allow"`
@@ -110,6 +111,14 @@ func main() {
110111
defer pprof.StopCPUProfile()
111112
}
112113

114+
var autoScaleUpAllowDenyConfig *server.AllowDenyConfig = nil
115+
if config.AutoScaleUpAllowDeny != "" {
116+
autoScaleUpAllowDenyConfig, err = server.ParseAllowDenyConfig(config.AutoScaleUpAllowDeny)
117+
if err != nil {
118+
logrus.WithError(err).Fatal("trying to parse autoscale up allow-deny-list file")
119+
}
120+
}
121+
113122
ctx, cancel := context.WithCancel(context.Background())
114123
defer cancel()
115124

@@ -143,7 +152,7 @@ func main() {
143152
trustedIpNets = append(trustedIpNets, ipNet)
144153
}
145154

146-
connector := server.NewConnector(metricsBuilder.BuildConnectorMetrics(), config.UseProxyProtocol, config.ReceiveProxyProtocol, trustedIpNets, config.RecordLogins)
155+
connector := server.NewConnector(metricsBuilder.BuildConnectorMetrics(), config.UseProxyProtocol, config.ReceiveProxyProtocol, trustedIpNets, config.RecordLogins, autoScaleUpAllowDenyConfig)
147156

148157
clientFilter, err := server.NewClientFilter(config.ClientsToAllow, config.ClientsToDeny)
149158
if err != nil {

docs/allow-deny-list.schema.json

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"$id": "https://github.com/itzg/mc-router/docs/allow-deny-list.schema.json",
4+
"title": "Player allow/deny list",
5+
"description": "Per-server and/or global player allow/deny list",
6+
"type": "object",
7+
"$defs": {
8+
"userInfo": {
9+
"description": "Player to allow/deny by uuid and/or name",
10+
"type": "object",
11+
"properties": {
12+
"uuid": {
13+
"description": "Player username (takes priority over name if specified)",
14+
"type": "string",
15+
"format": "uuid"
16+
},
17+
"name": {
18+
"description": "Player name",
19+
"type": "string"
20+
}
21+
},
22+
"additionalProperties": false
23+
},
24+
"allowDenyLists": {
25+
"description": "Allow and deny lists of player information",
26+
"type": "object",
27+
"properties": {
28+
"allowlist": {
29+
"description": "List of allowed players (takes priority over denylist if specified)",
30+
"type": "array",
31+
"items": {
32+
"$ref": "#/$defs/userInfo"
33+
}
34+
},
35+
"denylist": {
36+
"description": "List of denied players",
37+
"type": "array",
38+
"items": {
39+
"$ref": "#/$defs/userInfo"
40+
}
41+
}
42+
},
43+
"additionalProperties": false
44+
}
45+
},
46+
"properties": {
47+
"global": {
48+
"description": "Global allow and deny lists of player information (allowlists take priority over denylists so if a player is denylisted globally but allowlisted in a server block, they will be allowed on that server)",
49+
"$ref": "#/$defs/allowDenyLists"
50+
},
51+
"servers": {
52+
"description": "Server-specific allow and deny lists of player information with each object key being a server address",
53+
"type": "object",
54+
"patternProperties": {
55+
"^.+$": {
56+
"$ref": "#/$defs/allowDenyLists"
57+
}
58+
}
59+
}
60+
},
61+
"additionalProperties": false
62+
}

server/allow_deny_list.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package server
2+
3+
import (
4+
"encoding/json"
5+
"github.com/google/uuid"
6+
"os"
7+
)
8+
9+
type AllowDenyLists struct {
10+
Allowlist []PlayerInfo
11+
Denylist []PlayerInfo
12+
}
13+
14+
type AllowDenyConfig struct {
15+
Global AllowDenyLists
16+
Servers map[string]AllowDenyLists
17+
}
18+
19+
func ParseAllowDenyConfig(allowDenyListPath string) (*AllowDenyConfig, error) {
20+
allowDenyConfig := AllowDenyConfig{}
21+
data, err := os.ReadFile(allowDenyListPath)
22+
if err != nil {
23+
return nil, err
24+
}
25+
err = json.Unmarshal(data, &allowDenyConfig)
26+
if err != nil {
27+
return nil, err
28+
}
29+
return &allowDenyConfig, nil
30+
}
31+
32+
func entryMatchesPlayer(entry *PlayerInfo, userInfo *PlayerInfo) bool {
33+
// User has added an "empty" entry
34+
// This should never match player info
35+
if entry.Name == "" && entry.Uuid == uuid.Nil {
36+
return false
37+
}
38+
39+
if entry.Name != "" && entry.Uuid != uuid.Nil {
40+
return *entry == *userInfo
41+
}
42+
43+
if entry.Uuid != uuid.Nil {
44+
return entry.Uuid == userInfo.Uuid
45+
}
46+
47+
return entry.Name == userInfo.Name
48+
}
49+
50+
func (allowDenyConfig *AllowDenyConfig) ServerAllowsPlayer(serverAddress string, userInfo *PlayerInfo) bool {
51+
if allowDenyConfig == nil {
52+
return true
53+
}
54+
55+
allowlist := allowDenyConfig.Global.Allowlist
56+
denylist := allowDenyConfig.Global.Denylist
57+
serverAllowDenyConfig, ok := allowDenyConfig.Servers[serverAddress]
58+
// Merges global allow/deny lists with server-specific allow/deny lists if provided
59+
if ok {
60+
allowlist = append(allowlist, serverAllowDenyConfig.Allowlist...)
61+
denylist = append(denylist, serverAllowDenyConfig.Denylist...)
62+
}
63+
64+
// If the allowlist is not empty, the player must have an entry or they will be denied
65+
// If the allowlist is empty, then the denylist is checked
66+
// If the allowlist is empty and the player was not in the denylist, then they are allowed
67+
for _, allowedPlayer := range allowlist {
68+
if entryMatchesPlayer(&allowedPlayer, userInfo) {
69+
return true
70+
}
71+
}
72+
73+
if len(allowlist) > 0 {
74+
return false
75+
}
76+
77+
for _, deniedPlayer := range denylist {
78+
if entryMatchesPlayer(&deniedPlayer, userInfo) {
79+
return false
80+
}
81+
}
82+
83+
return true
84+
}

0 commit comments

Comments
 (0)