Improve: client/server - stable client ID via MachineGuid+path (V2)
This commit is contained in:
@@ -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<std::string>& clientInfo)
|
||||
{
|
||||
std::string s;
|
||||
@@ -236,6 +239,52 @@ uint64_t CalcalateID(const std::vector<std::string>& 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];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<ClientKey> 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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user