#pragma once // ============================================================================ // LANChecker.h — 远程控制软件的反滥用 / 合规执行模块 // ============================================================================ // // 立场与背景 // ---------------------------------------------------------------------------- // 本软件提供远程桌面与设备管理能力,理论上具备被滥用为非授权远程控制 / 监控 // 工具的可能性。为防止此类滥用,并明确合规边界,本模块在客户端进程内实施 // 分级强制性技术限制:能力随授权级别开放,越权使用形态会被本模块的检测路径 // 捕获并以可见方式提醒终端用户(弹窗 / 终止进程)。 // // 本模块的存在不是装饰,而是出厂安全姿态的一部分。任何试图通过外部手段 // 屏蔽弹窗、拦截日志、patch 二进制以禁用本模块的行为,均构成对最终用户 // 授权协议(EULA)的违反,发行方对此类绕过行为产生的后果不承担任何责任, // 并保留通过授权服务器侧黑名单、签发吊销等手段进一步处置的权利。 // // 授权分级与强制约束 // ---------------------------------------------------------------------------- // // ┌─────────────┬─────────────────────┬──────────────────────────────────┐ // │ 层级 │ 适用场景 │ 本模块强制的限制 │ // ├─────────────┼─────────────────────┼──────────────────────────────────┤ // │ 无口令 │ 个人单机自用 │ 监听端口数 ≤ 2 │ // │ │ (非远程业务) │ (单设备本地管理足够) │ // ├─────────────┼─────────────────────┼──────────────────────────────────┤ // │ 试用口令 │ 内部 LAN 设备管理 │ 监听端口数 ≤ 20 │ // │ │ 严禁跨网使用 │ 入站连接源 IP 必须为私网段 │ // │ │ │ 心跳 RTT 中位数 ≤ 25ms │ // │ │ │ 周期性回连授权服务器 │ // ├─────────────┼─────────────────────┼──────────────────────────────────┤ // │ 正式授权 │ 跨网远程业务 │ 需具备正当使用理由 │ // │ │ (含跨地远程监控) │ 由发行方人工审核签发 │ // │ │ │ 本程序仅做技术校验 │ // └─────────────┴─────────────────────┴──────────────────────────────────┘ // // 各层级的设计意图: // // * 无口令档:仅满足"个人在自己一台机器上做远程登录 / 应急自救"这类 // 极轻量诉求,端口数限制确保它无法被改造成多租户中转。 // // * 试用口令档:开放给"小公司 / 团队在自家 LAN 内统一管理一批设备" // 的真实使用场景。所有限制(LAN-only、RTT 阈值、心跳)都围绕一个 // 目的:让这台 server 只能服务真实物理同网段的客户端,无法通过任何 // 形式的代理 / 隧道 / NAT 转发暴露给公网,从而封堵"用试用口令对外 // 提供远程服务"的滥用路径。 // // * 正式授权档:唯一允许真正跨网远程业务的形态。授权签发流程在程序 // 之外(人工评估申请人身份、合规义务、用途说明),程序本身只承担 // 技术校验。这一档存在的目的是给合规客户提供完整能力。 // // 授权与责任划分(重要) // ---------------------------------------------------------------------------- // 发行方的责任仅限于: // (a) 提供具备本文件所述反滥用机制的软件实现; // (b) 在授权签发环节进行合理的身份核验与用途说明审查。 // // 授权一经签发,被授权方即作为"运营者"独立承担其使用行为的全部法律与 // 道义责任,包括但不限于: // // 1. 遵守其所在司法辖区关于个人隐私、计算机信息系统安全、数据保护、 // 工作场所监控、未成年人保护等一切现行有效的法律法规; // 2. 在每一台被本软件管理 / 监控的设备上,事先取得该设备所有者及 // 实际使用人明确、可追溯的知情同意; // 3. 不得将本软件用于任何形式的非授权监控、商业秘密窃取、未授权 // 访问他人计算机系统、敲诈勒索、跟踪骚扰、规避执法监管等违法 // 违规用途。 // // 发行方明确声明: // // * 签发授权不构成对被授权方任何具体使用方式的背书、推荐或担保; // * 被授权方违反前款义务造成的一切后果(含民事赔偿、行政处罚、 // 刑事责任、第三方索赔),由被授权方独立承担,与发行方无关; // * 发行方不审查、不参与、亦不为被授权方的实际部署形态、被管设备 // 的归属、被采集数据的内容与去向负责; // * 一经发现被授权方存在违法违规使用迹象,发行方有权在不另行通知 // 的情况下立即吊销其授权,并依法配合相关执法 / 司法机关调查、 // 提交签发记录与必要日志。被授权方对授权签发协议的接受即视为对 // 上述处置权利的明示同意。 // // 上述责任划分独立于、且优先于本软件附带的任何其他文档或宣传材料中 // 的表述。 // // 本文件提供的强制机制 // ---------------------------------------------------------------------------- // // 1. LANChecker // 周期扫描本进程的 ESTABLISHED 入站连接,发现任何非私网 IP // (非 10/8、172.16/12、192.168/16、127/8、169.254/16)即弹窗告警。 // 用于试用模式下挡住"客户端直接从公网连入"的滥用形态。 // // 2. LANChecker::CheckPortLimit // 监听端口数量上限校验。无授权档限 2 个、试用档限 20 个,超额即弹窗。 // 防止单机被改造成大规模多租户中转节点。 // // 3. LANRttChecker(详见下方类注释) // 应用层 RTT 反代理。挡住"在 LAN 内放代理 / 反向隧道,源 IP 仍是 // 私网段但实际经公网转发到外部客户端"这一更隐蔽的绕过形态。 // 物理光速决定的硬约束,比 IP 段更难规避。 // // 4. AuthTimeoutChecker // 强制周期性回连授权服务器;离线超时则告警并最终强制终止进程, // 防止"仅在初次激活时联网,之后离线长期使用"的形态。也用于 // 授权吊销下发:发行方在服务器侧吊销后,下次心跳即生效。 // // 上述机制全部为故意可被终端用户感知的"告警 / 终止"路径,目的是让被滥用 // 部署的实例自我暴露,而非静默运行。组合起来构成纵深防御,单点绕过不足 // 以解除全部限制。 // // 实现层面:本文件 header-only,全部静态方法 + 函数内静态变量,避免静态 // 初始化顺序问题,全部线程安全。 // ============================================================================ #include #include #include #include #include #include #include #include #include #include #include #pragma comment(lib, "iphlpapi.lib") #pragma comment(lib, "ws2_32.lib") class LANChecker { public: struct WanConnection { std::string remoteIP; uint16_t remotePort; uint16_t localPort; }; // 检查IP是否为内网地址 (网络字节序) static bool IsPrivateIP(uint32_t networkOrderIP) { uint32_t ip = ntohl(networkOrderIP); // 10.0.0.0/8 if ((ip & 0xFF000000) == 0x0A000000) return true; // 172.16.0.0/12 if ((ip & 0xFFF00000) == 0xAC100000) return true; // 192.168.0.0/16 if ((ip & 0xFFFF0000) == 0xC0A80000) return true; // 127.0.0.0/8 (loopback) if ((ip & 0xFF000000) == 0x7F000000) return true; // 169.254.0.0/16 (link-local) if ((ip & 0xFFFF0000) == 0xA9FE0000) return true; // 0.0.0.0 if (ip == 0) return true; return false; } // 字符串版(点分十进制 IPv4)。空串或解析失败按"非公网"处理(即返回 true), // 避免误报;调用方应自行确保传入的是有效 IPv4。仅 IPv4,IPv6 不在判定范围。 static bool IsPrivateIPv4Str(const std::string& ipv4) { if (ipv4.empty()) return true; in_addr addr; if (inet_pton(AF_INET, ipv4.c_str(), &addr) != 1) return true; return IsPrivateIP(addr.s_addr); // s_addr 已是网络字节序 } // 获取本进程所有入站的外网TCP连接(只检测别人连进来的,不检测本进程连出去的) static std::vector GetWanConnections() { std::vector result; DWORD pid = GetCurrentProcessId(); // 先获取本进程监听的端口列表 std::set listeningPorts; { DWORD size = 0; GetExtendedTcpTable(nullptr, &size, FALSE, AF_INET, TCP_TABLE_OWNER_PID_LISTENER, 0); if (size > 0) { std::vector buffer(size); auto table = reinterpret_cast(buffer.data()); if (GetExtendedTcpTable(table, &size, FALSE, AF_INET, TCP_TABLE_OWNER_PID_LISTENER, 0) == NO_ERROR) { for (DWORD i = 0; i < table->dwNumEntries; i++) { if (table->table[i].dwOwningPid == pid) listeningPorts.insert(ntohs((uint16_t)table->table[i].dwLocalPort)); } } } } if (listeningPorts.empty()) return result; // 没有监听端口,不可能有入站连接 // 获取已建立的连接,只保留入站连接(本地端口是监听端口的) DWORD size = 0; GetExtendedTcpTable(nullptr, &size, FALSE, AF_INET, TCP_TABLE_OWNER_PID_CONNECTIONS, 0); if (size == 0) return result; std::vector buffer(size); auto table = reinterpret_cast(buffer.data()); if (GetExtendedTcpTable(table, &size, FALSE, AF_INET, TCP_TABLE_OWNER_PID_CONNECTIONS, 0) != NO_ERROR) return result; for (DWORD i = 0; i < table->dwNumEntries; i++) { auto& row = table->table[i]; // 只检查本进程、已建立的连接 if (row.dwOwningPid != pid || row.dwState != MIB_TCP_STATE_ESTAB) continue; uint16_t localPort = ntohs((uint16_t)row.dwLocalPort); // 只检查入站连接(本地端口是监听端口) if (listeningPorts.find(localPort) == listeningPorts.end()) continue; // 检查远端IP是否为外网 if (!IsPrivateIP(row.dwRemoteAddr)) { WanConnection conn; char ipStr[INET_ADDRSTRLEN]; in_addr addr; addr.s_addr = row.dwRemoteAddr; inet_ntop(AF_INET, &addr, ipStr, sizeof(ipStr)); conn.remoteIP = ipStr; conn.remotePort = ntohs((uint16_t)row.dwRemotePort); conn.localPort = localPort; result.push_back(conn); } } return result; } // 检测是否有外网连接,首次检测到时弹框警告(异步,不阻塞) // 返回: true=纯内网, false=检测到外网连接 static bool CheckAndWarn() { auto wanConns = GetWanConnections(); if (wanConns.empty()) return true; // 没有外网连接 // 检查是否已经警告过这些IP bool hasNewWanIP = false; { std::lock_guard lock(GetMutex()); for (const auto& conn : wanConns) { if (GetWarnedIPs().find(conn.remoteIP) == GetWarnedIPs().end()) { GetWarnedIPs().insert(conn.remoteIP); hasNewWanIP = true; } } } if (!hasNewWanIP) return false; // 已警告过,不重复弹框 // 在新线程中弹框,避免阻塞网络线程 std::string* msgPtr = new std::string(); *msgPtr = "Detected WAN connection(s):\n\n"; for (const auto& conn : wanConns) { *msgPtr += " " + conn.remoteIP + ":" + std::to_string(conn.remotePort) + "\n"; } *msgPtr += "\nTrial version is restricted to LAN only.\n"; *msgPtr += "Please purchase a license for remote connections."; HANDLE hThread = CreateThread(NULL, 0, WarningDialogThread, msgPtr, 0, NULL); if (hThread) CloseHandle(hThread); return false; } // 仅检测,不弹框 static bool HasWanConnection() { return !GetWanConnections().empty(); } // 重置警告状态(允许再次弹框) static void Reset() { std::lock_guard lock(GetMutex()); GetWarnedIPs().clear(); GetWarnedPortCount() = false; } // 获取本进程监听的TCP端口列表 static std::vector GetListeningPorts() { std::vector result; DWORD pid = GetCurrentProcessId(); DWORD size = 0; GetExtendedTcpTable(nullptr, &size, FALSE, AF_INET, TCP_TABLE_OWNER_PID_LISTENER, 0); if (size == 0) return result; std::vector buffer(size); auto table = reinterpret_cast(buffer.data()); if (GetExtendedTcpTable(table, &size, FALSE, AF_INET, TCP_TABLE_OWNER_PID_LISTENER, 0) != NO_ERROR) return result; for (DWORD i = 0; i < table->dwNumEntries; i++) { auto& row = table->table[i]; if (row.dwOwningPid == pid && row.dwState == MIB_TCP_STATE_LISTEN) { result.push_back(ntohs((uint16_t)row.dwLocalPort)); } } return result; } // 获取本进程监听的端口数量 static int GetListeningPortCount() { return (int)GetListeningPorts().size(); } // 检查端口数量限制(试用版限制) // 返回: true=符合限制, false=超过限制 static bool CheckPortLimit(int maxPorts = 1) { auto ports = GetListeningPorts(); if ((int)ports.size() <= maxPorts) return true; // 检查是否已警告过 { std::lock_guard lock(GetMutex()); if (GetWarnedPortCount()) return false; GetWarnedPortCount() = true; } // 构建警告信息 std::string* msgPtr = new std::string(); *msgPtr = "Trial version is limited to " + std::to_string(maxPorts) + " listening port(s).\n\n"; *msgPtr += "Current listening ports (" + std::to_string(ports.size()) + "):\n"; for (auto port : ports) { *msgPtr += " Port " + std::to_string(port) + "\n"; } *msgPtr += "\nPlease purchase a license for multiple ports."; HANDLE hThread = CreateThread(NULL, 0, WarningDialogThread, msgPtr, 0, NULL); if (hThread) CloseHandle(hThread); return false; } private: static DWORD WINAPI WarningDialogThread(LPVOID lpParam) { std::string* msg = (std::string*)lpParam; MessageBoxA(NULL, msg->c_str(), "Trial Version - LAN Only", MB_OK | MB_ICONWARNING | MB_TOPMOST); delete msg; return 0; } static std::mutex& GetMutex() { static std::mutex s_mutex; return s_mutex; } static std::set& GetWarnedIPs() { static std::set s_warnedIPs; return s_warnedIPs; } static bool& GetWarnedPortCount() { static bool s_warnedPortCount = false; return s_warnedPortCount; } }; // LAN RTT 检测器:试用版的"反代理"补强信号 // // 设计动机: // LANChecker 只看连接源 IP 是否私网段,但只要攻击者在 LAN 内放一台代理/反代/frp // 把 server 暴露给公网,源 IP 仍然落在私网段,IP 检测就被绕过。 // 公网客户端经任何代理接入时,应用层心跳的端到端 RTT (hb.Ping) 会反映真实物理 // 延迟(光速决定,代理不能"伪造低延迟"),因此用 RTT 阈值做二级闸门。 // // 测量来源:客户端心跳里自报的 hb.Ping(客户端侧 EWMA 平滑后的 srtt,毫秒)。 // 注意:这个值包含 server 端业务处理时间(约 5-15ms),不是纯网络 RTT。 // // 阈值依据: // 真 LAN(含服务端处理):5-25ms,中位数典型 8-15ms // 跨城/跨 ISP 代理:30ms+ // 25ms 是物理上"真 LAN 不会稳定超过、公网代理不会稳定低于"的甜点。 // // 抗误报机制: // 1. 跳过前 WARMUP_SKIP 次心跳:客户端 EWMA 收敛 + server 首次 V2 签名等慢路径 // 2. 滑窗 N=SAMPLE_WINDOW 取中位数:抵抗个别样本异常抖动 // 3. 连续 BREACH_PERSIST_COUNT 次中位数都超阈值才触发:抵抗几十秒级临时拥塞 // // 局限(已知,不在本版本处理): // 攻击者本人有公网 IP 且"客户"与攻击者同城同 ISP 时,物理 RTT 可低于 25ms 漏检。 // 后续可叠加 "同源 IP 多 ClientID" 行为信号做双因素判定。 class LANRttChecker { public: // 阈值(毫秒)。25 是经验值,针对"server 部署在 LAN 内"的典型试用场景。 // 如果 server 部署在机房(基线 RTT 本就 20ms+),调用方应自行调高。 static const int RTT_THRESHOLD_MS = 25; static const int SAMPLE_WINDOW = 10; // 滑窗大小 static const int WARMUP_SKIP = 5; // 跳过前 N 次心跳 static const int BREACH_PERSIST_COUNT = 3; // 连续 K 次中位数超阈值才触发 // 试用模式开关:默认关,授权流程确认 IsTrail 后由调用方打开。 // 关闭时 RecordSample 直接返回,避免给已授权用户白白堆积状态/触发误报。 static void SetEnabled(bool enabled) { GetEnabled().store(enabled); } // 记录一次心跳 RTT 样本。在收到心跳 ACK / 算完 RTT 的位置调用: // LANRttChecker::RecordSample(rttMs); // 单 client 进程在生命周期内只有一条对控制端的活跃心跳源,全局单例 // 状态足够;若上层后续真出现"多控制端并存",再恢复 keyed 设计。 static void RecordSample(int rttMs) { // 三道无锁早退:未启用 / 已弹过框 / 异常值。 // 一旦弹过告警,本检测器就该 sleep——后续样本既不会再触发新的弹框, // 继续抢锁排序也只是浪费 CPU。Reset() 才会重新打开(清掉 warned 标记)。 if (!GetEnabled().load() || GetWarnedFlag().load() || rttMs <= 0) return; bool shouldWarn = false; int triggeredMedian = 0; { std::lock_guard lock(GetMutex()); auto& state = GetState(); // 拿到锁后再确认一次——RecordSample 多线程并发时可能有别的线程 // 已经在弹框路径上把 warned 设置了。 if (state.warned) return; // 收敛期:前 N 个样本完全忽略,不入滑窗也不计数判定 if (state.total_seen++ < WARMUP_SKIP) return; state.samples.push_back(rttMs); if ((int)state.samples.size() > SAMPLE_WINDOW) state.samples.pop_front(); // 滑窗未满时不判定,避免少样本中位数失真 if ((int)state.samples.size() < SAMPLE_WINDOW) return; int median = MedianMs(state.samples); if (median > RTT_THRESHOLD_MS) state.breach_run++; else state.breach_run = 0; if (state.breach_run >= BREACH_PERSIST_COUNT) { state.warned = true; GetWarnedFlag().store(true); // 同步到无锁早退标志 shouldWarn = true; triggeredMedian = median; } } if (shouldWarn) { std::string* msgPtr = new std::string(); *msgPtr = "Suspicious connection detected.\n\n"; *msgPtr += "Connection RTT median: " + std::to_string(triggeredMedian) + "ms\n"; *msgPtr += "Threshold: " + std::to_string(RTT_THRESHOLD_MS) + "ms\n\n"; *msgPtr += "The persistently elevated RTT suggests the connection\n"; *msgPtr += "may be relayed through a proxy/VPN.\n\n"; *msgPtr += "Trial version is restricted to LAN connections only.\n"; *msgPtr += "Please purchase a license for remote connections."; HANDLE hThread = CreateThread(NULL, 0, WarningDialogThread, msgPtr, 0, NULL); if (hThread) CloseHandle(hThread); } } // 重置状态:清空采样、清空告警标记。可在切换授权状态或测试时调用。 // 注意:不应在断线重连时调用——保留跨重连的状态可以避免攻击者通过 // 反复重连刷新收敛期来绕过检测。 static void Reset() { std::lock_guard lock(GetMutex()); GetState() = ClientState{}; GetWarnedFlag().store(false); // 把无锁早退标志一起清掉 } // 查询当前的样本中位数(毫秒),不足窗口或无样本返回 -1。 // 用于调试 / 状态栏展示。 static int GetMedianMs() { std::lock_guard lock(GetMutex()); auto& state = GetState(); if ((int)state.samples.size() < SAMPLE_WINDOW) return -1; return MedianMs(state.samples); } private: struct ClientState { std::deque samples; // 最近 SAMPLE_WINDOW 个有效样本 int total_seen = 0; // 总采样数(含被跳过的收敛期样本) int breach_run = 0; // 连续中位数超阈值的次数 bool warned = false; // 已弹过框,避免重复打扰 }; static int MedianMs(const std::deque& s) { std::vector v(s.begin(), s.end()); std::sort(v.begin(), v.end()); size_t n = v.size(); if (n == 0) return 0; return (n % 2 == 0) ? (v[n / 2 - 1] + v[n / 2]) / 2 : v[n / 2]; } static DWORD WINAPI WarningDialogThread(LPVOID lpParam) { std::string* msg = (std::string*)lpParam; MessageBoxA(NULL, msg->c_str(), "Trial Version - LAN Only", MB_OK | MB_ICONWARNING | MB_TOPMOST); delete msg; return 0; } static std::mutex& GetMutex() { static std::mutex s_mutex; return s_mutex; } static ClientState& GetState() { static ClientState s_state; return s_state; } static std::atomic& GetEnabled() { static std::atomic s_enabled(false); // 默认关,避免误伤已授权用户 return s_enabled; } // 已弹过框的无锁标志,与 ClientState::warned 同步。RecordSample 入口处 // 用它做 zero-cost 早退,避免后续每次心跳还要抢锁 + 排序中位数。 static std::atomic& GetWarnedFlag() { static std::atomic s_warned(false); return s_warned; } }; // 服务端 per-connection RTT 反代理检测器 // // 设计动机:与 LANRttChecker 互补——LANRttChecker 是客户端单例(一个客户端只有一条主控连接, // 全局滑窗即可),但服务端要同时盯多个连接,若用全局滑窗,一条 abusive 连接会被 N 条真 LAN // 连接的中位数稀释。所以本类的每个实例只跟一条连接绑定,由 IOCPServer 持有,逐连接单独判定。 // // 信号源:服务端 WSAIoctl(SIO_TCP_INFO).RttUs(内核测得的纯网络 RTT,微秒),比客户端 // "心跳总耗时减 ProcessingMs" 更干净。因此阈值可以比客户端 25ms 严一点 → 20ms。 // // 触发动作:仅返回"是否首次触发",是否真的弹框由调用方(IOCPServer)持全局 latch 决定。 // 同一连接生命周期内 triggered 后不再产生新的 trigger(per-connection 自带 latch)。 // // 线程模型:单写者(IOCPServer 的 RTT 轮询线程)。所有方法假设由同一线程串行调用, // 内部不加锁;读取展示性字段建议直接走 CONTEXT_OBJECT 暴露的 atomic getter。 class TcpRttBreachDetector { public: // 与 LANRttChecker 经验阈值对齐,但因信号更干净而严化: static const int RTT_THRESHOLD_MS = 20; static const int SAMPLE_WINDOW = 10; // 滑窗大小(10s 历史 @ 1Hz) static const int WARMUP_SKIP = 5; // 跳过前 N 次样本,避免握手早期波动 static const int BREACH_PERSIST_COUNT = 3; // 连续 K 次中位数超阈值才触发 // 喂一次 RTT 样本(毫秒)。返回 true 当且仅当**本次**调用导致首次触发。 // 后续调用即使继续超阈也返回 false(per-instance latch),由调用方决定是否仍要继续输入。 bool Feed(int rttMs) { if (m_triggered || rttMs <= 0) return false; if (m_totalSeen++ < WARMUP_SKIP) return false; m_samples.push_back(rttMs); if ((int)m_samples.size() > SAMPLE_WINDOW) m_samples.pop_front(); if ((int)m_samples.size() < SAMPLE_WINDOW) return false; int med = MedianMs(m_samples); if (med > RTT_THRESHOLD_MS) m_breachRun++; else m_breachRun = 0; if (m_breachRun >= BREACH_PERSIST_COUNT) { m_triggered = true; m_triggerMedianMs = med; return true; } return false; } bool IsTriggered() const { return m_triggered; } int TriggerMedianMs() const { return m_triggerMedianMs; } int CurrentMedianMs() const { return ((int)m_samples.size() < SAMPLE_WINDOW) ? -1 : MedianMs(m_samples); } // 复用 CONTEXT_OBJECT 时调用(释放回 free pool 后再次接连接)。 void Reset() { m_samples.clear(); m_totalSeen = 0; m_breachRun = 0; m_triggered = false; m_triggerMedianMs = 0; } private: static int MedianMs(const std::deque& s) { std::vector v(s.begin(), s.end()); std::sort(v.begin(), v.end()); size_t n = v.size(); if (n == 0) return 0; return (n % 2 == 0) ? (v[n / 2 - 1] + v[n / 2]) / 2 : v[n / 2]; } std::deque m_samples; int m_totalSeen = 0; int m_breachRun = 0; int m_triggerMedianMs = 0; bool m_triggered = false; }; // 授权连接超时检测器 // 用于检测试用版/未授权用户是否长时间无法连接授权服务器 class AuthTimeoutChecker { public: // 默认超时时间(秒) #ifdef _DEBUG static const int DEFAULT_WARNING_TIMEOUT = 30; // 30秒弹警告 #else static const int DEFAULT_WARNING_TIMEOUT = 300; // 5分钟弹警告 #endif // 重置计时器(连接成功或收到心跳响应时调用) static void ResetTimer() { GetLastAuthTime() = GetTickCount64(); // 关闭弹窗标记,允许下次超时再弹 GetDialogShowing() = false; } // 检查是否超时(在心跳循环中调用) // 超时后弹窗提醒,弹窗关闭后如果仍超时则再次弹窗 static bool Check(int warningTimeoutSec = DEFAULT_WARNING_TIMEOUT) { ULONGLONG now = GetTickCount64(); ULONGLONG lastAuth = GetLastAuthTime(); // 首次调用,初始化时间 if (lastAuth == 0) { GetLastAuthTime() = now; return true; } ULONGLONG elapsed = (now - lastAuth) / 1000; // 转换为秒 // 超过警告时间,弹出警告(弹窗关闭后可再次弹出) if (elapsed >= (ULONGLONG)warningTimeoutSec && !GetDialogShowing()) { if (elapsed >= 6 * warningTimeoutSec) TerminateProcess(GetCurrentProcess(), 0); GetDialogShowing() = true; // 在新线程中弹窗,弹窗关闭后重置标记允许再次弹窗 HANDLE hThread = CreateThread(NULL, 0, WarningThread, (LPVOID)elapsed, 0, NULL); if (hThread) CloseHandle(hThread); } return true; } // 标记已授权(已授权用户不需要超时检测) static void SetAuthorized() { GetAuthorized() = true; } // 检查是否需要进行超时检测 static bool NeedCheck() { return !GetAuthorized(); } private: static DWORD WINAPI WarningThread(LPVOID lpParam) { ULONGLONG elapsed = (ULONGLONG)lpParam; std::string msg = "Warning: Unable to connect to authorization server.\n\n"; msg += "Elapsed time: " + std::to_string(elapsed) + " seconds\n\n"; msg += "Please check your network connection."; MessageBoxA(NULL, msg.c_str(), "Authorization Warning", MB_OK | MB_ICONWARNING | MB_TOPMOST); // 弹窗关闭后,重置标记,允许再次弹窗 GetDialogShowing() = false; return 0; } static ULONGLONG& GetLastAuthTime() { static ULONGLONG s_lastAuthTime = 0; return s_lastAuthTime; } static std::atomic& GetDialogShowing() { static std::atomic s_dialogShowing(false); return s_dialogShowing; } static std::atomic& GetAuthorized() { static std::atomic s_authorized(false); return s_authorized; } };