WebShell
WebShell 是个面向浏览器的终端,通过整个平台共用的同一条 geminio tunnel 触达每个注册的 edge。没有独立的 SSH 堡垒、没有跳板机、没有入站端口。edge 照常往外拨号;manager 开一类多路复用流给 shell I/O。
用例:
- Agent 建议一个修复方案;你点 "在 edge-prod-04 上打开 shell" 确认改动,不 离开 SPA。
- 供应商 / 外包需要一次性看看一台 host,不用入 VPN。
- 事故响应:每条命令都跟发起会话的 audit 行一起被录。
架构
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 推送路由到正确的浏览器。
// 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 多路复用:
- 控制类 —— JSON RPC(技能执行、插件信令、告警 evaluator 探针)。
- shell 类 —— 原始字节流(每个 WebShell 会话一条,每个
tail -ffollower 一条,等等)。
在 tunnel 层就分开很重要,因为 shell I/O 是突发且无帧的;跟控制 RPC 混在 一起会饿死后者。每类有自己的反压预算。
会话元数据
每个活会话有一个 ActiveSession:
// 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 看门狗 会驱逐超过配置上限且最近无输入的会话 —— 防 "我关了带着跑命令的浏览器标签" 泄漏。
审计录像
两层:
- header 行 ——
webshell_sessions表:谁、何时、哪台 edge、退出码、 总入出字节。 - 流录像 —— 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。
关闭会话
三条路关一个会话:
- 浏览器关闭 —— WebSocket 断开传到 edge,edge
kill -HUPpty。 - 管理员 kill —— admin SPA 调
Sink上的Killer.Kill(reason="admin terminated"),把一个 close 通过 tunnel 推 到 edge。理由记在会话的 exit 行里。 - idle 驱逐 —— 看门狗对
LastInputAt超上限的会话触发Kill("idle timeout")。
// 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 之前。