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
- Transport und Endpunkte
- Protokollversion
- Steuernachrichten
- Binäre Datenframes
- Übersetzung zwischen zwei Sitzungsnummernräumen
- Handshake-Ablauf
- Fehlerbehandlung und Fehlerklassen
- Verbindungserhaltung und Zeitlimits
- Rückstau
- Android-Bildschirmsteuerung
- Ende-zu-Ende-versiegelte Sitzungen
Ü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 mitregister_ackundok: false;errornennt 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 zuserver_versionpassende 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: u16device_id: stringtoken: stringinfo: 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_idjob_idholdtimeout_secs |
Befehl gestartet; job_id dient zur hold-Liste und zum erneuten Anhängen nach Wiederverbindung |
exec_exit |
session_idcode: i32 | nulltimed_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_idjobs: ExecHoldInfo[] |
Liste der hold-Aufträge |
ubus_resp |
session_idcode: i32data: any |
Antwort eines ubus-Aufrufs. code ist der Rückgabecode von ubus; 0 bedeutet Erfolg |
android_resp |
session_idresult: AndroidResult |
Android-Bildschirmaktion abgeschlossen; diese Nachricht beendet die Sitzung |
pty_closed |
session_idcode: i32 | null |
PTY-Sitzung beendet; code ist der Exitcode der Shell |
session_error |
session_idmessage: 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: boolerror: string | nullheartbeat_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_idcommand: stringtimeout_secs: u64hold: 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_idjob_id |
Hängt hold-Auftrag an diese Sitzung an und spielt gepufferte Ausgabe nach |
exec_hold_kill |
session_idjob_id |
Beendet hold-Auftrag anhand der job_id |
exec_hold_tail |
session_idjob_id |
Verfolgt nur neue Ausgabe, ohne Pufferwiedergabe |
exec_hold_cat |
session_idjob_id |
Gibt aktuellen Puffer aus und endet mit exec_hold_cat_done |
ubus_req |
session_idobject: stringmethod: stringparams: any |
Startet ubus-Aufruf. Bei params gleich null wird kein drittes Positionsargument an ubus übergeben |
pty_open |
session_idrows: u16cols: u16 |
Öffnet PTY-Sitzung. Bei Zeilen-/Spaltenwert 0 wird 24×80 verwendet |
pty_resize |
session_idrows: u16cols: u16 |
Ändert Terminalfenstergröße |
pty_close |
session_id |
Schließt PTY-Sitzung |
android_req |
session_idrequest: 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_iddevice_idcommandtimeout_secshold (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_iddevice_id |
Fragt exec-Aufträge und laufende Downloads, Uploads und Verzeichnisabfragen ab |
list_device_sessions |
session_iddevice_id |
Fragt direkt die Exchange-Routingtabelle ab, ohne Gerätesitzungsplatz |
kill_device_session |
session_iddevice_idtarget_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_iddevice_idjob_id |
Hängt hold-Auftrag an und empfängt weitere Ausgabe |
exec_hold_kill / exec_hold_tail / exec_hold_cat |
session_iddevice_idjob_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_iddevice_id: stringobject: stringmethod: stringparams: any |
Startet ubus-Aufruf |
open_pty |
session_iddevice_id: stringrows: u16cols: u16 |
Öffnet interaktive PTY-Sitzung |
open_android |
session_iddevice_id: stringrequest: AndroidAction |
Startet Android-Bildschirmaktion; für schreibgeschützte Konten verboten |
pty_resize |
session_idrows: u16cols: 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: u16server_version: string |
Server sendet nach Verbindungsaufbau zur Prüfung übereinstimmender Protokollversionen |
device_sessions |
session_idsessions: DeviceSessionInfo[]limitcount |
Aktuelle geräteseitige Sitzungsliste von Exchange |
device_session_killed |
session_idtarget_session_idreleased: bool |
Lokal freigegeben; false, falls nicht vorhanden |
exec_exit |
session_idcode: i32 | nulltimed_out: bool |
Befehl beendet; aus gleichnamiger Gerätenachricht übersetzt |
ubus_resp |
session_idcode: i32data: any |
Antwort eines ubus-Aufrufs |
android_resp |
session_idresult: AndroidResult |
Android-Ergebnis mit übersetzter Operator-Sitzungsnummer |
pty_closed |
session_idcode: i32 | null |
PTY-Sitzung beendet |
session_failed |
session_idreason: SessionFailuremessage: 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
registerinnerhalb 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.