Skip to content

WebShell

WebShell é um terminal voltado ao browser que alcança cada edge registrado pelo mesmo tunnel geminio que o resto da plataforma usa. Não há bastion SSH separado, sem jumpbox, sem porta inbound. O edge continua discando para fora; o manager abre uma classe de stream multiplexada para I/O de shell.

Casos de uso:

  • O agent sugere um fix; você clica em "Open shell on edge-prod-04" e confirma a mudança sem sair do SPA.
  • Vendor / contratado precisa de uma olhada one-off em um host sem enrollment de VPN.
  • Resposta a incidente: cada comando é gravado com a linha de auditoria da sessão originária.

Arquitetura

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

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

                          └─ geminio Stream (shell class)


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

O router do lado manager está em internal/manager/biz/webshell/router.go. Mantém um diretório sessionID → Sink: handlers WebSocket registram no connect, o dispatcher tunnel-incoming roteia os pushes de saída / exit do edge para o browser certo.

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
}

O handler HTTP / WebSocket vive ao lado em internal/manager/server/webshell para que o router permaneça HTTP-agnostic e testável em unit.

As duas classes de stream

O tunnel geminio multiplexa:

  1. Classe de controle — RPCs JSON (execução de skill, sinalização de plugin, probes do evaluator de alerta).
  2. Classe de shell — streams brutos de bytes (um por sessão WebShell, um por follower de tail -f, etc.).

Separar no nível do tunnel importa porque I/O de shell é bursty e não-framed; misturar com os RPCs de controle deixa estes famélicos. Cada classe tem seu próprio budget de backpressure.

Metadados de sessão

Cada sessão viva tem uma 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 tique em cada tecla (Router.TouchInput). Um watchdog de idle-timeout despeja sessões mais velhas que o limite configurado sem input recente — defende contra o vazamento "fechei a aba do browser com um comando rodando".

Gravação de auditoria

Duas camadas:

  1. Linha de cabeçalho — tabela webshell_sessions: quem, quando, qual edge, exit code, total de bytes in/out.
  2. Gravação de stream — a interface Recorder do lado manager pega cada byte que cruza o fio (ambas direções, com timestamp) e anexa a um arquivo cast asciinema-compatible em /var/lib/ongrid/webshell-recordings/<session_id>.cast. A página admin /admin/webshell os reproduz.

A interface Recorder é estreita de propósito — produção usa um sink de arquivo; testes injetam um fake; backends futuros de cloud-blob entram sem tocar no resto da stack.

Limites de concorrência

Cap por usuário: Router.CountByUser é chamado do handler de open do WebSocket; conexões acima do cap são rejeitadas com HTTP 429. Cap padrão é 5 (configurável). Cap por edge defende contra um agent runaway abrindo 100 shells concorrentes.

Matando sessões

Três caminhos matam uma sessão:

  1. Browser close — desconexão do WebSocket propaga ao edge, que dá kill -HUP no pty.
  2. Admin kill — o admin SPA chama Killer.Kill(reason="admin terminated") no Sink, que tuneliza um close até o edge. A razão é gravada na linha de exit da sessão.
  3. Despejo por idle — o watchdog dispara Kill("idle timeout") em sessões cujo LastInputAt ultrapassou o cap.
go
// router.go:50
type Killer interface {
    Kill(reason string)
}

O handler do lado manager instala um Killer quando registra o Sink. Qualquer Sink que opt-in vira admin-killable; o resto só é browser-close-killable.

Gate de role

WebShell é gateado na role admin (RBAC do ADR-022). A role user pode conversar com o agent mas não pode abrir shells; a role viewer pode ler gravações de sessões passadas mas não pode abrir novas. O gate roda na entrada do handler HTTP, antes do upgrade do WebSocket.

Veja também

  • Skills — a skill bash é o equivalente one-shot do WebShell (um único comando, sem pty). Mesmo substrato de auditoria.
  • Instalação do edge — subir o edge agent de um host para o WebShell conseguir alcançá-lo.
  • Arquitetura — onde o tunnel geminio se senta.