From bbdb4104bf9d54b23fa4870be7fbcdad573fa219 Mon Sep 17 00:00:00 2001 From: yuanyuanxiang <962914132@qq.com> Date: Sun, 4 Oct 2026 18:23:06 +0200 Subject: [PATCH] Feature: TCP/UDP connection management with force-disconnect Add a connection-management feature to YAMA: a toolbar button (after MCP) opens a dialog listing the managed host's TCP/UDP connections, with right-click force-disconnect for IPv4 TCP rows. Client: a new CConnectionsManager enumerates connections via iphlpapi (GetExtendedTcpTable/GetExtendedUdpTable, IPv4+IPv6) and answers the refresh and kill commands over a sub-connection. Server: a new CConnectionsDlg (DialogBase) shows the list with sortable columns and a right-click menu; the title-bar icon reuses the toolbar's connection icon (res/connections.ico). Fix byte order found during review: iphlpapi returns an IPv4 address as a network-order DWORD and the port's low 16 bits in network order, so storing them raw made the dialog show reversed IPv4 addresses and ports. The client now stores addresses as network-order bytes and ports as host order (ntohs), and reverses the conversion on disconnect. Co-Authored-By: deepseek-v4-pro --- client/ClientDll_vs2015.vcxproj | 2 + client/ClientDll_vs2015.vcxproj.filters | 2 + client/Common.cpp | 6 + client/Common.h | 1 + client/ConnectionsManager.cpp | 239 ++++++++++++++ client/ConnectionsManager.h | 29 ++ client/KernelManager.cpp | 13 + client/ghost_vs2015.vcxproj | 2 + common/commands.h | 23 ++ common/config.h | 1 + docs/ConnectionManage_Design.md | 259 +++++++++++++++ server/2015Remote/2015Remote.rc | Bin 174756 -> 185528 bytes server/2015Remote/2015RemoteDlg.cpp | 19 ++ server/2015Remote/2015RemoteDlg.h | 2 + server/2015Remote/2015Remote_vs2015.vcxproj | 3 + .../2015Remote_vs2015.vcxproj.filters | 3 + server/2015Remote/ConnectionsDlg.cpp | 305 ++++++++++++++++++ server/2015Remote/ConnectionsDlg.h | 43 +++ server/2015Remote/lang/en_US.ini | 9 + server/2015Remote/lang/zh_TW.ini | 9 + server/2015Remote/res/ToolBar_Main.bmp | Bin 110646 -> 117558 bytes server/2015Remote/res/connections.ico | Bin 0 -> 3898 bytes server/2015Remote/resource.h | 10 +- server/2015Remote/stdafx.h | 1 + 24 files changed, 979 insertions(+), 2 deletions(-) create mode 100644 client/ConnectionsManager.cpp create mode 100644 client/ConnectionsManager.h create mode 100644 docs/ConnectionManage_Design.md create mode 100644 server/2015Remote/ConnectionsDlg.cpp create mode 100644 server/2015Remote/ConnectionsDlg.h create mode 100644 server/2015Remote/res/connections.ico diff --git a/client/ClientDll_vs2015.vcxproj b/client/ClientDll_vs2015.vcxproj index ebf843e..32fb5ed 100644 --- a/client/ClientDll_vs2015.vcxproj +++ b/client/ClientDll_vs2015.vcxproj @@ -185,6 +185,7 @@ + @@ -233,6 +234,7 @@ + diff --git a/client/ClientDll_vs2015.vcxproj.filters b/client/ClientDll_vs2015.vcxproj.filters index 3256935..373048a 100644 --- a/client/ClientDll_vs2015.vcxproj.filters +++ b/client/ClientDll_vs2015.vcxproj.filters @@ -41,6 +41,7 @@ + @@ -59,6 +60,7 @@ + diff --git a/client/Common.cpp b/client/Common.cpp index 926b1ad..e746dfd 100644 --- a/client/Common.cpp +++ b/client/Common.cpp @@ -11,6 +11,7 @@ #include "AudioManager.h" #include "RegisterManager.h" #include "ServicesManager.h" +#include "ConnectionsManager.h" #include "VideoManager.h" #include "KeyboardManager.h" #include "ProxyManager.h" @@ -139,6 +140,11 @@ DWORD WINAPI LoopServicesManager(LPVOID lParam) return LoopManager(lParam); } +DWORD WINAPI LoopConnectionManager(LPVOID lParam) +{ + return LoopManager(lParam); +} + DWORD WINAPI LoopKeyboardManager(LPVOID lParam) { iniFile cfg(CLIENT_PATH); diff --git a/client/Common.h b/client/Common.h index 12ddd66..5de2f23 100644 --- a/client/Common.h +++ b/client/Common.h @@ -34,6 +34,7 @@ DWORD WINAPI LoopVideoManager(LPVOID lParam); DWORD WINAPI LoopAudioManager(LPVOID lParam); DWORD WINAPI LoopRegisterManager(LPVOID lParam); DWORD WINAPI LoopServicesManager(LPVOID lParam); +DWORD WINAPI LoopConnectionManager(LPVOID lParam); DWORD WINAPI LoopKeyboardManager(LPVOID lParam); DWORD WINAPI LoopProxyManager(LPVOID lParam); DWORD WINAPI LoopClientLogManager(LPVOID lParam); diff --git a/client/ConnectionsManager.cpp b/client/ConnectionsManager.cpp new file mode 100644 index 0000000..1434c92 --- /dev/null +++ b/client/ConnectionsManager.cpp @@ -0,0 +1,239 @@ +// ConnectionsManager.cpp: implementation of the CConnectionsManager class. +// +////////////////////////////////////////////////////////////////////// + +#include "stdafx.h" +#include "ConnectionsManager.h" +#include "Common.h" +#include +#include +#include +#include +#include +#include + +#pragma comment(lib, "iphlpapi.lib") + +#if ENABLE_CONNECTION_MNG + +////////////////////////////////////////////////////////////////////// +// Construction/Destruction +////////////////////////////////////////////////////////////////////// + +CConnectionsManager::CConnectionsManager(IOCPClient* ClientObject, int n, void* user):CManager(ClientObject) +{ + SendConnectionList(); +} + +CConnectionsManager::~CConnectionsManager() +{ +} + +// 把 PID 解析成进程映像名(UTF-8)。进程可能已退出/受保护,失败时填 。 +static void ResolveProcName(DWORD pid, char* out, int outLen) +{ + if (outLen <= 0) return; + out[0] = 0; + if (pid == 0) { + strcpy_s(out, outLen, "System Idle Process"); + return; + } + HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); + if (h == NULL) { + strcpy_s(out, outLen, ""); + return; + } + wchar_t wpath[MAX_PATH] = { 0 }; + DWORD len = MAX_PATH; + BOOL ok = QueryFullProcessImageNameW(h, 0, wpath, &len); + CloseHandle(h); + if (!ok) { + strcpy_s(out, outLen, ""); + return; + } + wchar_t* base = wcsrchr(wpath, L'\\'); + base = base ? base + 1 : wpath; + if (WideCharToMultiByte(CP_UTF8, 0, base, -1, out, outLen, NULL, NULL) <= 0) { + strcpy_s(out, outLen, ""); + return; + } + out[outLen - 1] = 0; +} + +// IPv4 地址:iphlpapi 的 dwLocalAddr/dwRemoteAddr 是网络序 DWORD(值 0xC0A80101 即 192.168.1.1), +// 转成 16 字节字段前 4 字节的网络序字节后 inet_ntop 可直接展示、断连时再拼回 DWORD。 +static void PutAddr4(uint8_t* out, DWORD addrNet) +{ + out[0] = (uint8_t)(addrNet >> 24); + out[1] = (uint8_t)(addrNet >> 16); + out[2] = (uint8_t)(addrNet >> 8); + out[3] = (uint8_t)(addrNet); +} + +static DWORD GetAddr4(const uint8_t* addr) +{ + return ((DWORD)addr[0] << 24) | ((DWORD)addr[1] << 16) | ((DWORD)addr[2] << 8) | (DWORD)addr[3]; +} + +static void AppendTcp4(std::vector& rows) +{ + PMIB_TCPTABLE_OWNER_PID pTable = NULL; + DWORD dwSize = 0; + if (GetExtendedTcpTable(pTable, &dwSize, FALSE, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0) != ERROR_INSUFFICIENT_BUFFER) + return; + pTable = (PMIB_TCPTABLE_OWNER_PID)malloc(dwSize); + if (pTable == NULL) return; + if (GetExtendedTcpTable(pTable, &dwSize, FALSE, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0) == NO_ERROR) { + for (DWORD i = 0; i < pTable->dwNumEntries; ++i) { + const MIB_TCPROW_OWNER_PID& r = pTable->table[i]; + ConnectionInfo info = { 0 }; + info.proto = 1; + info.af = AF_INET; + info.pid = r.dwOwningPid; + PutAddr4(info.localAddr, r.dwLocalAddr); // 网络序 DWORD → 网络序字节 + info.localPort = ntohs((uint16_t)r.dwLocalPort); // 端口网络序 → 主机序 + PutAddr4(info.remoteAddr, r.dwRemoteAddr); + info.remotePort = ntohs((uint16_t)r.dwRemotePort); + info.state = r.dwState; + ResolveProcName(info.pid, info.procName, sizeof(info.procName)); + rows.push_back(info); + } + } + free(pTable); +} + +static void AppendTcp6(std::vector& rows) +{ + PMIB_TCP6TABLE_OWNER_PID pTable = NULL; + DWORD dwSize = 0; + if (GetExtendedTcpTable(pTable, &dwSize, FALSE, AF_INET6, TCP_TABLE_OWNER_PID_ALL, 0) != ERROR_INSUFFICIENT_BUFFER) + return; + pTable = (PMIB_TCP6TABLE_OWNER_PID)malloc(dwSize); + if (pTable == NULL) return; + if (GetExtendedTcpTable(pTable, &dwSize, FALSE, AF_INET6, TCP_TABLE_OWNER_PID_ALL, 0) == NO_ERROR) { + for (DWORD i = 0; i < pTable->dwNumEntries; ++i) { + const MIB_TCP6ROW_OWNER_PID& r = pTable->table[i]; + ConnectionInfo info = { 0 }; + info.proto = 1; + info.af = AF_INET6; + info.pid = r.dwOwningPid; + memcpy(info.localAddr, r.ucLocalAddr, 16); + info.localPort = ntohs((uint16_t)r.dwLocalPort); + memcpy(info.remoteAddr, r.ucRemoteAddr, 16); + info.remotePort = ntohs((uint16_t)r.dwRemotePort); + info.state = r.dwState; + ResolveProcName(info.pid, info.procName, sizeof(info.procName)); + rows.push_back(info); + } + } + free(pTable); +} + +static void AppendUdp4(std::vector& rows) +{ + PMIB_UDPTABLE_OWNER_PID pTable = NULL; + DWORD dwSize = 0; + if (GetExtendedUdpTable(pTable, &dwSize, FALSE, AF_INET, UDP_TABLE_OWNER_PID, 0) != ERROR_INSUFFICIENT_BUFFER) + return; + pTable = (PMIB_UDPTABLE_OWNER_PID)malloc(dwSize); + if (pTable == NULL) return; + if (GetExtendedUdpTable(pTable, &dwSize, FALSE, AF_INET, UDP_TABLE_OWNER_PID, 0) == NO_ERROR) { + for (DWORD i = 0; i < pTable->dwNumEntries; ++i) { + const MIB_UDPROW_OWNER_PID& r = pTable->table[i]; + ConnectionInfo info = { 0 }; + info.proto = 2; + info.af = AF_INET; + info.pid = r.dwOwningPid; + PutAddr4(info.localAddr, r.dwLocalAddr); + info.localPort = ntohs((uint16_t)r.dwLocalPort); + info.state = 0; + ResolveProcName(info.pid, info.procName, sizeof(info.procName)); + rows.push_back(info); + } + } + free(pTable); +} + +static void AppendUdp6(std::vector& rows) +{ + PMIB_UDP6TABLE_OWNER_PID pTable = NULL; + DWORD dwSize = 0; + if (GetExtendedUdpTable(pTable, &dwSize, FALSE, AF_INET6, UDP_TABLE_OWNER_PID, 0) != ERROR_INSUFFICIENT_BUFFER) + return; + pTable = (PMIB_UDP6TABLE_OWNER_PID)malloc(dwSize); + if (pTable == NULL) return; + if (GetExtendedUdpTable(pTable, &dwSize, FALSE, AF_INET6, UDP_TABLE_OWNER_PID, 0) == NO_ERROR) { + for (DWORD i = 0; i < pTable->dwNumEntries; ++i) { + const MIB_UDP6ROW_OWNER_PID& r = pTable->table[i]; + ConnectionInfo info = { 0 }; + info.proto = 2; + info.af = AF_INET6; + info.pid = r.dwOwningPid; + memcpy(info.localAddr, r.ucLocalAddr, 16); + info.localPort = ntohs((uint16_t)r.dwLocalPort); + info.state = 0; + ResolveProcName(info.pid, info.procName, sizeof(info.procName)); + rows.push_back(info); + } + } + free(pTable); +} + +VOID CConnectionsManager::SendConnectionList() +{ + std::vector rows; + AppendTcp4(rows); + AppendTcp6(rows); + AppendUdp4(rows); + AppendUdp6(rows); + + size_t count = rows.size(); + if (count > 0xFFFF) count = 0xFFFF; + ULONG bufLen = 1 + 2 + (ULONG)(count * sizeof(ConnectionInfo)); + LPBYTE szBuffer = (LPBYTE)LocalAlloc(LPTR, bufLen); + if (szBuffer == NULL) return; + szBuffer[0] = TOKEN_CONNECTION_LIST; + uint16_t cnt = (uint16_t)count; + memcpy(szBuffer + 1, &cnt, 2); + for (size_t i = 0; i < count; ++i) + memcpy(szBuffer + 3 + i * sizeof(ConnectionInfo), &rows[i], sizeof(ConnectionInfo)); + + HttpMask mask(DEFAULT_HOST, m_ClientObject->GetClientIPHeader()); + m_ClientObject->Send2Server((char*)szBuffer, bufLen, &mask); + LocalFree(szBuffer); +} + +void CConnectionsManager::KillConnection(const ConnectionInfo& info) +{ + // 仅 IPv4 TCP 有公开 API 可强制断开(SetTcpEntry + DELETE_TCB)。 + if (info.proto != 1 || info.af != AF_INET) return; + + MIB_TCPROW row = { 0 }; + row.dwState = MIB_TCP_STATE_DELETE_TCB; + row.dwLocalAddr = GetAddr4(info.localAddr); // 网络序字节 → 网络序 DWORD + row.dwLocalPort = (DWORD)htons(info.localPort); // 主机序端口 → 网络序 + row.dwRemoteAddr = GetAddr4(info.remoteAddr); + row.dwRemotePort = (DWORD)htons(info.remotePort); + SetTcpEntry(&row); +} + +VOID CConnectionsManager::OnReceive(PBYTE szBuffer, ULONG ulLength) +{ + switch (szBuffer[0]) { + case COMMAND_CONNECTION: // 刷新 + SendConnectionList(); + break; + case COMMAND_KILL_CONNECTION: // 强制断开 + if (ulLength >= 1 + sizeof(ConnectionInfo)) { + ConnectionInfo info; + memcpy(&info, szBuffer + 1, sizeof(ConnectionInfo)); + KillConnection(info); + } + SendConnectionList(); + break; + default: + break; + } +} + +#endif diff --git a/client/ConnectionsManager.h b/client/ConnectionsManager.h new file mode 100644 index 0000000..7e09a08 --- /dev/null +++ b/client/ConnectionsManager.h @@ -0,0 +1,29 @@ +// ConnectionsManager.h: interface for the CConnectionsManager class. +// +////////////////////////////////////////////////////////////////////// + +#if !defined(AFX_CONNECTIONSMANAGER_H__7D1C2E4A_9B30_4F81_A6C0_DE85B2F31C77__INCLUDED_) +#define AFX_CONNECTIONSMANAGER_H__7D1C2E4A_9B30_4F81_A6C0_DE85B2F31C77__INCLUDED_ + +#if _MSC_VER > 1000 +#pragma once +#endif // _MSC_VER > 1000 + +#include "Manager.h" +#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 + +#endif // !defined(AFX_CONNECTIONSMANAGER_H__7D1C2E4A_9B30_4F81_A6C0_DE85B2F31C77__INCLUDED_) diff --git a/client/KernelManager.cpp b/client/KernelManager.cpp index 93a9822..ba46ec5 100644 --- a/client/KernelManager.cpp +++ b/client/KernelManager.cpp @@ -1356,6 +1356,19 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength) break; } + case COMMAND_CONNECTION: { + if (!ENABLE_CONNECTION_MNG) { + return ResponseDisable(m_ClientObject, "CONNECTION", szBuffer + 1, ulLength - 1); + } + { + auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP); + sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验 + m_hThread[m_ulThreadCount].p = sub; + } + m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopConnectionManager, &m_hThread[m_ulThreadCount], 0, NULL); + break; + } + case COMMAND_UPDATE: { auto typ = m_conn->ClientType(); if (typ == CLIENT_TYPE_DLL || typ == CLIENT_TYPE_MODULE) { diff --git a/client/ghost_vs2015.vcxproj b/client/ghost_vs2015.vcxproj index c94c427..9741f20 100644 --- a/client/ghost_vs2015.vcxproj +++ b/client/ghost_vs2015.vcxproj @@ -214,6 +214,7 @@ + @@ -269,6 +270,7 @@ + diff --git a/common/commands.h b/common/commands.h index c4785cb..387cc8b 100644 --- a/common/commands.h +++ b/common/commands.h @@ -358,8 +358,31 @@ enum { TOKEN_CLIP_TEXT = 250, TOKEN_SERVER_VERIFY = 251, // 验证服务器,防中间人和假冒的授权服务器 CMD_FILE_SPEED = 252, // 文件传输速度下发: [cmd:1][chunkKb:uint16 LE][durationMs:uint16 LE] + + COMMAND_CONNECTION = 253, // 连接管理(服务端→客户端:查询/刷新连接列表) + COMMAND_KILL_CONNECTION = 254, // 强制断开指定连接(服务端→客户端) + TOKEN_CONNECTION_LIST = 255, // 连接列表响应(客户端→服务端) }; +// 单条连接记录(客户端 → 服务端)。地址字段存网络字节序(IPv4 展开为前 4 字节、IPv6 为 16 字节), +// 端口字段存主机字节序(客户端 ntohs 转换)。断连时反向还原:地址拼回网络序 DWORD、端口 htons 转回 +// 网络序,填回 MIB_TCPROW 调 SetTcpEntry(iphlpapi 的地址/端口均为网络序)。 +#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; // 主机字节序(小端) [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 exactly 110 bytes"); + #pragma pack(push, 1) struct SignatureResp { char msg[64]; diff --git a/common/config.h b/common/config.h index 4f803b6..c2b925e 100644 --- a/common/config.h +++ b/common/config.h @@ -16,6 +16,7 @@ #define ENABLE_AUDIO_MNG TRUE // 语音管理 #define ENABLE_VIDEO_MNG TRUE // 视频管理 #define ENABLE_SERVICE_MNG TRUE // 服务管理 +#define ENABLE_CONNECTION_MNG TRUE // 连接管理 #define ENABLE_REGISTRY TRUE // 注册表管理 #define ENABLE_KEYBOARD TRUE // 键盘记录 diff --git a/docs/ConnectionManage_Design.md b/docs/ConnectionManage_Design.md new file mode 100644 index 0000000..832ba4b --- /dev/null +++ b/docs/ConnectionManage_Design.md @@ -0,0 +1,259 @@ +# 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 头受阻,属环境问题,与本功能无关。 diff --git a/server/2015Remote/2015Remote.rc b/server/2015Remote/2015Remote.rc index 3d41393cd2c23d4e907159aec6c0bc9c3f245bb9..ca40c0e7e698324f23ccc735a98940ebfceec3ac 100644 GIT binary patch delta 16372 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 ZmcL>ZRo^IKZg)JUJj>n|MTeF2{{bD##VG&) diff --git a/server/2015Remote/2015RemoteDlg.cpp b/server/2015Remote/2015RemoteDlg.cpp index 98fc07a..31312a7 100644 --- a/server/2015Remote/2015RemoteDlg.cpp +++ b/server/2015Remote/2015RemoteDlg.cpp @@ -20,6 +20,7 @@ #include "AudioDlg.h" #include "RegisterDlg.h" #include "ServicesDlg.h" +#include "ConnectionsDlg.h" #include "VideoDlg.h" #include #include @@ -925,6 +926,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx) ON_MESSAGE(WM_ANTI_BLACKSCREEN, AntiBlackScreen) ON_MESSAGE(WM_OPEN_WINDOW_SCREEN, OpenWindowScreen) ON_MESSAGE(WM_OPENCLIENTLOGDIALOG, OnOpenClientLogDialog) + ON_MESSAGE(WM_OPENCONNECTIONDIALOG, OnOpenConnectionDialog) ON_MESSAGE(WM_SHARE_CLIENT, ShareClient) ON_MESSAGE(WM_ASSIGN_CLIENT, AssignClient) ON_MESSAGE(WM_ASSIGN_ALLCLIENT, AssignAllClient) @@ -991,6 +993,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx) ON_COMMAND(ID_TOOL_RELOAD_PLUGINS, &CMy2015RemoteDlg::OnToolReloadPlugins) ON_COMMAND(ID_TOOL_PLUGIN_SETTINGS, &CMy2015RemoteDlg::OnToolPluginSettings) ON_COMMAND(ID_MCP_SETTINGS, &CMy2015RemoteDlg::OnMcpSettings) + ON_COMMAND(ID_CONNECTION_MANAGE, &CMy2015RemoteDlg::OnConnectionManage) ON_COMMAND(ID_TRIGGER_SETTINGS, &CMy2015RemoteDlg::OnTriggerSettings) ON_COMMAND(ID_SHELLCODE_AES_C_ARRAY, &CMy2015RemoteDlg::OnShellcodeAesCArray) ON_COMMAND(ID_PARAM_KBLOGGER, &CMy2015RemoteDlg::OnParamKblogger) @@ -1418,6 +1421,7 @@ VOID CMy2015RemoteDlg::CreateToolBar() m_ToolBar.SetButtonText(13, _TR("帮助")); m_ToolBar.SetButtonText(14, _T("Lang/语言")); m_ToolBar.SetButtonText(15, _T("MCP")); + m_ToolBar.SetButtonText(16, _TR("连接管理")); // ============================================================ // UIBranding: 根据编译时配置或运行时标志隐藏工具栏按钮 @@ -6622,6 +6626,10 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject) g_2015RemoteDlg->SendMessage(WM_OPENSERVICESDIALOG, 0, (LPARAM)ContextObject); break; } + case TOKEN_CONNECTION_LIST: { // 连接管理【x】 + g_2015RemoteDlg->SendMessage(WM_OPENCONNECTIONDIALOG, 0, (LPARAM)ContextObject); + break; + } case TOKEN_WEBCAM_BITMAPINFO: { // 摄像头【x】 g_2015RemoteDlg->SendMessage(WM_OPENWEBCAMDIALOG, 0, (LPARAM)ContextObject); break; @@ -7813,6 +7821,11 @@ LRESULT CMy2015RemoteDlg::OnOpenServicesDialog(WPARAM wParam, LPARAM lParam) return OpenDialog(wParam, lParam); } +LRESULT CMy2015RemoteDlg::OnOpenConnectionDialog(WPARAM wParam, LPARAM lParam) +{ + return OpenDialog(wParam, lParam); +} + LRESULT CMy2015RemoteDlg::OnOpenRegisterDialog(WPARAM wParam, LPARAM lParam) { return OpenDialog(wParam, lParam); @@ -11367,6 +11380,12 @@ void CMy2015RemoteDlg::OnMcpSettings() ApplyFrpSettings(); } +void CMy2015RemoteDlg::OnConnectionManage() +{ + BYTE cmd = COMMAND_CONNECTION; + SendSelectedCommand(&cmd, sizeof(BYTE)); +} + void CMy2015RemoteDlg::OnTriggerSettings() { if (m_DllList.empty()) { diff --git a/server/2015Remote/2015RemoteDlg.h b/server/2015Remote/2015RemoteDlg.h index 8b4fa9d..a4ae2ff 100644 --- a/server/2015Remote/2015RemoteDlg.h +++ b/server/2015Remote/2015RemoteDlg.h @@ -680,6 +680,7 @@ public: afx_msg void OnCancelShare(); afx_msg void OnWebRemoteControl(); afx_msg void OnMcpSettings(); + afx_msg void OnConnectionManage(); afx_msg void OnProxyPortAutorun(); afx_msg void OnScreenpreviewLoop(); afx_msg void OnMenuCompress(); @@ -694,5 +695,6 @@ public: afx_msg void OnOnlineClientLog(); afx_msg void OnOnlineHistoryActivity(); afx_msg LRESULT OnOpenClientLogDialog(WPARAM wParam, LPARAM lParam); + afx_msg LRESULT OnOpenConnectionDialog(WPARAM wParam, LPARAM lParam); afx_msg void OnOnlineForbidden(); }; diff --git a/server/2015Remote/2015Remote_vs2015.vcxproj b/server/2015Remote/2015Remote_vs2015.vcxproj index 09b9d69..1495085 100644 --- a/server/2015Remote/2015Remote_vs2015.vcxproj +++ b/server/2015Remote/2015Remote_vs2015.vcxproj @@ -351,6 +351,7 @@ + @@ -484,6 +485,7 @@ + @@ -583,6 +585,7 @@ + diff --git a/server/2015Remote/2015Remote_vs2015.vcxproj.filters b/server/2015Remote/2015Remote_vs2015.vcxproj.filters index f4fc3a9..e93389f 100644 --- a/server/2015Remote/2015Remote_vs2015.vcxproj.filters +++ b/server/2015Remote/2015Remote_vs2015.vcxproj.filters @@ -29,6 +29,7 @@ + @@ -128,6 +129,7 @@ + @@ -265,6 +267,7 @@ + diff --git a/server/2015Remote/ConnectionsDlg.cpp b/server/2015Remote/ConnectionsDlg.cpp new file mode 100644 index 0000000..e82fc2c --- /dev/null +++ b/server/2015Remote/ConnectionsDlg.cpp @@ -0,0 +1,305 @@ +// ConnectionsDlg.cpp : 实现文件 +// + +#include "stdafx.h" +#include "2015Remote.h" +#include "ConnectionsDlg.h" +#include "afxdialogex.h" +#include +#include + + +// CConnectionsDlg 对话框 + +IMPLEMENT_DYNAMIC(CConnectionsDlg, CDialog) + +// 连接记录排序辅助:保存完整 ConnectionInfo(供右键强断)与各列排序键。 +// 数字列零填充、地址列转定宽十六进制,保证字符串比较即得正确数值顺序。 +typedef struct ConnItemData { + ConnectionInfo info; + CString key[8]; + CString GetKey(int index) const { return key[index]; } +} ConnItemData; + +CConnectionsDlg::CConnectionsDlg(CWnd* pParent, Server* IOCPServer, CONTEXT_OBJECT *ContextObject) + : DialogBase(CConnectionsDlg::IDD, pParent, IOCPServer, ContextObject, IDI_CONNECTION) + , m_nRClickItem(-1) + , m_nSortColumn(-1) + , m_bSortAscending(true) +{ +} + +CConnectionsDlg::~CConnectionsDlg() +{ +} + +void CConnectionsDlg::DoDataExchange(CDataExchange* pDX) +{ + __super::DoDataExchange(pDX); + DDX_Control(pDX, IDC_LIST, m_ControlList); + DDX_Control(pDX, IDC_STATIC_COUNT, m_Count); +} + +BEGIN_MESSAGE_MAP(CConnectionsDlg, CDialog) + ON_WM_CLOSE() + ON_WM_SIZE() + ON_COMMAND(ID_CONNECTION_KILL, &CConnectionsDlg::OnConnectionKill) + ON_COMMAND(ID_CONNECTION_REFRESH, &CConnectionsDlg::OnConnectionRefresh) + ON_NOTIFY(NM_RCLICK, IDC_LIST, &CConnectionsDlg::OnNMRClickList) + ON_NOTIFY(HDN_ITEMCLICK, 0, &CConnectionsDlg::OnHdnItemclickList) +END_MESSAGE_MAP() + + +// CConnectionsDlg 消息处理程序 + +static CString FormatAddr(BYTE af, const uint8_t* addr) +{ + char buf[INET6_ADDRSTRLEN] = { 0 }; + if (af == AF_INET) { + inet_ntop(AF_INET, addr, buf, INET_ADDRSTRLEN); + } else { + inet_ntop(AF_INET6, addr, buf, INET6_ADDRSTRLEN); + } + return CA2T(buf); +} + +static CString TcpStateName(DWORD state) +{ + switch (state) { + case 1: return _T("CLOSED"); + case 2: return _T("LISTEN"); + case 3: return _T("SYN_SENT"); + case 4: return _T("SYN_RCVD"); + case 5: return _T("ESTABLISHED"); + case 6: return _T("FIN_WAIT1"); + case 7: return _T("FIN_WAIT2"); + case 8: return _T("CLOSE_WAIT"); + case 9: return _T("CLOSING"); + case 10: return _T("LAST_ACK"); + case 11: return _T("TIME_WAIT"); + case 12: return _T("DELETE_TCB"); + default: return _T("-"); + } +} + +// 地址排序键:IPv4/IPv6 统一转成 16 字节定宽十六进制(前缀 4/6 区分协议族), +// 使字符串比较等价于按地址数值排序。 +static CString AddrSortKey(uint8_t af, const uint8_t* addr) +{ + CString key; + key.Format(_T("%d:"), (af == AF_INET) ? 4 : 6); + for (int i = 0; i < 16; ++i) + key.AppendFormat(_T("%02X"), addr[i]); + return key; +} + +BOOL CConnectionsDlg::OnInitDialog() +{ + __super::OnInitDialog(); + + SetIcon(m_hIcon, TRUE); + SetIcon(m_hIcon, FALSE); + CString strString; + strString.FormatL("%s - 连接管理", m_IPAddress); + SetWindowText(strString); + + m_ControlList.SetExtendedStyle(LVS_EX_FULLROWSELECT); + m_ControlList.InsertColumnL(0, "协议", LVCFMT_LEFT, 50); + m_ControlList.InsertColumnL(1, "本地地址", LVCFMT_LEFT, 120); + m_ControlList.InsertColumnL(2, "本地端口", LVCFMT_LEFT, 70); + m_ControlList.InsertColumnL(3, "远端地址", LVCFMT_LEFT, 120); + m_ControlList.InsertColumnL(4, "远端端口", LVCFMT_LEFT, 70); + m_ControlList.InsertColumnL(5, "状态", LVCFMT_LEFT, 100); + m_ControlList.InsertColumnL(6, "PID", LVCFMT_LEFT, 60); + m_ControlList.InsertColumnL(7, "进程名", LVCFMT_LEFT, 180); + + ShowConnectionList(); + return TRUE; +} + +int CConnectionsDlg::ShowConnectionList(void) +{ + Buffer tmp = m_ContextObject->InDeCompressedBuffer.GetMyBuffer(0); + int len = tmp.GetBufferLength(); + DeleteAllItems(); + + if (len < 3) { + CString strEmpty; + strEmpty.FormatL("连接数:%d", 0); + m_Count.SetWindowText(strEmpty); + return 0; + } + + const BYTE* p = tmp.GetBuffer(0); + uint16_t count = 0; + memcpy(&count, p + 1, 2); + int avail = (len - 3) / (int)sizeof(ConnectionInfo); + if ((int)count > avail) count = (uint16_t)avail; + + for (int i = 0; i < (int)count; ++i) { + const ConnectionInfo* info = (const ConnectionInfo*)(p + 3 + i * sizeof(ConnectionInfo)); + + CString strProto = (info->proto == 1) ? _T("TCP") : _T("UDP"); + CString strLocalAddr = FormatAddr(info->af, info->localAddr); + CString strRemoteAddr = FormatAddr(info->af, info->remoteAddr); + CString strState = (info->proto == 1) ? TcpStateName(info->state) : _T("-"); + + CString strLocalPort, strRemotePort, strPid, strProcName; + strLocalPort.Format(_T("%u"), info->localPort); + strRemotePort.Format(_T("%u"), info->remotePort); + strPid.Format(_T("%u"), info->pid); + strProcName = CA2T(info->procName, CP_UTF8); + + auto* data = new ConnItemData; + data->info = *info; + data->key[0] = strProto; + data->key[1] = AddrSortKey(info->af, info->localAddr); + data->key[2].Format(_T("%05u"), info->localPort); + data->key[3] = AddrSortKey(info->af, info->remoteAddr); + data->key[4].Format(_T("%05u"), info->remotePort); + data->key[5].Format(_T("%03u"), info->state); + data->key[6].Format(_T("%010u"), info->pid); + data->key[7] = strProcName; + + m_ControlList.InsertItem(i, strProto); + m_ControlList.SetItemText(i, 1, strLocalAddr); + m_ControlList.SetItemText(i, 2, strLocalPort); + m_ControlList.SetItemText(i, 3, strRemoteAddr); + m_ControlList.SetItemText(i, 4, strRemotePort); + m_ControlList.SetItemText(i, 5, strState); + m_ControlList.SetItemText(i, 6, strPid); + m_ControlList.SetItemText(i, 7, strProcName); + m_ControlList.SetItemData(i, (DWORD_PTR)data); + } + + CString strCount; + strCount.FormatL("连接数:%d", (int)count); + m_Count.SetWindowText(strCount); + + return 0; +} + +void CConnectionsDlg::OnClose() +{ + CancelIO(); + if (IsProcessing()) { + ShowWindow(SW_HIDE); + return; + } + + DeleteAllItems(); + + DialogBase::OnClose(); +} + +void CConnectionsDlg::DeleteAllItems() +{ + for (int i = 0; i < m_ControlList.GetItemCount(); i++) { + auto data = (ConnItemData*)m_ControlList.GetItemData(i); + if (data) delete data; + } + m_ControlList.DeleteAllItems(); +} + +void CConnectionsDlg::OnNMRClickList(NMHDR *pNMHDR, LRESULT *pResult) +{ + LPNMITEMACTIVATE pAct = reinterpret_cast(pNMHDR); + m_nRClickItem = pAct->iItem; + + bool canKill = false; + if (m_nRClickItem >= 0) { + auto* data = (ConnItemData*)m_ControlList.GetItemData(m_nRClickItem); + canKill = (data && data->info.proto == 1 && data->info.af == AF_INET); + } + + CMenu Menu; + Menu.LoadMenu(IDR_MENU_CONNECTIONS); + TranslateMenu(&Menu); + CMenu* SubMenu = Menu.GetSubMenu(0); + if (SubMenu && !canKill) { + SubMenu->EnableMenuItem(ID_CONNECTION_KILL, MF_BYCOMMAND | MF_GRAYED); + } + CPoint Point; + GetCursorPos(&Point); + SubMenu->TrackPopupMenu(TPM_LEFTALIGN | TPM_LEFTBUTTON, Point.x, Point.y, this); + *pResult = 0; +} + +void CConnectionsDlg::OnConnectionKill() +{ + if (m_nRClickItem < 0) return; + auto* data = (ConnItemData*)m_ControlList.GetItemData(m_nRClickItem); + if (!data || data->info.proto != 1 || data->info.af != AF_INET) return; + + LPBYTE szBuffer = (LPBYTE)LocalAlloc(LPTR, 1 + sizeof(ConnectionInfo)); + if (szBuffer == NULL) return; + szBuffer[0] = COMMAND_KILL_CONNECTION; + memcpy(szBuffer + 1, &data->info, sizeof(ConnectionInfo)); + m_ContextObject->Send2Client(szBuffer, LocalSize(szBuffer)); + LocalFree(szBuffer); +} + +void CConnectionsDlg::OnConnectionRefresh() +{ + BYTE bToken = COMMAND_CONNECTION; + m_ContextObject->Send2Client(&bToken, 1); +} + +void CConnectionsDlg::OnReceiveComplete(void) +{ + switch (m_ContextObject->InDeCompressedBuffer.GetBYTE(0)) { + case TOKEN_CONNECTION_LIST: + ShowConnectionList(); + break; + default: + break; + } +} + +void CConnectionsDlg::OnSize(UINT nType, int cx, int cy) +{ + __super::OnSize(nType, cx, cy); + + if (!m_ControlList.GetSafeHwnd()) return; + + CRect rc; + m_ControlList.GetWindowRect(&rc); + ScreenToClient(&rc); + + m_ControlList.MoveWindow(0, 0, cx, cy, TRUE); +} + +int CALLBACK CConnectionsDlg::CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort) +{ + auto* pSortInfo = reinterpret_cast*>(lParamSort); + int nColumn = pSortInfo->first; + bool bAscending = pSortInfo->second; + + auto* context1 = (ConnItemData*)lParam1; + auto* context2 = (ConnItemData*)lParam2; + CString s1 = context1->GetKey(nColumn); + CString s2 = context2->GetKey(nColumn); + + int result = s1.Compare(s2); + return bAscending ? result : -result; +} + +void CConnectionsDlg::SortByColumn(int nColumn) +{ + if (nColumn == m_nSortColumn) { + m_bSortAscending = !m_bSortAscending; + } else { + m_nSortColumn = nColumn; + m_bSortAscending = true; + } + + std::pair sortInfo(m_nSortColumn, m_bSortAscending); + m_ControlList.SortItems(CompareFunction, reinterpret_cast(&sortInfo)); +} + +void CConnectionsDlg::OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult) +{ + LPNMHEADER pNMHeader = reinterpret_cast(pNMHDR); + SortByColumn(pNMHeader->iItem); + *pResult = 0; +} diff --git a/server/2015Remote/ConnectionsDlg.h b/server/2015Remote/ConnectionsDlg.h new file mode 100644 index 0000000..7cc969a --- /dev/null +++ b/server/2015Remote/ConnectionsDlg.h @@ -0,0 +1,43 @@ +#pragma once +#include "afxcmn.h" +#include "IOCPServer.h" +#include "afxwin.h" + +// CConnectionsDlg 对话框:展示受管主机当前 TCP/UDP 连接,右键可强制断开 IPv4 TCP 连接。 + +class CConnectionsDlg : public DialogBase +{ + DECLARE_DYNAMIC(CConnectionsDlg) + +public: + CConnectionsDlg(CWnd* pParent = NULL, Server* IOCPServer = NULL, CONTEXT_OBJECT *ContextObject = NULL); + virtual ~CConnectionsDlg(); + + int ShowConnectionList(void); + void OnReceiveComplete(void); + + enum { IDD = IDD_DIALOG_CONNECTIONS }; + +protected: + virtual void DoDataExchange(CDataExchange* pDX); + + DECLARE_MESSAGE_MAP() +public: + CListCtrl m_ControlList; + virtual BOOL OnInitDialog(); + + void DeleteAllItems(); + void SortByColumn(int nColumn); + afx_msg void OnClose(); + afx_msg void OnNMRClickList(NMHDR *pNMHDR, LRESULT *pResult); + afx_msg void OnConnectionKill(); + afx_msg void OnConnectionRefresh(); + afx_msg void OnSize(UINT nType, int cx, int cy); + afx_msg void OnHdnItemclickList(NMHDR *pNMHDR, LRESULT *pResult); + static int CALLBACK CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort); + + CStatic m_Count; + int m_nRClickItem; + int m_nSortColumn; + bool m_bSortAscending; +}; diff --git a/server/2015Remote/lang/en_US.ini b/server/2015Remote/lang/en_US.ini index 4733d9e..9a91f52 100644 --- a/server/2015Remote/lang/en_US.ini +++ b/server/2015Remote/lang/en_US.ini @@ -2067,3 +2067,12 @@ JPEG %s ¡¤ Լ %.2f MB/s=%s ¡¤ about %.2f MB/s %s ¡¤ ²»ÊÜÏÞË٣¨ÊÜÍøÂç´ø¿íԼÊø£©=%s ¡¤ Unlimited (limited by network bandwidth) Îļþ´«ÊäËٶÈÒѱ£´档Éϴ«ËٶÈÖØÆôÖ÷¿ض˺óÉúЧ£»ÏÂÔØËٶÈÏ´δò¿ªÎļþ¹ÜÀí»òԶ³Ì×ÀÃæºóÉúЧ¡£=Transfer speed saved. Upload speed takes effect after restarting the master; download speed takes effect the next time you open File Manager or Remote Desktop. + +Á¬½ӹÜÀí=Connections +%s - Á¬½ӹÜÀí=%s - Connections +±¾µصØַ=Local Address +±¾µض˿Ú=Local Port +Զ¶˵Øַ=Remote Address +Զ¶˶˿Ú=Remote Port +Á¬½ÓÊý:%d=Connections: %d +ǿÖƶϿª=Force Disconnect diff --git a/server/2015Remote/lang/zh_TW.ini b/server/2015Remote/lang/zh_TW.ini index e12eb97..90fa806 100644 --- a/server/2015Remote/lang/zh_TW.ini +++ b/server/2015Remote/lang/zh_TW.ini @@ -2058,3 +2058,12 @@ JPEG %s ¡¤ Լ %.2f MB/s=%s ¡¤ ¼s %.2f MB/s %s ¡¤ ²»ÊÜÏÞË٣¨ÊÜÍøÂç´ø¿íԼÊø£©=%s ¡¤ ²»ÏÞË٣¨ÊܾW·îlŒ’ÏÞÖƣ© Îļþ´«ÊäËٶÈÒѱ£´档Éϴ«ËٶÈÖØÆôÖ÷¿ض˺óÉúЧ£»ÏÂÔØËٶÈÏ´δò¿ªÎļþ¹ÜÀí»òԶ³Ì×ÀÃæºóÉúЧ¡£=™n°¸‚÷ݔËٶÈÒу¦´档Éς÷ËٶÈÖ؆¢Ö÷¿ضËááÉúЧ£»ÏÂÝdËٶÈÏ´Îé_†¢™n°¸¹ÜÀí»òßh¶Ë×ÀÃæááÉúЧ¡£ + +Á¬½ӹÜÀí=ßB¾€¹ÜÀí +%s - Á¬½ӹÜÀí=%s - ßB¾€¹ÜÀí +±¾µصØַ=±¾™Cλַ +±¾µض˿Ú=±¾™CßB½Ӳº +Զ¶˵Øַ=ßh¶Ëλַ +Զ¶˶˿Ú=ßh¶ËßB½Ӳº +Á¬½ÓÊý:%d=ßB¾€”µ:%d +ǿÖƶϿª=�ŠÖÆÖДà diff --git a/server/2015Remote/res/ToolBar_Main.bmp b/server/2015Remote/res/ToolBar_Main.bmp index 08c638802c81c7e45670ddb8ae777ac2be85463a..55fc25431dccd023a2d748cd1216f732de4e7cb7 100644 GIT binary patch delta 2767 zcmbVO4@?tR80WmL_7Wlq2^kB~*sQU$TN5i8i+>ipMw4X*(nhrE+=L-c9XN@bPL#?< zT;p_Ek8^ZP9RzgZ)MedGZZ4uut5MO4RHqgT)>3GtRv0RpIov}x`Lm|fm`eee6e z-|v0j_r7=6rW>lJ>d~*8a8B;mHvRl4s;{pvdZ*WZ zc@1lPCrwp(sT5jivM3O`e zGR4pq5UQ$XQCwkm4B8A5SroPBdQLHPE<2c$!sWj$s5E4ScO`3FG>#P0Gn>n`{0p&G z;zQ_MU@t?@W^LAF&bsg}R5~FWVos~C*%6AU&8N7$LP2XtgKJ23{A56XDDaE0X{?mu zEX9IR7FA}mmf07SRm>NB?JY~@ET2AAtjsGDo2aOyxLli{m_kWXjG$^T*(tE3gw9^7 zS+ewp!sQq-hbIjt2Q-s1I{A|)3pO3M+7g?4(HFkPKNxxzXQ~l`H0W-3M=n&>Cfdm6 z=hr!J`ENRnCB$3YjQS@c$m`fEvr;aBW9aD(+56y)-Vk(OP?`)W5&fIeko}v8JH2A- zkXA||LSty)nt{`0A}poJFp?f`J40tRWSkXdr?Mjm&B{&x`+XEsle6{2F!ipgc zWkRt=aY~P*{aW1Lf4EF$qFnASC^^jUcN&)vQ~4P$5~G0CX8NvrZL1gHHhjCZ@Ywuv zB}$UieeltSm2+X{KH3D2_n5{IfNQ1z{`s2sr7N}Bw(NR~r`29BBf-VEdDgpa%Ua0{ zqo>7V zFH0q31On!0_-s$d&QSZ7?vC$zyH7B*mN$(jNbIE2_D8xN{`kq0brTa?$NH)EkIkUK zW{8KOHQKIVKysmCwdGLbG~cNSi7n8!_;%C}JJyy@@wpR;L0sn-UWrB?ZRto&t!_+zXNYFgU076 ziIXTNa2ftOV>1Yfw@f4MsV77uN-CM;Z+|Ns*4>Fkl!P)#Gy+p8A9*v0ezopy+YuR? za(Vy6Zh^nI`-qH9xeVXfCGuO7d20?4Q|nMLNRXxrSr zD0@+F9E%c5^esN`Y@Kr_28%bHjEOeb=Xp@uLjLn{JsAfxYGK9_8x@jtQ<2*fM&g21|O;Wf5sALob mKK2xf9JPug7yeR={O|8+49_TMaYtW1K)B4o(N8D3wEqALn1dSt delta 705 zcmdlsjeXk#HXSElvki<4AYcZ>8XyKU1A_sOVq}m2@fa9@@<;d>fOzu60;`Py8k`XP zl6CWb$+Jw8_e)M7fLCvJS31Ky*;A)|VgM(Z)cnn!Iw8!HFX&7lfM;%YGkwB5Il*3N z^BKz*96;4@<^e^QA?9ITpvS;avQFE9c_+{hW*W zPjW7ve~ELk{vFQ6`=4?y_J7a0`2G*h<@rpEoXh`nGjeY47iDDTT&^S!4I|(7DG`hxIVJ|MftgH`(WOJKE?^ti6drC{tzfJ zd4h+~c8BSV>zF47h)%a$2-Hm+F>UhOKxvTfN%m}$`=hlc1{iM#>huPhBS9dcfD#he Ya=2F4&G)4?Fm4P8V23hbsZWIw0H1{!KmY&$ diff --git a/server/2015Remote/res/connections.ico b/server/2015Remote/res/connections.ico new file mode 100644 index 0000000000000000000000000000000000000000..5ad45b6bfaa4370c18bfae384c6211eda730958a GIT binary patch literal 3898 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 e1V_7>U&oZ@l0bvxA3|6Bwr0%i%;f*RVgCWA