# MCP P8 设计:在线主机 TLS 字段 / 连接管理 / 主机管理 > 延续 `Mcp_Design.md` → `Mcp_Phase2_Design.md`(P2)→ P3~P6 → `Mcp_Manage_Design.md`(P7)。 > P7 复用的是「系统/窗口/服务」对话框使用的 `COMMAND_SYSTEM`、`COMMAND_WSLIST`、 > `COMMAND_SERVICECONFIG` 链路;本轮要覆盖的「连接管理」与「主机管理」走的是**另一套** > 命令(`COMMAND_CONNECTION` / `COMMAND_MACHINE_MANAGE`),必须在同一子连接上收发多步, > 因此新增一层会话抽象,并为工具族增加独立开关。 > > 安全门结论(已确认):新增独立 `host_*` 工具族;写入范围限定「只读 + 可恢复操作」; > 开关分两层——`McpMachine`(工具族总开关)与 `McpMachineDangerous`(高风险子集), > 均默认关闭,并与 `McpReadonly` 叠加。现有 27 个工具(含 P7)一律不改行为。 ## 1. 范围 | 交付 | 内容 | |---|---| | TLS 字段 | `list_online_hosts` / `search_hosts` / `get_host_detail` 增加 `tls` | | 连接管理 | `list_network_connections`、`kill_network_connection` | | 主机管理读 | 10 个标签页各一个:`host_list_processes` … `host_get_hosts_file` | | 主机管理写 | `host_control_process` / `host_control_window` / `host_control_service` / `host_control_task` | | 配置 | 设置对话框两个复选框 + 启动接线(`McpMachine` / `McpMachineDangerous`) | 不做的(明确排除):kill 进程、关闭窗口、删除服务/任务、卸载软件、写 hosts、 创建任务、强制删除文件、代码注入。 ## 2. TLS 字段 - `HostJson.cpp` 的 `BuildHostJson()` 末尾追加 `device["tls"] = (ctx->GetClientData(ONLINELIST_TLS) == "Y");`。 数据源是登录时写入的 `ONLINELIST_TLS`(`2015RemoteDlg.cpp` 收到 `RES_TLS=="1"` 时置 `"Y"`)。 - 语义:**登录凭据**,不是实时探测。老客户端不上报该值 → `false`。 - `BuildHostJson` 同时被 WebService 的设备列表复用,Web 端也会多出该字段(向后兼容)。 - `get_host_detail` 复用 `BuildHostItemSchema()`,自动带上。 ## 3. 会话架构 两条新链路都需要「主连接发一条命令 → 客户端新建子连接 → 子连接回就绪包 → 在同一子连接上多次收发」,现有 `m_Pending` 只能等主连接上的单包。做法是给 `PendingRequest` 增加 `kind`,**复用同一张表** `m_Pending`(从而天然保证「单设备单飞行」), 而不是另开一套状态。 ```cpp enum class PendingKind { Generic, FollowUp, Machine, ConnMgr }; ``` | 成员 | 用途 | |---|---| | `m_SubCtxToDevice` | 已绑定的子连接 → device;绑定的子连接上所有包都由 MCP 消费 | | `m_AbsorbedSubCtx` | `CancelIO` 后仍可能到达的包:静默丢弃 | | `m_OrphanByDevice` / `m_OrphanByPeer` | 5 秒墓碑,防止迟到的就绪包弹出 MFC 对话框 | ### 3.1 时序(工具线程) ``` BeginMachineSession(dev, tool, mainCtx) // 登记单飞行;忙 → -32003 → 主连接 Send2Client(COMMAND_MACHINE_MANAGE) → 客户端开 CMachineManager 子连接,回 TOKEN_SYSINFOLIST(61) → OnManagedSubPacket 绑定子连接、保存首包、notify_all WaitManagedPacket(dev, 61, …, 5000) // 就绪 每次读: ArmManagedResponse(dev, 期望 token) → 子连接 Send2Client([tab 0..9]) / WaitSendIdle → WaitManagedPacket(dev, 期望 token, …, 12000) EndManagedSession(dev, cancelIo=true) // 关闭子连接,不留长连接 ``` 整体 60 秒会话截止;每一步取 `min(step, deadline - now)`。所有超时返回 `-32001`。 ### 3.2 子连接路由(`OnManagedSubPacket`) 在 `3.1` 的模型下,UI 线程收到的每个包先经过钩子 (`MessageHandle` 中终端拦截块之后、`switch (cmd)` 之前): 1. 已绑定 → 只保留 `expectedToken` 匹配的包;`TOKEN_MACHINE_MSG(42)` 存为 `clientError`(CP936→UTF-8);其余丢弃。已绑定的子连接上的一切包都归 MCP。 2. `m_AbsorbedSubCtx` 命中 → 丢弃。 3. 未绑定且 token 不是 `61` / `255` → 交回主 switch(MFC 行为不变)。 4. 未绑定且是 `61` / `255`: - 该 context 就是主连接(`FindMainContext(parent, cid)`)→ 交回主 switch; - 否则在同 device 的未绑定会话中按 `clientID` 精确绑定; - 老客户端子连接 `clientID` 可能为 0 → 按 `peer` 兜底,**仅当同 peer 唯一**时才绑定; - 都不命中:墓碑未过期 → 吸收 + `CancelIO` + 消费;否则交回主 switch (用户手动打开对话框)。 ### 3.3 必须同步调整的现有语义 - `IsPending()` 排除 `Machine` / `ConnMgr`:否则会话在飞时用户点「进程管理」, `case TOKEN_PSLIST` 会把界面包抢走。 - `TakeMainResponse()` 顶部对 managed kind 直接返回。 - `OfflineProc` 在 `OnManageSubClosed` 之后、`RemoveFromHostList` **之前**调用 `OnManagedContextClosed`(否则连接池复用同一 context 指针后,新连接的包会被误吞)。 - 锁序:`parent->m_cs` → `m_PendingMutex`;持锁期间禁止 `Send2Client` / `CancelIO` / `WaitSendIdle`。 ### 3.4 健壮性 - UI 线程只做「有界拷贝 + 保存 + 唤醒」:单包上限 8 MiB,超限置 `clientError` 并 `CancelIO`。 - 解析全部在 MCP 工作线程上做;字符串一律用 `BoundedStrlen` / 显式偏移, 未对齐的多字节整数用 `memcpy`,禁止指针强转。 - 客户端部分列表用 `Send(buf, LocalSize(buf))` 发送,长度可能带出少量尾随 0 填充: 记录解析循环遇到「剩余全 0」即结束,定长记录列表忽略不足一条记录的尾部。 ## 4. 连接管理 ### 4.1 协议 - 主连接:`COMMAND_CONNECTION(253)`;客户端建 `CConnectionsManager` 子连接。 - 子连接回 `[255][u16 LE count][ConnectionInfo × count]`。 - `ConnectionInfo`(`common/commands.h`,`#pragma pack(1)`,110 字节,已 `static_assert`): `proto(1)` / `af(1)` / `pid(4)` / `localAddr[16]` / `localPort(2,主机序)` / `remoteAddr[16]` / `remotePort(2,主机序)` / `state(4)` / `procName[64](UTF-8)`。 地址为网络序;IPv4 取前 4 字节,`af == AF_INET(2)`,IPv6 为 `AF_INET6(23)`。 - 强制断开:子连接发 `[254][原始 110 字节记录]`;客户端 `SetTcpEntry(DELETE_TCB)` 后自动重发一次 255。 ### 4.2 工具 | 工具 | 门控 | 说明 | |---|---|---| | `list_network_connections` | `McpMachine` | 过滤 `protocol/family/pid/process/local_port/remote_port/remote_address/state` + 分页;输出 `killable = (proto==1 && af==AF_INET)` | | `kill_network_connection` | `McpMachine && McpManage && !McpReadonly` | 必填四元组;可选 `pid` 消歧、`dry_run` | `kill_network_connection` 的流程刻意避免「重新拼地址」: 1. 取最新列表,**按原始 110 字节记录精确匹配**四元组(可选同时校验 PID); 2. 匹配 0 条 → `-32009`(调用方应刷新);匹配多条 → `-32602`(要求给 `pid`); 3. 目标不是 IPv4 TCP → `-32005`,不发包; 4. `dry_run` → 直接返回目标; 5. `Arm(255)` → 发 `[254][原记录]` → 等刷新列表 → 复核该四元组是否消失。 客户端 `SetTcpEntry` 的返回值被忽略且需要管理员权限,所以「断开后仍在」返回 `structuredContent.ok=false, reason="still_present"`,**绝不谎报成功**。 每次调用写审计:`tcp a:p->b:q pid=N result=…`。 ## 5. 主机管理 ### 5.1 协议 - 主连接:`COMMAND_MACHINE_MANAGE(162)` → 客户端 `CMachineManager` 子连接回 `TOKEN_SYSINFOLIST(61)`。 - 之后在同一子连接上发 **1 字节 tab**(0–9),响应 token 31–42。 - 动作 10–30;多数动作客户端会自动回一份刷新后的列表(服务 = 40,任务 = 41), 进程冻结/解冻(16/17)与窗口操作(11)**不回包**。 `MachineManager` 枚举(`common/commands.h`):请求 0 PROCESS / 1 WINDOWS / 2 NETSTATE / 3 SOFTWARE / 4 HTML / 5 FAVORITES / 6 WIN32SERVICE / 7 DRIVERSERVICE / 8 TASK / 9 HOSTS; 响应 31 PROCESS / 32 WINDOWS / 33 NETSTATE / 34 SOFTWARE / 35 HTML / 36 FAVORITES / 37 WIN32SERVICE / 38 DRIVERSERVICE / 39 HOSTS / 40 SERVICE_LIST / 41 TASKLIST / 42 MACHINE_MSG。 ### 5.2 读工具 | 工具 | tab | 响应 | 主要过滤 | |---|---|---|---| | `host_list_processes` | 0 | 31 | `pid` / `name` / `arch` | | `host_list_windows` | 1 | 32 | `pid` / `title` | | `host_list_network_state` | 2 | 33 | `pid` / `process` / `protocol` / `local_port` / `remote_port` / `state` | | `host_list_software` | 3 | 34 | `name` / `publisher` | | `host_list_browser_history` | 4 | 35 | `title` / `url` | | `host_list_favorites` | 5 | 36 | `name` / `url` | | `host_list_win32_services` | 6 | 40 | `name` / `status` / `start_type` | | `host_list_driver_services` | 7 | 40 | 同上 | | `host_list_scheduled_tasks` | 8 | 41 | `name` / `folder` / `status` | | `host_get_hosts_file` | 9 | 39 | `include_content` / `max_bytes` | 公共分页 `offset`(默认 0)/ `limit`(默认 100,上限 1000);输出 `total`(过滤后)/ `totalAll` / `offset` / `limit` / `hasMore`。 ### 5.3 记录格式(与 `CMachineDlg::Show*` 逐字段对齐) | 列表 | 格式 | |---|---| | 进程 31 | `DWORD pid`、`bool is64`、`title[260]`、`exe`+pad、`path`+pad、`DWORD priClass`、`DWORD threads`、`user`+pad、`DWORD workingSetKb`、`DWORD fileSizeKb`;CP936 | | 窗口 32 | `WINDOWSINFO` 定长 1044 字节;**只含可见窗口**;空列表 = 1 字节 | | 网络状态 33 | 进程名、`DWORD pid`、`[TCP]/[UDP]`、`ip:port`、远端、状态,各串 +2;UDP 远端 `*.*.*.*:*`、状态为空;按最后一个 `:` 拆地址 | | 软件 34 | 5 个串各 +2:显示名、发布者、版本、安装日期、卸载命令 | | 浏览记录 35 | 定长 2148:`time[100]` + `title[1024]` + `url[1024]` | | 收藏夹 36 | 2 个串各 +2 | | 服务 40 | 8 个串各 +1 NUL:显示名、描述、状态、启动类型、登录身份、交互式、**服务名**、可执行路径 | | 计划任务 41 | `name/path/exe` 为 UTF-8、`status` 为 GBK,各 +1 NUL;两个 `DATE`;两个 0 字节结束;`folder` 由 `path` 去掉末尾 `"\"` 派生 | | hosts 39 | `[39][文件原文][0]`;有 BOM → UTF-8,否则合法 UTF-8 → UTF-8,否则 CP936 | ### 5.4 写工具 统一模式:**先取列表校验目标存在 → 发动作 → 再取列表复核**。目标不在最新快照里 返回 `-32009`,绝不盲发。 | 工具 | action | 额外门控 | 动作包 | 复核 | |---|---|---|---|---| | `host_control_process` | `freeze` / `thaw` | `McpMachineDangerous` | `[16/17][DWORD pid]`(无回包) | 重新取 tab 0:PID 仍在 | | `host_control_window` | `restore/hide/maximize/minimize` | — | `[11][DWORD hwnd][DWORD SW_*]` + 零填充到 20 字节(客户端只读前 9 字节) | 重新取 tab 1:hide 后 hwnd 应消失,其余应仍在 | | `host_control_service` | `start`/`pause`/`continue` | — | `[22/24/25][服务名 CP936][NUL]` | 客户端自动回 40 | | `host_control_service` | `stop`/`restart` | `McpMachineDangerous` | stop = `[23]`;restart = stop → 等 40 → start | 等 40(12 s),状态在期望集合内 | | `host_control_task` | `run` / `stop` | — | `[29/28][folder CP936][NUL][name CP936][NUL]` | 客户端自动回 41:任务仍在 + 当前状态 | 补充规则: - 服务动作前先取一次服务列表(prime),既校验目标,也把客户端 `m_dwServiceType` 钉到正确类型(Win32 / 驱动)。 - 已是目标状态时不下发动作,返回 `ok=true, changed=false, reason=already_*`。 - 冻结/解冻、窗口动作、任务动作**没有回执**:`verified` 只表示「已用动作后的快照复核」, 不表示「动作一定生效」。冻结不改变进程存在性,因此只能确认目标仍在。 - 写工具输入串(服务名、任务 folder/name)必须能转成 CP936,否则 `-32602`,不发包。 - 审计格式:`host : action=… target=… result=…`。 ## 6. 门控矩阵 | 配置 | 可见工具 | 直接调用被隐藏的工具 | |---|---|---| | `McpMachine=0`(默认) | 0 个 | `-32006` | | `McpMachine=1, McpReadonly=1` | 11 个读工具(`list_network_connections` + 10 个 `host_list_*` / `host_get_hosts_file`) | 5 个写工具 → `-32006` | | `McpMachine=1, McpReadonly=0, McpManage=1` | 16 个(含 5 个写) | — | | `McpMachine=1, McpReadonly=0, McpManage=0` | 15 个(无 `kill_network_connection`) | `kill_network_connection` → `-32006` | | `McpMachineDangerous=0` | 工具仍可见 | `host_control_process`、`host_control_service{stop,restart}` → `-32006` | 设置对话框勾选「危险主机操作」但未勾「主机管理」时,保存会清除危险开关并提示。 ## 7. 错误码 沿用 P1–P7 的约定,新增 `-32009`: | 码 | 含义 | |---|---| | `-32602` | 参数非法(含:目标多条匹配、字符无法用 CP936 表示) | | `-32000` | 解析失败 / 客户端返回错误文本(`TOKEN_MACHINE_MSG`) | | `-32001` | 超时(就绪 5 s / 列表 12 s / 会话 60 s) | | `-32002` | 主机离线 | | `-32003` | 设备忙(已有 MCP 会话在飞:单设备单飞行) | | `-32004` | 发送失败 | | `-32005` | 不支持(非 Windows 客户端;UDP/IPv6 断连) | | `-32006` | 开关未开启 | | `-32009` | **目标在最新快照中不存在**(调用方应刷新后重试) | ## 8. 已知限制 1. **token 数值碰撞**:机器响应 31–42 与 `COMMAND_KEYBOARD/AUDIO/SYSTEM/SHELL/…` 同值。 靠「先按子连接身份路由」解决——未绑定的子连接不会按 token 猜测。 2. **UI 争抢就绪包**:用户恰好在 MCP 请求期间点开同一对话框时,首包可能被 MCP 绑定, 界面对话框这次不弹(与 P7 的取舍一致),重试即可。 3. **5 s 墓碑**:MCP 异常结束后 5 s 内手动打开同一台主机的对话框可能被吞,重试即可。 4. **老客户端子连接 `clientID=0`**:靠 peer 兜底,仅在同 peer 唯一会话时绑定。 5. **权限**:`SetTcpEntry`、冻结/解冻、服务控制需要客户端管理员;失败按「未生效」如实返回。 6. **编码**:机器协议以 CP936 为主;任务 `name/path/exe` 为 UTF-8、`status` 为 GBK; 写回输入统一转 CP936。GBK 不可表示的字符明确报 `-32602`。 7. **`host_list_network_state` 只有 IPv4**:数据来自客户端自身的 `GetTcpTable` 视图, 与「连接管理」(`ConnectionInfo`,含 IPv6)不是同一份数据。 8. **`McpMachineDangerous` 只覆盖本轮高风险动作**;后续加入 kill/删除/卸载/注入时, 必须按 `McpMachine && McpMachineDangerous && !McpReadonly` 三层校验。 9. **`host_control_window {action:"hide"}` 是单向的**:客户端只枚举可见窗口,隐藏后该 hwnd 从快照中消失,再发 `restore` 会在发包前被 `-32009` 拦下(MFC 对话框同样无法再操作它)。 实测确认:`hide` 复核为 `state:"hidden"`,随后 `restore` 返回 `-32009`。 10. **`host_control_service {action:"pause"}` 依赖服务自身支持 PAUSE**:实测虚拟机上的 `Spooler` 下发 PAUSE 后状态仍为 `Running`,工具如实返回 `ok:false, reason:"unexpected_state"` (不谎报成功)。`start/stop/continue/restart` 在同一台机器上均通过。 11. **开关只在进程启动时读取**(Release 版配置来自注册表 `HKCU\Software\YAMA\settings`, 不是 `Bin\settings.ini`);在「MCP设置」里改动 `McpMachine` / `McpMachineDangerous` / `McpReadonly` 等必须重启服务端才生效。未重启时 `tools/list` 里看不到新工具, 直接调用返回 `-32006`。 ## 9. 验证要点 - TLS:`RES_TLS=1` 的主机三个工具都返回 `tls:true`;老客户端 `false`。 - 路由:MCP 关闭时手动打开「主机管理」「连接管理」照常工作; `host_list_favorites`(token 36,与键盘等命令同值)在飞时手动打开「进程管理」, 两边都不串包。 - 迟到包:强制就绪超时后,61/255 到达应被吸收且不弹对话框。 - 单飞行:同主机并发两个 host/connection 工具,第二个 `-32003`;两台主机并发都成功。 - 连接管理:与对话框逐条对比;断开后复核消失;UDP/IPv6 → `-32005`;过期四元组 → `-32009`。 - 10 个读工具:与 MFC 标签页对比行数、首末行、中文编码;空 tab 返回 `total:0`。 - 4 个写工具:freeze→thaw、窗口四动作、服务五动作、任务 run/stop;过期目标发包前 `-32009`。 - 回归:现有 27 个工具、终端、远程控制、文件传输、10 个 MFC 标签页行为不变。 - 资源:连续调用 20 次 host 工具,服务端无残留子连接。