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
16 KiB
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) 之前):
- 已绑定 → 只保留
expectedToken匹配的包;TOKEN_MACHINE_MSG(42)存为clientError(CP936→UTF-8);其余丢弃。已绑定的子连接上的一切包都归 MCP。 m_AbsorbedSubCtx命中 → 丢弃。- 未绑定且 token 不是
61/255→ 交回主 switch(MFC 行为不变)。 - 未绑定且是
61/255:- 该 context 就是主连接(
FindMainContext(parent, cid))→ 交回主 switch; - 否则在同 device 的未绑定会话中按
clientID精确绑定; - 老客户端子连接
clientID可能为 0 → 按peer兜底,仅当同 peer 唯一时才绑定; - 都不命中:墓碑未过期 → 吸收 +
CancelIO+ 消费;否则交回主 switch (用户手动打开对话框)。
- 该 context 就是主连接(
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 的流程刻意避免「重新拼地址」:
- 取最新列表,按原始 110 字节记录精确匹配四元组(可选同时校验 PID);
- 匹配 0 条 →
-32009(调用方应刷新);匹配多条 →-32602(要求给pid); - 目标不是 IPv4 TCP →
-32005,不发包; dry_run→ 直接返回目标;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. 已知限制
- token 数值碰撞:机器响应 31–42 与
COMMAND_KEYBOARD/AUDIO/SYSTEM/SHELL/…同值。 靠「先按子连接身份路由」解决——未绑定的子连接不会按 token 猜测。 - UI 争抢就绪包:用户恰好在 MCP 请求期间点开同一对话框时,首包可能被 MCP 绑定, 界面对话框这次不弹(与 P7 的取舍一致),重试即可。
- 5 s 墓碑:MCP 异常结束后 5 s 内手动打开同一台主机的对话框可能被吞,重试即可。
- 老客户端子连接
clientID=0:靠 peer 兜底,仅在同 peer 唯一会话时绑定。 - 权限:
SetTcpEntry、冻结/解冻、服务控制需要客户端管理员;失败按「未生效」如实返回。 - 编码:机器协议以 CP936 为主;任务
name/path/exe为 UTF-8、status为 GBK; 写回输入统一转 CP936。GBK 不可表示的字符明确报-32602。 host_list_network_state只有 IPv4:数据来自客户端自身的GetTcpTable视图, 与「连接管理」(ConnectionInfo,含 IPv6)不是同一份数据。McpMachineDangerous只覆盖本轮高风险动作;后续加入 kill/删除/卸载/注入时, 必须按McpMachine && McpMachineDangerous && !McpReadonly三层校验。host_control_window {action:"hide"}是单向的:客户端只枚举可见窗口,隐藏后该 hwnd 从快照中消失,再发restore会在发包前被-32009拦下(MFC 对话框同样无法再操作它)。 实测确认:hide复核为state:"hidden",随后restore返回-32009。host_control_service {action:"pause"}依赖服务自身支持 PAUSE:实测虚拟机上的Spooler下发 PAUSE 后状态仍为Running,工具如实返回ok:false, reason:"unexpected_state"(不谎报成功)。start/stop/continue/restart在同一台机器上均通过。- 开关只在进程启动时读取(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 工具,服务端无残留子连接。