remote.DokumentationStartseite

Drahtprotokoll

Dieses Dokument ist die vollständige Spezifikation des Nachrichtenformats zwischen agent, exchange und cli. Die Definitionen liegen in crates/proto; alle drei Seiten teilen denselben Code. Jedes Feld wird nur einmal definiert. Abweichungen werden beim Kompilieren statt erst in einer Produktionssitzung sichtbar.

Zielgruppe sind Betriebspersonal, das Paketmitschnitte untersucht, und Entwickler, die das Protokoll ändern oder Drittanbieterclients implementieren.


Inhaltsverzeichnis


Überblick

Das Protokoll besteht aus zwei Schichten, die dieselbe WebSocket-Verbindung nutzen:

Schicht Transport Kodierung Inhalt
Steuernachrichten WebSocket-Textframes JSON Registrierung, Heartbeats, Öffnen/Schließen von Sitzungen, Ergebnismeldungen
Datenframes WebSocket-Binärframes Fester 9-Byte-Header + Rohbytes PTY-Bytestrom, stdout/stderr von Befehlen

Streamingdaten als base64 in JSON einzubetten vergrößert sie um etwa 33 %. PTY erzeugt häufig kleine Pakete für einzelne Tastenechos; dieser Aufwand erhöht unmittelbar die Interaktionslatenz. Steuernachrichten sind selten, sodass die Lesbarkeit von JSON mehr Nutzen bringt: Mitschnitte sind direkt verständlich.

Ein binärer WebSocket-Frame enthält vereinbarungsgemäß genau einen Datenframe. Zusätzliche oder fehlende Bytes gelten als Fehler.


Transport und Endpunkte

Endpunkt Verbindungsinitiator Authentifizierung
GET /ws/device agent Der erste Frame nach Verbindungsaufbau muss eine register-Steuernachricht mit device_id und token sein
GET /ws/operator remote-cli HTTP-Anfrageheader Authorization: Bearer <运维令牌> beim WebSocket-Handshake

Die beiden Pfade verwenden unterschiedliche, nicht austauschbare Zugangsdaten. Ein Gerätetoken funktioniert am Operator-Endpunkt nicht und umgekehrt.

Operator-Zugangsdaten stehen im Anfrageheader statt in der URL-Abfrage, weil URLs in Reverse-Proxy-Protokollen, im Browserverlauf und im Referer auftauchen. Gerätetoken werden im ersten Frame übermittelt, weil einige eingebettete Clients nur schwer eigene WebSocket-Handshake-Header setzen können. Außerdem lassen sich dadurch eine inkompatible Protokollversion und ein ungültiges Token mit derselben Antwortnachricht ausdrücken.

In Produktionsumgebungen ist wss:// erforderlich: Beide Arten von Zugangsdaten erscheinen während des Handshakes im Klartext.


Protokollversion

pub const PROTOCOL_VERSION: u16 = 1;

Beim Handshake werden die Versionen auf beiden Seiten verglichen. Abweichungen führen sofort zur Ablehnung mit einer eindeutigen Meldung. So wird verhindert, dass eine Versionsabweichung erst später als unerklärlicher Parserfehler einer Nachricht auftritt, der schwer zu diagnostizieren wäre.

  • Geräteseite: agent übermittelt seine Version in register. Bei einer Abweichung antwortet der Server mit register_ack und ok: false; error nennt beide Versionen. Anschließend wird die Verbindung geschlossen.
  • Operator-Seite: Nach Verbindungsaufbau sendet der Server selbstständig welcome. Bei einer Abweichung meldet die CLI einen Fehler und fordert zum Upgrade auf eine zu server_version passende Version auf.

Unbekannte Nachrichtenwerte für type werden immer abgelehnt und niemals stillschweigend als bekannter Typ behandelt.


Steuernachrichten

Alle Steuernachrichten sind JSON-Objekte mit type als Kennzeichnung (internally-tagged Enums von serde). Kennzeichnungen und Feldnamen verwenden snake_case. session_id wird transparent als einfache Zahl serialisiert.

DeviceUp(agent → exchange)

type Felder Zweck
register protocol_version: u16
device_id: string
token: string
info: DeviceInfo
Muss die erste Nachricht nach Verbindungsaufbau sein. Vor ihrem Empfang akzeptiert der Server keine andere Nachricht; das Zeitlimit beträgt 15 Sekunden
heartbeat metrics: Metrics Regelmäßige Übermittlung betrieblicher Messwerte. Die Verbindungsprüfung erfolgt getrennt durch natives WebSocket-ping/pong: Ein fehlender Heartbeat bedeutet nicht zwangsläufig eine unterbrochene Verbindung und umgekehrt
exec_started session_id
job_id
hold
timeout_secs
Befehl gestartet; job_id dient zur hold-Liste und zum erneuten Anhängen nach Wiederverbindung
exec_exit session_id
code: i32 | null
timed_out: bool
Befehlsausführung beendet. Bei Beendigung durch ein Signal ist code gleich null; timed_out zeigt eine erzwungene Beendigung wegen Zeitüberschreitung an
exec_list_resp session_id
jobs: ExecHoldInfo[]
Liste der hold-Aufträge
ubus_resp session_id
code: i32
data: any
Antwort eines ubus-Aufrufs. code ist der Rückgabecode von ubus; 0 bedeutet Erfolg
android_resp session_id
result: AndroidResult
Android-Bildschirmaktion abgeschlossen; diese Nachricht beendet die Sitzung
pty_closed session_id
code: i32 | null
PTY-Sitzung beendet; code ist der Exitcode der Shell
session_error session_id
message: string
Fehler auf Sitzungsebene. Kann das Gerät eine Anfrage nicht erfüllen, antwortet es damit, statt sie stillschweigend zu verwerfen

Ein zweites register auf einer bereits registrierten Verbindung gilt als Protokollmissbrauch und wird als Warnung protokolliert.

Beispiel:

{"type":"register","protocol_version":1,"device_id":"lab-router","token":"a1b2….c2Vj…","info":{"hostname":"OpenWrt","model":"ath79/generic","openwrt_version":"OpenWrt 23.05.5","kernel":"5.15.137","arch":"mips","agent_version":"0.1.0"}}
{"type":"exec_exit","session_id":31,"code":0,"timed_out":false}

DeviceDown(exchange → agent)

