Skip to content

WebShell

WebShell 是个面向浏览器的终端,通过整个平台共用的同一条 geminio tunnel 触达每个注册的 edge。没有独立的 SSH 堡垒、没有跳板机、没有入站端口。edge 照常往外拨号;manager 开一类多路复用流给 shell I/O。

用例:

  • Agent 建议一个修复方案;你点 "在 edge-prod-04 上打开 shell" 确认改动,不 离开 SPA。
  • 供应商 / 外包需要一次性看看一台 host,不用入 VPN。
  • 事故响应:每条命令都跟发起会话的 audit 行一起被录。

架构

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

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

                          └─ geminio Stream (shell class)


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

manager 侧路由器在 internal/manager/biz/webshell/router.go。 它维护一个 sessionID → Sink 目录:WebSocket handler 在连接时注册,tunnel 入口分发器把 edge 的输出 / exit 推送路由到正确的浏览器。

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
}

HTTP / WebSocket handler 住在隔壁 internal/manager/server/webshell,让路由器保持 HTTP 无关、可单测。

两类流

geminio tunnel 多路复用:

  1. 控制类 —— JSON RPC(技能执行、插件信令、告警 evaluator 探针)。
  2. shell 类 —— 原始字节流(每个 WebShell 会话一条,每个 tail -f follower 一条,等等)。

在 tunnel 层就分开很重要,因为 shell I/O 是突发且无帧的;跟控制 RPC 混在 一起会饿死后者。每类有自己的反压预算。

会话元数据

每个活会话有一个 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 在每次键击时更新(Router.TouchInput)。idle-timeout 看门狗 会驱逐超过配置上限且最近无输入的会话 —— 防 "我关了带着跑命令的浏览器标签" 泄漏。

审计录像

两层:

  1. header 行 —— webshell_sessions 表:谁、何时、哪台 edge、退出码、 总入出字节。
  2. 流录像 —— manager 侧的 Recorder 接口把过线的每个字节(双向、带 时间戳)追加到 /var/lib/ongrid/webshell-recordings/<session_id>.cast 下的 asciinema 兼容 cast 文件。admin 的 /admin/webshell 页可回放。

Recorder 接口故意窄 —— 生产用文件 sink;测试注入 fake;未来的云 blob 后端可以塞进来,不用碰栈其他部分。

并发上限

每用户上限:WebSocket open handler 调 Router.CountByUser;超上限的连接 被拒,HTTP 429。默认 5(可配)。每 edge 上限防一个跑飞的 agent 开 100 路 并发 shell。

关闭会话

三条路关一个会话:

  1. 浏览器关闭 —— WebSocket 断开传到 edge,edge kill -HUP pty。
  2. 管理员 kill —— admin SPA 调 Sink 上的 Killer.Kill(reason="admin terminated"),把一个 close 通过 tunnel 推 到 edge。理由记在会话的 exit 行里。
  3. idle 驱逐 —— 看门狗对 LastInputAt 超上限的会话触发 Kill("idle timeout")
go
// router.go:50
type Killer interface {
    Kill(reason string)
}

manager 侧 handler 在注册 Sink 时安装 Killer。任何 opt-in 的 Sink 就变成 admin-killable;其余只能浏览器关闭关掉。

角色门控

WebShell 门控在 admin 角色(ADR-022 RBAC)。user 角色能跟 agent 聊 但开不了 shell;viewer 能看过去会话的录像但开不了新的。门在 HTTP handler 入口跑,在 WebSocket upgrade 之前。

另见

  • 技能 —— bash 技能是 WebShell 的一次性等价(单条命令,无 pty)。 同一个审计底座。
  • Edge 安装 —— 让 host 的 edge agent 起来,WebShell 才能 连上。
  • 架构 —— geminio tunnel 的位置。