Technische Beschreibung
Dieses Dokument beschreibt zentrale Mechanismen des aktuellen Codes. Vollständige Nachrichtenfelder, Frameformate und Handshake-Abläufe stehen im Wire-Protokoll.
Operator und Gerät verbinden sich ausgehend mit exchange
Geräte in Heimnetzen und Unternehmensfilialen liegen meist hinter NAT und sind für exchange nicht direkt erreichbar. Deshalb bauen beide Seiten WebSocket-Verbindungen zu exchange auf:
- Das Gerät verbindet sich mit
wss://<主机>/ws/device(bei unverschlüsseltem Debuggingws://) und registriert sich im ersten Frame mit dem Gerätetoken. - Der Operator verbindet sich mit
wss://<主机>/ws/operator; der Handshake verwendet das Operatortoken.
exchange übernimmt Authentifizierung, Routing und Auditprotokollierung. --listen oder REMOTE_LISTEN bestimmt die Listenadresse; TLS terminiert am vorgeschalteten Reverse Proxy.
Zwei Sitzungsnummernräume
Operator und Gerät verwalten eigene Sitzungsnummern, ohne den Nummernraum der anderen Seite zu belegen. Ein Operator kann seine Nummer bereits in der Anfrage senden und muss nicht auf eine Serverzuweisung warten. Geräteseitige Nummern werden für die aktuelle Geräteverbindung separat ab 1 aufsteigend vergeben. Verwenden zwei Operatoren lokal beide Nummer 1, entstehen auf dem Gerät unterschiedliche Nummern.
Nummer 0 bleibt verbindungsweiten Nachrichten vorbehalten. Eine Registrierung mit Operatornummer 0 oder einer auf dieser Operatorverbindung bereits belegten Nummer schlägt direkt fehl und verändert die Routingtabelle nicht.
Die Übersetzungstabelle liegt in crates/exchange/src/hub/session_table.rs. hub.rs ordnet die Ergebnisse konkreten Verbindungen zu: geräteseitig „Gerätenummer → Operatornummer“, operatorseitig „Operatornummer → (Geräteverbindung, Gerätenummer)“.
Weiterleitung ändert nur die Sitzungsnummer
PTY-Bytes und stdout/stderr von Befehlen verwenden Binärframes. Der Header umfasst 9 Bytes: 1 Byte Streamtyp, 4 Bytes session_id und 4 Bytes Nutzdatenlänge, gefolgt von rohen Nutzdaten. Das Format ist in crates/proto/src/frame.rs definiert.
exchange ersetzt beim Weiterleiten ausschließlich session_id:
- Operator → Gerät:
crates/exchange/src/ws/operator.rsersetzt die Nummer mitDataFrame::with_session_iddurch die Gerätenummer und legt die kodierten Bytes in die Geräte-Sendequeue. - Gerät → Operator:
handle_dataincrates/exchange/src/ws/device.rsersetzt die Gerätenummer durch die eigene Operatornummer. Genau diese Änderung beschreibt der Quelltextkommentar.
Die Nutzdaten bleiben unverändert. Routingentscheidungen hängen nicht vom Inhalt der PTY-Eingaben oder Befehlsausgaben ab. Ohne passende Sitzung wird der Frame verworfen. Steuernachrichten sind JSON-Textframes; auch beim Öffnen und Beenden wird die Nummer auf den Nummernraum der Gegenseite abgebildet, während fachliche Felder unverändert bleiben.
Wiederverbindung ersetzt die alte Geräteverbindung
Verbindet sich dasselbe Gerät erneut, wird die neue Verbindung in die Onlinetabelle eingetragen und ersetzt die alte, deren Schließung dem Aufrufer überlassen wird. Das Ablehnen vermeintlich doppelter Verbindungen wäre falsch: Eine halboffene alte Verbindung könnte noch in der Tabelle hängen und das Gerät bis zu deren Timeout blockieren.
Jede Geräteverbindung erhält in Hub::register_device eine epoch (UUID). Beim Aufräumen über Hub::unregister_device wird das Gerät nur entfernt und offline gesetzt, wenn die gespeicherte epoch mit der epoch dieses Aufräumvorgangs übereinstimmt. Wird eine alte Verbindung erst nach Registrierung der neuen bereinigt, stimmen die Werte nicht überein: Die Funktion liefert eine leere Liste und die neue Verbindung bleibt eingetragen.
Begrenzte Sendequeues schließen überlastete Verbindungen
Jeder WebSocket besitzt eine Queue mit Kapazität 512, erzeugt durch channel in crates/exchange/src/hub/outbound.rs. Da der Routingpfad die Verbindungstabelle gesperrt hält, verwendet die Zustellung ausschließlich Sender::try_send und wartet innerhalb der Sperre nicht auf die Gegenseite.
Bei voller Queue liefert try_send SendError::Lagging und setzt das Schließsignal. Auch die Steuernachricht Close setzt nur dieses Signal, ohne die Queue zu benutzen. So lässt sich eine Verbindung selbst bei blockierter Queue beenden. Geräte- und Operator-Eingangsschleifen warten in tokio::select! auf sender.closed() (crates/exchange/src/ws/device.rs, crates/exchange/src/ws/operator.rs). Beim Signal endet die Weiterleitung; danach wird das Gerät anhand seiner epoch abgemeldet beziehungsweise werden die Operatorsitzungen bereinigt. Hat die Gegenseite den Empfänger bereits verworfen, wird stattdessen SendError::Closed geliefert, um diesen Fall von zu langsamer Verarbeitung zu unterscheiden.
Tokens bestehen aus Präfix und Geheimnis; gespeichert wird nur argon2
Das Klartextformat lautet <prefix>.<secret>. split_token in crates/exchange/src/auth.rs trennt am ersten .. Ein leeres Präfix oder Geheimnis ist ein Formatfehler.
Bei Ausstellung besteht das Präfix aus 8 zufälligen Bytes in Hexdarstellung, das Geheimnis aus 32 zufälligen Bytes in base64url. Die Datenbank speichert nur zwei Spalten:
token_prefix: Klartext mit eindeutigem Index für genau eine Suche bei der Prüfungtoken_hash: durchhash_secreterzeugter argon2id-PHC-String des Geheimnisses
Geräte und Operatorkonten verwenden dasselbe Format. Zur Prüfung wird der Hash über das Präfix geladen und das Geheimnis mit verify_secret verglichen. Der vollständige Klartext wird nur bei Ausstellung zurückgegeben und lässt sich aus der Datenbank nicht rekonstruieren.
Die argon2-Parameter betragen 8 MiB Speicher, 2 Iterationen und Parallelität 1 und liegen unter den Bibliotheksstandards. Das Geheimnis ist ein zufälliger 256-Bit-Wert; langsames Hashing bringt hier keinen zusätzlichen Schutz gegen Durchprobieren, würde aber gleichzeitige Geräte-Wiederverbindungen durch Handshake-Latenz belasten. Diese Parameter eignen sich ausschließlich für solche Zufallstokens.
Weitere Dokumentation
- Entwicklung
- Bedienung
- Wire-Protokoll
- Bereitstellungsanleitung im Repository unter
docs/deployment.md(mit Serveradressen, auf dieser Website nicht öffentlich)