type Felder Zweck
register_ack ok: bool
error: string | null
heartbeat_interval_secs: u64
Registrierungsergebnis. Bei ok: false nennt error den Grund; danach schließt der Server die Verbindung. Der Server legt das Heartbeat-Intervall fest, damit es bei wachsender Gerätezahl zentral verlängert werden kann. agent begrenzt 0 auf mindestens 1 Sekunde, um eine CPU-auslastende Timerschleife zu vermeiden
device_access managers: DeviceManager[]
updated_at: u64 (Millisekunden)
Zugriffsberechtigte für dieses Gerät. DeviceManager = operator_id, username, email?, role, level (manage/view), source (role_default/grant). Wird nach Registrierung und bei Berechtigungsänderungen nur an agents mit Fähigkeit device_access gesendet; siehe docs/permissions.md im Repository
exec_req session_id
command: string
timeout_secs: u64
hold: bool (Standard true)
hold_timeout_secs?: u64
Führt einen Befehl aus. Mit hold=true (Standard) darf der Prozess nach Trennung des Operators weiterlaufen. Zeitlimit aus hold_timeout_secs (fehlend/0 → 3600, harte Obergrenze 86400). hold=false bricht den Befehl beim Trennen des Operators ab
exec_cancel session_id Beendet eine Befehlsausführung vorzeitig (kill)
exec_detach session_id Stoppt Ausgabe an aktuellen Operator, erhält den hold-Prozess
exec_list_req session_id Fragt aktuelle hold-Aufträge ab
exec_attach_req session_id
job_id
Hängt hold-Auftrag an diese Sitzung an und spielt gepufferte Ausgabe nach
exec_hold_kill session_id
job_id
Beendet hold-Auftrag anhand der job_id
exec_hold_tail session_id
job_id
Verfolgt nur neue Ausgabe, ohne Pufferwiedergabe
exec_hold_cat session_id
job_id
Gibt aktuellen Puffer aus und endet mit exec_hold_cat_done
ubus_req session_id
object: string
method: string
params: any
Startet ubus-Aufruf. Bei params gleich null wird kein drittes Positionsargument an ubus übergeben
pty_open session_id
rows: u16
cols: u16
Öffnet PTY-Sitzung. Bei Zeilen-/Spaltenwert 0 wird 24×80 verwendet
pty_resize session_id
rows: u16
cols: u16
Ändert Terminalfenstergröße
pty_close session_id Schließt PTY-Sitzung
android_req session_id
request: AndroidAction
Einzelne Android-Bildschirmaktion
android_cancel session_id Bricht eine noch nicht abgeschlossene Android-Anfrage ab

OperatorUp(cli → exchange)

Die Authentifizierung erfolgt beim WebSocket-Handshake; daher gibt es keine Anmeldenachrichten.

type Felder Zweck
open_exec session_id
device_id
command
timeout_secs
hold (Standard true)
hold_timeout_secs?
Befehl auf Zielgerät ausführen. hold ist standardmäßig aktiv; hold=true und Zeitlimit 0 → 3600s. hold=false und Zeitlimit 0 → Exchange-Standard 60s
list_exec_holds session_id
device_id
Fragt exec-Aufträge und laufende Downloads, Uploads und Verzeichnisabfragen ab
list_device_sessions session_id
device_id
Fragt direkt die Exchange-Routingtabelle ab, ohne Gerätesitzungsplatz
kill_device_session session_id
device_id
target_session_id
Bricht anhand der geräteseitigen target_session_id ab und gibt Route lokal frei; einschließlich hold exec, ohne zusätzlichen Sitzungsplatz
attach_exec session_id
device_id
job_id
Hängt hold-Auftrag an und empfängt weitere Ausgabe
exec_hold_kill / exec_hold_tail / exec_hold_cat session_id
device_id
job_id
kill / tail / cat für hold-Aufträge
detach_session session_id Beendet Warten im Vordergrund und trennt Sitzung ab, ohne hold-Prozess zu beenden
open_ubus session_id
device_id: string
object: string
method: string
params: any
Startet ubus-Aufruf
open_pty session_id
device_id: string
rows: u16
cols: u16
Öffnet interaktive PTY-Sitzung
open_android session_id
device_id: string
request: AndroidAction
Startet Android-Bildschirmaktion; für schreibgeschützte Konten verboten
pty_resize session_id
rows: u16
cols: u16
Ändert Terminalfenstergröße. Ohne device_id, da der Server das Zielgerät der bestehenden Sitzung kennt
close_session session_id Beendet Sitzung aktiv: reguläres PTY-Ende oder Abbruch der Befehlsausführung durch Benutzer

Die session_id der vier open_*-Nachrichten wird von der CLI selbst vergeben (siehe Übersetzung zwischen zwei Sitzungsnummernräumen). Folgeframes können deshalb sofort nach der Anfrage gesendet werden, ohne Nummernbestätigung abzuwarten.

OperatorDown(exchange → cli)

type Felder Zweck
welcome protocol_version: u16
server_version: string
Server sendet nach Verbindungsaufbau zur Prüfung übereinstimmender Protokollversionen
device_sessions session_id
sessions: DeviceSessionInfo[]
limit
count
Aktuelle geräteseitige Sitzungsliste von Exchange
device_session_killed session_id
target_session_id
released: bool
Lokal freigegeben; false, falls nicht vorhanden
exec_exit session_id
code: i32 | null
timed_out: bool
Befehl beendet; aus gleichnamiger Gerätenachricht übersetzt
ubus_resp session_id
code: i32
data: any
Antwort eines ubus-Aufrufs
android_resp session_id
result: AndroidResult
Android-Ergebnis mit übersetzter Operator-Sitzungsnummer
pty_closed session_id
code: i32 | null
PTY-Sitzung beendet
session_failed session_id
reason: SessionFailure
message: string
Sitzung fehlgeschlagen. reason unterscheidet Ursachen, damit CLI über Wiederholungen entscheiden kann

Gemeinsame Datenstrukturen

ExecHoldInfo: job_id, kind (exec / download / upload / file), age_secs, session_id?, command_preview, status, timeout_secs, started_at_unix_ms, exit_code?, timed_out. Fehlendes kind wird kompatibel als exec interpretiert, fehlendes age_secs als 0. Listenantworten berechnen das Alter anhand der aktuellen Uhrzeit. Befehls-/Dateipfadkurzfassungen umfassen höchstens etwa 120 Zeichen. Andere Aufträge als exec unterstützen nur kill, kein tail/cat/attach.

DeviceSessionInfo: session_id: u32 (geräteseitige Nummer), operator_session_id: u32, kind: string, hold: bool, age_secs: u64, deadline_unix_ms: u64. Sitzungslisten und erzwungene Abbrüche beantwortet Exchange direkt, ohne neue Gerätesitzung. Schreibgeschützte Konten erhalten forbidden.

DeviceInfo

