fix
This commit is contained in:
+84
-13
@@ -144,11 +144,21 @@ Flags : VarInt # feature flags; bit 0x01 (STREAM_FC) MUST be set
|
||||
RecvWindow: VarInt # client's per-stream receive window, bytes (§7.3)
|
||||
```
|
||||
|
||||
`Flags` is a bitfield of features. Bit `0x01` (STREAM_FC) declares
|
||||
**per-stream flow control** (§7.3) and is mandatory: `RecvWindow` advertises
|
||||
the client's per-stream receive window in bytes and must be positive. The hub
|
||||
closes the connection if the flag is missing, `RecvWindow` is absent or
|
||||
non-positive, or the fields are malformed.
|
||||
`Flags` is a bitfield of features.
|
||||
|
||||
| Bit | Name | Meaning |
|
||||
|-----|------|---------|
|
||||
| `0x01` | STREAM_FC | **Per-stream flow control** (§7.3). Mandatory. |
|
||||
| `0x02` | WORKER_HEARTBEAT | Mux-level `PING`/`PONG` on worker conns (§7.4). Optional. |
|
||||
|
||||
STREAM_FC is mandatory: `RecvWindow` advertises the client's per-stream receive
|
||||
window in bytes and must be positive. The hub closes the connection if the flag
|
||||
is missing, `RecvWindow` is absent or non-positive, or the fields are malformed.
|
||||
|
||||
Optional bits are **negotiated**: the hub echoes in `SessionReady` only those it
|
||||
accepts, and the client enables a feature only when its bit comes back. A hub
|
||||
that does not know WORKER_HEARTBEAT simply omits the bit and the client falls
|
||||
back to TCP keepalive alone.
|
||||
|
||||
The hub:
|
||||
|
||||
@@ -269,6 +279,8 @@ Data : Bytes[...] # remainder of the frame payload
|
||||
| `0x02` | FIN | both | *(empty)* — graceful close of the stream (both directions). This is the "disconnect" the hub sends when the player leaves. |
|
||||
| `0x03` | RST | both | *(optional 1 byte reason)* — abnormal close (e.g. CID unknown/expired, destination dial failed). |
|
||||
| `0x04` | WND | both | `Delta: VarInt` — flow-control credit grant (§7.3). |
|
||||
| `0x05` | PING | both | `Nonce: I64` — liveness probe on reserved StreamID `0` (§7.4). |
|
||||
| `0x06` | PONG | both | `Nonce: I64` — echoes the probe's nonce (§7.4). |
|
||||
|
||||
There is no explicit SYN-ACK: success is implied by the hub forwarding the
|
||||
buffered Handshake as the stream's first `DATA`; failure is an `RST`.
|
||||
@@ -276,12 +288,22 @@ buffered Handshake as the stream's first `DATA`; failure is an `RST`.
|
||||
### 7.1 Stream allocation (client side)
|
||||
|
||||
The client keeps a pool of `1 ≤ N ≤ max_conn` worker conns (`max_conn`
|
||||
configurable, `1..8`). To place a new stream:
|
||||
configurable, `1..8`). The pool grows **breadth-first**: spreading streams over
|
||||
several connections keeps any single TCP connection from becoming the shared
|
||||
point of failure for every player on the tunnel. To place a new stream:
|
||||
|
||||
1. Pick the worker conn with the **fewest active streams**.
|
||||
2. If that minimum conn is **saturated** (active streams `> 8`) **and**
|
||||
`poolSize < max_conn`, dial a new worker conn and use it instead.
|
||||
3. Otherwise use the least-loaded conn (even if it exceeds 8 at `max_conn`).
|
||||
1. Pick the worker conn with the **fewest active streams**, and use it.
|
||||
2. If that conn already carries at least one stream and
|
||||
`poolSize + dialsInFlight < max_conn`, dial another worker conn **in the
|
||||
background**. The stream just placed is not delayed by that dial; the new
|
||||
conn becomes the least-loaded one and picks up subsequent streams.
|
||||
3. Once the pool is at `max_conn`, streams stack on the least-loaded conn.
|
||||
Exceeding `8` active streams there is logged as pool saturation.
|
||||
|
||||
Only when the pool is *empty* does a caller dial synchronously, and then exactly
|
||||
one caller dials while the others wait for its result. A dial is never performed
|
||||
while holding the pool lock: session establishment is network I/O, and one
|
||||
unresponsive hub must not be able to block unrelated players.
|
||||
|
||||
### 7.2 End-to-end player flow
|
||||
|
||||
@@ -333,6 +355,37 @@ Every stream carries an independent credit window per direction:
|
||||
Both windows may differ (each side enforces the one its peer advertised).
|
||||
`Delta` must be positive; a `WND` for an unknown stream id is ignored.
|
||||
|
||||
### 7.4 Liveness
|
||||
|
||||
TCP alone cannot tell a healthy idle connection from a dead one. When a stateful
|
||||
middlebox forgets an established flow — conntrack expiry, a firewall reload, a
|
||||
cloud load balancer's idle timeout — it sends neither `FIN` nor `RST`. Both ends
|
||||
keep a socket that will never again carry a byte, and a reader parked on it waits
|
||||
forever. Without an application-level probe the client cannot notice: its worker
|
||||
conn stays in the pool, the hub keeps routing players to a control session nobody
|
||||
reads, and service does not return until the client process is restarted.
|
||||
|
||||
Every established session is therefore covered by a heartbeat:
|
||||
|
||||
* **Control session** — the client sends `Ping` every `pingIntervalMs` and the
|
||||
hub answers `Pong`. If no `Pong` arrives for `3 × pingIntervalMs`, the client
|
||||
closes the session, which triggers its normal reconnect with backoff.
|
||||
* **Worker conns** — when WORKER_HEARTBEAT was negotiated, the same exchange
|
||||
runs as mux `PING`/`PONG` frames on the reserved StreamID `0`. On timeout the
|
||||
client closes the conn; its streams are reset and it is dropped from the pool,
|
||||
so the next player gets a freshly dialed connection.
|
||||
* **Hub side** — an established redapricot session that receives no frame for
|
||||
`sessionIdleTimeoutMs` (default 90000, `0` disables) is closed. Player
|
||||
connections are never subject to this.
|
||||
|
||||
Both ends also enable TCP keepalive, which catches the narrower case of a peer
|
||||
that has become unreachable at the IP layer.
|
||||
|
||||
Session establishment (§3.2) is bounded by a single deadline covering the dial,
|
||||
the `Rekey` write and the `SessionReady` read, and every frame write is bounded
|
||||
too — a peer that stops reading must not be able to park a whole multiplexed
|
||||
connection inside one write.
|
||||
|
||||
## 8. HAProxy protocol v2 (optional)
|
||||
|
||||
When a mapping has `proxyProtocol: true`, the client prepends a PROXY v2 header
|
||||
@@ -361,13 +414,18 @@ big-endian.
|
||||
"psk": "change-me",
|
||||
"timestampWindowMs": 30000,
|
||||
"pendingTimeoutMs": 10000,
|
||||
"streamWindowBytes": 262144
|
||||
"streamWindowBytes": 262144,
|
||||
"sessionIdleTimeoutMs": 90000
|
||||
}
|
||||
```
|
||||
|
||||
`streamWindowBytes` (optional, default 262144, clamped to [32768, 8388608]) is
|
||||
the hub's advertised per-stream receive window (§7.3).
|
||||
|
||||
`sessionIdleTimeoutMs` (optional, default 90000) closes an established control
|
||||
session or worker conn that has gone silent for that long (§7.4). It must stay
|
||||
comfortably above the client's `pingIntervalMs`; `0` disables the watchdog.
|
||||
|
||||
### 9.2 Client — JSON
|
||||
|
||||
```json
|
||||
@@ -392,6 +450,15 @@ which is `mc\\.example\\.com` in JSON); an unescaped `.` matches any character.
|
||||
Use ordinary regex to route wildcards, e.g. `.*\.example\.com` for every
|
||||
subdomain or `(alpha|beta)\.mc\.net` for a fixed set.
|
||||
|
||||
`velocitySecret` (optional, per mapping) makes the client speak Velocity
|
||||
"modern forwarding" towards that destination: during the Minecraft login phase
|
||||
it swallows the backend's `velocity:player_info` Login Plugin Request and
|
||||
answers with an HMAC-SHA256-signed payload carrying the player's real IP,
|
||||
username and UUID (the UUID claimed in Login Start, or the offline-mode UUID
|
||||
for protocols that carry none). Set it to the backend's
|
||||
`proxies.velocity.secret`. This is purely client↔destination behavior — it does
|
||||
not appear on the tunnel wire, and the exchange is invisible to the player.
|
||||
|
||||
## 10. Constants summary
|
||||
|
||||
| Name | Value |
|
||||
@@ -408,9 +475,13 @@ subdomain or `(alpha|beta)\.mc\.net` for a fixed set.
|
||||
| pattern matching | case-insensitive, whole-string regex; first match wins |
|
||||
| CID length | 16 bytes |
|
||||
| max frame payload | 1 MiB |
|
||||
| saturation threshold | active streams `> 8` |
|
||||
| pool growth | breadth-first: grow to `max_conn` before stacking |
|
||||
| saturation threshold (logged) | active streams `> 8` at `max_conn` |
|
||||
| max worker conns | `max_conn ∈ [1,8]` |
|
||||
| feature flag: per-stream flow control | `0x01` |
|
||||
| feature flag: per-stream flow control | `0x01` (mandatory) |
|
||||
| feature flag: worker heartbeat | `0x02` (negotiated) |
|
||||
| heartbeat timeout | `3 × pingIntervalMs` |
|
||||
| hub session idle timeout | 90000 ms (`0` disables) |
|
||||
| stream window default / bounds | 256 KiB, clamped to [32 KiB, 8 MiB] |
|
||||
| WND grant batching (reference) | one grant per window/2 consumed |
|
||||
| DATA chunk cap (reference) | 32 KiB |
|
||||
|
||||
Reference in New Issue
Block a user