diff --git a/client/LoginServer.cpp b/client/LoginServer.cpp index ab2b9a8..3d102bd 100644 --- a/client/LoginServer.cpp +++ b/client/LoginServer.cpp @@ -225,7 +225,10 @@ std::string GetCurrentUserNameA() #define XXH_INLINE_ALL #include "common/xxhash.h" -// 基于客户端信息计算唯一ID: { IP, PC, OS, CPU, PATH } + +// 老算法:基于客户端信息计算唯一ID: { IP, PC, OS, CPU, PATH } +// 注意:pubIP 不稳定(DHCP/换网络)会让 ID 跳变;同 hostname+同安装路径的多机会撞库。 +// 保留此函数仅为协议兼容(老服务端仍按这个算法验算 RES_CLIENT_ID)。 uint64_t CalcalateID(const std::vector& clientInfo) { std::string s; @@ -236,6 +239,52 @@ uint64_t CalcalateID(const std::vector& clientInfo) return XXH64(s.c_str(), s.length(), 0); } +// 读取 Windows 安装时生成的机器 GUID。 +// HKLM\Software\Microsoft\Cryptography\MachineGuid 是 Windows 安装时生成的随机 GUID, +// 重装系统才会变;局域网每台机器都不同(即便同镜像,sysprep 也会重置)。 +// 这是比 pubIP/PCName/CPU 都更稳定且更具区分度的硬件标识。 +static std::string GetMachineGuidWindows() +{ + HKEY hKey = NULL; + // KEY_WOW64_64KEY: 32 位进程也访问 64 位注册表视图,避免 WOW6432Node 重定向。 + if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, + "SOFTWARE\\Microsoft\\Cryptography", + 0, KEY_READ | KEY_WOW64_64KEY, &hKey) != ERROR_SUCCESS) { + return std::string(); + } + char buf[64] = {}; + DWORD sz = sizeof(buf) - 1; // 留 1 字节给 NUL + DWORD type = 0; + LSTATUS s = RegQueryValueExA(hKey, "MachineGuid", NULL, &type, + (BYTE*)buf, &sz); + RegCloseKey(hKey); + if (s != ERROR_SUCCESS || type != REG_SZ) return std::string(); + return std::string(buf); +} + +// 路径归一化:先尝试展开成长路径(如 PROGRA~1 -> Program Files),再小写化。 +// 用于 V2 ID 的输入,保证大小写或长短名变化时同一可执行文件得到同一 ID。 +static std::string NormalizeExePathLower(const char* path) +{ + char longPath[MAX_PATH] = {}; + if (GetLongPathNameA(path, longPath, MAX_PATH) == 0) { + // 展开失败(路径不存在等罕见情况):直接用原值 + strcpy_s(longPath, path); + } + CharLowerA(longPath); // 原地小写化(对 ASCII 简单,对中文路径会按宽字符规则处理) + return std::string(longPath); +} + +// 新算法:machineGuid + 归一化路径 +// - 同机同程序:永远同 ID(不依赖 IP/PCName/OS/CPU)。 +// - 局域网多机相同镜像:MachineGuid 必不同 → ID 必不同。 +// - 一台机两份程序在不同目录 → ID 不同。 +uint64_t CalcalateIDv2(const std::string& machineGuid, const std::string& normalizedPath) +{ + std::string s = machineGuid + "|" + normalizedPath; + return XXH64(s.c_str(), s.length(), 0); +} + BOOL IsAuthKernel(std::string &str) { BOOL isAuthKernel = FALSE; std::string pid = std::to_string(GetCurrentProcessId()); @@ -332,7 +381,17 @@ LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS& conn, const std::string LoginInfor.AddReserved(IsRunningAsAdmin()); char cpuInfo[32]; sprintf(cpuInfo, "%dMHz", dwCPUMHz); - conn.clientID = CalcalateID({ pubIP, szPCName, LoginInfor.OsVerInfoEx, cpuInfo, buf }); + // V2 ID 算法:MachineGuid + 归一化路径 + // - 同机同程序路径永远同 ID(不依赖 IP/PCName/OS/CPU 漂移) + // - 局域网多机即便同镜像(sysprep 会让 MachineGuid 各不同)也不撞库 + // MachineGuid 读取失败的极端情况退化到老算法,保兼容。 + std::string machineGuid = GetMachineGuidWindows(); + if (!machineGuid.empty()) { + conn.clientID = CalcalateIDv2(machineGuid, NormalizeExePathLower(buf)); + } else { + Mprintf("WARN: MachineGuid 读取失败,回退到老 ID 算法\n"); + conn.clientID = CalcalateID({ pubIP, szPCName, LoginInfor.OsVerInfoEx, cpuInfo, buf }); + } auto clientID = std::to_string(conn.clientID); Mprintf("此客户端的唯一标识为: %s\n", clientID.c_str()); char reservedInfo[64]; diff --git a/linux/main.cpp b/linux/main.cpp index f52a22b..b72ea24 100644 --- a/linux/main.cpp +++ b/linux/main.cpp @@ -690,6 +690,49 @@ std::string getUsername() return u ? u : "?"; } +// 读取 systemd / dbus 维护的 machine-id(与 Windows MachineGuid 等价) +// /etc/machine-id 在系统首次启动时生成的随机 32 字符 hex GUID。 +// 对应 Windows: HKLM\Software\Microsoft\Cryptography\MachineGuid。 +// 重装系统才会变;同一镜像 dd 出来的多机会撞——但规范的批量部署 +// 工具 (cloud-init / kickstart) 会重置它。 +static std::string getMachineId() +{ + // 优先 /etc/machine-id;某些精简系统可能放在 /var/lib/dbus/machine-id + const char* paths[] = { "/etc/machine-id", "/var/lib/dbus/machine-id" }; + for (const char* p : paths) { + std::ifstream f(p); + if (!f.is_open()) continue; + std::string id; + std::getline(f, id); + // 去掉尾部空白和换行 + while (!id.empty() && (id.back() == '\n' || id.back() == '\r' || + id.back() == ' ' || id.back() == '\t')) { + id.pop_back(); + } + if (!id.empty()) return id; + } + return std::string(); +} + +// 路径归一化(Linux 版):解析符号链接 + 转小写 +// realpath 等价于 Windows 的 GetLongPathName,把 /usr/local/bin/../foo 这种 +// 折回到规范形式;小写化避免大小写差异引起 ID 不同(Linux 文件系统本身大小写 +// 敏感,但保持与 Windows V2 算法一致的归一化策略,跨端一致性优先)。 +static std::string normalizeExePathLower(const std::string& path) +{ + char resolved[PATH_MAX] = {}; + std::string out; + if (realpath(path.c_str(), resolved) != nullptr) { + out = resolved; + } else { + out = path; // 解析失败(罕见):用原值 + } + for (auto& c : out) { + if (c >= 'A' && c <= 'Z') c = c - 'A' + 'a'; + } + return out; +} + // 获取屏幕分辨率字符串(格式 "显示器数:宽*高") std::string getScreenResolution() { @@ -1010,17 +1053,30 @@ int main(int argc, char* argv[]) logInfo.AddReserved(getUsername().c_str()); // [13] RES_USERNAME logInfo.AddReserved(getuid() == 0 ? 1 : 0); // [14] RES_ISADMIN logInfo.AddReserved(getScreenResolution().c_str()); // [15] RES_RESOLUTION - // 计算客户端 ID(与服务端 CONTEXT_OBJECT::CalculateID 相同算法) - // 格式: pubIP|hostname|os|cpu|path - char cpuStr[32]; - snprintf(cpuStr, sizeof(cpuStr), "%uMHz", logInfo.dwCPUMHz); - std::string idInput = (pubIP.empty() ? "?" : pubIP) + "|" + - hostname + "|" + - distro + "|" + - cpuStr + "|" + - exePath; - g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0); - Mprintf("Calculated clientID: %llu (from: %s)\n", g_myClientID, idInput.c_str()); + // V2 ID 算法:machine-id + 归一化路径 + // - 同机同程序路径永远同 ID(不依赖 IP/hostname/distro/CPU 漂移) + // - 局域网多机即便同镜像,cloud-init/kickstart 会让 machine-id 各不同 + // - machine-id 读取失败时退化到老算法(pubIP|hostname|distro|cpu|path)保兼容 + std::string machineId = getMachineId(); + if (!machineId.empty()) { + std::string normPath = normalizeExePathLower(exePath); + std::string idInput = machineId + "|" + normPath; + g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0); + Mprintf("Calculated clientID(v2): %llu (machineId=%s, path=%s)\n", + g_myClientID, machineId.c_str(), normPath.c_str()); + } else { + // 老算法兜底(与服务端 CONTEXT_OBJECT::CalculateID 相同算法) + // 格式: pubIP|hostname|os|cpu|path + char cpuStr[32]; + snprintf(cpuStr, sizeof(cpuStr), "%uMHz", logInfo.dwCPUMHz); + std::string idInput = (pubIP.empty() ? "?" : pubIP) + "|" + + hostname + "|" + + distro + "|" + + cpuStr + "|" + + exePath; + g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0); + Mprintf("Calculated clientID(v1 fallback): %llu (machine-id 读取失败)\n", g_myClientID); + } logInfo.AddReserved(std::to_string(g_myClientID).c_str()); // [16] RES_CLIENT_ID logInfo.AddReserved((int)getpid()); // [17] RES_PID diff --git a/macos/main.mm b/macos/main.mm index a6336ac..90a80a8 100644 --- a/macos/main.mm +++ b/macos/main.mm @@ -214,6 +214,54 @@ static std::string getUsername() return user ? std::string(user) : "unknown"; } +// 读取 IOKit 维护的 IOPlatformUUID(与 Windows MachineGuid 等价) +// 这是主板/系统级 UUID,由 IOPlatformExpertDevice 服务提供,重装系统通常不变。 +// 对应:Windows HKLM\Software\Microsoft\Cryptography\MachineGuid +// Linux /etc/machine-id +static std::string getMachineId() +{ + std::string result; + io_service_t platformExpert = IOServiceGetMatchingService( + kIOMasterPortDefault, + IOServiceMatching("IOPlatformExpertDevice")); + if (platformExpert != IO_OBJECT_NULL) { + CFTypeRef uuidProperty = IORegistryEntryCreateCFProperty( + platformExpert, CFSTR(kIOPlatformUUIDKey), + kCFAllocatorDefault, 0); + if (uuidProperty != nullptr) { + if (CFGetTypeID(uuidProperty) == CFStringGetTypeID()) { + CFStringRef uuidStr = (CFStringRef)uuidProperty; + char buf[64] = {}; + if (CFStringGetCString(uuidStr, buf, sizeof(buf), kCFStringEncodingUTF8)) { + result = buf; + } + } + CFRelease(uuidProperty); + } + IOObjectRelease(platformExpert); + } + return result; +} + +// 路径归一化(macOS 版):解析符号链接 + 转小写 +// realpath 把 /Applications/foo/../bar 之类折回规范形式; +// 小写化保持与 Windows/Linux 跨端一致。macOS HFS+/APFS 默认大小写不敏感, +// 转小写不改变文件标识、但避免路径串大小写差异引起 ID 不同。 +static std::string normalizeExePathLower(const std::string& path) +{ + char resolved[PATH_MAX] = {}; + std::string out; + if (realpath(path.c_str(), resolved) != nullptr) { + out = resolved; + } else { + out = path; // 解析失败:用原值 + } + for (auto& c : out) { + if (c >= 'A' && c <= 'Z') c = c - 'A' + 'a'; + } + return out; +} + // Get screen resolution static std::string getScreenResolution() { @@ -552,16 +600,30 @@ static void fillLoginInfo(LOGIN_INFOR& info) std::string resolution = getScreenResolution(); info.AddReserved(resolution.c_str()); - // 17. Client ID (calculated from system info, same algorithm as server) - // Format: pubIP|hostname|os|cpu|path - char cpuStr[32]; - snprintf(cpuStr, sizeof(cpuStr), "%uMHz", info.dwCPUMHz); - std::string idInput = (pubIP.empty() ? "?" : pubIP) + "|" + - hostname + "|" + - osVer + "|" + - cpuStr + "|" + - exePath; - g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0); + // 17. Client ID + // V2 算法:IOPlatformUUID + 归一化路径 + // - 同机同程序路径永远同 ID(不依赖 IP/hostname/os/CPU 漂移) + // - IOPlatformUUID 主板级,重装系统通常不变;多机各不相同 + // - 读取失败时退化到老算法(pubIP|hostname|os|cpu|path)保兼容 + std::string machineId = getMachineId(); + if (!machineId.empty()) { + std::string normPath = normalizeExePathLower(exePath); + std::string idInput = machineId + "|" + normPath; + g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0); + NSLog(@"ClientID(v2): %llu (machineId=%s, path=%s)", + g_myClientID, machineId.c_str(), normPath.c_str()); + } else { + // 老算法兜底 + char cpuStr[32]; + snprintf(cpuStr, sizeof(cpuStr), "%uMHz", info.dwCPUMHz); + std::string idInput = (pubIP.empty() ? "?" : pubIP) + "|" + + hostname + "|" + + osVer + "|" + + cpuStr + "|" + + exePath; + g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0); + NSLog(@"ClientID(v1 fallback): %llu (IOPlatformUUID 读取失败)", g_myClientID); + } info.AddReserved(std::to_string(g_myClientID).c_str()); NSLog(@"LOGIN_INFOR filled: OS=%s, Host=%s, CPU=%dMHz, PubIP=%s, ClientID=%llu", diff --git a/server/2015Remote/2015RemoteDlg.cpp b/server/2015Remote/2015RemoteDlg.cpp index eeebe23..a5b59a2 100644 --- a/server/2015Remote/2015RemoteDlg.cpp +++ b/server/2015Remote/2015RemoteDlg.cpp @@ -1342,15 +1342,25 @@ VOID CMy2015RemoteDlg::AddList(CString strIP, CString strAddr, CString strPCName verDisplay, install, startTime, v[RES_CLIENT_TYPE].empty() ? "?" : v[RES_CLIENT_TYPE].c_str(), path, v[RES_CLIENT_PUBIP].empty() ? strIP : v[RES_CLIENT_PUBIP].c_str(), startTime, capStr, }; - auto id = CONTEXT_OBJECT::CalculateID(data); - auto id_str = std::to_string(id); - if (v[RES_CLIENT_ID].empty()) { - v[RES_CLIENT_ID] = id_str; - } else if (id_str != v[RES_CLIENT_ID]) { - Mprintf("上线消息 - 主机ID错误: calc=%llu, recv=%s, IP=%s, Path=%s\n", - id, v[RES_CLIENT_ID].c_str(), strIP.GetString(), path.GetString()); + // 优先采用客户端自报的 ID(新客户端用 V2 算法 = MachineGuid + 归一化路径, + // 比服务端按老算法 IP+PC+OS+CPU+PATH 重算更稳定)。 + // 客户端未发或解析失败时,回退到服务端老算法重算(兼容老客户端)。 + auto computedId = CONTEXT_OBJECT::CalculateID(data); + uint64_t id = 0; + if (!v[RES_CLIENT_ID].empty()) { + id = std::strtoull(v[RES_CLIENT_ID].c_str(), nullptr, 10); } + if (id == 0) { + id = computedId; + v[RES_CLIENT_ID] = std::to_string(id); + } + bool modify = false, needConvert = true; + + // 新客户端 V2 ID 算法首次上线时,把老 ID 下的元数据迁过来。 + if (TryMigrateClientMetadata(id, strPCName, path)) { + modify = true; + } CString loc = m_ClientMap->GetClientMapData(id, MAP_LOCATION); if (loc.IsEmpty()) { loc = v[RES_CLIENT_LOC].c_str(); @@ -5612,6 +5622,65 @@ LRESULT CMy2015RemoteDlg::OnUserToOnlineList(WPARAM wParam, LPARAM lParam) } +// 启发式 ID 迁移:当新客户端 V2 算法生成的 ID 在 m_ClientMap 里没条目时, +// 按 (ComputerName, ProgramPath) 扫老条目找唯一匹配,把元数据搬过去。 +// 设计取舍见相关讨论: +// - 严格 ComputerName 相等 + 大小写不敏感 ProgramPath 匹配。 +// - 多候选保守跳过——避免局域网同 hostname+同路径多机互相串备注。 +// - 老条目不删,作为审计/回滚依据;dat 文件不会显著膨胀。 +// +// 线程安全说明: +// - AddList 由 SendMessage(WM_USERTOONLINELIST) 进入,跑在 UI 线程,串行。 +// - newId 在本函数返回前没进入 m_HostList,UI 上看不到这台机器, +// 用户无法通过右键触发 OnOnlineHostnote 写 newId 的备注。 +// - IO 线程的 AUTH 写发生在登录认证流之后,针对的是已注册客户端,不会 +// 在 newId 首次出现的瞬间踩进来。 +// - GetAll() 返回快照副本,迭代期间老条目被并发修改不影响匹配(我们只 +// 看 ComputerName/ProgramPath,这两个字段不会被并发改写)。 +// - _ClientList 实现内部有同步(项目里其它路径同样不持 m_cs 调用它)。 +// 故无需额外加锁。 +bool CMy2015RemoteDlg::TryMigrateClientMetadata(uint64_t newId, const CString& pcName, const CString& exePath) +{ + if (!m_ClientMap) return false; + if (m_ClientMap->Exists(newId)) return false; // 已有条目,无需迁移 + + const std::string targetPC = pcName.GetString(); + const std::string targetPath = exePath.GetString(); + + // 扫描所有条目,找匹配的老 ID(多于一个就停,按歧义处理) + std::vector candidates; + for (const auto& kv : m_ClientMap->GetAll()) { + if (kv.first == newId) continue; + if (strcmp(kv.second.ComputerName, targetPC.c_str()) == 0 && + _stricmp(kv.second.ProgramPath, targetPath.c_str()) == 0) { + candidates.push_back(kv.first); + if (candidates.size() > 1) break; + } + } + + if (candidates.size() > 1) { + Mprintf("ID 迁移歧义: PC=%s, Path=%s 命中 %zu+ 个候选,保守跳过——" + "需要运维手动确认是哪台机器的元数据\n", + targetPC.c_str(), targetPath.c_str(), candidates.size()); + return false; + } + if (candidates.empty()) return false; // 真正的新机器 + + // 唯一匹配:复制用户可设置的元数据到新 ID(其它字段下次心跳/上线会覆盖) + ClientKey oldId = candidates[0]; + m_ClientMap->SetClientMapData(newId, MAP_NOTE, + m_ClientMap->GetClientMapData(oldId, MAP_NOTE).GetString()); + m_ClientMap->SetClientMapData(newId, MAP_LOCATION, + m_ClientMap->GetClientMapData(oldId, MAP_LOCATION).GetString()); + m_ClientMap->SetClientMapInteger(newId, MAP_LEVEL, + m_ClientMap->GetClientMapInteger(oldId, MAP_LEVEL)); + m_ClientMap->SetClientMapInteger(newId, MAP_AUTH, + m_ClientMap->GetClientMapInteger(oldId, MAP_AUTH)); + Mprintf("ID 迁移: %llu -> %llu (PC=%s, Path=%s)\n", + oldId, newId, targetPC.c_str(), targetPath.c_str()); + return true; +} + // 根据列表显示索引获取 context(考虑分组过滤) context* CMy2015RemoteDlg::GetContextByListIndex(int iItem) { diff --git a/server/2015Remote/2015RemoteDlg.h b/server/2015Remote/2015RemoteDlg.h index 5d9c61d..5068015 100644 --- a/server/2015Remote/2015RemoteDlg.h +++ b/server/2015Remote/2015RemoteDlg.h @@ -245,6 +245,12 @@ public: std::string m_v2KeyPath; // V2 密钥文件路径 void RebuildFilteredIndices(); // 重建过滤索引 context* GetContextByListIndex(int iItem); // 根据列表索引获取 context(考虑分组过滤) + + // 启发式 ID 迁移:新客户端首次上线时,按 (ComputerName, ProgramPath) 在 m_ClientMap + // 里找老条目,若唯一匹配则把元数据(备注/位置/级别/授权)拷贝到 newId。 + // 多于一个候选时保守跳过,写日志让运维手动处理,避免误聚合。 + // 返回 true 表示有迁移发生(调用方需触发 dat 文件落盘)。 + bool TryMigrateClientMetadata(uint64_t newId, const CString& pcName, const CString& exePath); void LoadListData(const std::string& group); void DeletePopupWindow(BOOL bForce = FALSE); void CheckHeartbeat();