Statische Geräteinformationen, einmalig bei register gemeldet und in devices.info gespeichert. Außer agent_version sind alle Felder optional. In Nicht-OpenWrt-Umgebungen wie Entwicklungsrechnern und Containern fehlen viele Quellen; agent soll sich trotzdem registrieren statt beim Start zu scheitern. Nicht gesetzte Felder werden bei Serialisierung ausgelassen.

Feld Typ Quelle
hostname string? /proc/sys/kernel/hostname
model string? DISTRIB_TARGET aus /etc/openwrt_release; falls nicht lesbar, model aus ubus call system board
openwrt_version string? DISTRIB_DESCRIPTION oder release.description von ubus
kernel string? /proc/sys/kernel/osrelease
arch string? Konstante zur Kompilierzeit, zuverlässiger als Laufzeiterkennung
agent_version string Eigene agent-Version zur Diagnose von Versionsabweichungen

Metrics

Wird in jedem Heartbeat-Zyklus neu erfasst. Erfasst werden nur direkt aus /proc lesbare Werte, ohne Kindprozesse zu starten. Einzelne Lesefehler dürfen die Gesamterfassung nicht scheitern lassen: Ein fehlender Messwert ist besser als ein vollständig fehlgeschlagener Heartbeat, der das Gerät fälschlich offline erscheinen lässt.

Feld Typ Beschreibung
uptime_secs u64 /proc/uptime
load [f32; 3] Durchschnittslast über 1/5/15 Minuten aus /proc/loadavg
mem_total_kb u64 /proc/meminfo
mem_available_kb u64 MemAvailable statt MemFree: Letzteres berücksichtigt keine freigebbaren Caches, unterschätzt auf Routern verfügbaren Speicher deutlich und erzeugt Fehlalarme
cpu_usage_pct f32? Aus Differenz zweier /proc/stat-Messungen berechnet; beim ersten Heartbeat fehlt der Vergleichswert, daher ausgelassen
interfaces InterfaceStats[] /proc/net/dev. Meldet kumulierte Sende-/Empfangsbytes statt Raten, damit Server Raten für beliebige Zeitfenster berechnen kann. Bei leerem Array ausgelassen

InterfaceStats: { name: string, rx_bytes: u64, tx_bytes: u64 }


Binäre Datenframes

 0        1                    5                    9
 +--------+--------------------+--------------------+
 |  kind  |     session_id     |    payload_len     |  payload (payload_len 字节)
 |   u8   |    u32 (大端)      |    u32 (大端)      |
 +--------+--------------------+--------------------+
 └──────────── 9 字节定长头 ────────────┘
Offset Länge Feld Beschreibung
0 1 kind Streamtyp, siehe folgende Tabelle
1 4 session_id Big-Endian-u32. 0 ist Sentinel für „keiner Sitzung zugehörig“
5 4 payload_len Big-Endian-u32, Nutzlastbytes, höchstens 1 MiB
9 payload_len payload Rohbytes ohne Kodierung oder Escape-Sequenzen

Streamtypen

Wert Name Richtung Beschreibung
1 ExecStdout Gerät → Operator Standardausgabe des entfernten Befehls
2 ExecStderr Gerät → Operator Standardfehlerausgabe. Getrennt von stdout, damit CLI sie unterschiedlich darstellen kann, etwa in Rot
3 PtyData Beidseitig PTY-Bytestrom: aufwärts Tastatureingabe, abwärts Bildschirmausgabe; derselbe Typ in beiden Richtungen

Empfängt agent abwärts ExecStdout / ExecStderr, protokolliert er eine Warnung und verwirft sie: Die Gegenstelle ist dann fehlerhaft implementiert.

Entwurfsentscheidungen

payload_len ist tatsächlich redundant zum WebSocket-Längenfeld. Es bleibt aus zwei Gründen erhalten: Der Decoder erkennt abgeschnittene Frames, etwa angekündigte 11 statt tatsächlicher 5 Bytes, statt sie stillschweigend als kurzes Paket anzunehmen. Bei einem späteren Wechsel zu reinem TCP bleibt zudem das Frameformat unverändert.

Die Nutzlastgrenze von 1 MiB ist nicht für normalen Verkehr gedacht: PTY und Befehlsausgabe werden inkrementell gestreamt und erreichen diese Größenordnung nicht. Sie verhindert, dass eine Gegenstelle mit einem Header, der 2 GiB Nutzlast behauptet, den Serverspeicher erschöpft.

Leere Nutzlasten sind gültig: In PTY-Sitzungen kommen gelegentlich Lesevorgänge mit null Bytes vor. Der Frame besteht dann nur aus dem 9-Byte-Header.

Die Nutzlast enthält beliebige Bytes, einschließlich ungültiger UTF-8-Sequenzen, und muss unverändert bleiben.

Dekodierfehler

Fehler Auslöser
FrameTooShort { len } Gesamtlänge unter 9 Bytes
UnknownStreamKind(v) Erstes Byte ist nicht 1/2/3
PayloadTooLarge { len } Angegebenes payload_len über 1 MiB
LengthMismatch { declared, actual } Angegebene Länge weicht von tatsächlicher Zahl verbleibender Bytes ab

Ein Dekodierfehler betrifft nur diesen Datenframe: exchange protokolliert eine Warnung und verwirft ihn, ohne die Verbindung zu trennen.


Übersetzung zwischen zwei Sitzungsnummernräumen

Dieser Teil des Protokolls wird am leichtesten missverstanden.

In einem Satz

session_id wird vom Initiator vergeben und ist nur innerhalb seiner eigenen Verbindung eindeutig. Operator- und Geräteseite besitzen unabhängige Nummernräume; exchange übersetzt dazwischen. Dieselbe logische Sitzung hat auf beiden Seiten unterschiedliche Nummern.

  remote-cli                    exchange                      remote-agent
                                                        (设备 lab-router)

  ┌────────────┐        ┌────────────────────┐            ┌────────────┐
  │ 运维编号   │        │      路由表        │            │ 设备编号   │
  │  空间      │        │                    │            │  空间      │
  │            │        │  lab-router:       │            │            │
  │  session 7 │───────▶│    31 → (op-A, 7)  │───────────▶│ session 31 │
  │            │◀───────│                    │◀───────────│            │
  └────────────┘        │  op-A:             │            └────────────┘
                        │    7 → (lab-router,│
   由 CLI 自己的        │         31)        │             由 exchange
   SessionIdGenerator   └────────────────────┘             的 DeviceHandle
   分配(1,2,3…)                                          分配(1,2,3…)

Warum dieser Entwurf?

