Skip to content

Commit f54394f

Browse files
committed
feat: add a fake status response when autoscaling is up
1 parent f6300d6 commit f54394f

File tree

4 files changed

+114
-3
lines changed

4 files changed

+114
-3
lines changed

cmd/mc-router/main.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ type Config struct {
6565

6666
SimplifySRV bool `default:"false" usage:"Simplify fully qualified SRV records for mapping"`
6767

68+
FakeOnline bool `default:"false" usage:"Enable fake online MOTD when backend is offline and auto-scale-up is enabled"`
69+
FakeOnlineMOTD string `default:"Server is sleeping\nJoin to wake it up" usage:"Custom MOTD to show when backend is offline and auto-scale-up is enabled"`
70+
6871
Webhook WebhookConfig `usage:"Webhook configuration"`
6972
}
7073

@@ -152,7 +155,7 @@ func main() {
152155
trustedIpNets = append(trustedIpNets, ipNet)
153156
}
154157

155-
connector := server.NewConnector(metricsBuilder.BuildConnectorMetrics(), config.UseProxyProtocol, config.ReceiveProxyProtocol, trustedIpNets, config.RecordLogins, autoScaleUpAllowDenyConfig)
158+
connector := server.NewConnector(metricsBuilder.BuildConnectorMetrics(), config.UseProxyProtocol, config.ReceiveProxyProtocol, trustedIpNets, config.RecordLogins, autoScaleUpAllowDenyConfig, config.FakeOnline, config.FakeOnlineMOTD)
156159

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

mcproto/types.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,30 @@ type ByteReader interface {
8484
const (
8585
PacketLengthFieldBytes = 1
8686
)
87+
88+
type StatusResponse struct {
89+
Version StatusVersion `json:"version"`
90+
Players StatusPlayers `json:"players"`
91+
Description StatusText `json:"description"`
92+
Favicon string `json:"favicon,omitempty"`
93+
}
94+
95+
type StatusVersion struct {
96+
Name string `json:"name"`
97+
Protocol int `json:"protocol"`
98+
}
99+
100+
type StatusPlayers struct {
101+
Max int `json:"max"`
102+
Online int `json:"online"`
103+
Sample []PlayerEntry `json:"sample,omitempty"`
104+
}
105+
106+
type PlayerEntry struct {
107+
Name string `json:"name"`
108+
ID string `json:"id"`
109+
}
110+
111+
type StatusText struct {
112+
Text string `json:"text"`
113+
}

mcproto/write.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package mcproto
2+
3+
import (
4+
"encoding/json"
5+
"io"
6+
)
7+
8+
func WriteStatusResponse(w io.Writer, motd string) error {
9+
resp := StatusResponse{
10+
Version: StatusVersion{
11+
Name: "1.21.5",
12+
Protocol: 770,
13+
},
14+
Players: StatusPlayers{
15+
Max: 0,
16+
Online: 0,
17+
},
18+
Description: StatusText{
19+
Text: motd,
20+
},
21+
}
22+
data, err := json.Marshal(resp)
23+
if err != nil {
24+
return err
25+
}
26+
27+
jsonLen := encodeVarInt(len(data))
28+
payload := append(jsonLen, data...)
29+
return WritePacket(w, 0x00, payload)
30+
}
31+
32+
func WritePacket(w io.Writer, packetID int, data []byte) error {
33+
packet := append(encodeVarInt(packetID), data...)
34+
length := encodeVarInt(len(packet))
35+
_, err := w.Write(append(length, packet...))
36+
return err
37+
}
38+
39+
// encodeVarInt encodes an int as a Minecraft VarInt.
40+
func encodeVarInt(value int) []byte {
41+
var buf []byte
42+
for {
43+
temp := byte(value & 0x7F)
44+
value >>= 7
45+
if value != 0 {
46+
temp |= 0x80
47+
}
48+
buf = append(buf, temp)
49+
if value == 0 {
50+
break
51+
}
52+
}
53+
return buf
54+
}

server/connector.go

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525

2626
const (
2727
handshakeTimeout = 5 * time.Second
28+
backendTimeout = 1 * time.Second
2829
)
2930

3031
var noDeadline time.Time
@@ -67,7 +68,7 @@ func (p *PlayerInfo) String() string {
6768
return fmt.Sprintf("%s/%s", p.Name, p.Uuid)
6869
}
6970

70-
func NewConnector(metrics *ConnectorMetrics, sendProxyProto bool, receiveProxyProto bool, trustedProxyNets []*net.IPNet, recordLogins bool, autoScaleUpAllowDenyConfig *AllowDenyConfig) *Connector {
71+
func NewConnector(metrics *ConnectorMetrics, sendProxyProto bool, receiveProxyProto bool, trustedProxyNets []*net.IPNet, recordLogins bool, autoScaleUpAllowDenyConfig *AllowDenyConfig, fakeOnline bool, fakeOnlineMOTD string) *Connector {
7172
return &Connector{
7273
metrics: metrics,
7374
sendProxyProto: sendProxyProto,
@@ -76,6 +77,8 @@ func NewConnector(metrics *ConnectorMetrics, sendProxyProto bool, receiveProxyPr
7677
trustedProxyNets: trustedProxyNets,
7778
recordLogins: recordLogins,
7879
autoScaleUpAllowDenyConfig: autoScaleUpAllowDenyConfig,
80+
fakeOnline: fakeOnline,
81+
fakeOnlineMOTD: fakeOnlineMOTD,
7982
}
8083
}
8184

@@ -93,6 +96,9 @@ type Connector struct {
9396
clientFilter *ClientFilter
9497
autoScaleUpAllowDenyConfig *AllowDenyConfig
9598

99+
fakeOnline bool
100+
fakeOnlineMOTD string
101+
96102
connectionNotifier ConnectionNotifier
97103
}
98104

@@ -394,7 +400,7 @@ func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net.
394400
WithField("player", playerInfo).
395401
Info("Connecting to backend")
396402

397-
backendConn, err := net.Dial("tcp", backendHostPort)
403+
backendConn, err := net.DialTimeout("tcp", backendHostPort, backendTimeout)
398404
if err != nil {
399405
logrus.
400406
WithError(err).
@@ -412,6 +418,27 @@ func (c *Connector) findAndConnectBackend(ctx context.Context, frontendConn net.
412418
}
413419
}
414420

421+
// Verify that the packet is a status request && autoScaleUp is enabled
422+
if c.fakeOnline && waker != nil && nextState == mcproto.StateStatus {
423+
logrus.Info("Server is offline, sending fakeOnlineMOTD")
424+
425+
// Send a response to the client indicating that the server is sleeping
426+
writeStatusErr := mcproto.WriteStatusResponse(
427+
frontendConn,
428+
c.fakeOnlineMOTD,
429+
)
430+
431+
if writeStatusErr != nil {
432+
logrus.
433+
WithError(writeStatusErr).
434+
WithField("client", clientAddr).
435+
WithField("serverAddress", serverAddress).
436+
WithField("backend", backendHostPort).
437+
WithField("player", playerInfo).
438+
Error("Failed to write status response")
439+
}
440+
}
441+
415442
return
416443
}
417444

0 commit comments

Comments
 (0)