Files
SimpleRemoter/docs/Mcp_Machine_Design.md
T
yuanyuanxianganddeepseek-flash 38cd207f5d Feature: Add host management and connection management MCP tools
Expose the online-list TLS column through list_online_hosts, search_hosts
and get_host_detail. The value comes from the flag the client reports at
login, so old clients always read false.

Add list_network_connections and kill_network_connection on top of the
connection manager sub-connection protocol. The kill path matches the
original 110-byte record instead of rebuilding addresses, supports a dry
run, refuses UDP/IPv6 targets, and verifies that the quadruple really
disappeared instead of assuming success.

Add a host_* family backed by the machine manager protocol: ten read
tools covering every tab of the host management dialog, and four
recoverable write tools for process freeze/thaw, window restore/hide/
maximize/minimize, service start/stop/pause/continue/restart, and
scheduled task run/stop. Every write re-reads the snapshot before sending
and re-checks afterwards, reporting -32009 for stale targets rather than
acting blindly. The family is gated by McpMachine, the risky subset by
McpMachineDangerous, and writes additionally require read-only mode to
be off.

Route managed sub-connections from MessageHandle and OfflineProc, keep
the single-in-flight rule by sharing the pending table with a request
kind, and move the shared JSON/lock/encoding helpers into McpInternal so
both tool families use one implementation. Settings dialog, design docs
and en_US/zh_TW/de_DE translations are updated accordingly.

Co-Authored-By: deepseek-flash
2026-10-21 20:01:41 +02:00

274 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` 去掉末尾 `"\<name>"` 派生 |
| 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 <id> <tool>: 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 工具,服务端无残留子连接。