|
1 | | -# 架构摘要 |
| 1 | +# Architecture |
2 | 2 |
|
3 | | -## 总体结构 |
| 3 | +**Analysis Date:** 2026-05-10 |
4 | 4 |
|
5 | | -当前项目推荐采用“控制面 + 宿主机代理 + 用户容器运行时”的单宿主机架构: |
| 5 | +## System Overview |
6 | 6 |
|
7 | | -- 控制面:负责用户、会话、出口 IP 绑定、生命周期、到期和审计状态 |
8 | | -- 宿主机代理:负责 Docker、命名空间、隧道和防火墙等特权操作 |
9 | | -- 用户容器:承载 OpenSSH、Shell 工具和 `claude code` |
10 | | -- 持久化层:使用 PostgreSQL 保存系统真实状态 |
| 7 | +Cloud CLI Proxy 是一个面向单宿主机的容器化 SSH 云主机平台。用户从一个很短的 `curl` 入口开始,在终端里输入用户名和密码,等待专属 Docker 容器启动完成后,直接进入该容器内的 SSH 会话。所有网络流量都必须通过指定出口 IP 的全局隧道路由发送。 |
11 | 8 |
|
12 | | -## 关键边界 |
| 9 | +```text |
| 10 | +┌─────────────────────────────────────────────────────────────────────────────┐ |
| 11 | +│ cloud-claude CLI (终端用户) │ |
| 12 | +│ `cmd/cloud-claude/` — Go + Cobra │ |
| 13 | +├─────────────────────────────────────────────────────────────────────────────┤ |
| 14 | +│ Entry Short-Link │ |
| 15 | +│ `GET /entry/{username}` → bootstrap script │ |
| 16 | +│ `POST /v1/entry/{username}/auth` → SSH 凭证 │ |
| 17 | +├─────────────────────────────────────────────────────────────────────────────┤ |
| 18 | +│ Control Plane (HTTP API) │ |
| 19 | +│ `internal/controlplane/` — Go net/http, JWT, SSE │ |
| 20 | +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ |
| 21 | +│ │ Admin API │ │ Bootstrap │ │ Entry API │ │ Scheduler │ │ |
| 22 | +│ │ `/v1/admin` │ │ `/v1/boot` │ │ `/entry` │ │ expiry + reconcile │ │ |
| 23 | +│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────────────┘ │ |
| 24 | +├─────────────────────────────────────────────────────────────────────────────┤ |
| 25 | +│ Host Agent (特权操作边界) │ |
| 26 | +│ `internal/agent/` + `internal/agentapi/` │ |
| 27 | +│ Unix socket 通信 (`/run/cloud-cli-proxy/host-agent.sock`) │ |
| 28 | +├─────────────────────────────────────────────────────────────────────────────┤ |
| 29 | +│ Docker + Network Layer │ |
| 30 | +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ |
| 31 | +│ │ Runtime │ │ Network │ │ SSH Proxy │ │ Broadcast │ │ |
| 32 | +│ │ `internal/ │ │ `internal/ │ │ `internal/ │ │ `internal/ │ │ |
| 33 | +│ │ runtime/` │ │ network/` │ │ sshproxy/` │ │ broadcast/` │ │ |
| 34 | +│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────────────┘ │ |
| 35 | +├─────────────────────────────────────────────────────────────────────────────┤ |
| 36 | +│ Data Persistence │ |
| 37 | +│ `internal/store/` — PostgreSQL + pgx/v5 │ |
| 38 | +└─────────────────────────────────────────────────────────────────────────────┘ |
| 39 | + │ |
| 40 | + ▼ |
| 41 | +┌─────────────────────────────────────────────────────────────────────────────┐ |
| 42 | +│ Admin Dashboard (Web) │ |
| 43 | +│ `web/admin/` — React 19.2 + Vite + TanStack Router │ |
| 44 | +└─────────────────────────────────────────────────────────────────────────────┘ |
| 45 | +``` |
13 | 46 |
|
14 | | -- Web / API 层不要直接持有过宽的宿主机特权 |
15 | | -- Docker 和网络 namespace 的操作应集中在独立边界中执行 |
16 | | -- 用户容器的默认出网必须被隧道网络接管,不能保留旁路 |
| 47 | +## Component Responsibilities |
17 | 48 |
|
18 | | -## 核心数据流 |
| 49 | +| Component | Responsibility | Key File | |
| 50 | +|-----------|----------------|----------| |
| 51 | +| Control Plane | HTTP API 服务、认证、任务编排、SSE 推送、调度器 | `internal/controlplane/app/app.go` | |
| 52 | +| Host Agent | Docker 特权操作、容器生命周期、网络配置 | `internal/agent/server.go` | |
| 53 | +| Agent API Client | 通过 Unix socket 与 Host Agent 通信 | `internal/agentapi/client.go` | |
| 54 | +| Runtime Service | 将 host action 转换为 Docker 命令队列 | `internal/runtime/runtime_service.go` | |
| 55 | +| Worker | 实际执行 docker create/start/stop/rebuild | `internal/runtime/tasks/worker.go` | |
| 56 | +| Network Provider | sing-box + nftables/iptables 全隧道出网 | `internal/network/container_proxy_provider.go` | |
| 57 | +| SSH Proxy | 用户 SSH 接入代理,转发到容器内部 SSH | `internal/sshproxy/proxy.go` | |
| 58 | +| Broadcast | SSE 实时事件广播中心 | `internal/broadcast/sse.go` | |
| 59 | +| Store / Repository | PostgreSQL 数据访问层 | `internal/store/repository/queries.go` | |
| 60 | +| cloud-claude CLI | 终端用户入口,认证、挂载、SSH 会话 | `cmd/cloud-claude/main.go` | |
| 61 | +| Admin Web | 管理后台前端 | `web/admin/src/main.tsx` | |
19 | 62 |
|
20 | | -1. 用户执行 `curl` 启动入口 |
21 | | -2. 控制面完成认证与权限检查 |
22 | | -3. 控制面下发启动任务 |
23 | | -4. 宿主机代理创建容器并接入受控隧道网络 |
24 | | -5. 系统验证 SSH 与网络路径都已就绪 |
25 | | -6. 用户被接入最终的 SSH 会话 |
| 63 | +## Pattern Overview |
26 | 64 |
|
27 | | -## 当前架构原则 |
| 65 | +**Overall:** 分层架构 + 端口适配器模式(Ports and Adapters) |
28 | 66 |
|
29 | | -- 单宿主机优先,不为 v1 提前引入多节点调度复杂度 |
30 | | -- 网络强约束优先,先保证“所有流量都必须走指定出口” |
31 | | -- 启动体验建立在真实可验证的运行时正确性之上 |
| 67 | +**Key Characteristics:** |
| 68 | +- 控制面(Control Plane)不直接持有 Docker 特权,所有特权操作通过 Host Agent 或 Embedded Worker 执行 |
| 69 | +- 数据流以 Task(任务)为核心:每个 host action 创建一条 task 记录,异步执行,SSE 广播状态变更 |
| 70 | +- 网络隔离通过 per-host Docker network + sing-box gateway sidecar 实现 |
| 71 | +- 单宿主机优先,但架构预留了多宿主机扩展边界(host-agent socket 模型) |
| 72 | + |
| 73 | +## Layers |
| 74 | + |
| 75 | +### Web / API Layer |
| 76 | +- **Purpose:** HTTP API 入口、认证、路由分发 |
| 77 | +- **Location:** `internal/controlplane/http/` |
| 78 | +- **Contains:** Handler 函数、JWT 中间件、路由表 |
| 79 | +- **Depends on:** store/repository, runtime, broadcast, scheduler |
| 80 | +- **Used by:** Admin Dashboard, cloud-claude CLI, curl bootstrap |
| 81 | + |
| 82 | +### Application / Service Layer |
| 83 | +- **Purpose:** 业务逻辑编排、任务调度、生命周期管理 |
| 84 | +- **Location:** `internal/controlplane/app/`, `internal/controlplane/scheduler/`, `internal/runtime/` |
| 85 | +- **Contains:** App 组装器、ExpiryScanner、Reconciler、RuntimeService |
| 86 | +- **Depends on:** repository, agentapi, network |
| 87 | +- **Used by:** HTTP handlers |
| 88 | + |
| 89 | +### Domain / Worker Layer |
| 90 | +- **Purpose:** 实际执行 Docker 和网络操作 |
| 91 | +- **Location:** `internal/runtime/tasks/`, `internal/agent/`, `internal/network/` |
| 92 | +- **Contains:** Worker, Host Agent Server, ContainerProxyProvider |
| 93 | +- **Depends on:** Docker CLI, sing-box, nftables/iptables |
| 94 | +- **Used by:** RuntimeService (embedded mode) or Host Agent (独立模式) |
| 95 | + |
| 96 | +### Data Layer |
| 97 | +- **Purpose:** 持久化存储 |
| 98 | +- **Location:** `internal/store/repository/`, `internal/store/migrations/` |
| 99 | +- **Contains:** Repository (raw SQL), Migrator |
| 100 | +- **Depends on:** PostgreSQL + pgx/v5 |
| 101 | +- **Used by:** 所有上层服务 |
| 102 | + |
| 103 | +## Data Flow |
| 104 | + |
| 105 | +### Primary: Bootstrap Entry Flow |
| 106 | + |
| 107 | +1. 用户执行 `curl https://gw.example.com/entry/alice | bash` |
| 108 | + - Handler: `internal/controlplane/http/entry.go` → `Script()` |
| 109 | +2. 脚本运行后调用 `POST /v1/entry/alice/auth` |
| 110 | + - Handler: `internal/controlplane/http/entry.go` → `Auth()` |
| 111 | +3. 控制面检查用户/主机状态,如未创建则 queue `create_host` task |
| 112 | + - `internal/runtime/runtime_service.go` → `QueueHostAction()` |
| 113 | +4. Task 通过 dispatcher 发往 Host Agent(或 embedded worker) |
| 114 | + - `internal/agent/server.go` 或 `internal/runtime/tasks/worker.go` |
| 115 | +5. Worker 执行 docker create/start,配置 sing-box 网络 |
| 116 | + - `internal/runtime/tasks/worker.go` → `createHost()` |
| 117 | + - `internal/network/container_proxy_provider.go` → `PrepareHost()` |
| 118 | +6. 容器就绪后,auth 返回 SSH 四元组(host/port/user/pass) |
| 119 | +7. cloud-claude CLI 建立 SSH 连接并启动 claude code |
| 120 | + - `cmd/cloud-claude/main.go` → `ConnectAndRunClaudeV3()` |
| 121 | + |
| 122 | +### Secondary: Admin Dashboard CRUD Flow |
| 123 | + |
| 124 | +1. 管理员登录 → `POST /v1/auth/login` → JWT token |
| 125 | + - `internal/controlplane/http/auth.go` |
| 126 | +2. 前端 API 调用带 `Authorization: Bearer <token>` |
| 127 | + - `internal/controlplane/http/admin_*.go` |
| 128 | +3. 写操作创建 task 或更新 DB,SSE 广播变更 |
| 129 | + - `internal/broadcast/sse.go` → `Broadcast()` |
| 130 | +4. 前端 `use-sse.ts` 接收事件,触发 React Query 刷新 |
| 131 | + |
| 132 | +### Tertiary: Expiry & Reconcile Flow |
| 133 | + |
| 134 | +1. `ExpiryScanner` 每 60s 扫描过期用户 |
| 135 | + - `internal/controlplane/scheduler/expiry.go` |
| 136 | +2. 过期用户状态改为 `expired`,自动停止其运行中主机 |
| 137 | +3. `Reconciler` 每 60s 对比 DB 状态与 Docker 实际状态 |
| 138 | + - `internal/controlplane/scheduler/reconciler.go` |
| 139 | +4. 发现漂移(DB=running 但容器不存在)则自动恢复或标记 stopped |
| 140 | + |
| 141 | +## Key Abstractions |
| 142 | + |
| 143 | +### Task-Driven Async Execution |
| 144 | +- **Purpose:** 所有耗时的 host action(create/start/stop/rebuild)都异步执行 |
| 145 | +- **Pattern:** 创建 Task 记录 → dispatch 到 worker → worker 更新 task 状态 → SSE 广播 |
| 146 | +- **Files:** `internal/store/repository/models.go` (Task 模型), `internal/runtime/tasks/worker.go` |
| 147 | + |
| 148 | +### Host Action Contract |
| 149 | +- **Purpose:** 控制面与 host-agent 之间的操作契约 |
| 150 | +- **Pattern:** `agentapi.HostActionRequest` / `HostActionResponse` JSON over HTTP (Unix socket) |
| 151 | +- **Files:** `internal/agentapi/contracts.go` |
| 152 | + |
| 153 | +### Network Provider Interface |
| 154 | +- **Purpose:** 抽象网络配置,支持 Linux 实现和测试桩 |
| 155 | +- **Pattern:** `network.Provider` interface: `PrepareHost` / `CleanupHost` |
| 156 | +- **Files:** `internal/network/provider.go`, `internal/network/container_proxy_provider.go` |
| 157 | + |
| 158 | +### Repository Pattern |
| 159 | +- **Purpose:** 数据访问抽象,直接 SQL 无 ORM |
| 160 | +- **Pattern:** `repository.Repository` 封装 pgxpool,每个表一组 Query/Scan 方法 |
| 161 | +- **Files:** `internal/store/repository/queries.go` |
| 162 | + |
| 163 | +## Entry Points |
| 164 | + |
| 165 | +**Control Plane:** |
| 166 | +- Location: `cmd/control-plane/main.go` |
| 167 | +- Triggers: systemd service 或 `go run` |
| 168 | +- Responsibilities: 启动 HTTP server、运行 migrations、启动 scheduler、启动 SSH proxy |
| 169 | + |
| 170 | +**Host Agent:** |
| 171 | +- Location: `cmd/host-agent/main.go` |
| 172 | +- Triggers: systemd service(生产环境)或 `go run` |
| 173 | +- Responsibilities: 监听 Unix socket,执行 Docker 和网络操作 |
| 174 | + |
| 175 | +**cloud-claude CLI:** |
| 176 | +- Location: `cmd/cloud-claude/main.go` |
| 177 | +- Triggers: 终端用户直接执行 |
| 178 | +- Responsibilities: 认证、等待容器就绪、SSH 连接、目录挂载、启动 claude code |
| 179 | + |
| 180 | +**Admin Dashboard:** |
| 181 | +- Location: `web/admin/src/main.tsx` |
| 182 | +- Triggers: 浏览器访问 |
| 183 | +- Responsibilities: 管理用户、主机、出口 IP、查看事件和任务 |
| 184 | + |
| 185 | +## Architectural Constraints |
| 186 | + |
| 187 | +- **Threading:** Go 协程模型。HTTP handlers 在 goroutine 中运行;SSE 每个连接一个 goroutine;scheduler 每个 job 一个 goroutine |
| 188 | +- **Global state:** `broadcast.defaultHub` 是包级单例(SSE 连接管理);`errcodes.registry` 是包级单例(错误码注册表) |
| 189 | +- **Circular imports:** 未发现明显循环依赖。`internal/agentapi` 作为共享契约包,被 `controlplane` 和 `agent` 共同依赖 |
| 190 | +- **特权分离:** Web/API 层不直接执行 Docker 命令。特权操作集中在 `internal/agent/` 或 `internal/runtime/tasks/` |
| 191 | +- **网络隔离:** 每个 host 拥有独立的 Docker network + sing-box gateway sidecar,bridge 网络断开以防止 IP 泄漏 |
| 192 | + |
| 193 | +## Anti-Patterns |
| 194 | + |
| 195 | +### Direct Docker Exec from Control Plane |
| 196 | + |
| 197 | +**What happens:** 在 embedded 模式下,control-plane 直接实例化 worker 执行 docker 命令 |
| 198 | +**Why it's wrong:** 模糊了特权边界,未来多宿主机扩展时需要重构 |
| 199 | +**Do this instead:** 始终通过 `agentapi.Dispatcher` 接口 dispatch,embedded 模式使用 `EmbeddedDispatcher` 适配器 |
| 200 | + |
| 201 | +### HTTP Status Code as Business Logic |
| 202 | + |
| 203 | +**What happens:** 部分 handler 使用 HTTP 200 返回内部错误,或在 success body 中嵌入 error 字段 |
| 204 | +**Why it's wrong:** 客户端难以统一处理错误 |
| 205 | +**Do this instead:** 使用正确的 HTTP status code(4xx/5xx),body 统一为 `{error: string}` 或业务数据结构 |
| 206 | + |
| 207 | +## Error Handling |
| 208 | + |
| 209 | +**Strategy:** 分层错误处理 + 统一错误码注册表 |
| 210 | + |
| 211 | +**Patterns:** |
| 212 | +- 基础设施层(docker exec, pgx): 包装为 `fmt.Errorf("...: %w", err)` |
| 213 | +- 业务层: 使用 `internal/cloudclaude/errcodes` 注册表定义结构化错误码 |
| 214 | +- HTTP 层: 统一 `writeJSON` 输出,status code 反映错误类别 |
| 215 | + |
| 216 | +## Cross-Cutting Concerns |
| 217 | + |
| 218 | +**Logging:** `log/slog` 结构化日志,支持 `LOG_LEVEL` 和 `LOG_FORMAT=json` 环境变量 |
| 219 | +**Validation:** 输入校验分散在各 handler 中,无统一校验中间件 |
| 220 | +**Authentication:** JWT (HS256) for admin/user API;entry auth 使用用户名+密码 |
| 221 | +**Authorization:** Role-based (`admin` / `user`),中间件 `RequireRole` |
| 222 | + |
| 223 | +--- |
| 224 | + |
| 225 | +*Architecture analysis: 2026-05-10* |
0 commit comments