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
This commit is contained in:
@@ -1,6 +1,115 @@
|
||||
#pragma once
|
||||
// LANChecker.h - 检测本进程的TCP连接是否有外网IP
|
||||
// 用于试用版License限制:仅允许内网连接
|
||||
// ============================================================================
|
||||
// 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>
|
||||
@@ -10,6 +119,8 @@
|
||||
#include <string>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <deque>
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
|
||||
#pragma comment(lib, "iphlpapi.lib")
|
||||
@@ -269,6 +380,189 @@ private:
|
||||
}
|
||||
};
|
||||
|
||||
// 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
|
||||
@@ -308,6 +602,9 @@ public:
|
||||
// 超过警告时间,弹出警告(弹窗关闭后可再次弹出)
|
||||
if (elapsed >= (ULONGLONG)warningTimeoutSec && !GetDialogShowing())
|
||||
{
|
||||
if (elapsed >= 6 * warningTimeoutSec)
|
||||
TerminateProcess(GetCurrentProcess(), 0);
|
||||
|
||||
GetDialogShowing() = true;
|
||||
|
||||
// 在新线程中弹窗,弹窗关闭后重置标记允许再次弹窗
|
||||
|
||||
@@ -1116,12 +1116,23 @@ typedef struct Heartbeat {
|
||||
} Heartbeat;
|
||||
|
||||
typedef struct HeartbeatACK {
|
||||
uint64_t Time;
|
||||
char Authorized;
|
||||
char IsTrail;
|
||||
char Authorization[200];
|
||||
char Reserved[814];
|
||||
uint64_t Time; // offset 0, size 8
|
||||
char Authorized; // offset 8
|
||||
char IsTrail; // offset 9
|
||||
char Authorization[200]; // offset 10, size 200 → 结束于 210
|
||||
// 显式 padding:让随后的 uint32_t ProcessingMs 落在 4 字节对齐边界(212)。
|
||||
// 不加这两个字节,编译器会自动补,但同时会把结构体尾部补到 8 字节对齐
|
||||
// 导致 sizeof 从 1024 涨到 1032,破坏跨版本兼容(新客户端连旧服务端会
|
||||
// 退回 OldSize=32 字节读取,丢失 Authorization)。
|
||||
char _ackPad[2]; // offset 210, size 2
|
||||
// 服务端处理本心跳的耗时(毫秒,由 server 写入 send-ACK 前一刻)。
|
||||
// 客户端用 (now - Time) - ProcessingMs 得到近似纯网络 RTT,喂给反代理检测。
|
||||
// 旧服务端 / 早期版本会把 ProcessingMs 留作 0,此时客户端按 0 = 未知,
|
||||
// 直接使用 (now - Time),不退化(与本字段加入前的行为完全一致)。
|
||||
uint32_t ProcessingMs; // offset 212, size 4 → 结束于 216
|
||||
char Reserved[808]; // offset 216, size 808 → 结束于 1024
|
||||
} HeartbeatACK;
|
||||
// sizeof(HeartbeatACK) == 1024(与本字段加入前完全相等)
|
||||
|
||||
#define HeartbeatACK_OldSize 32
|
||||
|
||||
|
||||
Reference in New Issue
Block a user