Files
creator-hub/docs/architecture/container-control.md
T

8.8 KiB
Raw Blame History

浏览器容器控制面

技术选型

  • 前端:React 19 + react-adminra-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}generation 不匹配返回 409,控制面生命周期编排不依赖无 fence 的直连 start
  • 网关固定命令、网络、挂载和资源限制;外部输入是受校验的别名,以及平台下发的镜像引用、启动参数和卷名——镜像引用来自平台维护的版本表,新增/变更由人工在页面审核启用,不再写死在代码中;
  • 启停和删除前必须同时匹配固定名称前缀及 io.creatorhub.managedio.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] 不返回引用 keypause/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、v3、v4;每一步都在事务和 advisory lock 下前向执行。v3 保留旧表、列和历史记录,旧账号回填为 platform=mock 并暂停,仅账号 ID 与环境 alias 相同的记录自动建立 binding;v4 只追加环境动作审计字段与索引。其余记录等待显式绑定。本阶段不提供破坏性自动回滚。

POST /api/network-exits 只接受协议、主机、端口、已有 credential_reference: {id} 和预期出口身份;新出口为 unchecked,由 POST /api/network-exits/:id/check 经实际代理链路变为 healthyunhealthydisable 不可被检查重新启用。credential reference 的 reference_key 不出现在 API、日志或审计中;OS Keyring/Secret Manager bridge 在控制面进程启动前注入 CREATORHUB_CREDENTIAL_<SHA256(reference_key)>(大写十六进制),值为请求期解析的 username:password,控制面不持久化解析值。

POST /api/browsers 必须同时给出 account_idnetwork_exit_id。环境创建、启动和升级都会重新检查出口身份,只有 healthy 才调用网关;控制面强制下发代理和 disable_non_proxied_udpfingerprint 中的代理字段会被拒绝。显式 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 的无凭据本地代理地址。stopped 环境启动时先删除旧容器并确认 runtime lease 释放,再按当前 binding 重建;控制面每 20 秒及列表读取时调和网关,续租 running runtime、释放 stopped/missing runtime,过期 lease 也会在绑定事务中回收。