Eine zusätzliche Hin- und Rückübertragung vermeiden. Bei zentraler Nummernvergabe müsste die CLI nach dem Öffnen einer PTY erst eine Bestätigung wie „Ihre Sitzungsnummer ist 31“ abwarten, bevor sie Tastatureingaben senden kann. Durch eigene Vergabe kann sie Datenframes unmittelbar nach open_pty senden.

Nummernkollisionen zwischen Operatoren vermeiden. Zwei Operatoren können gleichzeitig die lokale Nummer 1 für dasselbe Gerät verwenden. Das Gerät muss sie unterscheiden können, sonst gelangt die Ausgabe einer Person zur anderen. exchange vergibt geräteseitige Nummern pro Geräteverbindung; zwei session 1 werden auf 31 und 32 abgebildet.

Gerätenummern von Operatorverhalten unabhängig halten. Die geräteseitigen Nummern werden vollständig von exchange kontrolliert; kein Operator kann sie festlegen oder erraten.

Zwei Tabellen

exchange verwaltet pro Verbindung eine Tabelle. Beide werden stets gemeinsam aktualisiert:

Ort Tabelle Schlüssel → Wert
DeviceHandle.sessions Vorwärtstabelle der Geräteseite Gerätenummer → (Operator-Sendehandle, Operatornummer, Operator-Konto-ID, Sitzungstyp, Audit-Zeilen-ID)
OperatorHandle.sessions Rückwärtsindex der Operator-Seite Operatornummer → (Geräte-ID, Gerätenummer)

Der Rückwärtsindex dient der Bereinigung: Beim Trennen eines Operators müssen seine Sitzungen auf allen Geräten gefunden und einzeln beendet werden. Andernfalls blieben Prozesse und PTYs zurück, deren Ausgabe niemand empfängt.

Wo die Übersetzung erfolgt

Richtung Nachricht Übersetzung
Operator → Gerät (open_*) Steuernachricht Neue Nummer im Gerätenummernraum vergeben, beide Tabellen eintragen und neue Nummer in Nachricht ans Gerät verwenden
Operator → Gerät (pty_resize / close_session) Steuernachricht Gerätenummer im Rückwärtsindex suchen, ersetzen und weiterleiten
Operator → Gerät Datenframe Gerätenummer im Rückwärtsindex suchen, 4 Headerbytes direkt überschreiben; Nutzlast unverändert
Gerät → Operator Steuernachricht Operatornummer und Zielverbindung in Vorwärtstabelle suchen, ersetzen und zustellen
Gerät → Operator Datenframe Operatornummer in Vorwärtstabelle suchen, Header direkt überschreiben; Nutzlast unverändert

Die Übersetzung ist die einzige Änderung an Datenframes: exchange analysiert, puffert oder verändert kein Nutzlastbyte. DataFrame::with_session_id ersetzt die Nummer direkt, ohne die Nutzlast neu zu allozieren.

Vergaberegeln

Beide Zähler beginnen bei 1, zählen aufwärts und überspringen 0. 0 ist Sentinel für „keiner Sitzung zugehörig“, etwa bei verbindungsbezogenen Fehlern.

  • CLI (SessionIdGenerator): Nach Überlauf beginnt der Zähler erneut und überspringt 0. Vier Milliarden Sitzungen auf einer Verbindung sind unrealistisch, aber Umlauf eignet sich für langlebige Prozesse besser als panic.
  • Geräteseite (DeviceHandle::open_session): Trifft der Zähler nach Umlauf auf eine noch aktive Sitzung, sucht er den nächsten freien Platz.

exchange lehnt 0 und Wiederverwendung aktiver Operator-Sitzungsnummern ab. Gültigkeit der Geräteverbindung, Parallelitätsgrenze und Einträge in beiden Indizes werden im selben kritischen Abschnitt geprüft, damit gleichzeitiges Öffnen durch mehrere Operatoren die Grenze nicht überschreitet.

Verhalten bei fehlender Route

Stillschweigend verwerfen, keinen Fehler melden. Nach dem Schließen noch eintreffende Restdaten sind ein normaler Wettlauf. Die Regel gilt in beiden Richtungen:

  • Gerät antwortet für nicht mehr existierende Sitzung → debug-Protokoll; möglicherweise hat sich der Operator vorher getrennt.
  • Operator sendet Daten für verwaiste Sitzung → trace-Protokoll und verwerfen.

Bereinigungspfade für Sitzungen

Auslöser Bereinigung
Gerät beendet Sitzung regulär (exec_exit / ubus_resp / android_resp / pty_closed / session_error) Vorwärtsroute und Rückwärtsindex der zugehörigen Verbindung gemeinsam entfernen, Audit vervollständigen, übersetzte Nachricht an Operator senden
Operator sendet close_session Beide Indizes entfernen, ursprüngliche Geräteverbindung zur Beendigung auffordern, sofort Abschlussbestätigung an Operator senden und Abbruchaudit vervollständigen. Wiederholtes Schließen hat keine Wirkung
Gerät trennt Verbindung Alle Sitzungen dieser Verbindung entnehmen, jeweils session_failed / device_disconnected an zugehörige Operatoren senden, Audit mit {"outcome":"device_disconnected"} ergänzen
Operator trennt Verbindung Über Rückwärtsindex eigene Sitzungen auf allen Geräten finden. Nicht-hold-Sitzungen erhalten Beendigungsbefehle; hold exec nur exec_detach, der Prozess bleibt bestehen. Nur eigene Sitzungen bereinigen; andere Operatoren auf demselben Gerät bleiben unbeeinträchtigt
Neue Geräteverbindung ersetzt alte Alle Sitzungen der alten Verbindung ungültig machen, Operatoren jeweils mit device_disconnected benachrichtigen

Der Rückwärtsindex ist an die konkrete Geräteverbindung gebunden. Er wird nach Wiederverbindung nicht anhand der Geräte-ID auf die neue Verbindung umgeleitet. Mehrere Operator-Verbindungen desselben Kontos besitzen eigene Indizes; Trennen einer Verbindung beendet keine Sitzungen einer anderen.

Abschlussbestätigungen bei aktivem Abbruch: exec erhält exec_exit (code: null, timed_out: false), PTY pty_closed (code: null), ubus session_failed (reason: device_error). Damit bestätigt exchange das Schließen der Sitzung, nicht das Ende des entfernten Prozesses. Spätere Restdaten und Exitmeldungen werden verworfen.

