add credit based mux window

This commit is contained in:
iceBear67
2026-07-15 14:59:32 +00:00
parent ada07e0e36
commit bbe4efbe16
15 changed files with 651 additions and 139 deletions
+54 -7
View File
@@ -140,8 +140,16 @@ Magic : u8 # 0x01 = control session, 0x02 = worker conn
RandLen : VarInt # 8 ≤ RandLen ≤ 64
Rand : Bytes[RandLen] # cryptographically random
Timestamp : I64 # client's epoch milliseconds
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.
The hub:
1. Decrypts frame 1 with Phase A.
@@ -158,9 +166,13 @@ frame (both directions) is Phase B, counters reset to 0.
The hub then sends one Phase-B frame to confirm success:
```
SessionReady : payload = [ 0x00 ]
SessionReady : payload = [ 0x00, Flags: VarInt, RecvWindow: VarInt ]
```
The hub echoes the accepted flags (STREAM_FC set) followed by its own
per-stream receive window. A client must reject a SessionReady without the
STREAM_FC flag or without a positive window (an unsupported hub).
A hub that rejects the session simply closes the TCP connection (optionally
after a Phase-B `Error` frame, §6). After `SessionReady`:
@@ -258,6 +270,7 @@ Data : Bytes[...] # remainder of the frame payload
| `0x01` | DATA | both | raw tunneled bytes for the stream. |
| `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). |
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`.
@@ -294,11 +307,33 @@ configurable, `1..8`). To place a new stream:
closes the destination. When the destination closes, the client sends `FIN`;
the hub closes the player socket. `RST` is treated the same way (hard close).
Data on a worker conn is subject to that TCP connection's back-pressure. Each
stream has a bounded outbound queue on the receiving side; overflow resets the
stream (`RST`). (This is a deliberate simplification — no per-stream credit
windows — acceptable for the interactive, low-throughput Minecraft handshake +
gameplay traffic pattern.)
Data on a worker conn is subject to that TCP connection's back-pressure for
its **aggregate** bandwidth; *per-stream* fairness is governed by the credit
windows of §7.3.
### 7.3 Per-stream flow control
Every stream carries an independent credit window per direction:
* Each side advertised its **receive window** W (bytes) at session setup. A
sender may have at most W un-credited DATA bytes outstanding per stream; the
initial budget is W, spent as DATA is sent (`Data` length only — SYN/FIN/RST
frames are free) starting with the very first DATA on the stream (including
the hub's forwarded handshake).
* The receiver returns credit with `WND(Delta)` once bytes are **delivered to
the terminal socket** (written to the player / destination connection), not
when they are merely buffered. Receivers should batch grants (the reference
implementations send one `WND` per W/2 bytes consumed).
* A sender whose window is exhausted pauses reading **that stream's source
socket only**; the shared worker conn is never paused because of a single
stream. A receiver that observes more than W un-credited bytes on a stream
may reset it (`RST`) as a protocol violation.
* Senders should also cap individual DATA payloads (the reference
implementations use 32 KiB) so one stream cannot monopolize the link for a
full 1-MiB frame.
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.
## 8. HAProxy protocol v2 (optional)
@@ -327,10 +362,14 @@ big-endian.
"listen": "0.0.0.0:25565",
"psk": "change-me",
"timestampWindowMs": 30000,
"pendingTimeoutMs": 10000
"pendingTimeoutMs": 10000,
"streamWindowBytes": 262144
}
```
`streamWindowBytes` (optional, default 262144, clamped to [32768, 8388608]) is
the hub's advertised per-stream receive window (§7.3).
### 9.2 Client — JSON
```json
@@ -339,12 +378,16 @@ big-endian.
"psk": "change-me",
"maxConn": 4,
"pingIntervalMs": 20000,
"streamWindowBytes": 262144,
"mappings": [
{ "pattern": "mc\\.example\\.com", "destination": "127.0.0.1:25566", "proxyProtocol": true }
]
}
```
`streamWindowBytes` (optional, default 262144, clamped to [32768, 8388608]) is
the client's advertised per-stream receive window (§7.3).
Each `pattern` is a regular expression (§5.1) matched against the whole
normalized player hostname, case-insensitively. Escape literal dots (`mc\.example\.com`,
which is `mc\\.example\\.com` in JSON); an unescaped `.` matches any character.
@@ -369,4 +412,8 @@ subdomain or `(alpha|beta)\.mc\.net` for a fixed set.
| max frame payload | 1 MiB |
| saturation threshold | active streams `> 8` |
| max worker conns | `max_conn ∈ [1,8]` |
| feature flag: per-stream flow control | `0x01` |
| 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 |
```