166 lines
5.1 KiB
Go
166 lines
5.1 KiB
Go
package client
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Protocol constants (mirror of the Java Protocol class; see PROTOCOL.md).
|
|
const (
|
|
IntentRedapricot = 17
|
|
ProtocolVersion = 767 // arbitrary; the hub ignores it
|
|
|
|
MagicControl = 0x01
|
|
MagicWorker = 0x02
|
|
|
|
CIDLen = 16
|
|
|
|
CtlSessionReady = 0x00
|
|
CtlRegister = 0x01
|
|
CtlUnregister = 0x02
|
|
CtlRegisterAck = 0x03
|
|
CtlControlRequest = 0x04
|
|
CtlPing = 0x05
|
|
CtlPong = 0x06
|
|
|
|
MuxSyn = 0x00
|
|
MuxData = 0x01
|
|
MuxFin = 0x02
|
|
MuxRst = 0x03
|
|
MuxWnd = 0x04
|
|
MuxPing = 0x05
|
|
MuxPong = 0x06
|
|
|
|
// MuxCtlSid is the reserved stream id carrying connection-scoped mux frames
|
|
// (PING/PONG). Real streams are numbered from 1.
|
|
MuxCtlSid = 0
|
|
|
|
FrameError = 0x7F
|
|
|
|
// SaturationThreshold caps how many streams share one worker conn once the
|
|
// pool has grown to maxConn. Below maxConn the pool grows first (§7.1), so a
|
|
// single connection is never a shared point of failure for every player.
|
|
SaturationThreshold = 8
|
|
|
|
// Session-establishment feature flags (trailing VarInt on the Rekey message).
|
|
FlagStreamFC = 0x01
|
|
// FlagWorkerHeartbeat enables mux-level PING/PONG on worker conns. Without
|
|
// it a worker conn whose path is silently blackholed (NAT/conntrack drop,
|
|
// firewall) is never detected: the read loop parks forever, the dead conn
|
|
// stays in the pool, and no player can be served until the client restarts.
|
|
FlagWorkerHeartbeat = 0x02
|
|
|
|
// Per-stream flow-control window bounds (bytes). The advertised window is the
|
|
// receiver's promise of how much un-credited DATA it will buffer per stream.
|
|
DefaultStreamWindow = 256 * 1024
|
|
MinStreamWindow = 32 * 1024
|
|
MaxStreamWindow = 8 << 20
|
|
|
|
// DataChunkSize caps a single DATA frame's payload so no stream monopolizes
|
|
// the shared worker connection for long.
|
|
DataChunkSize = 32 * 1024
|
|
)
|
|
|
|
// Timeouts. Every tunnel socket is covered by one of these: without them a
|
|
// silently dropped path (no FIN/RST) leaves the client parked forever.
|
|
const (
|
|
// HandshakeTimeout bounds session establishment end to end — the TCP dial,
|
|
// the Rekey write and the SessionReady read. A hub that accepts the socket
|
|
// but never answers must not park the caller (and, for the pool, every other
|
|
// player behind it) indefinitely.
|
|
HandshakeTimeout = 15 * time.Second
|
|
|
|
// TCPKeepAlivePeriod asks the kernel to probe idle tunnel sockets, so a peer
|
|
// that becomes unreachable is detected even when no frames are in flight.
|
|
TCPKeepAlivePeriod = 30 * time.Second
|
|
|
|
// MissedHeartbeats is how many ping intervals may pass with no reply before
|
|
// a session is declared dead and dropped.
|
|
MissedHeartbeats = 3
|
|
|
|
// MinPingIntervalMs floors the configured ping interval so the derived
|
|
// heartbeat timeout can never be short enough to cause spurious drops.
|
|
// Applied in LoadConfig, i.e. to configs that come from disk.
|
|
MinPingIntervalMs = 1000
|
|
)
|
|
|
|
// heartbeatTimeout is how long a session may go without a reply before it is
|
|
// considered dead, derived from the configured ping interval.
|
|
func (c *Config) heartbeatTimeout() time.Duration {
|
|
return c.pingInterval() * MissedHeartbeats
|
|
}
|
|
|
|
// pingInterval is the configured heartbeat period.
|
|
func (c *Config) pingInterval() time.Duration {
|
|
return time.Duration(c.PingIntervalMs) * time.Millisecond
|
|
}
|
|
|
|
// Mapping routes a registered pattern to a real destination.
|
|
type Mapping struct {
|
|
Pattern string `json:"pattern"`
|
|
Destination string `json:"destination"`
|
|
ProxyProtocol bool `json:"proxyProtocol"`
|
|
// VelocitySecret, when non-empty, answers the destination's Velocity
|
|
// modern-forwarding login query (velocity:player_info) with this secret,
|
|
// forwarding the player's real IP, username and UUID (see velocity.go).
|
|
VelocitySecret string `json:"velocitySecret"`
|
|
}
|
|
|
|
// Config is the client configuration (PROTOCOL.md §9.2).
|
|
type Config struct {
|
|
Server string `json:"server"`
|
|
PSK string `json:"psk"`
|
|
MaxConn int `json:"maxConn"`
|
|
PingIntervalMs int `json:"pingIntervalMs"`
|
|
StreamWindowBytes int `json:"streamWindowBytes"` // per-stream receive window; 0 = default
|
|
Mappings []Mapping `json:"mappings"`
|
|
}
|
|
|
|
// LoadConfig reads and validates a JSON config file.
|
|
func LoadConfig(path string) (*Config, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var c Config
|
|
if err := json.Unmarshal(data, &c); err != nil {
|
|
return nil, fmt.Errorf("parse config: %w", err)
|
|
}
|
|
if c.Server == "" {
|
|
return nil, fmt.Errorf("server is required")
|
|
}
|
|
if c.PSK == "" {
|
|
return nil, fmt.Errorf("psk is required")
|
|
}
|
|
if c.MaxConn < 1 {
|
|
c.MaxConn = 1
|
|
}
|
|
if c.MaxConn > 8 {
|
|
c.MaxConn = 8
|
|
}
|
|
if c.PingIntervalMs <= 0 {
|
|
c.PingIntervalMs = 20000
|
|
}
|
|
if c.PingIntervalMs < MinPingIntervalMs {
|
|
c.PingIntervalMs = MinPingIntervalMs
|
|
}
|
|
if len(c.Mappings) == 0 {
|
|
return nil, fmt.Errorf("at least one mapping is required")
|
|
}
|
|
return &c, nil
|
|
}
|
|
|
|
// NormalizeAddress matches the hub's normalization: lower-cased, FML-suffix and
|
|
// trailing-dot stripped.
|
|
func NormalizeAddress(addr string) string {
|
|
if i := strings.IndexByte(addr, 0); i >= 0 {
|
|
addr = addr[:i]
|
|
}
|
|
addr = strings.ToLower(addr)
|
|
addr = strings.TrimRight(addr, ".")
|
|
return addr
|
|
}
|