Beim Erreichen der Parallelitätsgrenze bereinigt Exchange unter derselben Sitzungssperre zuerst Routen ohne Operator oder mit überschrittener Frist und prüft dann erneut die Grenze. Für exec gilt das tatsächlich übermittelte Zeitlimit plus 15 Sekunden, für Dateien 3600 Sekunden plus 15 Sekunden, für andere Typen 24 Stunden. hold exec innerhalb der Frist wird beim Verlassen des Operators nicht beendet. Bereinigung oder explizites kill_device_session sendet den passenden Abbruchbefehl ans Gerät und entfernt sofort beide Indizes, ohne Geräteantwort abzuwarten. Erreichbare Operatoren erhalten für exec exec_exit (timed_out:true), sonst session_failed (device_error, session reclaimed). Das Audit wird nach Möglichkeit asynchron vervollständigt.

Gerätewiederverbindung und epoch

Jede Geräteverbindung erhält bei Registrierung eine zufällige epoch (UUID). Verbindet sich dasselbe Gerät erneut, ersetzt die neue Verbindung die alte. Doppelte Verbindungen lassen sich nicht zuverlässig von bereits toten, aber serverseitig unerkannten halb offenen Verbindungen unterscheiden. Bei Ablehnung könnte das Gerät bis zum Timeout der alten Verbindung nicht verbinden.

Die Bereinigung der alten Verbindung kann nach Registrierung der neuen erfolgen. Beim Austragen muss deshalb die epoch geprüft werden: Nur wenn die aktuelle Verbindung in der Tabelle dieselbe epoch besitzt, darf sie entfernt werden. Ohne diese Prüfung würde alte Bereinigung das gerade verbundene Gerät sofort wieder offline setzen.


Handshake-Ablauf

