This commit is contained in:
iceBear67
2026-07-25 16:33:28 +08:00
parent a41cb7965e
commit e63a34d53a
20 changed files with 1787 additions and 95 deletions
+17 -3
View File
@@ -45,7 +45,8 @@ that carries many players as lightweight *streams* — opens a stream for that C
dials the real destination (optionally announcing the player's real IP with the
**HAProxy v2** protocol), and bridges the two ends. Worker connections are
pooled: the client uses up to `maxConn` of them and always places a new stream
on the least-loaded one.
on the least-loaded one, growing the pool to `maxConn` before stacking streams
so no single TCP connection carries every player.
## Repository layout
@@ -126,6 +127,10 @@ just add the hub's IP with that hostname), then join `mc.example.com` in
Minecraft. The hub matches the hostname against the registered regex patterns
and tunnels you to `127.0.0.1:25566` behind the client. With `proxyProtocol: true`, the real server sees your true IP
(enable `proxy-protocol` / a compatible front-end on that server to consume it).
For a Paper backend, setting `velocitySecret` instead is usually nicer: the
client answers the backend's Velocity modern-forwarding login query, so the
server sees your real IP, username and UUID without any front-end — configure
the backend with `proxies.velocity.enabled: true` and the same secret.
## Container image (client)
@@ -163,6 +168,7 @@ secrets. The base image and build flags live in `.ko.yaml`.
| `psk` | *(required)* | Shared secret; must match every client. |
| `timestampWindowMs` | `30000` | Allowed clock skew for a client's rekey timestamp. |
| `pendingTimeoutMs` | `10000` | How long a matched player waits for a worker to take over. |
| `sessionIdleTimeoutMs` | `90000` | Close an established control/worker session that receives no frame for this long. Must exceed the client's `pingIntervalMs`; `0` disables. Player connections are unaffected. |
### Client (`client/config.example.json`)
@@ -171,11 +177,12 @@ secrets. The base image and build flags live in `.ko.yaml`.
| `server` | *(required)* | Hub `host:port`. |
| `psk` | *(required)* | Shared secret; must match the hub. |
| `maxConn` | `1` (clamped 18) | Max worker connections in the pool. |
| `pingIntervalMs` | `20000` | Control-session keepalive interval. |
| `pingIntervalMs` | `20000` (min 1000) | Heartbeat interval for the control session and every worker conn. A session with no reply for `3×` this is dropped and re-established. |
| `mappings[]` | *(≥1 required)* | Route table (below). |
| `mappings[].pattern` | — | Regex matched against the whole player hostname, case-insensitively. Escape dots (`mc\.example\.com`); `.` is a wildcard. |
| `mappings[].destination` | — | Real server `host:port` to forward to. |
| `mappings[].proxyProtocol` | `false` | Prepend a HAProxy v2 header carrying the player's IP. |
| `mappings[].velocitySecret` | *(off)* | Answer the destination's [Velocity modern forwarding](https://docs.papermc.io/velocity/player-information-forwarding/) login query with this secret, forwarding the player's real IP, username and UUID. Match it to the backend's `proxies.velocity.secret` (Paper). The forwarded profile carries no skin properties — the tunnel performs no Mojang authentication. |
## Testing
@@ -201,7 +208,8 @@ The e2e suite covers: a full player round-trip with verbatim handshake
forwarding and case-insensitive matching, regex wildcard pattern routing,
multi-megabyte transfers, concurrent
streams spreading across multiple worker connections, HAProxy v2 source-address
propagation, player- and destination-initiated disconnect propagation, wrong-PSK
propagation, Velocity modern-forwarding interception (signed player-info
handoff to a mock Paper backend), player- and destination-initiated disconnect propagation, wrong-PSK
rejection, dropping of unmatched hostnames, stream isolation under a slow
player and under a slow destination (no head-of-line blocking), and rejection
of pre-flow-control peers. The Go and Java crypto layers are
@@ -220,6 +228,12 @@ drift apart.
application-level head-of-line blocking between streams. What remains is
TCP-level HOL (packet loss stalls a whole worker connection briefly);
raising `maxConn` spreads that.
* **Liveness is explicit.** Every session heartbeats, every socket write is
bounded, and session establishment has a deadline. A path that dies silently —
no `FIN`, no `RST`, as when a NAT or firewall forgets an established flow — is
detected within `3 × pingIntervalMs`, the dead connection is dropped from the
pool, and service is restored without operator action. TCP keepalive is on as
a second line of defence.
* **Single hub event loop.** The hub deploys one Vert.x verticle, so all state
is confined to one event loop (no locking). Throughput is bounded by one core;
ample for hundreds of players, not designed for tens of thousands.