break: replace muxed workers with 1:1 tunnels
Worker frames are now FrameType + payload; there is no stream id. Each player gets its own worker conn. maxTunnels (default 256) caps concurrent tunnels. The old maxConn pool size is ignored so existing configs do not silently admit only a handful of players. Resume, per-direction windows, the control session, and the DATA-only shaper stay. A dropped worker still hangs that one player and reattaches over a fresh conn. Add a hub-side per-IP limiter for player intents only (default 8/s, burst 16, 64 concurrent). Unmatched hostnames consume a token; Intent 17 is never counted. 0 disables each knob.
This commit is contained in:
+41
-34
@@ -43,19 +43,15 @@ const (
|
||||
MuxWnd = 0x04
|
||||
MuxPing = 0x05
|
||||
MuxPong = 0x06
|
||||
// MuxResume reattaches a parked stream to this conn (CID + our accepted
|
||||
// MuxResume reattaches a parked player to this conn (CID + our accepted
|
||||
// offset); MuxResumeAck carries the hub's accepted offset and a fresh CID.
|
||||
MuxResume = 0x07
|
||||
MuxResumeAck = 0x08
|
||||
|
||||
// MuxCtlSid is the reserved stream id carrying connection-scoped mux frames
|
||||
// (PING/PONG). Real streams are numbered from 1.
|
||||
MuxCtlSid = 0
|
||||
|
||||
// RST reason codes (optional trailing byte; absence means "unspecified").
|
||||
// Distinguishing them matters for resume: an unknown stream is terminal,
|
||||
// while "already bound" means the hub has the stream on another conn — the
|
||||
// reattach retries until that bind dies and the hub re-parks the stream.
|
||||
// while "already bound" means the hub has the player on another conn — the
|
||||
// reattach retries until that bind dies and the hub re-parks the player.
|
||||
RstUnspecified = 0x00
|
||||
RstUnknownStream = 0x01
|
||||
RstAlreadyBound = 0x02
|
||||
@@ -65,33 +61,33 @@ const (
|
||||
|
||||
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
|
||||
// DefaultMaxTunnels / MaxMaxTunnels bound concurrent 1:1 worker conns.
|
||||
// The old mux-era maxConn cap of 8 would silently become "8 players".
|
||||
DefaultMaxTunnels = 256
|
||||
MaxMaxTunnels = 4096
|
||||
|
||||
// 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 enables connection-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 and that
|
||||
// player is stuck until the client restarts.
|
||||
FlagWorkerHeartbeat = 0x02
|
||||
// FlagStreamResume enables stream resumption (PROTOCOL.md §7.5): a worker
|
||||
// conn drop parks its streams instead of killing them, the hub hangs the
|
||||
// player sockets, and the client reattaches each stream byte-exactly over a
|
||||
// fresh conn. Negotiated, so either side may decline and get today's
|
||||
// behaviour (immediate teardown) unchanged.
|
||||
// conn drop parks the player instead of killing them, the hub hangs the
|
||||
// player socket, and the client reattaches byte-exactly over a fresh conn.
|
||||
// Negotiated, so either side may decline and get today's behaviour
|
||||
// (immediate teardown) unchanged.
|
||||
FlagStreamResume = 0x04
|
||||
|
||||
// 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.
|
||||
// Per-connection flow-control window bounds (bytes). The advertised window
|
||||
// is the receiver's promise of how much un-credited DATA it will buffer.
|
||||
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 caps a single DATA frame's payload so one write cannot
|
||||
// occupy the link for a full 1-MiB frame.
|
||||
DataChunkSize = 32 * 1024
|
||||
)
|
||||
|
||||
@@ -211,14 +207,20 @@ type Mapping struct {
|
||||
|
||||
// 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
|
||||
Server string `json:"server"`
|
||||
PSK string `json:"psk"`
|
||||
// MaxTunnels is the max concurrent 1:1 worker connections (PROTOCOL.md §7.1).
|
||||
// 0 means the default. Clamped to [1, 4096].
|
||||
MaxTunnels int `json:"maxTunnels"`
|
||||
// MaxConn is the retired mux-era pool size. Ignored when loading a file:
|
||||
// honouring a value of 4 as a player cap would silently break existing
|
||||
// configs. Tests that construct a Config should set MaxTunnels instead.
|
||||
MaxConn int `json:"maxConn"`
|
||||
PingIntervalMs int `json:"pingIntervalMs"`
|
||||
StreamWindowBytes int `json:"streamWindowBytes"` // per-connection receive window; 0 = default
|
||||
// MaxBandwidth caps what the client sends to the hub, aggregated over every
|
||||
// stream on every worker conn — the direction that carries the game server's
|
||||
// output to the players, and the one a residential uplink runs out of first.
|
||||
// worker conn — the direction that carries the game server's output to the
|
||||
// players, and the one a residential uplink runs out of first.
|
||||
// Empty means no limit. See parseBandwidth for the accepted syntax.
|
||||
MaxBandwidth string `json:"maxBandwidth"`
|
||||
// StreamResume enables stream resumption (PROTOCOL.md §7.5). A pointer so an
|
||||
@@ -310,11 +312,16 @@ func LoadConfig(path string) (*Config, error) {
|
||||
if c.PSK == "" {
|
||||
return nil, fmt.Errorf("psk is required")
|
||||
}
|
||||
if c.MaxConn < 1 {
|
||||
c.MaxConn = 1
|
||||
if c.MaxConn != 0 && c.MaxTunnels == 0 {
|
||||
// Old mux pool size. Must not become the player cap: a previously-working
|
||||
// maxConn: 4 would admit only four players.
|
||||
fmt.Fprintf(os.Stderr, "redapricot-client: maxConn is ignored (it was the mux pool size); use maxTunnels (default %d)\n", DefaultMaxTunnels)
|
||||
}
|
||||
if c.MaxConn > 8 {
|
||||
c.MaxConn = 8
|
||||
if c.MaxTunnels < 1 {
|
||||
c.MaxTunnels = DefaultMaxTunnels
|
||||
}
|
||||
if c.MaxTunnels > MaxMaxTunnels {
|
||||
c.MaxTunnels = MaxMaxTunnels
|
||||
}
|
||||
if c.PingIntervalMs <= 0 {
|
||||
c.PingIntervalMs = DefaultPingIntervalMs
|
||||
|
||||
Reference in New Issue
Block a user