mirror of
https://github.com/Corfirean/coa-server-manager.git
synced 2026-11-11 03:51:01 +00:00
docs: sync control protocol transfer flow and infra public relay port range; fix coord and relay test suites
This commit is contained in:
1 parent
65ace578f7
commit
ad39bd2020
6 files changed
+41
-30
No files matched your search
+24
-12
@@ -86,7 +86,7 @@ Player <-> Host requests and responses (JSON), one at a time, in order
|
||||
|
||||
A Coordinator that tries to sit in the middle would have to run two handshakes, each with its own `h`; the Host's signature covers only the Host's `h`, so the Player rejects it. The proofs of one channel are useless on another.
|
||||
Messages are split into frames of ≤ 16 KiB of plaintext (+ a flag byte + the 16-byte tag, so ≤ 20 KiB with the connection number), reassembled up to 8 MiB; a frame that does not authenticate ends the channel.
|
||||
Application messages: requests ≤ 4096 bytes, strict JSON with unknown fields refused, at most 60 requests per channel, 300 s per channel at the Host (idle 45 s).
|
||||
Application messages: standard control requests ≤ 4096 bytes (4 KiB, `MAX_CONTROL_REQUEST_BYTES`), while bulk transfer requests (`transfer_offer` and `transfer_chunk`) support bounded payloads up to 4 MiB (`MAX_TRANSFER_REQUEST_BYTES`). Strict JSON with unknown fields refused, at most 60 requests per channel, 300 s per channel at the Host (idle 45 s).
|
||||
|
||||
## 5. Application messages
|
||||
|
||||
@@ -98,8 +98,12 @@ Application messages: requests ≤ 4096 bytes, strict JSON with unknown fields r
|
||||
| `list_characters` | `characters{characters}` | the characters of the player's account (opaque per-connection handles; the realm's own numbers never leave the Host) |
|
||||
| `claim{token}` | `claimed{character_id, sha256, payload, collections}` | export one character (section 7) |
|
||||
| `claim_ack{character_id}` | `done` | the player stored it; the claim is final |
|
||||
| `transfer_character{character_id, revision, sha256, payload, collections}` | `transferred{character_id, local_guid, session_id}` | transactional remote character import/projection (Phase 12.1) |
|
||||
| `allocate_relay` | `relay_allocated{relay_host, auth_port, world_port, token, expires_at}` | dynamic game relay allocation for NAT traversal (Phase 13) |
|
||||
| `transfer_offer{transfer_id, character_id, canonical_revision, content_hash, total_size, collections}` | `transfer_ready{transfer_id, accepted_offset}` | initiate or resume remote character transfer (Phase 12.1) |
|
||||
| `transfer_chunk{transfer_id, offset, data}` | `transfer_progress{transfer_id, received_bytes, total_bytes}` | transmit chunk of encoded payload (≤ 4 MiB) |
|
||||
| `transfer_status{transfer_id}` | `transfer_progress{transfer_id, received_bytes, total_bytes}` | check upload progress / resume offset |
|
||||
| `transfer_commit{transfer_id}` | `transferred{character_id, local_guid, session_id}` | validate, import/project into realm, arm session |
|
||||
| `transfer_ack{transfer_id, character_id}` | `done` | acknowledge successful transfer and arming |
|
||||
| `allocate_relay{player_id}` | `relay_allocated{relay_host, auth_port, world_port, token, expires_at}` | dynamic game relay allocation for NAT traversal (Phase 13) |
|
||||
| `route` | `route_info{address}` | the address the game client can use right now, if the owner stated one |
|
||||
|
||||
Errors (`error{code,message}`): `unsupported_version`, `invalid`, `provisioning_off`, `no_account`, `not_at_character_select`, `not_yours`, `already_claimed`, `not_eligible`, `wrong_credentials`, `account_taken`, `rate_limited`, `unavailable`, `incompatible`, `relay_unavailable`.
|
||||
@@ -144,15 +148,23 @@ its profile, and only then sends `claim_ack`; the Host marks the binding `Portab
|
||||
When a player selects a portable character that exists in their Player Manager but does not yet exist on the remote realm, the Player Manager performs an automated, transactional transfer over the encrypted Noise channel:
|
||||
|
||||
1. **Preflight Compatibility Check**: The Player Manager checks the canonical character against the target realm's advertised capabilities (Phase 7/8). If incompatible, the transfer is refused immediately before any network mutation.
|
||||
2. **Payload Transfer**: Inside the end-to-end encrypted Noise channel, the Player Manager sends `transfer_character{character_id, revision, sha256, payload, collections}`. The Coordinator sees only opaque ciphertext.
|
||||
3. **Host Validation**: The Host validates:
|
||||
- PlayerIdentity proof of ownership;
|
||||
- `PortableCharacter` format version, schema integrity, and SHA-256 payload hash;
|
||||
- Target realm account mapping for this player;
|
||||
- Level projection policy (e.g. projecting level 80 down to realm cap 60 via Phase-8 oracle);
|
||||
- Idempotency: if this exact canonical revision was already imported, the existing working copy GUID and armed session ID are returned immediately without duplicating the character;
|
||||
- Revision update: if a newer canonical revision is transferred, the Host updates the existing working copy rather than creating a duplicate character.
|
||||
4. **Outcome & Session Arming**: On successful import/projection, the Host returns `transferred{character_id, local_guid, session_id}`. The session engine arms the session for game login.
|
||||
2. **Transfer Offer (`TransferOffer`)**: Inside the end-to-end encrypted Noise channel, the Player Manager sends `transfer_offer{transfer_id, character_id, canonical_revision, content_hash, total_size, collections}`.
|
||||
- The Host validates the PlayerIdentity, character ownership, format revision, hash format, size limit (≤ 4 MiB, `MAX_TRANSFER_REQUEST_BYTES`), and collection counts (≤ 100,000 items).
|
||||
- If the exact revision was already committed or an active transfer session exists, the Host responds with `transfer_ready{transfer_id, accepted_offset}`.
|
||||
3. **Payload Transfer & Chunking (`TransferChunk` / `TransferStatus`)**:
|
||||
- The Player sends the payload in bounded chunks via `transfer_chunk{transfer_id, offset, data}`.
|
||||
- The Host records incoming chunks, tracks received bytes, and replies with `transfer_progress{transfer_id, received_bytes, total_bytes}`.
|
||||
- If a connection is interrupted, the Player can query progress via `transfer_status{transfer_id}` and resume from `accepted_offset`.
|
||||
4. **Validation & Atomic Commit (`TransferCommit`)**:
|
||||
- Once all chunks arrive, the Player sends `transfer_commit{transfer_id}`.
|
||||
- The Host verifies the SHA-256 content hash of the assembled payload against the offer.
|
||||
- The Host runs the level projection engine (e.g. projecting level 80 down to realm cap 60 via Phase-8 oracle).
|
||||
- The Host imports the character into the realm's characters database, updates/creates the working copy mapping, and arms the session.
|
||||
- The Host returns `transferred{character_id, local_guid, session_id}`.
|
||||
5. **Acknowledgement (`TransferAck`)**:
|
||||
- The Player Manager receives `transferred`, verifies local session arming, and returns `transfer_ack{transfer_id, character_id}`.
|
||||
- The Host finalizes the transfer transaction and responds with `done`.
|
||||
- The session engine is now fully armed for game login.
|
||||
|
||||
## 7.2 Game Relay Allocation (Phase 13)
|
||||
|
||||
|
||||
@@ -84,14 +84,14 @@ ssh ubuntu@<VPS_IP>
|
||||
|
||||
## Firewall policy
|
||||
|
||||
UFW, default deny incoming. Currently allowed: OpenSSH, 80/tcp, 443/tcp.
|
||||
UFW, default deny incoming. Currently allowed: OpenSSH (22/tcp), 80/tcp, 443/tcp, and the Game Relay port pool 40000-43999/tcp.
|
||||
|
||||
```bash
|
||||
sudo ufw status verbose
|
||||
```
|
||||
|
||||
Docker-published ports bypass UFW (see VPS_BASELINE.md). Check `sudo docker ps` and `sudo ss -tulnH` after every
|
||||
deployment; the only public listeners must be 22, 80, 443.
|
||||
deployment; the only public listeners must be 22, 80, 443, and Game Relay listeners in 40000–43999/tcp.
|
||||
|
||||
fail2ban protects SSH (default `sshd` jail):
|
||||
|
||||
|
||||
@@ -29,12 +29,10 @@ Date configured: 2026-10-06 (UTC)
|
||||
- Base packages: curl, wget, git, ca-certificates, gnupg, jq, unzip, htop, ufw, fail2ban, unattended-upgrades.
|
||||
- Hostname `coa-infra-01`, timezone UTC.
|
||||
- SSH: key-only, no root login (details below).
|
||||
- UFW: default deny incoming, allow outgoing; only 22/tcp, 80/tcp, 443/tcp allowed.
|
||||
- UFW: default deny incoming, allow outgoing; 22/tcp, 80/tcp, 443/tcp, and the Game Relay dynamic pool 40000-43999/tcp allowed.
|
||||
- fail2ban: default `sshd` jail only.
|
||||
- unattended-upgrades: enabled for the `-security` pockets (Ubuntu, ESM apps, ESM infra); no automatic reboot,
|
||||
no release upgrades.
|
||||
- Docker Engine from the official Docker apt repository (docker-ce, docker-ce-cli, containerd.io,
|
||||
docker-buildx-plugin, docker-compose-plugin).
|
||||
- unattended-upgrades: enabled for the `-security` pockets (Ubuntu, ESM apps, ESM infra); no automatic reboot, no release upgrades.
|
||||
- Docker Engine from the official Docker apt repository (docker-ce, docker-ce-cli, containerd.io, docker-buildx-plugin, docker-compose-plugin).
|
||||
- `/opt/coa` layout, Caddy running as the future ingress, PostgreSQL definition prepared but not started.
|
||||
|
||||
## Public ports (verified from an external machine)
|
||||
@@ -73,14 +71,15 @@ Everything else is closed, including 5432 (Postgres), 8080 (Registry internal),
|
||||
|
||||
```
|
||||
Default: deny (incoming), allow (outgoing)
|
||||
22/tcp OpenSSH ALLOW IN (v4+v6)
|
||||
80/tcp ALLOW IN (v4+v6)
|
||||
443/tcp ALLOW IN (v4+v6)
|
||||
22/tcp OpenSSH ALLOW IN (v4+v6)
|
||||
80/tcp ALLOW IN (v4+v6)
|
||||
443/tcp ALLOW IN (v4+v6)
|
||||
40000:43999/tcp ALLOW IN (v4+v6)
|
||||
```
|
||||
|
||||
**Docker caveat:** ports published by Docker (`ports:` in a compose file) are inserted into iptables ahead of UFW and
|
||||
are reachable from the internet even if UFW has no rule for them. Rule for this host: never publish a port other than
|
||||
Caddy's 80/443; services talk to each other over Docker networks, and anything that must be reachable only locally is
|
||||
Caddy's 80/443 and the dedicated Game Relay pool `40000:43999/tcp`; services talk to each other over Docker networks, and anything that must be reachable only locally is
|
||||
published as `127.0.0.1:<port>:<port>`.
|
||||
|
||||
## Docker
|
||||
|
||||
Generated
+1
-1
@@ -356,7 +356,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "coa-core"
|
||||
version = "0.6.6"
|
||||
version = "0.6.7"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
|
||||
@@ -108,8 +108,8 @@ fn routes_both_ways_by_connection_number_and_never_interprets() {
|
||||
let mut h = host(&s, realm, &hk);
|
||||
let mut p1 = player(&s, realm, &key(10));
|
||||
let mut p2 = player(&s, realm, &key(11));
|
||||
let Frame::Open { conn: c1 } = text(&mut h) else { panic!("open 1") };
|
||||
let Frame::Open { conn: c2 } = text(&mut h) else { panic!("open 2") };
|
||||
let Frame::Open { conn: c1, .. } = text(&mut h) else { panic!("open 1") };
|
||||
let Frame::Open { conn: c2, .. } = text(&mut h) else { panic!("open 2") };
|
||||
assert_ne!(c1, c2);
|
||||
assert!(matches!(text(&mut p1), Frame::PlayerReady { conn, .. } if conn == c1));
|
||||
assert!(matches!(text(&mut p2), Frame::PlayerReady { conn, .. } if conn == c2));
|
||||
|
||||
@@ -179,12 +179,12 @@ async fn test_relay_host_auth_and_allocation_and_game_traffic() {
|
||||
resp
|
||||
});
|
||||
|
||||
// Host receives Connect for WORLD (skipping any Close from stream 1)
|
||||
// Host receives Connect for WORLD (skipping any Close/Reset from stream 1)
|
||||
let (world_sid, world_target) = loop {
|
||||
let msg = ws_src.next().await.unwrap().unwrap();
|
||||
match serde_json::from_str::<TunnelMsg>(msg.to_text().unwrap()).unwrap() {
|
||||
TunnelMsg::Connect { stream_id, target, .. } => break (stream_id, target),
|
||||
TunnelMsg::Close { .. } => continue,
|
||||
TunnelMsg::Close { .. } | TunnelMsg::Reset { .. } => continue,
|
||||
other => panic!("unexpected msg awaiting Connect: {other:?}"),
|
||||
}
|
||||
};
|
||||
@@ -193,12 +193,12 @@ async fn test_relay_host_auth_and_allocation_and_game_traffic() {
|
||||
// Host acknowledges
|
||||
ws_sink.send(Message::Text(serde_json::to_string(&TunnelMsg::ConnectOk { stream_id: world_sid }).unwrap().into())).await.unwrap();
|
||||
|
||||
// Host receives world data (skipping any Close from stream 1)
|
||||
// Host receives world data (skipping any Close/Reset from stream 1)
|
||||
let world_chunk = loop {
|
||||
let msg = ws_src.next().await.unwrap().unwrap();
|
||||
match serde_json::from_str::<TunnelMsg>(msg.to_text().unwrap()).unwrap() {
|
||||
TunnelMsg::Data { stream_id, chunk } if stream_id == world_sid => break chunk,
|
||||
TunnelMsg::Close { .. } => continue,
|
||||
TunnelMsg::Close { .. } | TunnelMsg::Reset { .. } => continue,
|
||||
other => panic!("unexpected msg awaiting Data: {other:?}"),
|
||||
}
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user