11 KiB
浏览器容器控制面
技术选型
- 前端:React 19 + react-admin(ra-core)+ MUI,包含运行环境、镜像版本、网关管理三个页面。
- 后端:Go 模块化单体,Fiber v3 提供 HTTP 路由,Viper 读取并校验启动配置,Logrus 输出 JSON 结构化日志,Cobra 保持当前两个服务入口。控制面提供同源 API 和静态文件,并编排网关;受限网关单独封装 Docker Engine API,是纯执行器。
- 数据:环境配置(别名、中文名、网关、镜像版本、指纹参数)持久化在 Postgres,运行态实时查询网关;Profile 使用命名卷持久化;阶段 A 账号、凭据引用、确认、任务、尝试和审计实体同样由控制面持久化到 Postgres。
- 部署:Docker Compose 启动控制面和受限网关;浏览器容器由网关按平台下发的镜像引用动态创建,缺失时自动拉取。
调用链与契约
React ──> control-plane ── /api/browsers ──(Bearer token)──> docker-gateway ──> docker.sock
│ │
│ └─> browser container
└─ /api/phase-a, /api/browser-images, /api/gateways ──> PostgreSQL
控制面是唯一事实源:网关不持有镜像清单和业务规则,镜像引用、启动命令和卷名均随请求下发。
POST /api/browsers接受{alias, name, gateway, image_version, fingerprint}(严格 JSON,未知字段拒绝),校验后先落库,再调网关创建并启动;网关失败时回滚数据库行。name为中文环境名,alias限^[a-z0-9][a-z0-9-]{0,31}$,容器名creatorhub-browser-<alias>,Profile 卷creatorhub-profile-<alias>。GET /api/browsers合并数据库环境与网关实时状态;环境在网关无容器时显示为未部署。POST /api/browsers/{alias}/start|stop改变状态;POST /api/browsers/{alias}/upgrade收{version},由平台编排:停止并删除旧容器(保留 Profile 卷)→ 用新镜像引用与原指纹参数重建 → 启动;失败直接重试,不做自动回滚。DELETE /api/browsers/{alias}回收容器并删除数据库行,保留 Profile 数据卷。GET/POST /api/browser-images维护可用镜像版本(版本号不可改,PUT /{version}仅接受image_ref/note/enabled);仅启用版本可用于创建与升级;被环境引用时拒绝删除。GET/POST /api/gateways注册网关(POST可携带令牌,否则平台生成 48 位十六进制令牌并明文存储),DELETE /api/gateways/{name}删除;仍被环境引用时拒绝删除。- 启停接受幂等响应,不自动重试未知结果;别名唯一约束由数据库保证。
docker.sock 安全边界
将 socket 以只读文件挂载不会限制 Docker API 的写操作;拥有 socket 等价于拥有宿主机 root 权限。因此:
- 只有
docker-gateway挂载 socket,控制面和浏览器容器均不可见;网关加入 control 与 browser 网络,浏览器只拿到无凭据的内存转发代理地址,/v1仍必须通过容器内不可见的网关令牌; - 网关只暴露面向领域的路由,不提供通用 Docker 代理;
/v1全部接口校验Authorization: Bearer <GATEWAY_TOKEN>(常数时间比较),令牌由部署者在网关环境变量与平台注册表中保持一致; - 网关直连的
POST /v1/browsers/{alias}/start|stop仅供内部维护使用,必须提交并精确匹配容器标签中的{binding_version,runtime_id,network_id};直连 start 拒绝空network_id,generation 不匹配返回409,控制面生命周期编排不依赖无 fence 的直连 start; - 网关恢复内存代理时会同时 fence 隔离网络成员及网关成员 IPv4,地址变化返回
409并关闭刚恢复的监听;代理移除或代际替换会立即关闭所有已 hijack 的 CONNECT 双向连接,任一端先关闭也会关闭隧道两端,不等待优雅 drain; - 网关固定命令、网络、挂载和资源限制;外部输入是受校验的别名,以及平台下发的镜像引用、启动参数和卷名——镜像引用来自平台维护的版本表,新增/变更由人工在页面审核启用,不再写死在代码中;
- 启停和删除前必须同时匹配固定名称前缀及
io.creatorhub.managed、io.creatorhub.runtime-id标签; - 动态容器使用只读根文件系统、非 root
1000:1000与固定镜像入口、全部 capability drop、no-new-privileges、CPU/内存/PID 限制,且无宿主机端口和目录挂载; - 控制面发布到宿主机所有网卡;控制网络为固定名称的 Compose 网络;浏览器 bridge 按 ownership、role、driver、Internal 失败关闭校验,且拒绝复用 control 网络;
- Compose 基础镜像锁定 digest;浏览器镜像推荐使用
@sha256:摘要引用以获得不可变性,tag 引用由部署者自行把控。
网关自身一旦被攻破,socket 仍允许接管宿主机;应用内校验不能消除这个平台级风险。开发阶段控制面不做认证或访问限制,安全由部署者自行把控。
运行
Docker socket 的 GID 因宿主机而异:
export GATEWAY_TOKEN="$(openssl rand -hex 24)" # 亦可在 .env 中设置
DOCKER_GID=$(stat -c %g /var/run/docker.sock) docker compose up --build
打开 http://127.0.0.1:8080,局域网内用宿主机 IP 访问同一端口。首次使用:在「网关管理」用 GATEWAY_TOKEN 注册 http://docker-gateway:8081,在「镜像版本」添加镜像引用(缺失时网关自动拉取,拉取上限 10 分钟)。浏览器容器可访问外网。
创建成功但启动失败时,网关会立即删除失败容器并保留命名 Profile 卷,控制面回滚数据库行,允许同名请求安全重试。
阶段 A 离线闭环
POST /api/phase-a/accounts 接受 {id, platform, platform_account_key, authorization_kind, credential_reference},只允许 OS Keyring/Secret Manager 引用,不接受秘密值;新账号默认 paused,(platform, platform_account_key) 全局唯一。GET /api/phase-a/accounts[/:id] 不返回引用 key;pause/revoke 会递增账号版本并将 queued 任务置为 policy_hold,只有具备 binding 和 healthy 出口的未撤销账号才能 resume。账号与浏览器环境通过一对一 environment_binding 关联,出口可复用;运行实例保留历史,并以 binding 和外部 runtime id 的部分唯一索引限制活动实例。
草稿经 POST /api/phase-a/confirmations 显式确认后才可投递到 /api/phase-a/tasks。任务由幂等键去重;POST /api/phase-a/mock/execute 使用 FOR UPDATE SKIP LOCKED 领取一分钟租约,执行前统一核对账号、草稿和确认版本。缺少确认或版本不一致会进入 needs_confirmation,暂停账号或 Mock 策略结果会进入 policy_hold,不确定结果与过期租约进入 needs_confirmation;这些状态都不会自动重试。GET /api/phase-a/audit 只导出账号、确认版本、尝试和结果等非秘密证据。
启动时控制面先应用 Phase A v1,再由 Hub runner 顺序应用 v2 至 v12;每一步都在事务和 advisory lock 下前向执行。v3 保留旧表、列和历史记录,旧账号回填为 platform=mock 并暂停,仅账号 ID 与环境 alias 相同的记录自动建立 binding;v4 追加环境动作审计字段与索引,v5 清理持久 fingerprint 中的旧代理字段,v6 增加可重试的 runtime cleanup 状态,v7 为 runtime lease 增加 binding version 并回填可确定的既有记录,v8 至 v10 补齐 cleanup/runtime 的不可变 generation 与兼容约束,v11、v12 增加任务恢复状态并修复兼容约束。其余记录等待显式绑定。本阶段不提供破坏性自动回滚。
POST /api/network-exits 只接受协议、主机、端口、已有 credential_reference: {id} 和预期出口身份;新出口为 unchecked,由 POST /api/network-exits/:id/check 经实际代理链路变为 healthy 或 unhealthy,disable 不可被检查重新启用。credential reference 的 reference_key 不出现在 API、日志或审计中;OS Keyring/Secret Manager bridge 在控制面进程启动前注入 CREATORHUB_CREDENTIAL_<SHA256(reference_key)>(大写十六进制),值为请求期解析的 username:password,控制面不持久化解析值。
POST /api/browsers 必须同时给出 account_id 和 network_exit_id。环境创建、启动和升级都会重新检查出口身份,只有 healthy 才调用网关;控制面强制下发代理和 disable_non_proxied_udp,fingerprint 中的代理字段会被拒绝。显式 POST /api/browsers/:alias/rebind 只允许 paused、无 executing task 且无活动 runtime 的账号。DELETE /api/browsers/:alias 回收容器但保留稳定 binding、环境和命名 Profile 卷,后续 create 复用它们。create/start/stop/upgrade/recycle 均写共享 operation ID 的 requested/finished 审计对;网关断连且无法调和时 outcome 为 unknown。
解析后的出口凭据只存在于控制面单次请求和网关内存转发器中;Docker inspect、容器环境、标签、挂载、Config.Cmd 与进程参数只包含 docker-gateway 的无凭据本地代理地址。网关内存代理以 alias、binding version 和 exit ID 共同标识 generation;生命周期操作按 alias 串行,重启恢复或重建必须重新核对该 generation,旧出口代理不能被新容器复用。
stopped 环境启动时先删除旧容器并确认 runtime lease 释放,再按当前 binding 重建;控制面每 20 秒及列表读取时调和网关,续租 running runtime、释放 stopped/missing runtime,过期 lease 也会在绑定事务中回收。控制面用 PostgreSQL advisory transaction lock 按 alias 协调多副本;每个 Store 最多允许 5 个锁会话占用 10 连接池的一半,为锁内数据库调用保留连接。create 同时锁定账号 ID、alias、请求出口和请求镜像;start、reconcile/rebuild、rebind 和 upgrade 锁定 alias、当前出口及当前镜像(upgrade 还锁目标镜像),拿锁后重新读取出口与镜像版本。账号 pause/resume/revoke 使用账号 ID 与当前 binding alias 加入同一协调域;镜像禁用、引用更新或账号状态变更不能穿透在途生命周期。
非法 upgrade/rebind 目标在进入 advisory lock key 前按公开格式校验;审计仅保留环境原有的非秘密资源关联,并以 upgrade_input_rejected / rebind_input_rejected 写同一 operation ID 的 requested/finished 对。reconcile 恢复或重建后会重新读取 context,finished 事件关联实际激活的 runtime instance、binding version 与出口;后台释放 runtime 的成功或失败也写独立的 reconcile 审计对。