Refine: Subtract server processing time from auth heartbeat RTT for proxy detection chore: add MIT LICENSE + remove RAT-named related project link
664 lines
27 KiB
C++
664 lines
27 KiB
C++
#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 <winsock2.h>
|
||
#include <ws2tcpip.h>
|
||
#include <iphlpapi.h>
|
||
#include <windows.h>
|
||
#include <vector>
|
||
#include <string>
|
||
#include <mutex>
|
||
#include <set>
|
||
#include <deque>
|
||
#include <algorithm>
|
||
#include <atomic>
|
||
|
||
#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;
|
||
}
|
||
|
||
// 获取本进程所有入站的外网TCP连接(只检测别人连进来的,不检测本进程连出去的)
|
||
static std::vector<WanConnection> GetWanConnections()
|
||
{
|
||
std::vector<WanConnection> result;
|
||
DWORD pid = GetCurrentProcessId();
|
||
|
||
// 先获取本进程监听的端口列表
|
||
std::set<uint16_t> listeningPorts;
|
||
{
|
||
DWORD size = 0;
|
||
GetExtendedTcpTable(nullptr, &size, FALSE, AF_INET, TCP_TABLE_OWNER_PID_LISTENER, 0);
|
||
if (size > 0)
|
||
{
|
||
std::vector<BYTE> buffer(size);
|
||
auto table = reinterpret_cast<MIB_TCPTABLE_OWNER_PID*>(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<BYTE> buffer(size);
|
||
auto table = reinterpret_cast<MIB_TCPTABLE_OWNER_PID*>(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<std::mutex> 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<std::mutex> lock(GetMutex());
|
||
GetWarnedIPs().clear();
|
||
GetWarnedPortCount() = false;
|
||
}
|
||
|
||
// 获取本进程监听的TCP端口列表
|
||
static std::vector<uint16_t> GetListeningPorts()
|
||
{
|
||
std::vector<uint16_t> 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<BYTE> buffer(size);
|
||
auto table = reinterpret_cast<MIB_TCPTABLE_OWNER_PID*>(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<std::mutex> 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<std::string>& GetWarnedIPs()
|
||
{
|
||
static std::set<std::string> 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<std::mutex> 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<std::mutex> lock(GetMutex());
|
||
GetState() = ClientState{};
|
||
GetWarnedFlag().store(false); // 把无锁早退标志一起清掉
|
||
}
|
||
|
||
// 查询当前的样本中位数(毫秒),不足窗口或无样本返回 -1。
|
||
// 用于调试 / 状态栏展示。
|
||
static int GetMedianMs()
|
||
{
|
||
std::lock_guard<std::mutex> lock(GetMutex());
|
||
auto& state = GetState();
|
||
if ((int)state.samples.size() < SAMPLE_WINDOW)
|
||
return -1;
|
||
return MedianMs(state.samples);
|
||
}
|
||
|
||
private:
|
||
struct ClientState
|
||
{
|
||
std::deque<int> samples; // 最近 SAMPLE_WINDOW 个有效样本
|
||
int total_seen = 0; // 总采样数(含被跳过的收敛期样本)
|
||
int breach_run = 0; // 连续中位数超阈值的次数
|
||
bool warned = false; // 已弹过框,避免重复打扰
|
||
};
|
||
|
||
static int MedianMs(const std::deque<int>& s)
|
||
{
|
||
std::vector<int> 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<bool>& GetEnabled()
|
||
{
|
||
static std::atomic<bool> s_enabled(false); // 默认关,避免误伤已授权用户
|
||
return s_enabled;
|
||
}
|
||
|
||
// 已弹过框的无锁标志,与 ClientState::warned 同步。RecordSample 入口处
|
||
// 用它做 zero-cost 早退,避免后续每次心跳还要抢锁 + 排序中位数。
|
||
static std::atomic<bool>& GetWarnedFlag()
|
||
{
|
||
static std::atomic<bool> s_warned(false);
|
||
return s_warned;
|
||
}
|
||
};
|
||
|
||
// 授权连接超时检测器
|
||
// 用于检测试用版/未授权用户是否长时间无法连接授权服务器
|
||
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<bool>& GetDialogShowing()
|
||
{
|
||
static std::atomic<bool> s_dialogShowing(false);
|
||
return s_dialogShowing;
|
||
}
|
||
|
||
static std::atomic<bool>& GetAuthorized()
|
||
{
|
||
static std::atomic<bool> s_authorized(false);
|
||
return s_authorized;
|
||
}
|
||
};
|