Skip to content

WebShell

WebShell ist ein browserorientiertes Terminal, das jede registrierte Edge durch denselben geminio-Tunnel erreicht, den der Rest der Plattform verwendet. Es gibt keinen separaten SSH-Bastion, keine Jumpbox, keinen eingehenden Port. Die Edge wählt weiter nach außen; der Manager öffnet eine multiplexte Stream-Klasse für Shell-I/O.

Use-Cases:

  • Der Agent schlägt einen Fix vor; Sie klicken „Open shell on edge-prod-04" und bestätigen die Änderung, ohne die SPA zu verlassen.
  • Anbieter / Auftragnehmer braucht einen einmaligen Blick auf einen Host ohne VPN-Enrollment.
  • Incident-Response: jeder Befehl wird mit der Audit-Zeile der ursprünglichen Session aufgezeichnet.

Architektur

text
browser ──WebSocket──> manager:/v1/webshell/ws

                          ├─ Router.Register(sessionID, sink, ActiveSession)

                          └─ geminio Stream (shell class)


                              edge agent
                                  └─ pty.Start("/bin/bash")

Der manager-seitige Router ist in internal/manager/biz/webshell/router.go. Er hält ein sessionID → Sink-Verzeichnis: WebSocket-Handler registrieren beim Connect, der Tunnel-Incoming-Dispatcher routet die Output- / Exit-Pushes der Edge an den richtigen Browser.

go
// internal/manager/biz/webshell/router.go:57
type Router struct {
    mu          sync.RWMutex
    sinks       map[string]Sink
    meta        map[string]*ActiveSession // sessionID → metadata
    stdoutBytes sync.Map                  // sessionID → *uint64
}

Der HTTP- / WebSocket-Handler liegt nebenan in internal/manager/server/webshell, sodass der Router HTTP-agnostisch und unit-testbar bleibt.

Die zwei Stream-Klassen

Der geminio-Tunnel multiplext:

  1. Control-Klasse — JSON-RPCs (Skill-Ausführung, Plugin-Signaling, Alert-Evaluator-Proben).
  2. Shell-Klasse — Roh-Byte-Streams (einer pro WebShell-Session, einer pro tail -f-Follower, etc.).

Auf Tunnel-Ebene aufzuteilen zählt, weil Shell-I/O burstig und ungeframet ist; sie mit den Steuer-RPCs zu mischen verhungerte letztere. Jede Klasse hat ihr eigenes Backpressure-Budget.

Session-Metadaten

Jede Live-Session hat eine ActiveSession:

go
// router.go:37
type ActiveSession struct {
    SessionID    string
    OngridUserID uint64
    SSHUser      string
    DeviceID     uint64
    EdgeID       uint64
    StartedAt    time.Time
    LastInputAt  time.Time // updated on every browser → edge frame
}

LastInputAt tickt bei jedem Tastendruck (Router.TouchInput). Ein Idle-Timeout-Watchdog vertreibt Sessions, die älter als das konfigurierte Limit ohne kürzlichen Input sind — verteidigt gegen den „Ich habe den Browser-Tab mit einem laufenden Befehl geschlossen"-Leak.

Audit-Aufzeichnung

Zwei Schichten:

  1. Header-Zeilewebshell_sessions-Tabelle: wer, wann, welche Edge, Exit-Code, gesamte Bytes ein/aus.
  2. Stream-Aufzeichnung — das manager-seitige Recorder-Interface nimmt jedes Byte, das über die Leitung geht (beide Richtungen, zeitgestempelt) und hängt es an eine asciinema-kompatible Cast-Datei unter /var/lib/ongrid/webshell-recordings/<session_id>.cast an. Die Admin-/admin/webshell-Seite spielt sie zurück.

Das Recorder-Interface ist absichtlich schmal — Produktion verwendet einen File-Sink; Tests injizieren ein Fake; zukünftige Cloud-Blob-Backends fallen ein, ohne den Rest des Stacks zu berühren.

Concurrency-Limits

Per-User-Cap: Router.CountByUser wird vom WebSocket-Open-Handler aufgerufen; Over-Cap-Verbindungen werden mit HTTP 429 abgelehnt. Default-Cap ist 5 (konfigurierbar). Per-Edge-Cap verteidigt gegen einen außer Kontrolle geratenen Agenten, der 100 gleichzeitige Shells öffnet.

Sessions killen

Drei Pfade killen eine Session:

  1. Browser-Close — WebSocket-Disconnect propagiert zur Edge, die das pty kill -HUPt.
  2. Admin-Kill — die Admin-SPA ruft Killer.Kill(reason="admin terminated") auf dem Sink auf, was einen Close zur Edge tunnelt. Der Grund wird in der Exit-Zeile der Session aufgezeichnet.
  3. Idle-Eviction — der Watchdog feuert Kill("idle timeout") auf Sessions, deren LastInputAt den Cap überschritten hat.
go
// router.go:50
type Killer interface {
    Kill(reason string)
}

Der manager-seitige Handler installiert einen Killer, wenn er den Sink registriert. Jeder Sink, der opt-in geht, wird admin-kill-bar; die anderen sind nur browser-close-killbar.

Rollen-Gating

WebShell ist auf die admin-Rolle gegatet (ADR-022 RBAC). Die user-Rolle kann mit dem Agenten chatten, kann aber keine Shells öffnen; viewer kann Aufzeichnungen vergangener Sessions lesen, kann aber keine neuen öffnen. Das Gate läuft am HTTP-Handler-Eintrag, vor dem WebSocket-Upgrade.

Siehe auch

  • Skills — das bash-Skill ist das One-Shot-Äquivalent von WebShell (einzelner Befehl, kein pty). Gleiches Audit-Substrat.
  • Edge-Installation — den Edge-Agenten eines Hosts hochfahren, damit WebShell ihn erreichen kann.
  • Architektur — wo der geminio-Tunnel sitzt.