# 审计 — 浏览器 RDP（guacd）+ 3389远程页

**Changelog**: `docs/changelog/2026-06-10-rdp-browser-guacd.md`  
**设计**: `docs/design/specs/2026-06-10-rdp-browser-guacd-design.md`

## 变更文件

| 文件 | 说明 |
|------|------|
| `docker/docker-compose.prod.yml` | 增加 `guacd` 侧车、`NEXUS_GUACD_*` |
| `docker-compose.yml` | 开发环境 guacd |
| `server/config.py` | `GUACD_HOST` / `GUACD_PORT` |
| `server/main.py` | 注册 `rdp_router` |
| `server/api/rdp.py` | `WS /ws/rdp/{server_id}` JWT + guacd 桥 |
| `server/api/servers.py` | `GET /{id}/rdp-connect`、RDP 字段 CRUD |
| `server/api/schemas.py` | `rdp_*` 表单字段 |
| `server/utils/rdp_config.py` | `extra_attrs` 存储、密码加密 |
| `server/infrastructure/guacamole/tunnel.py` | WS↔TCP 透明转发 |
| `frontend/package.json` | `guacamole-common-js` |
| `frontend/src/App.vue` | 菜单「3389远程」 |
| `frontend/src/router/index.ts` | `/rdp` 路由 |
| `frontend/src/composables/useRoutePrefetch.ts` | 预载 RdpPage |
| `frontend/src/composables/rdp/useRdpSession.ts` | Guacamole 客户端 |
| `frontend/src/composables/rdp/useRdpServerList.ts` | RDP 服务器选择器 |
| `frontend/src/pages/RdpPage.vue` | 独立页 + 选择器 |
| `frontend/src/pages/ServersPage.vue` | 列表 RDP 跳转 |
| `frontend/src/components/servers/ServerFormDialog.vue` | RDP 配置 UI |
| `frontend/src/composables/servers/useServerFormDialog.ts` | RDP 表单 payload |
| `frontend/src/types/api.ts` | `rdp_*` 类型 |
| `frontend/src/types/guacamole.d.ts` | Guacamole 类型声明 |
| `frontend/src/api/index.ts` | `downloadGet`（保留，非 RDP 主路径） |
| `tests/test_rdp_config.py` | 配置与 connect 字符串单测 |

## 入口表

| 方法 | 路径 | 鉴权 | 说明 |
|------|------|------|------|
| GET | `/api/servers/{id}/rdp-connect` | `get_current_admin` | 返回 hostname/port/user/**明文密码** |
| WS | `/ws/rdp/{server_id}?token=` | access JWT（拒绝 `purpose=webssh`） | 校验 `rdp_enabled` 后桥接 guacd |
| PUT/POST | `/api/servers` `rdp_*` 字段 | `get_current_admin` | 密码 `encrypt_value` 入 `extra_attrs` |

## Step 3（规则扫描）

| 规则 | 结论 | 说明 |
|------|------|------|
| 鉴权 | PASS | REST/WS 均需管理员 JWT；WS 拒绝 webssh 专用票 |
| IDOR | PASS（平台模型） | 与终端/文件一致：任意管理员可连任意已启用 RDP 的服务器 |
| 密钥落库 | PASS | `rdp_password` Fernet 加密存 `extra_attrs`；列表 API 仅 `rdp_password_set` |
| 明文回传 | RISK 接受 | `rdp-connect` 向已登录管理员返回解密密码（Guacamole 客户端必需）；HTTPS 依赖生产 TLS |
| WS token 在 URL | RISK 已知 | 同 WebSSH/ADR-011；access JWT 非短票 |
| 连接审计 | 设计外 | **故意无** `rdp_connect` / `SshSession` 审计（用户确认） |
| guacd 暴露 | PASS | 仅 Docker 内网 `guacd:4822`，不映射宿主机端口 |
| 静默吞错 | PASS | guacd 失败打 `warning`/`exception` 后 close WS；前端 `formatApiError` |
| 注入 | PASS | 无 shell；Guacamole 参数由服务端校验后前端 encodeURIComponent |
| 依赖 | RISK 低 | `guacamole-common-js@1.5.0` npm 包；`guacd` 官方镜像 |

## Closure（核心文件走读）

| 文件 | 行数 | 结论 |
|------|------|------|
| `server/api/rdp.py` | 110 | SAFE — JWT+tv 校验；`rdp_enabled` 后才 `accept`；无审计为设计项 |
| `server/infrastructure/guacamole/tunnel.py` | 58 | SAFE — 纯字节转发；断开时 cancel task + close writer |
| `server/utils/rdp_config.py` | 151 | SAFE — 密码 encrypt/decrypt；`apply_rdp_payload` 空密码可 clear |
| `server/api/servers.py`（rdp-connect） | §1141 | SAFE — 503 无 guacd；400 无密码/未启用 |
| `frontend/.../useRdpSession.ts` | 144 | SAFE — token 来自 auth store；密码仅内存+connect 字符串 |
| `frontend/.../RdpPage.vue` | 185 | SAFE — 仅选 `rdp_enabled` 服务器；无 v-html |

**规则零命中（额外 P0/P1）**：无未授权 RCE、无 SQL 拼接、无硬编码生产密钥。

## 验证

```bash
pytest tests/test_rdp_config.py -q          # 4 passed
cd frontend && npm run type-check         # pass
cd frontend && npx vite build             # pass（含 guacamole-common-js）
```

## DoD

- [x] 设计文档 + changelog
- [x] Step 3 + Closure + 入口表
- [x] 单测与前端 type-check
- [ ] 生产部署：guacd 容器 + `NEXUS_GUACD_HOST` + 前端 sync（待执行）
- [ ] 浏览器验收：侧栏「3389远程」→ 选机 → 画布连接（待生产）
