support regex and player name sniff

This commit is contained in:
iceBear67
2026-07-15 21:26:50 +08:00
parent 3a5ad7e318
commit ada07e0e36
11 changed files with 250 additions and 49 deletions
+48 -10
View File
@@ -72,7 +72,7 @@ The hub reads exactly one Handshake packet and dispatches on `Intent`:
|---------------|---------|
| `17` | redapricot session establishment (control session *or* worker conn). |
| `18` | Reserved for redapricot management/status. Never matched against patterns. The reference hub replies with a status line and closes. |
| anything else | **Player** connection. `ServerAddress` is matched (case-insensitively) against registered PATTERNs. |
| anything else | **Player** connection. `ServerAddress` is matched against the registered **regex** PATTERNs (§5.1). |
For `Intent == 17` the hub additionally requires
`ServerAddress == lowercase_hex(SHA3-224(PSK))` — a 56-character hex string.
@@ -81,7 +81,8 @@ the connection.
For player connections the hub normalizes `ServerAddress` before matching:
lower-cased, and any trailing `.` or Forge/FML `\0`-suffix (`host\0FML\0`)
stripped to the bare hostname.
stripped to the bare hostname. The resulting hostname is then tested against the
registered regex patterns (§5.1).
## 3. Encryption
@@ -181,21 +182,50 @@ Type : u8
| `0x00` | SessionReady | S → C | *(none)* — the confirmation frame from §4 |
| `0x01` | Register | C → S | `Pattern: String` |
| `0x02` | Unregister | C → S | `Pattern: String` |
| `0x03` | RegisterAck | S → C | `Pattern: String`, `Status: u8` (0 = ok) |
| `0x04` | ControlRequest | S → C | `CID: Bytes[16]`, `Pattern: String`, `PlayerIP: String`, `PlayerPort: U16` |
| `0x03` | RegisterAck | S → C | `Pattern: String`, `Status: u8` (0 = ok, 1 = invalid pattern) |
| `0x04` | ControlRequest | S → C | `CID: Bytes[16]`, `Pattern: String`, `PlayerIP: String`, `PlayerPort: U16`, `Username: String` |
| `0x05` | Ping | C → S | `Nonce: I64` |
| `0x06` | Pong | S → C | `Nonce: I64` |
* **Register / Unregister**: the client may (un)register a PATTERN at any time.
Patterns are stored lower-cased. Re-registering an existing pattern reassigns
it to the newest session (last writer wins).
A PATTERN is a **regular expression** (§5.1) and is stored **verbatim** — the
exact string is the registry key (never normalized, so regex metacharacters are
preserved). Re-registering the identical pattern string reassigns it to the
newest session (last writer wins); `Unregister` only removes it if the
requesting session still owns it.
* **RegisterAck**: acknowledges a `Register`. `Status` is `0` on success, or `1`
if the pattern is not a valid regular expression (in which case nothing is
registered). The `Pattern` echoes the string that was registered.
* **ControlRequest**: emitted by the hub when a player Handshake matches a
PATTERN this session registered. `CID` is 16 cryptographically-random bytes
generated by the hub, unique to that pending player. `PlayerIP`/`PlayerPort`
are the player's source address (used for HAProxy v2).
PATTERN this session registered. `Pattern` is the **registered pattern string
that matched** (echoed verbatim), *not* the player's hostname — so the client
can look the pattern up in its own route table. `CID` is 16
cryptographically-random bytes generated by the hub, unique to that pending
player. `PlayerIP`/`PlayerPort` are the player's source address (used for
HAProxy v2). `Username` is the player's name, read best-effort from the Login
Start packet — present when the client pipelined it with the Handshake (the
usual case), otherwise an empty string. It is informational (logging) only.
* **Ping/Pong**: optional keepalive so idle control sessions survive NAT
timeouts. The client pings periodically; the hub echoes the nonce.
### 5.1 Pattern matching
A registered PATTERN is a **regular expression** (the reference hub uses
`java.util.regex`). Matching is:
* **Case-insensitive** — patterns are compiled with a case-insensitive flag, and
the player hostname is lower-cased during normalization (§2.1).
* **Whole-string (anchored)** — the pattern must match the *entire* normalized
hostname, as if wrapped in `^…$`. `mc\.example\.com` matches `mc.example.com`
but not `mc.example.com.evil` or `sub.mc.example.com`.
* **First match wins** — the hostname is tested against every registered pattern;
the first that matches routes the player. If several patterns overlap, which
one wins is unspecified.
Because the pattern is a regex, a literal dot must be escaped (`mc\.example\.com`);
an unescaped `.` is the regex "any character" wildcard. A pattern that fails to
compile is rejected at `Register` time with `RegisterAck` status `1`.
## 6. Error frame (any redapricot connection)
At any time either side may send, then close:
@@ -310,11 +340,17 @@ big-endian.
"maxConn": 4,
"pingIntervalMs": 20000,
"mappings": [
{ "pattern": "mc.example.com", "destination": "127.0.0.1:25566", "proxyProtocol": true }
{ "pattern": "mc\\.example\\.com", "destination": "127.0.0.1:25566", "proxyProtocol": true }
]
}
```
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.
Use ordinary regex to route wildcards, e.g. `.*\.example\.com` for every
subdomain or `(alpha|beta)\.mc\.net` for a fixed set.
## 10. Constants summary
| Name | Value |
@@ -327,6 +363,8 @@ big-endian.
| key derivation | `SHA3-256(PK ‖ 0x01)` c→s, `SHA3-256(PK ‖ 0x02)` s→c |
| rekey material | `Rand ‖ Timestamp(I64 BE)` |
| Magic: control / worker | `0x01` / `0x02` |
| RegisterAck status: ok / invalid pattern | `0x00` / `0x01` |
| pattern matching | case-insensitive, whole-string regex; first match wins |
| CID length | 16 bytes |
| max frame payload | 1 MiB |
| saturation threshold | active streams `> 8` |