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

16 KiB
Raw Blame History

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(从而天然保证「单设备单飞行」), 而不是另开一套状态。

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 工具,服务端无残留子连接。