# TCP/UDP 连接管理设计 > 版本:v1.1(已实现并评审) · 状态:**已施工,含字节序修正** · 日期:2026-10-04 > 目标:在 YAMA 服务端新增「连接管理」,实时展示受管主机当前的 TCP/UDP 连接, > 并支持对选中的连接强制断开。这是服务管理、进程管理之外又一项「观测 + 管控」类功能。 > > 需求原文(三点,逐一对应下文): > 1. YAMA 工具栏增加一个图标,**唯有 MCP 后面**,通过这个图标打开连接管理对话框; > 2. 客户端需要增加一个管理类来处理服务端请求; > 3. 服务端有一个对话框类展示连接信息,支持右键菜单对选中的连接强制断开。 ## 1. 目标与范围 ### MVP | 项 | 说明 | |---|---| | 工具栏入口 | `IDR_TOOLBAR_MAIN` 在 `ID_MCP_SETTINGS` 之后追加 `ID_CONNECTION_MANAGE` 按钮(第 17 个),点击向选中主机发 `COMMAND_CONNECTION` | | 客户端管理类 | 新增 `CConnectionsManager : public CManager`,枚举本机 TCP/UDP 连接并回传,处理「刷新 / 强制断开」两条服务端命令 | | 服务端对话框 | 新增 `CConnectionsDlg : public DialogBase`,表格展示连接,右键菜单「强制断开」发 `COMMAND_KILL_CONNECTION` | ### 非目标(Non-goals,本次不做) - 不做 **UDP / IPv6 TCP 的强制断开**(无公开 API,见 §6),仅 IPv4 TCP 可断开,其余行只读展示。 - 不做「按进程批量断开」「按 PID 过滤」「连接历史/趋势图」。 - 不做 Linux/Mac/Android 客户端连接枚举(沿用 Windows 客户端先行的惯例)。 - 不改 MCP 工具集(`list_network_connections` 属于 P8 路线图,本设计只做 MFC 图形界面)。 ## 2. 复用现状 本功能与「服务管理」端到端同构,直接以其为实现模板(`ServicesManager` / `ServicesDlg`)。 ### 客户端模板(读参考实现,全部已核实) - 命令分发:`client/KernelManager.cpp` `OnReceive` 的 `case COMMAND_SERVICES:`(约 1346 行): ``` { auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP); sub->EnableSubConnAuth(); m_hThread[m_ulThreadCount].p = sub; } m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopServicesManager, &m_hThread[m_ulThreadCount], 0, NULL); ``` 连接管理照抄,仅换 `LoopConnectionManager`。 - 子连接线程模板:`client/Common.cpp:33` `LoopManager`(`Manager m(ClientObject, n, user)` 构造即发首包,随后跑 `RunEventLoop` 分发到 `Manager::OnReceive`);`LoopServicesManager = LoopManager`。 - 管理类基类:`client/Manager.h` `CManager`(`OnReceive` 虚函数、`m_ClientObject`、`Send/SendData`、子连接生命周期)。 - 管理类实例:`client/ServicesManager.cpp`——构造即 `SendServicesList()`(17 行);`SendServicesList()` 里用 `HttpMask mask(DEFAULT_HOST, m_ClientObject->GetClientIPHeader()); m_ClientObject->Send2Server((char*)szBuffer, LocalSize(szBuffer), &mask);`(36–37 行)回传;`OnReceive` 里 `case COMMAND_SERVICELIST`(刷新)→ 重发、`case COMMAND_SERVICECONFIG`(动作)→ 执行后再重发(199–231 行)。 - 编译开关:`client/ServicesManager.h:13` `#if ENABLE_SERVICE_MNG==0 #define CServicesManager CManager` 的空实现占位模式;开关定义在 `common/config.h`(`ENABLE_SERVICE_MNG TRUE`)。 ### 服务端模板(读参考实现,全部已核实) - 对话框基类:`server/2015Remote/IOCPServer.h:158` `DialogBase`(`OnInitDialog` 建列 + 解析缓冲,`OnReceiveComplete` 按 token 分发,`OnNMRClickList` 右键菜单,`OnClose`→`CancelIO`)。 - 对话框实例:`server/2015Remote/ServicesDlg.cpp`——构造 `DialogBase(CServicesDlg::IDD, pParent, IOCPServer, ContextObject, IDI_SERVICE)`(24 行);`OnInitDialog` 里 `InsertColumnL` 建列 + `ShowServicesList()`(67–73 行);`OnNMRClickList` 里 `LoadMenu(IDR_MENU_SERVICES)` + `TrackPopupMenu`(216–226 行);`OnReceiveComplete` 里 `case TOKEN_SERVERLIST: ShowServicesList()`(231–235 行)。 - 打开对话框模板:`server/2015Remote/2015RemoteDlg.h:182` `OpenDialog`。 - 命令发送:`OnOnlineServerManager()`(`2015RemoteDlg.cpp:4758`):`BYTE bToken = COMMAND_SERVICES; SendSelectedCommand(&bToken, sizeof(BYTE));`。 - token 路由:`2015RemoteDlg.cpp:6609` `case TOKEN_SERVERLIST:` → `g_2015RemoteDlg->SendMessage(WM_OPENSERVICESDIALOG, 0, (LPARAM)ContextObject);`。 - 消息注册/处理:`ON_MESSAGE(WM_OPENSERVICESDIALOG, OnOpenServicesDialog)`(903 行)→ `OnOpenServicesDialog`(7811 行)→ `OpenDialog`。 ### 工具栏(读参考,全部已核实) - 资源:`2015Remote.rc:1806` `IDR_TOOLBAR_MAIN TOOLBAR 48, 48`,现有 16 个 `BUTTON`,末尾为 `BUTTON ID_CHANGE_LANG`(1822 行)、`BUTTON ID_MCP_SETTINGS`(1823 行)。 - 真彩位图条:`IDB_BITMAP_MAIN`(133) → `res\ToolBar_Main.bmp`(**768×48 = 16 个 48×48 图标**,24 位), `LoadTrueColorToolBar(48, IDB_BITMAP_MAIN, IDB_BITMAP_MAIN, IDB_BITMAP_MAIN)`(`2015RemoteDlg.cpp:1390`) 按 `nNbBtn = bmWidth/48` 建图像列表(`TrueColorToolBar.cpp` `SetTrueColorToolBar`),因此**新增第 17 个按钮必须同步把位图条加宽为 816×48 并补第 17 个图标**。 - 按钮文字:`SetButtonText(0..15)`(`2015RemoteDlg.cpp:1405–1420`),`SetButtonText(15, _T("MCP"))` 为当前最后一个。 - 全隐藏判定:`if (hiddenCount >= 16)`(1471 行)→ **无需改动**。该计数只统计「可隐藏功能按钮」 (终端/进程/窗口/桌面/文件/语音/视频/服务/注册表/键盘/设置/生成/搜索/关于,共 14 个), `ID_CHANGE_LANG`、`ID_MCP_SETTINGS` 本就不在隐藏清单内;新增的连接按钮同样不参与隐藏, `hiddenCount` 语义不变。 - 命令处理:`ON_COMMAND(ID_MCP_SETTINGS, &CMy2015RemoteDlg::OnMcpSettings)`(993 行)→ 参照加 `ID_CONNECTION_MANAGE`。 ### 网络 API(客户端,新增依赖) - 枚举:`GetExtendedTcpTable(TCP_TABLE_OWNER_PID_ALL, ...)` + `MIB_TCPTABLE_OWNER_PID`; 同理 `GetExtendedUdpTable(UDP_TABLE_OWNER_PID)`;IPv6 用 `AF_INET6` 版本(`MIB_TCP6TABLE_OWNER_PID` / `MIB_UDP6TABLE_OWNER_PID`)。 - 断开:`SetTcpEntry` + `MIB_TCP_STATE_DELETE_TCB`(12)。 - 链接:`iphlpapi.lib`(用 `#pragma comment(lib, "iphlpapi.lib")` 内联,与代码库既有 `#pragma comment(lib, ...)` 风格一致,避免改 vcxproj)。 ## 3. 协议与数据结构 ### 3.1 新命令(`common/commands.h`,追加在 `CMD_FILE_SPEED = 252` 之后,均 ≤ 255 满足单字节 token) ``` COMMAND_CONNECTION = 253, // 服务端 → 客户端:查询/刷新连接列表(打开连接管理) COMMAND_KILL_CONNECTION = 254, // 服务端 → 客户端:强制断开指定连接 TOKEN_CONNECTION_LIST = 255, // 客户端 → 服务端:连接列表响应 ``` ### 3.2 单条连接记录(客户端 → 服务端,固定 110 字节,小端) ```c #pragma pack(push, 1) struct ConnectionInfo { uint8_t proto; // 1=TCP, 2=UDP [1] uint8_t af; // 2=AF_INET(IPv4), 23=AF_INET6(IPv6) [1] uint32_t pid; // 所属进程 PID [4] uint8_t localAddr[16]; // 网络字节序;IPv4 存 [0..3],IPv6 存 [0..15],其余 0 [16] uint16_t localPort; // 主机字节序(小端,ntohs 转换后) [2] uint8_t remoteAddr[16]; // 网络字节序,同上 [16] uint16_t remotePort; // 主机字节序,同上 [2] uint32_t state; // TCP: MIB_TCP_STATE_*;UDP: 0 [4] char procName[64]; // 进程映像名 UTF-8,NUL 填充 [64] }; // 合计 110 #pragma pack(pop) static_assert(sizeof(ConnectionInfo) == 110, "ConnectionInfo must be 110 bytes"); ``` > **字节序约定(评审后修正)**:字段不能「原样 memcpy」iphlpapi 的结构成员。`dwLocalAddr`/`dwRemoteAddr` > 是**网络序 DWORD**(值 `0xC0A80101` 即 `192.168.1.1`),客户端展开为 16 字节字段前 4 字节的**网络序字节** > (`[0xC0,0xA8,0x01,0x01]`),服务端 `inet_ntop` 可直接展示、排序键也按数值序;`dwLocalPort`/`dwRemotePort` > 低 16 位为**网络序**,客户端用 `ntohs` 转**主机序**再存入。断连时反向还原(地址拼回 DWORD、端口 `htons`)。 > 地址统一 16 字节字段承载,IPv4/IPv6 用同一个结构,避免两套记录。 ### 3.3 列表响应(客户端 → 服务端) ``` [TOKEN_CONNECTION_LIST:1][count:2][ConnectionInfo:110] * count ``` - `count` 为 `uint16_t` 小端,上限 65535(枚举超限则截断,实际主机连接数远低于此)。 - 列表 >1460 字节会走 HTTP 分片;服务端 `HeaderParser::Parse` 的 `m_bShouldUnmask` 分片 bug 已在 `a72e6eb` 修复,续包可正常到达(与进程/服务列表一致)。 ### 3.4 强制断开请求(服务端 → 客户端) ``` [COMMAND_KILL_CONNECTION:1][ConnectionInfo:110] ``` - 客户端校验 `proto==1 && af==2` 才执行(仅 IPv4 TCP),否则忽略并在回包列表中保留该行。 ## 4. 客户端实现 新增 `client/ConnectionsManager.{h,cpp}`,镜像 `CServicesManager`: - 类声明(`ConnectionsManager.h`): ```cpp #if ENABLE_CONNECTION_MNG==0 #define CConnectionsManager CManager #else class CConnectionsManager : public CManager { public: CConnectionsManager(IOCPClient* ClientObject, int n, void* user = nullptr); virtual ~CConnectionsManager(); VOID SendConnectionList(); // 枚举并回传 VOID OnReceive(PBYTE szBuffer, ULONG ulLength); void KillConnection(const ConnectionInfo& info); }; #endif ``` - `SendConnectionList()`: 1. 依次 `GetExtendedTcpTable`(AF_INET / AF_INET6)+ `GetExtendedUdpTable`(AF_INET / AF_INET6), 汇总到 `std::vector`;IPv4 地址展开为网络序字节(`PutAddr4`)、端口 `ntohs` 转主机序。 2. 每个 PID 用 `OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)` + `GetModuleBaseNameW` 取映像名, 转 UTF-8 写入 `procName[64]`;失败/PID 0 填 `""`(进程可能已退出,端点仍有效)。 3. 组包 `[TOKEN_CONNECTION_LIST:1][count:2][records...]`,用 `HttpMask mask(DEFAULT_HOST, m_ClientObject->GetClientIPHeader()); m_ClientObject->Send2Server(...);` 回传。 - `OnReceive()`: - `case COMMAND_CONNECTION:` → `SendConnectionList()`(刷新)。 - `case COMMAND_KILL_CONNECTION:` → 校验长度 ≥ 111,读 `ConnectionInfo`,`KillConnection()`,再 `SendConnectionList()`。 - `KillConnection()`:仅 `proto==TCP && af==AF_INET`;组 `MIB_TCPROW`(`dwState=MIB_TCP_STATE_DELETE_TCB`), 地址由网络序字节拼回 DWORD(`GetAddr4`)、端口 `htons` 转回网络序,`SetTcpEntry(&row)`。 - 编译:`common/config.h` 增 `#define ENABLE_CONNECTION_MNG TRUE`(`// 连接管理`)。 - 分发(`client/KernelManager.cpp`):在 `case COMMAND_SERVICES:` 后加 ``` case COMMAND_CONNECTION: { if (!ENABLE_CONNECTION_MNG) return ResponseDisable(...); { auto* sub = new IOCPClient(...); sub->EnableSubConnAuth(); ...; } m_hThread[m_ulThreadCount++].h = __CreateThread(..., LoopConnectionManager, ...); break; } ``` - 线程注册(`client/Common.cpp` / `Common.h`):`#define LoopConnectionManager LoopManager`。 ## 5. 服务端实现 新增 `server/2015Remote/ConnectionsDlg.{h,cpp}`,镜像 `CServicesDlg`: - `CConnectionsDlg(CWnd*, Server*, CONTEXT_OBJECT*) : DialogBase(IDD_DIALOG_CONNECTIONS, pParent, IOCPServer, ContextObject, IDI_CONNECTION)`。 - `OnInitDialog`:`InsertColumnL` 建列 `协议 | 本地地址 | 本地端口 | 远端地址 | 远端端口 | 状态 | PID | 进程名`(8 列),调 `ShowConnectionList()`。 - `ShowConnectionList()`:解析 `m_ContextObject->InDeCompressedBuffer`(`[token:1][count:2][records...]`)填表, 每条 `SetItemData` 存行内偏移(或直接把 `ConnectionInfo` 存进项关联数据),供右键断连取回记录。 - `OnReceiveComplete()`:`case TOKEN_CONNECTION_LIST: ShowConnectionList()`。 - `OnNMRClickList()`:用 `pNMItemActivate->iItem` 取**鼠标右键所在行**(而非仅依赖已选中行), 由该行 `SetItemData` 里存的 `ConnectionInfo` 拿回记录;`LoadMenu(IDR_MENU_CONNECTIONS)` → `TrackPopupMenu`;菜单项「强制断开」(`ID_CONNECTION_KILL`)对**仅 IPv4 TCP 行可用**,发 `[COMMAND_KILL_CONNECTION:1][record:110]`;UDP/IPv6 行禁用该菜单项(`MF_GRAYED`)。 - 对话框标题栏图标 `IDI_CONNECTION`(`.ico`,`DialogBase` 构造第 5 参)与工具栏位图条第 17 个 48×48 图标是两回事:前者复用现有图标(如 `IDI_SERVICE`)或新增一个 `.ico`;后者编辑 `ToolBar_Main.bmp` 补第 17 格(见下)。 - 提供刷新(补发 `COMMAND_CONNECTION`)与 `OnClose`→`CancelIO()`。 工具栏 / 消息路由(`2015RemoteDlg.{h,cpp}` + `Resource.h` + `.rc`): - `Resource.h`:`IDD_DIALOG_CONNECTIONS 404`、`IDR_MENU_CONNECTIONS 405`、`IDI_CONNECTION`(新图标 ID)、 `ID_CONNECTION_MANAGE 33086`(`_APS_NEXT_COMMAND_VALUE` 现为 33086,用后 +2)、`ID_CONNECTION_KILL`; 同步上调 `_APS_NEXT_RESOURCE_VALUE`/`_APS_NEXT_COMMAND_VALUE`。 - `.rc`(UTF-16LE,Node 脚本改):`IDR_TOOLBAR_MAIN` 末尾加 `BUTTON ID_CONNECTION_MANAGE`; 新增 `IDD_DIALOG_CONNECTIONS` 对话框与 `IDR_MENU_CONNECTIONS` 菜单。 - `res\ToolBar_Main.bmp`:**加宽为 816×48,补第 17 个 48×48「连接」图标**(实现时用脚本合成一个 双节点/插头样式图标插入条尾;图标观感属评审项)。 - `2015RemoteDlg.cpp`: - `SetButtonText(16, _TR("连接管理"))`(1420 行后)。 - `hiddenCount >= 16`(1471 行)**无需改动**(连接按钮不参与隐藏,理由见 §2)。 - `ON_COMMAND(ID_CONNECTION_MANAGE, &CMy2015RemoteDlg::OnConnectionManage)`(993 行后)。 - `OnConnectionManage()`:`BYTE bToken = COMMAND_CONNECTION; SendSelectedCommand(&bToken, sizeof(BYTE));`。 - `MessageHandle` 加 `case TOKEN_CONNECTION_LIST:` → `SendMessage(WM_OPENCONNECTIONDIALOG, 0, (LPARAM)ContextObject)`(6609 行后)。 - `ON_MESSAGE(WM_OPENCONNECTIONDIALOG, OnOpenConnectionDialog)` + `OnOpenConnectionDialog` → `OpenDialog(wParam, lParam)`。 - `stdafx.h`:`#define WM_OPENCONNECTIONDIALOG WM_USER + 3045`(现最高占用 `WM_USER+3044`,已核实无冲突)。 - 语言:`lang/en_US.ini` / `zh_TW.ini`(GBK,Python 改)补「连接管理 / 强制断开 / 8 列表头」文案。 ## 6. 强制断开的实现与限制 - **仅 IPv4 TCP** 用 `SetTcpEntry` + `MIB_TCP_STATE_DELETE_TCB`(公开 API 唯一支持)。 - **重构 `MIB_TCPROW` 要点**:`dwState=12`;`dwLocalAddr/dwRemoteAddr` 由 16 字节字段前 4 字节的 网络序字节拼回网络序 DWORD(`GetAddr4`);`dwLocalPort/dwRemotePort` 由主机序 `htons` 转回网络序。 - **UDP**:无「按端点关闭 socket」的公开 API,MVP 只读展示(菜单置灰)。 - **IPv6 TCP**:`SetTcpEntry` 的 `MIB_TCPROW` 为 IPv4 结构,无公开的 IPv6 对应删除 API,MVP 只读展示。 - **权限**:`SetTcpEntry` 需管理员/足够权限;YAMA 客户端通常以服务(SYSTEM)运行,满足。若非提权, `SetTcpEntry` 返回错误,`KillConnection` 静默失败并在刷新后的列表中保留该行(可观察结果,不误报)。 ## 7. 安全与边界 1. **可能断开 YAMA 自身控制通道**:连接列表包含客户端↔服务端的主/子连接本身,若管理员选中并强制断开, 对应主机会立即离线。对话框对此不做特殊保护(与 `kill_process` 可杀自身守护进程同理), 但列表按 PID 可辨识本程序进程名,便于人工避让。 2. **强制断开不可逆**:断的是对端已建立的 TCP 连接,应用层不保存状态,可能造成数据丢失;对话框不弹二次确认 (确认责任在操作者),但右键菜单为显式单点操作。 3. **枚举成本**:每行一次 `OpenProcess` 取进程名,数百行连接约毫秒~十毫秒级,可接受;`OpenProcess` 失败直接 `""`,不阻塞枚举。 4. **PID 复用 / 进程退出**:断连用的是**端点**而非 PID,进程退出不影响 `SetTcpEntry` 命中;`procName` 仅用于展示。 5. **HTTP 分片**:连接数多时列表可超 1460 字节,依赖已修复的分片续包处理(§3.3)。 6. **老版本客户端**:老客户端无 `COMMAND_CONNECTION` 处理,点工具栏会无响应(与「老客户端不支持活动历史/日志」 同类),属预期;不影响服务端其它功能。 ## 8. 验证计划(施工后填写) 1. 服务端:MSBuild 加 `-p:SolutionDir` 编译通过(见项目记忆 yama-server-msbuild-solutiondir);客户端同步编译。 2. 部署客户端到真实 Windows 主机 → 服务端选中主机 → 点新图标 → 对话框列出 TCP/UDP(含 IPv4/IPv6)连接, 字段(协议/地址/端口/状态/PID/进程名)与远端 `netstat -ano` 一致。 3. 右键一条 **IPv4 TCP** 已建立连接(如对测试服务器的 ESTABLISHED)→ 强制断开 → 刷新后该行消失、 远端 `netstat` 不再显示;进程自身未受影响。 4. 右键 **UDP / IPv6** 行 → 菜单项置灰,不可断连。 5. 刷新按钮 / 重复打开对话框 → 列表更新正常;关对话框后子连接释放(无泄漏)。 6. 回归:服务管理 / 进程管理 / MCP 等既有功能不受影响(无 command/token/ID 冲突)。 ## 9. 实现记录 - 已实现并评审通过(2026-10-04)。实现与 §4/§5 一致:工具栏第 17 个图标(`ID_CONNECTION_MANAGE`,紧邻 MCP 之后) 打开连接管理对话框;客户端新增 `CConnectionsManager` 枚举并回传本机 TCP/UDP 连接;服务端新增 `CConnectionsDlg` 表格展示、右键「强制断开」发 `COMMAND_KILL_CONNECTION`、支持表头点击排序。 - **评审修正一处关键缺陷(字节序)**:iphlpapi 的 IPv4 地址 `dwLocalAddr` 是网络序 DWORD、端口 `dwLocalPort` 低 16 位是网络序。初版「原样 memcpy / 原样回填」导致展示的 IPv4 地址与端口字节序颠倒(强断因往返对称仍能命中)。 已改为:地址存网络序字节(`PutAddr4`)、端口 `ntohs` 转主机序;断连时地址拼回 DWORD(`GetAddr4`)、端口 `htons` 转回网络序。详见 §3.2 / §6。 - 对话框标题栏小图标 `IDI_CONNECTION` 由工具栏第 17 个图标生成 `res/connections.ico`(48/32/16,32 位带透明)。 - 编译:`client/ConnectionsManager.cpp`(UTF-8 BOM + CRLF,`ws2tcpip.h` 引入 `_WS2IPDEF_` 以使用 IPv6 MIB 结构) 经 MSBuild(v143 工具集覆盖)编译通过;整仓 ghost 构建在本机 BuildTools 因缺少 ATL 头受阻,属环境问题,与本功能无关。