# AIOps RDP 实现规格 > 更新:2026-07-05 > 目标:让另一个项目仅阅读本文档,也能实现一个可用的 Web RDP 堡垒机会话。 > 范围:RDP 图形连接、键鼠输入、剪贴板、RDPDR 虚拟盘、文件上传下载、录像回放、实时审计、安全策略。 --- ## 1. 功能边界 ### 1.1 必须实现 | 能力 | 验收标准 | |------|----------| | 图形登录 | 浏览器打开 Windows 桌面,登录后能稳定看到完整画面 | | 键鼠输入 | 鼠标移动/点击/滚轮、普通键、组合键能到达远端 | | 双向剪贴板 | 本地复制到远端、远端 Ctrl+C 后复制到浏览器 | | 文件上传 | 浏览器上传文件后,远端 `\\Go-client\AIOps\Upload` 可见 | | 文件下载 | 远端文件写入 `\\Go-client\AIOps\Download` 后,本地浏览器下载 | | 录像回放 | 会话结束后能按时间顺序播放图形快照 | | 实时审计 | 审计端可只读观看活动 RDP 会话 | | 安全策略 | 后端能强制禁用剪贴板、上传、下载、录像、水印 | ### 1.2 不建议作为第一版实现 | 能力 | 原因 | |------|------| | RDPGFX/H.264 | 协议复杂,先用 Bitmap/FastPath 能完成可用闭环 | | 动态虚拟通道 `drdynvc` | 文件和剪贴板可先用静态通道完成 | | SMB 文件传输 | 外网 445 经常不可达,权限和安全边界差 | | 远端任意路径浏览 | 容易变成文件管理器和权限绕过问题,先用虚拟盘 | | 断点续传 | 大文件体验优化,基础链路稳定后再做 | --- ## 2. 端到端架构 ```text Browser ├── RdpDialog.tsx │ ├── Canvas 绘制 RDPR 二进制帧 │ ├── 捕获 mouse/key/wheel/paste/drop │ └── 发送 JSON input event └── RdpToolsPanel.tsx ├── 粘贴到远端 ├── 复制远端内容 └── 文件上传 Backend HTTP/WebSocket ├── /ws/rdp │ ├── JWT 鉴权 │ ├── 加载资产和 RDP 凭据 │ ├── 加载会话安全策略 │ ├── 创建 sys_rdp_sessions │ ├── 启动 rdp.Engine.Run() │ └── 写入录像、审计、实时广播 ├── /ws/rdp-audit/:sessionId └── /api/v1/rdp-staged/:token RDP Engine ├── TCP + TPKT + X.224 ├── TLS + CredSSP/NLA ├── MCS/GCC/Channel Join ├── DemandActive/ConfirmActive/FontList/SuppressOutput ├── FastPath Bitmap/SurfaceCmd ├── FastPath Input ├── cliprdr 静态虚拟通道 └── rdpdr 静态虚拟通道 ``` 核心分层原则: 1. 控制器负责鉴权、策略、审计、WebSocket、数据库。 2. RDP 引擎只负责协议状态机和帧收发,不依赖 Gin/GORM/React。 3. 前端只知道 WebSocket 帧协议,不知道 CredSSP、MCS、PDU 细节。 4. 文件传输不走远端 SMB,不依赖远端开放 445。 --- ## 3. 后端接口契约 ### 3.1 连接配置 ```go type Config struct { Host string Port int Username string Password string Domain string Security string Width int Height int Debug bool } ``` 实现要求: - `Password` 只允许在服务端内存中存在,不返回给浏览器。 - `Width`、`Height` 是远端桌面初始分辨率,不是浏览器 CSS 尺寸。 - 浏览器最大化优先缩放 Canvas,不要重建 RDP 会话。 ### 3.2 浏览器输入事件 ```json { "type": "mouse_move", "x": 100, "y": 200 } { "type": "mouse_down", "x": 100, "y": 200, "button": 0 } { "type": "mouse_up", "x": 100, "y": 200, "button": 0 } { "type": "wheel", "x": 100, "y": 200, "delta_y": -120 } { "type": "key", "code": "KeyC", "key": "c", "down": true, "ctrl": true } { "type": "clipboard", "text": "from local" } { "type": "remote_copy" } { "type": "rdp_file_upload", "name": "php.ini", "text": "base64..." } ``` 处理规则: - `mouse_move` 高频事件可丢弃或合并。 - `mouse_down` / `mouse_up` / `key down` / `key up` 不允许丢弃。 - `key` 事件必须带 `ctrl`、`alt`、`shift`、`meta`,后端根据状态补齐修饰键。 - `remote_copy` 表示请求后端向远端剪贴板索取文本。 - `rdp_file_upload.text` 是 base64 文件内容,第一版可限制 50 MB。 ### 3.3 服务端帧 JSON 帧: ```json { "type": "session", "message": "43", "width": 900, "height": 596 } { "type": "status", "message": "正在连接 RDP 1.2.3.4:3389" } { "type": "error", "message": "连接失败" } { "type": "clipboard", "text": "from remote" } { "type": "rdp_file_download", "name": "a.txt", "url": "/api/v1/rdp-staged/token" } ``` 二进制图形帧: ```text offset size type desc 0 4 bytes magic, fixed "RDPR" 4 4 uint32 LE x 8 4 uint32 LE y 12 4 uint32 LE width 16 4 uint32 LE height 20 n bytes RGBA, length = width * height * 4 ``` 实现要求: - 实时画面优先使用 `RDPR + RGBA`,避免 PNG/base64 带来的 CPU 和体积开销。 - 历史回放可以保存 PNG data URL,便于持久化和跨版本播放。 - 服务端发送图形帧前必须裁剪到桌面范围,避免前端越界写 Canvas。 --- ## 4. RDP 协议状态机 ### 4.1 连接阶段 ```text TCP connect → X.224 Connection Request → X.224 Connection Confirm → TLS handshake → CredSSP/NLA → MCS Connect Initial → MCS Connect Response → ErectDomainRequest → AttachUserRequest/Confirm → Channel Join: user + global + cliprdr + rdpdr → Client Info → DemandActive → ConfirmActive → Synchronize / Control Cooperate / Control Request Control → FontList / FontMap → SuppressOutput(ALLOW_DISPLAY_UPDATES) → Bitmap/FastPath updates ``` 关键点: - 每一次协议探测失败后,下一次重试要新建 TCP 连接。 - CredSSP 的证书公钥绑定必须按服务端证书 `SubjectPublicKey` 做,不要误用完整 SPKI。 - 收到 `DemandActive` 后才能发送 `ConfirmActive`,能力集必须包含图形、输入、虚拟通道相关能力。 - `SuppressOutput(ALLOW_DISPLAY_UPDATES)` 对部分 Windows 主机很关键,否则登录后可能长时间无画面。 ### 4.2 图形更新处理 ```text FastPath/SlowPath packet → parse update type → bitmap/surface command → MPPC decompress if compressed → RLE decode if bitmap compressed → convert BGRA/RGB565 to RGBA → draw into backend framebuffer → flush dirty rectangle → send RDPR binary frame → throttle snapshot into recorder ``` 实现要求: - 后端维护完整 framebuffer,不能只保存最后一个小块,否则历史回放会碎片化。 - 实时可发送脏矩形,录像建议按固定间隔保存合成后的快照。 - 启动阶段纯黑全屏帧应过滤,减少登录闪屏和黑屏回放。 ### 4.3 输入处理 | 浏览器事件 | RDP 行为 | |------------|----------| | `mouse_move` | Pointer move | | `mouse_down` | Pointer button down | | `mouse_up` | Pointer button up | | `wheel` | Wheel event | | `key down/up` | Scancode keyboard event | | `clipboard` | 写入 cliprdr 本地剪贴板并通知远端 | | `remote_copy` | 请求远端当前剪贴板文本 | 组合键规则: - `Ctrl+C`、`Ctrl+X` 必须同时发修饰键和主键,不能把 `c`、`x` 当普通输入。 - macOS 浏览器中的 `Command+C/V` 可映射成远端 `Ctrl+C/V`。 - 后端审计应识别 `[RDP复制] Ctrl+C`、`[RDP剪切] Ctrl+X`、`[RDP粘贴快捷键] Ctrl+V`。 --- ## 5. 剪贴板实现 ### 5.1 本地到远端 ```text Browser navigator.clipboard.readText() → WebSocket { type: "clipboard", text } → backend policy check clipboard_enabled → cliprdr set local data → send FormatList / FormatDataResponse → Windows remote clipboard update ``` 验收: 1. 本地复制 `hello`。 2. 点击“粘贴到远端”或在 RDP Canvas 执行 Ctrl/Command+V。 3. 远端文本框出现 `hello`。 ### 5.2 远端到本地 ```text User selects text in Windows → remote Ctrl+C → Windows updates remote clipboard → cliprdr FormatList arrives → backend requests CF_UNICODETEXT → backend WebSocket { type: "clipboard", text } → Browser navigator.clipboard.writeText(text) ``` 验收: 1. 远端记事本输入 `rdp-copy-ok` 并选中。 2. 在远端按 Ctrl+C。 3. 点击“复制远端内容”或等待回传。 4. 本地 Command+V 能粘贴 `rdp-copy-ok`。 常见失败: | 现象 | 原因 | 处理 | |------|------|------| | 远端 Ctrl+C 只输入 c | 修饰键状态未发送或被前端拦截 | key event 带 `ctrl=true`,后端补发 Ctrl down/up | | 浏览器无法写入剪贴板 | 浏览器权限限制 | 必须在用户点击按钮的交互上下文执行 | | 工具提示远端剪贴板为空 | 未收到 FormatList 或请求过早 | Ctrl+C 后延迟 200-500ms 再请求 | --- ## 6. RDPDR 虚拟盘文件传输 ### 6.1 远端展示 ```text 此电脑 └── Go-client 上的 AIOps ├── Upload └── Download ``` 固定命名: | 项 | 值 | |----|----| | Client Name | `Go-client` | | Drive Name | `AIOps` | | UNC | `\\Go-client\AIOps` | | Upload | `\\Go-client\AIOps\Upload` | | Download | `\\Go-client\AIOps\Download` | ### 6.2 RDPDR 最小能力 必须处理: 1. Server Announce Request。 2. Client Announce Reply。 3. Client Name Request。 4. Server Client ID Confirm。 5. Client Device List Announce。 6. Device Create/Open。 7. Directory Query。 8. File Read。 9. File Write。 10. Close。 虚拟文件系统模型: ```text / ├── Upload/ # 后端提供给远端读 │ └── user uploaded files └── Download/ # 远端写入,后端捕获 └── remote copied files ``` ### 6.3 上传链路 ```text Browser file input/drop → FileReader base64 → WebSocket { type: "rdp_file_upload", name, text } → policy check file_upload_enabled → rdpdr.AddUpload(name, bytes) → Explorer opens \\Go-client\AIOps\Upload → Windows reads file from virtual disk ``` 验收: - 上传 `php.ini` 后,远端 `Upload` 目录出现文件。 - 远端可以打开文件。 - 远端可以复制到桌面或其他目录。 ### 6.4 下载链路 ```text Windows copies file to \\Go-client\AIOps\Download → rdpdr Device Write → backend captures bytes → filestage.Put(name, data, ttl=30min) → WebSocket { type: "rdp_file_download", name, url } → Browser opens url → /api/v1/rdp-staged/:token returns attachment ``` 验收: - 远端将 `a.txt` 拖入 `Download`。 - 浏览器自动触发下载。 - 本地下载文件内容与远端一致。 实现注意: - 不要用 SMB 管理共享作为主路径。 - 文件名必须清理路径分隔符,避免 `../`。 - `filestage` token 必须有 TTL,下载后可删除或等待过期。 - 大文件后续应改为分片和磁盘暂存,避免长期占用内存。 --- ## 7. 审计、录像与安全策略 ### 7.1 数据模型 RDP 会话表: ```text sys_rdp_sessions id user_id / username server_id / server_name / server_ip / server_port client_ip width / height status: active | closed | failed duration error record_data record_size start_time / end_time ``` 操作审计表: ```text sys_session_commands session_id session_type = rdp user_id / username server_id / server_name command output_size timestamp ``` 策略表: ```text sys_audit_settings session_watermark_enabled session_record_enabled clipboard_enabled file_upload_enabled file_download_enabled command_policy_enabled approval_required ``` ### 7.2 录像策略 - 实时帧可以是 RGBA 脏矩形。 - 录像帧必须是合成快照,避免回放只看到小块。 - 推荐最小间隔 500ms。 - 过滤启动阶段纯黑全屏。 - 关闭 `session_record_enabled` 后仍记录会话元数据,但不保存 `record_data`。 ### 7.3 审计策略 建议记录: ```text [RDP输入] hello [RDP复制] Ctrl+C [RDP剪切] Ctrl+X [RDP粘贴] http://localhost:8080/servers [RDP上传到虚拟盘] php.ini [RDP下载] a.txt ``` 不建议记录: - 每个 `mousemove`。 - 每个 RGBA 实时帧。 - 空白按键和无业务意义坐标。 ### 7.4 策略拦截 前端禁用按钮只是体验优化,后端必须再次拦截: | Event | 策略 | |-------|------| | `clipboard` | `clipboard_enabled` | | `remote_copy` | `clipboard_enabled` | | `rdp_file_upload` | `file_upload_enabled` | | `rdp_file_download` | `file_download_enabled` | | 录像写入 | `session_record_enabled` | --- ## 8. 前端实现要求 ### 8.1 Canvas 绘制 ```text WebSocket binary → parse RDPR header → create ImageData(width, height) → imageData.data.set(rgba) → canvas.putImageData(imageData, x, y) ``` 要求: - Canvas 内部分辨率等于远端桌面分辨率。 - CSS 尺寸可以缩放,但鼠标坐标必须换算回远端坐标。 - 最大化和窗口缩放不应重新连接 RDP。 ### 8.2 输入捕获 - RDP Canvas 必须可 focus,点击后接收键盘事件。 - 捕获 `keydown` 和 `keyup`,都发给后端。 - 浏览器默认快捷键如 Ctrl+C/Ctrl+V 需要按场景阻止默认行为。 - macOS Command 复制粘贴要映射到远端 Ctrl。 ### 8.3 工具面板 工具面板只保留高价值动作: | 动作 | 说明 | |------|------| | 粘贴到远端 | 从浏览器剪贴板读取并写入远端 | | 复制远端内容 | 请求远端剪贴板并写入浏览器剪贴板 | | 上传文件 | 发送到虚拟盘 Upload | | 虚拟盘说明 | 展示 `Go-client 上的 AIOps`、Upload、Download 的用途 | 不要再展示 SMB、445、管理共享等文案。 --- ## 9. 调试和验收 ### 9.1 日志关键字 ```bash tail -f logs/aiops-local.log | rg "RDP|cliprdr|rdpdr|RDPR" ``` | 日志 | 含义 | |------|------| | `[RDP] ws connected` | 浏览器已连接 | | `[RDP] engine start` | 开始连接远端 3389 | | `[RDP] bitmap session=` | 收到图形帧 | | `[cliprdr]` | 剪贴板通道 | | `[rdpdr] device list announced` | 虚拟盘公告 | | `[rdpdr] upload staged` | 上传文件已暂存 | | `[rdpdr] download captured` | 下载文件已捕获 | ### 9.2 自动测试建议 | 层级 | 用例 | |------|------| | `credssp` | NTLMv2、公钥绑定、证书变体 | | `rle` | 8/15/16/24/32 bpp 位图解码 | | `display` | framebuffer 合成、脏矩形裁剪 | | `rdpdr` | 目录枚举、文件读写、非法路径 | | `filestage` | token TTL、并发下载、文件名清理 | | 前端 | RDPR 解析、坐标缩放、快捷键映射 | ### 9.3 手动验收矩阵 | 场景 | 预期 | |------|------| | 13 寸屏打开 RDP | 能看到边框、工具栏、Canvas | | 最大化窗口 | 不重连、不闪屏 | | 登录初期黑屏 | 回放中不出现大量黑屏段 | | 本地复制远端粘贴 | 成功 | | 远端复制本地粘贴 | 成功 | | 上传小文件 | `Upload` 可见、可打开、可复制 | | 远端下载文件 | 浏览器自动下载 | | 关闭剪贴板策略 | 前端禁用,后端拒绝 | | 关闭上传策略 | 前端禁用,后端拒绝 | | 实时审计 | 审计端可看到当前画面 | | 历史回放 | 画面完整,不是大量小块 | --- ## 10. 常见问题 | 问题 | 优先排查 | |------|----------| | 只能看到 C 盘,没有 AIOps 虚拟盘 | `rdpdr` 是否完成 Device List Announce,Windows 是否刷新资源管理器 | | `\\tsclient\AIOps` 无权限 | 路径名/ClientName 不一致,或 rdpdr create/read/write 返回码错误 | | 远端 Ctrl+C 不能复制出来 | cliprdr FormatList/FormatDataRequest 是否到达,是否过早请求 | | 文件可打开但不能复制到桌面 | rdpdr Read/Close/QueryInfo 返回不完整,或文件属性/长度错误 | | 回放是小块或黑屏 | 没有 framebuffer 合成,或录像保存了实时脏矩形 | | 最大化闪屏 | 前端重建了 WebSocket,应只缩放 Canvas | | 上传大文件卡顿 | base64 + WebSocket 一次性发送,后续改分片 |