# Technical overview

> HTML: https://remote.ivjn.us/docs/en/technical/

This document describes core mechanisms in the current code. Complete message fields, frame formats, and handshake sequences are in [Wire protocol](/docs/en/websocket-protocol.md).

## Operators and devices both connect outbound to exchange

Home and branch-office devices usually sit behind NAT, so exchange cannot dial into them. Both sides initiate WebSocket connections to exchange:

- Devices connect to `wss://<主机>/ws/device` (`ws://` for plaintext debugging), registering with a device token in the first frame.
- Operators connect to `wss://<主机>/ws/operator`, using an operator token for the handshake.

exchange handles authentication, routing, and audit trails. `--listen` or `REMOTE_LISTEN` determines its listen address; the reverse proxy in front terminates TLS.

## Two session ID spaces

Operators and devices maintain independent session IDs. Operators can include their own IDs in requests without first waiting for server allocation before sending subsequent frames. Device-side IDs are allocated independently for each current device connection, incrementing from 1. Even when two operators both use local ID 1, they receive different IDs on the device.

ID 0 is reserved for connection-level messages. Registration fails without inserting a route if the operator-side ID is 0 or already used on that operator connection.

The translation table is in `crates/exchange/src/hub/session_table.rs`. `hub.rs` attaches translations to individual connections: device ID → operator ID on the device side, and operator ID → (device connection, device ID) on the operator side.

## Forwarding rewrites only the session ID

PTY bytes and command stdout/stderr use binary frames. The nine-byte header contains a one-byte stream type, four-byte `session_id`, and four-byte payload length, followed by raw payload. Its definition is in `crates/proto/src/frame.rs`.

exchange replaces only `session_id` when forwarding:

- Operator to device: `crates/exchange/src/ws/operator.rs` uses `DataFrame::with_session_id` to substitute the device-side ID, then places encoded bytes in the device send queue.
- Device to operator: `handle_data` in `crates/exchange/src/ws/device.rs` replaces the device-side ID with the operator's original ID. The source comment describes this rewrite.

Payloads pass through unchanged. Routing does not inspect PTY keystrokes or command output; frames without a matching session are discarded. Control messages are JSON text frames. Opening and closing sessions likewise translate IDs into the recipient's ID space while preserving business fields.

## Device reconnection replaces the old connection

When a device reconnects, its new connection enters the online table and replaces the old one, which the caller closes. Rejecting the new connection as a duplicate is unsuitable: a half-open old connection may remain registered, forcing the device to wait for its timeout.

Each device connection has an `epoch` (UUID), generated in `Hub::register_device`. Disconnect cleanup uses `Hub::unregister_device`, removing the device and marking it offline only when the registered epoch matches the cleanup epoch. If old-connection cleanup arrives after new-connection registration, the epochs differ, the function returns an empty list, and the new connection remains registered.

## Send queues are bounded; a full queue closes its connection

Each WebSocket has a queue of capacity 512, implemented by `channel` in `crates/exchange/src/hub/outbound.rs`. Routing holds the connection-table lock, so delivery uses only `Sender::try_send`, without waiting for the peer under the lock.

A full queue makes `try_send` return `SendError::Lagging` and set the close signal. A `Close` control message bypasses the queue and sets the same signal, so even a blocked queue can request shutdown. Both device and operator receive loops await `sender.closed()` inside `tokio::select!` (`crates/exchange/src/ws/device.rs`, `crates/exchange/src/ws/operator.rs`). Once signaled, forwarding ends, then unregisters the device by epoch or cleans up the operator's sessions. A peer that already dropped the receiver produces `SendError::Closed`, distinguished from a consumer that cannot keep up.

## Tokens contain a prefix and secret; only an argon2 hash of the secret is stored

Plaintext tokens have the form `<prefix>.<secret>`. `split_token` in `crates/exchange/src/auth.rs` splits at the first `.`, rejecting an empty prefix or secret as malformed.

At issuance, the prefix is eight random bytes encoded as hexadecimal, and the secret is 32 random bytes encoded as base64url. The database stores only two columns:

- `token_prefix`: plaintext with a unique index, used for one lookup during verification.
- `token_hash`: the secret's argon2id PHC string, generated by `hash_secret`.

Device and operator accounts use this format. Verification retrieves the hash by prefix, then compares the secret with `verify_secret`. Plaintext is returned only at issuance and cannot be recovered from the database.

argon2 uses 8 MiB of memory, two iterations, and parallelism 1, below library defaults. Secrets are 256-bit random values; additional slow hashing adds no practical brute-force protection but increases handshake latency when devices reconnect together. These parameters apply only to this kind of random token.

## Related documents

- [Development](/docs/en/development.md)
- [Usage](/docs/en/getting-started.md)
- [Wire protocol](/docs/en/websocket-protocol.md)
- Deployment guide: repository `docs/deployment.md` (contains server addresses; not published on this site)
