# 技術ガイド

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

このドキュメントでは、現在のコードの主要な仕組みを説明します。メッセージのフィールド、フレーム形式、ハンドシェイクの順序の完全な一覧は[通信プロトコル](/docs/ja/websocket-protocol.md)を参照してください。

## オペレーターとデバイスの両方から exchange に接続

家庭や企業の拠点のデバイスは通常 NAT の内側にあり、exchange から直接接続できません。そのため、両者が exchange に WebSocket 接続を開始します。

- デバイスは `wss://<主机>/ws/device`（平文デバッグ時は `ws://`）に接続し、最初のフレームにデバイストークンを含めて登録します。
- オペレーターは `wss://<主机>/ws/operator` に接続し、ハンドシェイクに運用トークンを使用します。

exchange 自体は認証、ルーティング、操作記録のみを行います。待受アドレスは `--listen` または `REMOTE_LISTEN` で決まり、TLS は前段のリバースプロキシで終端します。

## 2 系統のセッション番号

オペレーターとデバイスはそれぞれ独自のセッション番号を管理し、双方の番号は互いに干渉しません。オペレーターはリクエスト送信時に自分の番号を指定でき、サーバーからの割り当てを待たずに後続フレームを送れます。デバイス側番号は、そのデバイスの現在の接続ごとに 1 から順に割り当てます。2 人のオペレーターがどちらもローカル番号 1 を使っても、デバイス上では別の番号になります。

番号 0 はどのセッションにも割り当てず、接続レベルのメッセージ用に予約します。オペレーター側番号が 0 の場合や、その接続ですでに同じ番号を使っている場合は登録に失敗し、ルーティングテーブルには書き込みません。

変換テーブルは `crates/exchange/src/hub/session_table.rs` にあります。`hub.rs` が変換結果を各接続に関連付けます。デバイス側は「デバイス番号 → オペレーター番号」、オペレーター側は「オペレーター番号 →（デバイス接続、デバイス番号）」です。

## 転送時に書き換えるのはセッション番号だけ

PTY のバイト列とコマンドの stdout/stderr はバイナリフレームで送ります。ヘッダーは 9 バイトで、ストリーム種別 1 バイト、`session_id` 4 バイト、ペイロード長 4 バイト、その後に生のペイロードが続きます。定義は `crates/proto/src/frame.rs` にあります。

exchange はこのフレームを転送する際、`session_id` のみを置き換えます。

- オペレーターからデバイス: `crates/exchange/src/ws/operator.rs` が `DataFrame::with_session_id` でデバイス側番号に置き換え、エンコードしたバイト列をデバイスの送信キューに入れます。
- デバイスからオペレーター: `crates/exchange/src/ws/device.rs` の `handle_data` がデバイス側番号をオペレーター自身の番号に戻します。ソースのコメントが指す書き換えはこの箇所です。

ペイロードはそのまま通過します。ルーティングは PTY のキー入力やコマンド出力の内容を判断材料にしません。セッションが見つからなければフレームを破棄します。制御メッセージは JSON テキストフレームで、セッション開始時と終了時にも相手側の番号体系に変換しますが、業務フィールドは維持します。

## デバイスの再接続は古い接続を置き換える

同じデバイスが再接続すると、新しい接続をオンラインテーブルに登録して古い接続を置き換え、古い接続は呼び出し元が閉じます。「重複接続」への対処として新しい接続を拒否してはいけません。半開きの古い接続がテーブルに残っている可能性があり、拒否するとデバイスは古い接続のタイムアウトを待ち続けることになるためです。

各デバイス接続には `Hub::register_device` で生成する `epoch`（UUID）があります。切断時のクリーンアップは `Hub::unregister_device` で行い、テーブルの現在の epoch とクリーンアップ対象の epoch が一致する場合だけ、デバイスを削除してオフラインにします。古い接続のクリーンアップが新しい接続の登録より遅れても、epoch が一致しなければ空リストを返し、新しい接続はテーブルに残ります。

## 送信キューには上限があり、満杯なら接続を切断

各 WebSocket には容量 512 のキューがあり、`crates/exchange/src/hub/outbound.rs` の `channel` が実装します。ルーティング処理は接続テーブルのロックを保持するため、配信には `Sender::try_send` だけを使い、ロック中に相手を待ちません。

キューが満杯になると `try_send` は `SendError::Lagging` を返し、終了シグナルを設定します。`Close` 制御メッセージはキューに入れず、同じシグナルだけを設定するため、キューが詰まっていても接続を終了できます。デバイス側とオペレーター側の接続ループはどちらも `tokio::select!` 内で `sender.closed()` を待ちます（`crates/exchange/src/ws/device.rs`、`crates/exchange/src/ws/operator.rs`）。シグナルが届くと転送ループを終了し、epoch に従ってデバイスを登録解除するか、オペレーターのセッションをクリーンアップします。相手が受信側を破棄済みの場合は `SendError::Closed` を返し、「消費が間に合わない」状態と区別します。

## トークンはプレフィックスと秘密値で構成し、秘密値は argon2 ハッシュのみ保存

平文の形式は `<prefix>.<secret>` です。`crates/exchange/src/auth.rs` の `split_token` は最初の `.` で分割し、プレフィックスまたは秘密値が空なら形式エラーとします。

発行時のプレフィックスはランダムな 8 バイトの 16 進表現、秘密値はランダムな 32 バイトの base64url 表現です。データベースは次の 2 列だけを保存します。

- `token_prefix`: 平文のプレフィックス。一意インデックスがあり、検証時の検索に使用します。
- `token_hash`: `hash_secret` が生成する秘密値の argon2id PHC 文字列。

デバイスと運用アカウントは同じ形式で保存します。検証時はプレフィックスでハッシュを取得し、`verify_secret` で秘密値を照合します。平文は発行時にのみ呼び出し元へ返し、データベースからは復元できません。

argon2 のパラメーターはメモリ 8 MiB、反復 2 回、並列度 1 で、ライブラリの既定値より低く設定しています。秘密値は 256 ビットのランダム値なので、低速ハッシュで総当たり攻撃への防御を追加しても効果は乏しく、デバイスが同時に再接続する際のハンドシェイク遅延を増やします。この設定はこの種のランダムトークン専用です。

## 関連ドキュメント

- [開発ガイド](/docs/ja/development.md)
- [使用方法](/docs/ja/getting-started.md)
- [通信プロトコル](/docs/ja/websocket-protocol.md)
- デプロイガイドはリポジトリの `docs/deployment.md`（サーバーアドレスを含むため、本サイトでは非公開）