Geräteseite

  remote-agent                                        exchange
      │                                                 │
      │  ①  WebSocket 握手(wss://host/ws/device)      │
      │────────────────────────────────────────────────▶│
      │◀────────────────────────────────────────────────│
      │            101 Switching Protocols              │
      │                                                 │
      │                                          ┌──────┴──────┐
      │                                          │ 启动 15 秒  │
      │                                          │ 注册计时器  │
      │                                          └──────┬──────┘
      │  ②  {"type":"register","protocol_version":1,    │
      │      "device_id":"…","token":"…","info":{…}}    │
      │────────────────────────────────────────────────▶│
      │                                                 │
      │                              ┌──────────────────┴──────────────────┐
      │                              │ a. 比对 protocol_version            │
      │                              │ b. split_token → 前缀查 devices 表  │
      │                              │ c. 校验 enabled                     │
      │                              │ d. argon2 verify(前缀不符也照做,  │
      │                              │    避免用耗时区分前缀是否存在)     │
      │                              └──────────────────┬──────────────────┘
      │                                                 │
      │  ③  {"type":"register_ack","ok":true,           │
      │      "error":null,"heartbeat_interval_secs":30} │
      │◀────────────────────────────────────────────────│
      │                                                 │
      │                                          ┌──────┴──────┐
      │                                          │ 登记连接表  │
      │                                          │ 顶替旧连接  │
      │                                          │ 回写 info 与│
      │                                          │ last_seen_at│
      │                                          └──────┬──────┘
      │                                                 │
      │  ④  退避曲线复位,写状态文件 state=connected     │
      │                                                 │
      │══════════════ 转发循环 ═══════════════════════════│
      │                                                 │
      │  heartbeat(每 30 秒,携带 Metrics)             │
      │────────────────────────────────────────────────▶│
      │  WebSocket Ping(随每次 heartbeat 发出)         │
      │────────────────────────────────────────────────▶│
      │                                     WebSocket Pong
      │◀────────────────────────────────────────────────│
      │                                     WebSocket Ping(每 30 秒)
      │◀────────────────────────────────────────────────│
      │  Pong(tungstenite 排队,agent 显式 flush)       │
      │────────────────────────────────────────────────▶│

Fehlerpfade:

  • Erster Frame ist kein Textframe, kein gültiges JSON oder kein register → Warnung protokollieren und direkt schließen, ohne ack.
  • Kein register innerhalb von 15 Sekunden → Verbindung schließen.
  • Versionsabweichung → register_ack{ok:false, error:"协议版本不匹配:设备为 X,服务端为 Y"} senden und schließen.
  • Ungültiges Token / Gerät nicht vorhanden / Gerät deaktiviert → immer dieselbe Meldung "设备 ID 或令牌无效" senden und schließen. Keine Ursachenunterscheidung, da sonst der Gerätebestand aufgezählt werden könnte.

agent behandelt diese beiden Fehlerklassen getrennt:

agent-Klasse Auslöser Wiederverbindungsstrategie
Rejected ack mit ok: false empfangen Feste Wartezeit 300 Sekunden. Ein falsches Token wird durch Wiederholen nicht richtig; häufige Versuche verschwenden beiderseits Ressourcen und überfluten Serverprotokolle. Die längere Pause lässt Zeit zur Konfigurationskorrektur
Transport Verbindung unmöglich, Handshake-Timeout (45 Sekunden), fehlerhaftes Antwortformat, Verbindungsunterbrechung Exponentielles Backoff: anfangs 1 Sekunde, jeweils verdoppelt, maximal 300 Sekunden, multipliziert mit zufälligem Jitter aus [0.5, 1.5)

Die Backoff-Kurve wird erst nach erfolgreicher Registrierung zurückgesetzt, nicht beim TCP-Verbindungsaufbau. Sonst würden sofort abgelehnte Verbindungen zu dichten Wiederholungsversuchen im Millisekundentakt führen.

Jitter ist notwendig. Beim Serverneustart erkennen Hunderte oder Tausende Geräte gleichzeitig die Trennung. Mit derselben deterministischen Wiederverbindungskurve blieben sie synchron und würden den gerade gestarteten Server wellenweise überlasten. Ein einzelner Neustart könnte so eine dauerhafte Überlastung auslösen.

Operator-Seite

  remote-cli                                          exchange
      │                                                 │
      │  ①  WebSocket 握手(wss://host/ws/operator)    │
      │      Authorization: Bearer <prefix>.<secret>    │
      │────────────────────────────────────────────────▶│
      │                                                 │
      │                              ┌──────────────────┴──────────────────┐
      │                              │ 在 upgrade 之前完成鉴权:           │
      │                              │ split_token → 前缀查 operators 表   │
      │                              │ → argon2 verify → 校验 enabled      │
      │                              └──────────────────┬──────────────────┘
      │                                                 │
      │      鉴权失败 → HTTP 401「凭证无效」(不升级)   │
      │◀────────────────────────────────────────────────│
      │                                                 │
      │◀────────────────────────────────────────────────│
      │            101 Switching Protocols              │
      │                                                 │
      │  ②  {"type":"welcome","protocol_version":1,     │
      │      "server_version":"0.1.0"}                  │
      │◀────────────────────────────────────────────────│
      │                                                 │
      │  ③  比对 protocol_version(15 秒超时)          │
      │      不一致 → 报错退出,提示升级 CLI            │
      │                                                 │
      │══════════════ 可以发起会话 ═══════════════════════│
      │                                                 │
      │  {"type":"open_pty","session_id":1,             │
      │   "device_id":"lab-router","rows":40,"cols":120}│
      │────────────────────────────────────────────────▶│
      │                                                 │──▶ 查设备在线
      │                                                 │──▶ 查并发上限
      │                                                 │──▶ 查 readonly 角色
      │                                                 │──▶ 写审计(先于下发)
      │                                                 │──▶ 分配设备侧编号
      │  二进制帧(PtyData, session 1, 键盘输入)        │
      │────────────────────────────────────────────────▶│──▶ 改写为设备侧编号
      │                                                 │
      │  二进制帧(PtyData, session 1, 屏幕输出)        │
      │◀────────────────────────────────────────────────│◀── 改写为运维侧编号

Die CLI kann die Datenframes des zweiten Schritts ohne jede Bestätigung abzuwarten senden. Genau dafür vergibt der Initiator die Nummern selbst.


Fehlerbehandlung und Fehlerklassen

SessionFailure

Steht im Feld reason von session_failed; JSON-Werte verwenden snake_case.

Wert Bedeutung Erzeuger Wiederholbar
device_offline Gerät existiert nicht oder ist offline exchange findet bei open_* kein Gerät in der Verbindungstabelle ✅
device_disconnected Gerät verliert während der Sitzung die Verbindung Gerät getrennt / durch neue Verbindung ersetzt / Sendewarteschlange beim Zustellen bereits geschlossen ✅
device_error Gerät lehnt ab oder Ausführung scheitert agent sendet session_error (PTY-Erstellung, Befehlsstart oder ubus-Aufruf fehlgeschlagen), oder Gegenstelle verarbeitet weitergeleitete Daten zu langsam ❌
rate_limited Grenze gleichzeitiger Sitzungen überschritten exchange erkennt bei open_*, dass aktive Gerätesitzungen --max-sessions-per-device (Standard 32) erreicht haben ✅
forbidden Operator-Zugangsdaten erlauben keinen Zugriff Schreibgeschütztes Konto (role = readonly) versucht irgendeine Sitzung zu öffnen ❌

is_retryable() schließt forbidden und device_error bewusst aus. Das Wiederholen eines Berechtigungsproblems oder internen Gerätefehlers ist sinnlos und würde die CLI nur beschäftigen.

Die CLI ergänzt die ursprüngliche message um einen Handlungshinweis. Bei device_offline empfiehlt sie etwa remote-cli device list, um den Gerätestatus zu prüfen. Ausnahme ist device_error: Die Gerätenachricht erklärt den Fehler am genauesten und soll nicht durch Standardfloskeln verwässert werden.

Fehlersemantik nach Schicht

Schicht Verhalten Auswirkung
Frame-Dekodierfehler Protokollieren, Frame verwerfen Einzelner Frame
Steuernachricht nicht parsebar Warnung protokollieren, Nachricht ignorieren Einzelne Nachricht; Verbindung bleibt bestehen
Sitzungsfehler session_failed / session_error Einzelne Sitzung; Verbindung bleibt bestehen
Registrierung fehlgeschlagen Nach register_ack{ok:false} schließen Gesamte Verbindung
Versionsabweichung Nach eindeutiger Fehlermeldung schließen Gesamte Verbindung
Warteschlange überlastet Verbindung aktiv schließen Gesamte Verbindung

Grundsatz: Lokalisierbare Fehler werden niemals zu Verbindungsfehlern hochgestuft, denn eine Geräteverbindung kann gleichzeitig Sitzungen mehrerer Operatoren tragen.


Verbindungserhaltung und Zeitlimits

Mechanismus Wert Beschreibung
exchange WebSocket Ping Alle --heartbeat-secs (Standard 30 Sekunden) exchange sendet an Geräte und Operatoren. Clientbibliotheken reihen Pong automatisch ein; agent muss nach Ping den sink ausdrücklich flushen, damit Pong rechtzeitig gesendet wird
agent WebSocket Ping Alle heartbeat_interval_secs (Servervorgabe im ack) Zusätzlich zu jedem JSON-heartbeat sendet agent einen nativen Ping, damit verzögerte exchange-Pings nicht die betriebliche Cloudflare-Grenze von etwa 100 Sekunden Inaktivität erreichen
Verbindungsinaktivitätslimit heartbeat × --missed-pings (Standard 30 × 3 = 90 Sekunden) Ohne eingehende Nachricht innerhalb dieser Zeit gilt Geräte-/Operator-Verbindung als verloren und wird geschlossen. Pong zählt als Aktivität. Bei --missed-pings gleich 0 wird mindestens ein Heartbeat-Zyklus gewartet
exchange-Schreibzeitlimit 10 Sekunden Timeout eines einzelnen WebSocket-Daten- oder Ping-Schreibvorgangs beendet die Verbindung, damit langsame Verbindungen die Bereinigung nicht blockieren
Betrieblicher Heartbeat Alle heartbeat_interval_secs (Servervorgabe im ack) agent sendet JSON-heartbeat mit Metrics und separat WebSocket-Ping. Messwertübermittlung und natives ping/pong sind getrennte Aufgaben; Verbindungszustand darf nicht allein aus betrieblichen Heartbeats abgeleitet werden
Online-/Offline-Ereignisse tick_count der Benachrichtigungsauslöser online / offline (Standard 3, Bereich 1–100); Broadcast-/Audit-Fallback REMOTE_MISSED_THRESHOLD / REMOTE_ONLINE_THRESHOLD Operatoren konfigurieren zwei Auslöser, nicht zwei Zähler eines Auslösers. offline meldet nach tick_count aufeinanderfolgenden Fehlschlägen offline, online nach tick_count aufeinanderfolgenden Erfolgen online. device_ids darf eine Geräte-ID (String), mehrere IDs (Array) oder fehlend / null für alle Workspace-Geräte sein. Bereits gemeldeter Zustand wird nicht erneut gemeldet; zuerst müssen tick_count gegenteilige Ergebnisse den Auslöser erneut scharfstellen. Prozessinterne presence-Broadcasts und Audit lesen diese Auslöser nicht, sondern verwenden REMOTE_MISSED_THRESHOLD / REMOTE_ONLINE_THRESHOLD (ebenfalls Standard 3). Ob der Socket noch in der Verbindungstabelle steht, bestimmt separat --missed-pings
Geräte-Registrierungszeitlimit 15 Sekunden register muss innerhalb dieser Frist nach Verbindungsaufbau gesendet werden
agent-Handshake-Zeitlimit Je Phase 45 Sekunden (HANDSHAKE_TIMEOUT) connect_device_ws (TCP, optional HTTP CONNECT, TLS, WebSocket-Handshake) und Warten auf register_ack nach register erhalten jeweils diese Frist; nicht zusammen 45 Sekunden
agent-Schreib-/Pong-flush-Zeitlimit 30 Sekunden (WRITE_TIMEOUT) Jedes send (Registrierung, JSON-Heartbeat, Ping, Sitzungsausgabe) und jeder Pong-flush nach erfolgreicher Registrierung hat eigene Frist. Schreibfehler oder Timeout beenden Verbindung und lösen Wiederverbindung aus. Während Registrierung gilt für Pong-flush die oben genannte 45-Sekunden-ack-Frist
agent TCP-Keepalive Prüfung nach 30 Sekunden Inaktivität Nach Direktverbindung oder HTTP CONNECT werden TCP_NODELAY und TCP keepalive aktiviert. Diese TCP-Einstellungen ersetzen WebSocket Ping/Pong nicht
CLI-Welcome-Zeitlimit 15 Sekunden Wartefrist der verbundenen CLI auf welcome
Befehlszeitlimit timeout_secs in open_exec agent begrenzt auf mindestens 1 Sekunde. Bei Timeout erhält die gesamte Prozessgruppe SIGTERM, nach 3 Sekunden Schonfrist SIGKILL
ubus-Aufrufzeitlimit 15 Sekunden (fest in agent) ubus ist lokales RPC; nach dieser Zeit kann praktisch von einem blockierten Dienst ausgegangen werden

Auch der Client einer inaktiven Shell antwortet mit Pong. agent flusht die automatisch eingereihte Bibliotheksantwort ausdrücklich, ohne auf den nächsten betrieblichen Heartbeat zu warten. Deshalb wird die Verbindung ohne Tastatureingaben nicht fälschlich als verloren betrachtet (#42). --heartbeat-secs muss größer als 0 sein; ungültige Werte werden beim Parsen der Startargumente abgelehnt.


Rückstau

exchange verwaltet pro Verbindung eine begrenzte Sendewarteschlange mit 512 Nachrichten.

Eine volle Warteschlange bedeutet, dass die Gegenstelle nicht schnell genug konsumiert; bei PTY meist durch blockiertes Netzwerk oder angehaltenen Client. Die Verbindung wird aktiv getrennt, statt weiter zu puffern. Unbegrenzte Puffer würden einen blockierten Client in ein Speicherleck des gesamten Dienstes verwandeln. 512 Nachrichten bieten interaktiven PTYs großen Spielraum; normalerweise liegt die Länge im einstelligen Bereich. Bei Erreichen der Grenze verarbeitet die Gegenstelle praktisch keine Daten mehr.

Zustellung verwendet immer nicht blockierendes try_send. Im Routingpfad wird die Sperre der Verbindungstabelle gehalten. Ein await würde deren Haltedauer an die Verarbeitungsgeschwindigkeit der Gegenstelle koppeln; ein langsamer Client würde die gesamte Tabelle blockieren.

Zwei Zustellfehler werden unterschieden:

Ergebnis Bedeutung Behandlung
Lagging Warteschlange voll Verbindung über separates Schließsignal unterbrechen, ihre Sitzungen bereinigen und andere Seite nach Möglichkeit über Fehler informieren
Closed Empfänger bereits freigegeben Normales Verbindungsende; nur debug-Protokoll

Das Schließsignal durchläuft die Sendewarteschlange nicht. Es löst daher auch bei voller Warteschlange oder blockiertem Schreiben die Bereinigung aus. Geräteverbindungsersetzung und Tokenrotation verwenden denselben Mechanismus.

Auch agent besitzt eine ausgehende Warteschlange mit Kapazität 256. Ist sie voll, kann das Netzwerk überhaupt keine Daten mehr versenden. Weiteres Sammeln ist sinnlos; daher wird getrennt und neu verbunden. Ist die PTY-Eingabewarteschlange (64) voll, werden Tasten direkt verworfen: Kleine interaktive Eingaben anzustauen ist nicht sinnvoll.

Android-Bildschirmsteuerung

Die Definitionen liegen in crates/proto/src/android.rs. Verwendet werden bestehendes Routing mit zwei Nummernräumen, Authentifizierung, Parallelitätsgrenzen und Audit. request in open_android / android_req ist ein durch action unterschiedenes JSON-Objekt:

action Weitere Felder Verhalten
status Keine Lokale Berechtigungen und SDK-Version abfragen; Bildschirmfreigabe muss nicht aktiviert sein, Remotedienst muss jedoch laufen
tree Keine Baum zugänglicher Knoten der aktuell aktiven App
screenshot Keine Einzelner JPEG-Screenshot
tap x, y: u32 Tippen auf ursprüngliche Bildschirm-Pixelkoordinaten
swipe x1, y1, x2, y2, duration_ms: u32 Wischgeste, Dauer 50–5000 ms
swipe_path points: [{x, y}] (2–64 Punkte), duration_ms: u32 Mehrteilige Wischgeste: einmal aufsetzen und kontinuierlich alle Punkte durchlaufen, gleichmäßig nach Länge interpoliert, Dauer 50–5000 ms
global key: string back / home / recents / notifications / quick_settings
input_text text: string Ersetzt aktuell fokussiertes bearbeitbares Feld; höchstens 8192 UTF-8-Bytes, kein NUL

AndroidResult wird durch kind unterschieden; alle Felder sind Pflichtfelder:

kind Felder
status controls_enabled, accessibility_enabled, screen_capture_enabled: bool; sdk: u32
tree nodes: AndroidNode[]; screen_width, screen_height: u32; truncated: bool
screenshot mime_type: "image/jpeg"; data_base64: string; width, height, screen_width, screen_height: u32
ok Keine; Eingabe, Geste oder Systemaktion abgeschlossen

Knoten enthalten Strings class_name, package, view_id, text, description, bounds: [left, top, right, bottom] (vorzeichenbehaftete Bildschirm-Pixelkoordinaten) und boolesche Felder clickable, editable, focused, password. Höchstens 200 Knoten und 256 UTF-16-Einheiten pro String; truncated kennzeichnet nur die Knotengrenze. Passwortwerte und -beschreibungen bleiben leer; agent-eigener Konfigurationsunterbaum wird ausgelassen.

Die lange Screenshotseite hat höchstens 1600 Pixel. width/height sind Bildabmessungen, screen_width/screen_height Abmessungen vor Skalierung. Gestenkoordinaten beziehen sich auf unskalierte Abmessungen. Die Java-Brücke liefert höchstens 2 MiB JSON; WebSocket-Verpackung erlaubt zusätzlich 1024 Bytes. Kein neuer binärer Frametyp wird eingeführt.

{"type":"open_android","session_id":1,"device_id":"phone","request":{"action":"input_text","text":"Hello\nWorld"}}
{"type":"android_req","session_id":31,"request":{"action":"input_text","text":"Hello\nWorld"}}
{"type":"android_resp","session_id":31,"result":{"kind":"ok"}}
{"type":"android_resp","session_id":1,"result":{"kind":"ok"}}

screen_capture_enabled zeigt eine derzeit verfügbare Screenshotberechtigung an (MediaProjection oder Bedienungshilfe ab Android 11). Alle tatsächlichen Bildschirmaktionen benötigen weiterhin controls_enabled. Fehlende Berechtigungen, unzugängliche Fenster oder fehlgeschlagene Aktionen beantwortet agent mit session_error; Exchange wandelt dies in session_failed um. Nicht-Android-agents melden ausdrücklich fehlende Unterstützung.

Agent verarbeitet höchstens 4 Android-Anfragen gleichzeitig und kodiert jeweils nur einen Screenshot. Java-Operationen warten höchstens 10 Sekunden, auf Bildschirmframes höchstens 5 Sekunden. Das agent-Anfragelimit beträgt 15 Sekunden, das CLI-Limit 20 Sekunden. Bei Abbruch oder Operator-Trennung sendet Exchange android_cancel; agent schließt lokalen Socket, und APK bricht noch nicht ausgeführte Warteschlangenarbeit ab. Bereits ausgeführte Gesten oder Textänderungen werden nicht rückgängig gemacht.

Die Audit-action lautet android; detail speichert Aktion und Koordinaten. Bei input_text wird nur characters (Anzahl der Unicode-Zeichen) gespeichert; erfolgreiche Ergebnisse enthalten nur outcome: "completed". Textinhalt, Oberflächenbaum und Bilder werden nicht im Audit gespeichert. readonly-Konten dürfen auch status, tree und Screenshots nicht anfordern.

Erweiterung für Dateisitzungen

Agents mit Dateiunterstützung melden Fähigkeit files. Exchange übersetzt open_file {session_id, device_id, request} in file_req {session_id, request}. request.action ist list, download oder upload; Felddefinitionen stehen in crates/proto/src/files.rs.

Steuernachrichten beider Richtungen verwenden JSON: Der Controller sendet file_input {session_id, input}, das Gerät antwortet file_event {session_id, event}. input.kind ist chunk {offset, data}, ack {offset} oder finish; event.kind ist ready {size}, chunk {offset, data}, progress {received}, complete {size} oder list {path, entries, truncated}. Blockinhalte verwenden Standard-Base64. Pro Block höchstens 64 KiB; der Sender wartet vor dem Fortfahren auf Bestätigung.

Das erste ready eines Downloads benötigt ack {offset: 0}; danach bestätigt jeder Block die kumulierte Bytezahl. Ein Upload beginnt mit ready; jedes progress löst nächsten Block aus, nach vollständiger Übertragung wird mit finish abgeschlossen. complete und list sind Abschlussnachrichten; Fehler verwenden vorhandenes session_error / session_failed. close_session sendet file_cancel; Abbruch und Verbindungsverlust entfernen jeweils die Sitzungsroute. Dateinachrichten werden ausschließlich für Dateisitzungen weitergeleitet, damit Operator- und Gerätesitzungs-IDs getrennt bleiben.

Die Protokollversion bleibt 1. Neue Nachrichten erfordern passende Upgrades von Exchange und Agent. Betriebsgrenzen und LAN-Schnittstellen beschreibt Dateiübertragung und direkte LAN-Verbindung.

Plattformübergreifende Erweiterungen

Die Protokollversion bleibt 1. Das Registrierungs-info erhält optionale Felder platform (openwrt / linux / freebsd / netbsd / windows / android / macos), os_version, capabilities (exec / pty / ubus / android_control / files / sensors / net). OpenWrt meldet openwrt_version.

Heartbeat-metrics.unavailable ist ein optionales Stringarray für derzeit nicht erfassbare Werte uptime, load, memory, cpu, interfaces. Fehlt das Array, wird das alte Format verwendet. Werte der darin genannten Messgrößen sind nur Kompatibilitätsplatzhalter; Clients sollen sie als fehlend anzeigen. Windows bietet keine Unix-Durchschnittslast; Android kann manche /proc-Quellen durch Systemberechtigungen beschränken.

Befehle werden durch die Shell des Zielgeräts interpretiert: Linux/OpenWrt/macOS verwenden /bin/sh, Windows PowerShell und Android /system/bin/sh. PTY-Frameformat und Steuernachrichten sind auf den drei Systemen identisch; Windows verwendet ConPTY. Nicht-OpenWrt-Geräte antworten auf ubus-Anfragen mit session_error.

Ende-zu-Ende-versiegelte Sitzungen

Die Protokollversion bleibt 1. Neue Nachrichten: OperatorUp open_sealed {session_id, device_id, hello}, sealed_input {session_id, packet}; DeviceDown sealed_open {session_id, hello}, sealed_input, sealed_close {session_id, detach}; DeviceUp / OperatorDown sealed_accept {session_id, accept}, sealed_event {session_id, packet, last}. Hinzu kommen binärer Frametyp 4 = Sealed (Nutzlast counter ‖ AEAD(inner_kind ‖ bytes)), DeviceInfo.seal_key und Fähigkeit sealed.

Exchange übernimmt bei versiegelten Sitzungen nur Nummernübersetzung, Parallelitätsbegrenzung und Routing. Es analysiert oder auditiert keine Nutzlast; das Audit erfasst lediglich öffentliche Schlüssel, Berechtigungsstufe und Ablauf sowie Abschlussergebnis. Es entscheidet nicht über Berechtigungen, sondern prüft nur, ob das Konto das Gerät sehen darf. Bei last:true wird die Route freigegeben. Versiegelte Routen akzeptieren ausschließlich Sealed-Frames, normale Routen keine Sealed-Frames. agent verwirft jeden Klartext an versiegelte Sitzungsnummern. Autorisierung, Handshake, Schlüsselableitung, Bedrohungsmodell beschreibt Ende-zu-Ende-versiegelte Sitzungen.