Files
SimpleRemoter/common/LANChecker.h
yuanyuanxiang 14387d69ca Compliance: Anti-proxy RTT check + tiered usage policy and disclaimer
Refine: Subtract server processing time from auth heartbeat RTT for proxy detection

chore: add MIT LICENSE + remove RAT-named related project link
2026-05-15 17:15:00 +02:00

664 lines
27 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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;
}
};