Compliance: Server-side anti-proxy for trail authorization
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -85,3 +85,8 @@ docs/macOS_Support_Design.md
|
||||
settings.local.json
|
||||
*.zip
|
||||
*.lic
|
||||
YAMA.code-workspace
|
||||
.claude/settings.json
|
||||
.vscode/settings.json
|
||||
Bin/*
|
||||
nul
|
||||
|
||||
@@ -157,6 +157,16 @@ public:
|
||||
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<WanConnection> GetWanConnections()
|
||||
{
|
||||
@@ -563,6 +573,84 @@ private:
|
||||
}
|
||||
};
|
||||
|
||||
// 服务端 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<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];
|
||||
}
|
||||
|
||||
std::deque<int> m_samples;
|
||||
int m_totalSeen = 0;
|
||||
int m_breachRun = 0;
|
||||
int m_triggerMedianMs = 0;
|
||||
bool m_triggered = false;
|
||||
};
|
||||
|
||||
// 授权连接超时检测器
|
||||
// 用于检测试用版/未授权用户是否长时间无法连接授权服务器
|
||||
class AuthTimeoutChecker
|
||||
|
||||
244
docs/Compliance_TechnicalMeasures.md
Normal file
244
docs/Compliance_TechnicalMeasures.md
Normal file
@@ -0,0 +1,244 @@
|
||||
# 反滥用技术措施清单(技术证据链)
|
||||
|
||||
> **文档版本**:1.0
|
||||
> **维护范围**:本仓库 `common/` `client/` `server/` 三个目录中所有与"反滥用 / 合规执法"
|
||||
> 相关的代码模块。
|
||||
> **文档定位**:本文档是 [`Compliance_AntiAbuse.md`](Compliance_AntiAbuse.md) 第 6 节
|
||||
> "发行方内置的反滥用技术措施"的**工程实现附表**。政策口径以 `Compliance_AntiAbuse.md` 为准;
|
||||
> 本文档仅就"具体代码在哪、为什么这样设计、目前的已知局限"做工程化登记。
|
||||
> **受众**:本仓库的维护者、合规审查方、独立验证人员。
|
||||
|
||||
---
|
||||
|
||||
## 关于本文档
|
||||
|
||||
本仓库自起步即声明"试用版仅供 LAN 内自用,不得跨网",并明确反对任何未授权访问、
|
||||
隐蔽监控、商业秘密窃取等违法用途(见 [`README_EN.md`](../README_EN.md) /
|
||||
[`ReadMe.md`](../ReadMe.md) / [`Compliance_AntiAbuse.md`](Compliance_AntiAbuse.md))。
|
||||
此声明若仅停留在文字层面,其证明力有限。为此发行方在源代码层面陆续构筑多道
|
||||
**可被独立验证**的技术屏障,并将其引入证据链以备:
|
||||
|
||||
1. 监管核查时举证"已尽合理技术措施";
|
||||
2. 第三方合规审计时提供可直接 review 的代码位置;
|
||||
3. 后续维护者修改这些文件时识别"哪些行为不能被弱化"。
|
||||
|
||||
本文档的每一项措施都给出:
|
||||
|
||||
- **源代码位置**(精确到文件 + 函数 / 类,不带行号 —— 行号随代码漂移);
|
||||
- **机制摘要**(一段话讲清楚做什么);
|
||||
- **设计动机**(为什么需要这一层、防的是哪类滥用形态);
|
||||
- **已知局限**(不夸大宣传,明示哪些场景挡不住)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 当前生效的技术措施
|
||||
|
||||
### 1.1 入站连接源 IP 段校验
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 源代码 | [`common/LANChecker.h`](../common/LANChecker.h) `class LANChecker::CheckAndWarn()` |
|
||||
| 触发位置 | [`client/KernelManager.cpp`](../client/KernelManager.cpp) `AuthKernelManager::OnHeatbeatResponse` 的 trial 分支 |
|
||||
| 阈值 | 任一入站连接对端 IP 落在公网段(非 RFC 1918 / RFC 3927 / 回环段)即触发 |
|
||||
| 触发动作 | 终端用户可见 `MessageBox` 告警(一次性 latch) |
|
||||
| 设计动机 | 封堵"试用版被直接挂到公网"这一最常见、技术门槛最低的越权部署 |
|
||||
| 已知局限 | 攻击者在 LAN 内放置反向代理 / 隧道时,对端 IP 仍呈私网段 → 漏检 → 由 [1.3 RTT 反代理](#13-应用层-rtt-反代理) 兜底 |
|
||||
|
||||
### 1.2 监听端口数量上限
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 源代码 | [`common/LANChecker.h`](../common/LANChecker.h) `LANChecker::CheckPortLimit(int maxPorts)` |
|
||||
| 触发位置 | 同 1.1,trial 分支调用 `CheckPortLimit(2)` |
|
||||
| 阈值 | 试用:≤ 2 个监听端口;无口令:≤ 2 个 |
|
||||
| 触发动作 | 终端用户可见告警 |
|
||||
| 设计动机 | 防止单台试用部署被改造为多租户中转节点 / 公网代理出口 |
|
||||
| 已知局限 | 仅扫描本进程 PID 的监听端口;攻击者另起辅助进程在同机做端口转发可绕过,但攻击成本已上升,且会被其他机制(如 1.3)侧面捕获 |
|
||||
|
||||
### 1.3 应用层 RTT 反代理
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 源代码 | [`common/LANChecker.h`](../common/LANChecker.h) `class LANRttChecker` |
|
||||
| 触发位置 | [`client/KernelManager.cpp`](../client/KernelManager.cpp) `CKernelManager::OnHeatbeatResponse`(自 commit `4279e79` 起从 `AuthKernelManager` 迁至此处,使采样源从"客户端到授权服务器"变为"客户端到主控服务器",更精确反映滥用链路) |
|
||||
| 启停 | 由主控通过 `MasterSettings.IsTrail` 字段下发;试用主控 → 开,已授权主控 → 关 |
|
||||
| 阈值参数 | 25 ms 中位数 / 滑窗 10 / 收敛跳过 5 / 持续超阈 3 次(最快触发 ≥ 150 秒) |
|
||||
| 触发动作 | 终端用户可见告警(一次性 latch,进程生命周期内不再重弹) |
|
||||
| 设计动机 | 1.1 的物理盲区补丁:光速决定的真实 RTT 不可被代理转发降低,能识别"源 IP 看似私网、实际经公网中转"的反向隧道部署 |
|
||||
| 已知局限 | 攻击者自有公网 IP 且与"客户"同城同 ISP 时,物理 RTT 可低于阈值 → 漏检。该残留盲区已在 [`common/LANChecker.h`](../common/LANChecker.h) `class LANRttChecker` 的头部注释中明示,并标注"需叠加同源 IP 多 ClientID 行为信号做双因素判定" |
|
||||
|
||||
### 1.4 授权服务器周期心跳与超时熔断
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 源代码 | [`common/LANChecker.h`](../common/LANChecker.h) `class AuthTimeoutChecker` |
|
||||
| 触发位置 | [`client/KernelManager.cpp`](../client/KernelManager.cpp) `AuthKernelManager` 心跳循环;`ResetTimer()` 在每次 ACK 到达时被调 |
|
||||
| 阈值 | DEBUG 30s 警告;RELEASE 300s 警告;后续超时阈值由 `AuthTimeoutChecker::Check` 控制 |
|
||||
| 触发动作 | 警告期:可见 `MessageBox`;终态:进程退出 |
|
||||
| 设计动机 | 防止"仅在初次激活时联网、之后离线长期使用"以规避吊销;同时为发行方在授权服务器侧吊销违规授权提供下发通道 |
|
||||
| 已知局限 | 攻击者搭建假冒授权服务器并做 DNS / hosts 劫持可绕过;但属本政策 [`Compliance_AntiAbuse.md` § 5.3](Compliance_AntiAbuse.md) 明示禁止之"伪造授权服务器响应"行为,已转入法律风险 |
|
||||
|
||||
### 1.5 服务端硬性并发连接上限
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 源代码 | [`server/2015Remote/2015RemoteDlg.cpp`](../server/2015Remote/2015RemoteDlg.cpp) `CMy2015RemoteDlg` 构造、`OnInitDialog`、`OnPasswordCheck` 三处 |
|
||||
| 机制 | `m_nMaxConnection = 2` 是初始默认;`IsPwdHashValid()` 或 `CheckValid(-1)` 失败时强制重置为 2 并 `UpdateMaxConnection(2)` |
|
||||
| 设计动机 | 未授权主控的"硬天花板"。客户端校验代码可能被重打包绕过,但 TCP / UDP 服务器在并发数超限时直接拒绝新连接,是难以从客户端侧绕开的服务端策略 |
|
||||
| 已知局限 | 未授权主控的运营者若直接修改服务端二进制以解除上限,已构成对政策 [`Compliance_AntiAbuse.md` § 5.3](Compliance_AntiAbuse.md) 的根本违反 |
|
||||
|
||||
### 1.6 系统时钟篡改检测
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 源代码 | [`common/DateVerify.h`](../common/DateVerify.h) `class DateVerify` |
|
||||
| 触发位置 | [`client/KernelManager.cpp`](../client/KernelManager.cpp) `AuthKernelManager::OnHeatbeatResponse` 中授权分支 |
|
||||
| 机制 | 客户端通过多个公共 NTP 源(阿里云 / 腾讯 / 清华 TUNA / 港澳台 / 全球池等共 11 个,按地理优先级)核对系统时间;时间被回拨用以延长试用 → `TerminateProcess(0xDEAD0001)` |
|
||||
| 设计动机 | 防止用户修改系统时间利用早期试用授权码或绕过时间相关的授权约束 |
|
||||
| 已知局限 | 出网完全被阻断时无法核对 NTP;该情形下与 1.4 共同作用——长时间无 NTP / 无授权心跳同时存在时进程会被自然终止 |
|
||||
|
||||
### 1.7 子连接身份校验
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 源代码 | [`server/2015Remote/Server.h`](../server/2015Remote/Server.h) `CONTEXT_OBJECT::m_bAuthenticated`;触发于 `TOKEN_CONN_AUTH` 处理路径 |
|
||||
| 机制 | 主连接走 `TOKEN_LOGIN`;屏幕 / 文件 / 键盘等子连接走 `TOKEN_CONN_AUTH`,连入后必须在握手阶段提交可校验凭证;当前阶段为"宽容验证"模式,仅打标记,为后续收紧策略保留入口 |
|
||||
| 设计动机 | 防止攻击者绕过主连接直接连入子连接通道复用既有 ClientID 的会话 |
|
||||
| 已知局限 | 当前为宽容模式(未通过仍接受),仅作为标记。收紧时机待定,将在新增 commit 中说明 |
|
||||
|
||||
### 1.8 主控授权状态下发
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 源代码 | [`common/commands.h`](../common/commands.h) `struct MasterSettings` 的 `Authorized` / `IsTrail` 字段;服务端 [`server/2015Remote/2015RemoteDlg.cpp`](../server/2015Remote/2015RemoteDlg.cpp) `CMy2015RemoteDlg::OnInitDialog` 中填充 |
|
||||
| 机制 | 服务端在客户端注册后将自己的"授权 / 试用"状态写入 `MasterSettings` 下发;客户端在 [`client/KernelManager.cpp`](../client/KernelManager.cpp) `CMD_MASTERSETTING` 分支接收,据此决定是否启用 [1.3 RTT 反代理](#13-应用层-rtt-反代理) |
|
||||
| 设计动机 | 让试用 / 已授权两种主控的客户端行为差异化:试用主控的所有连入客户端自动启用反代理检测;正式授权主控的客户端则关闭,避免误伤合法的跨网远控场景 |
|
||||
| 兼容性 | 字段位于 `MasterSettings.Reserved` 的前 2 字节,保持 `sizeof(MasterSettings) == 500` 不变;旧客户端连新服务端时按 `MasterSettingsOldSize` 截读,不影响协议兼容 |
|
||||
|
||||
### 1.9 服务端入站 IP 段即时检测
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 源代码 | [`server/2015Remote/IOCPServer.cpp`](../server/2015Remote/IOCPServer.cpp) `OnAccept` 末段;[`common/LANChecker.h`](../common/LANChecker.h) `LANChecker::IsPrivateIPv4Str` |
|
||||
| 触发位置 | [`server/2015Remote/2015RemoteDlg.cpp`](../server/2015Remote/2015RemoteDlg.cpp) `CMy2015RemoteDlg::OnTrialWanIpAbuse`(由 `OnAccept` `PostMessage(WM_TRIAL_WAN_IP_ABUSE)` 触发) |
|
||||
| 检测时机 | 每个新连接 `accept` 后立即检测,**比 RTT 路径快**(无需累计 30 秒的中位数样本) |
|
||||
| 信号源 | Proxy Protocol v2 透出的真实客户端 IP(若存在)→ 否则回退到 `getpeername` 的 raw TCP 对端 IP |
|
||||
| 阈值 | 任一入站 IP 不在 RFC 1918 / RFC 3927 / 回环段 → 即触发 |
|
||||
| 触发动作 | 每个 abusive 连接独立记 `Mprintf` 日志(含真实 IP + 解析来源);与 [1.10 服务端内核级 RTT 监测](#110-服务端内核级-rtt-监测sio_tcp_info) 共用 `IOCPServer::s_TrialAbuseWarned` 进程级 latch → 主窗口 `MessageBox` 弹一次 |
|
||||
| 启停 | 仅在 `m_bTrialMode` 为真时执行(`StartServer` 读 `IsTrail(passcode)` 缓存);正式授权主控该分支彻底跳过 |
|
||||
| 设计动机 | 补 [1.10](#110-服务端内核级-rtt-监测sio_tcp_info) 的 TCP 终结代理盲点 —— 合作型代理(FRP / HAProxy)若发送 PP2 头,本检测就能拿到真实 IP 并直接命中。即时触发,无需等待 RTT 累积 |
|
||||
| 与客户端 [1.1](#11-入站连接源-ip-段校验) 的关系 | 两层不重复:[1.1] 在 master 进程内**周期性**扫 `GetExtendedTcpTable`,看到的是内核态 raw IP,不透 PP2;本节在 `OnAccept` **即时**触发,能透 PP2 真实 IP。两层互补 |
|
||||
| 已知局限 | 不发 PP2 头的代理(socat、自制 TCP 转发、不配 PP2 的 FRP)→ 看到的仍是代理的 LAN IP,**本节漏检**,由 [1.3 客户端 RTT 反代理](#13-应用层-rtt-反代理) 端到端 RTT 兜底 |
|
||||
| 性能 | 每新连接增加一次 `inet_pton` + 几个位运算(< 1µs)。非试用模式下 `m_bTrialMode == false`,整段分支彻底跳过 |
|
||||
|
||||
### 1.10 服务端内核级 RTT 监测(`SIO_TCP_INFO`)
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 源代码 | [`server/2015Remote/IOCPServer.cpp`](../server/2015Remote/IOCPServer.cpp) `RttPollThreadProc` + `QuerySocketTcpRttUs`;[`server/2015Remote/Server.h`](../server/2015Remote/Server.h) `CONTEXT_OBJECT::SetRttUs/GetRttUs`;[`common/LANChecker.h`](../common/LANChecker.h) `class TcpRttBreachDetector` |
|
||||
| 触发位置 | [`server/2015Remote/2015RemoteDlg.cpp`](../server/2015Remote/2015RemoteDlg.cpp) `CMy2015RemoteDlg::OnTrialRttAbuse`(由 RTT 轮询线程 `PostMessage(WM_TRIAL_RTT_ABUSE)` 触发) |
|
||||
| 信号源 | Win10 1703+ / Server 2016+ 提供的 `WSAIoctl(SIO_TCP_INFO)`,返回 `TCP_INFO_v0.RttUs`(内核测得的纯网络 RTT,微秒精度,不含任何应用层处理) |
|
||||
| 阈值参数 | 20 ms 中位数 / 滑窗 10 / 收敛跳过 5 / 持续超阈 3 次 @1Hz(最快触发 ≥ 30 秒,是 [1.3 客户端 RTT 反代理](#13-应用层-rtt-反代理) 的 5 倍) |
|
||||
| 触发动作 | 每个 abusive 连接独立记 `Mprintf` 日志(含 ClientID + 真实 IP + median RTT);全 server 进程一次性 latch(`s_TrialRttWarned` CAS)→ 主窗口 `MessageBox` 弹一次 |
|
||||
| 启停 | 仅在主控自身为试用模式时(`StartServer` 读 `IsTrail(passcode)`)启动专用轮询线程;正式授权主控不启动,零运行时开销 |
|
||||
| OS 不支持 | 首次 `WSAIoctl` 拿到 `WSAEOPNOTSUPP` 时打一行 `Mprintf` 日志后线程自杀;[1.3 客户端 RTT 反代理](#13-应用层-rtt-反代理) 仍作为兜底 |
|
||||
| 设计动机 | 服务端检测周期 30 s(vs 客户端 150 s),更快识别直接挂公网的 abusive 部署;且代码运行于发行方 / 运营商可控的服务端二进制,比客户端校验更难绕过 |
|
||||
| **已知局限(重要)** | `SIO_TCP_INFO` 测得的是**服务端 ↔ 直接 TCP 对端**的 RTT。任何在 TCP 层终结的代理(FRP / ngrok / nginx / HAProxy / socat 等)会让服务端只看到"我 ↔ 代理"那段 LAN RTT,**完全漏检 WAN 段**。本机制只能识别直挂公网 / NAT / VPN 等"不终结 TCP"的部署形态;TCP 终结型代理由 [1.3](#13-应用层-rtt-反代理) 的端到端应用层 RTT 兜底(客户端心跳总耗时无法被任何中间层降低) |
|
||||
| 范围 | 仅 `IOCPServer`(TCP);UDP / KCP 通道由 [1.3 客户端 RTT 反代理](#13-应用层-rtt-反代理) 继续兜底 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 分档授权与对应限制
|
||||
|
||||
详见 [`Compliance_AntiAbuse.md` § 7](Compliance_AntiAbuse.md) 与 [`MultiLayerLicense.md`](MultiLayerLicense.md)。本表仅列对应的强制限制点:
|
||||
|
||||
| 授权档 | 强制限制 | 由哪一节技术措施实施 |
|
||||
| --- | --- | --- |
|
||||
| 无口令 | 监听端口 ≤ 2;服务端并发 ≤ 2 | [1.2](#12-监听端口数量上限) + [1.5](#15-服务端硬性并发连接上限) |
|
||||
| 试用口令 | 入站 IP 必须私网段;端口 ≤ 20;客户端 RTT ≤ 25 ms;服务端内核 RTT ≤ 20 ms;周期回连授权服务器;系统时钟可信 | [1.1](#11-入站连接源-ip-段校验) + [1.2](#12-监听端口数量上限) + [1.3](#13-应用层-rtt-反代理) + [1.4](#14-授权服务器周期心跳与超时熔断) + [1.6](#16-系统时钟篡改检测) + [1.9](#19-服务端入站-ip-段即时检测) + [1.10](#110-服务端内核级-rtt-监测sio_tcp_info) |
|
||||
| 正式授权 | 由签发协议另行约定,技术上解除 1.1 / 1.3 / 1.9 / 1.10 限制 | [1.8](#18-主控授权状态下发) 由服务端下发 `IsTrail=0` 触发客户端关闭;服务端 `IsTrail` 为假时 [1.9](#19-服务端入站-ip-段即时检测) / [1.10](#110-服务端内核级-rtt-监测sio_tcp_info) 均跳过 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 演进时间线(合规相关提交)
|
||||
|
||||
按发布时间倒序排列。`SHA` 列为 `git log --oneline` 可直接核对的短哈希;任何
|
||||
独立第三方均可在公开仓库通过 `git show <SHA>` 自行验证。
|
||||
|
||||
| 日期 | SHA | 主题 | 关联节 |
|
||||
| --- | --- | --- | --- |
|
||||
| 2026-05-16 | `7f95f00` | Compliance: Server-side anti-proxy — accept-time WAN-IP check + SIO_TCP_INFO kernel-RTT | [1.9](#19-服务端入站-ip-段即时检测) + [1.10](#110-服务端内核级-rtt-监测sio_tcp_info) |
|
||||
| 2026-05-15 | `4279e79` | Compliance fix: Move LAN RTT check to KernelManager heartbeat | [1.3](#13-应用层-rtt-反代理) / [1.8](#18-主控授权状态下发) |
|
||||
| 2026-05-14 | `14387d6` | Compliance: Anti-proxy RTT check + tiered usage policy and disclaimer | [1.3](#13-应用层-rtt-反代理) / [`Compliance_AntiAbuse.md`](Compliance_AntiAbuse.md) v1.0 发布 |
|
||||
| —(既往) | — | `LANChecker` IP 段 / 端口数检测、`AuthTimeoutChecker` 授权心跳、`DateVerify` 时钟校验等机制 | [1.1](#11-入站连接源-ip-段校验) / [1.2](#12-监听端口数量上限) / [1.4](#14-授权服务器周期心跳与超时熔断) / [1.6](#16-系统时钟篡改检测) |
|
||||
|
||||
> 后续提交按 commit message 前缀 `Compliance:` 或 `Compliance fix:` 识别,加入本表。
|
||||
|
||||
---
|
||||
|
||||
## 4. 规划中的技术措施
|
||||
|
||||
> 以下条目为已确定方向但尚未合入主分支的工作项,列出便于审查方了解未来演进。
|
||||
|
||||
### 4.1 行为信号融合(占位)
|
||||
|
||||
`LANRttChecker` 源码注释中明示的"同源 IP 多 ClientID"行为信号待并入。
|
||||
计划在授权服务器侧实现,与本仓库客户端 / 主控代码解耦。
|
||||
|
||||
针对 [1.10](#110-服务端内核级-rtt-监测sio_tcp_info) 的 TCP 终结代理盲点 ——
|
||||
合作型代理(FRP / HAProxy)发送 Proxy Protocol v2 头的情况已由 [1.9](#19-服务端入站-ip-段即时检测) 兜住;
|
||||
不发 PP2 的代理(socat、自制 TCP 转发)的残留盲区由 [1.3 客户端 RTT 反代理](#13-应用层-rtt-反代理)
|
||||
端到端 RTT 兜底,本仓库内无进一步可加的服务端措施。后续考虑授权服务器侧"同源 IP 多 ClientID"
|
||||
行为信号作为外部交叉验证。
|
||||
|
||||
---
|
||||
|
||||
## 5. 维护者注意事项
|
||||
|
||||
> **对未来修改本仓库的任何贡献者**:以下文件 / 模块的削弱、移除、绕过型修改
|
||||
> 应被视为对发行方反滥用立场的直接违反,PR 一律拒收。即使您是为了"优化体验"
|
||||
> 或"修复误报",也请优先选择**调阈值 / 加白名单**而非**禁用机制**。
|
||||
|
||||
| 不可被静默削弱的内容 | 文件 |
|
||||
| --- | --- |
|
||||
| `LANChecker::CheckAndWarn` 中私网段判定逻辑 | [`common/LANChecker.h`](../common/LANChecker.h) |
|
||||
| `LANChecker::CheckPortLimit` 的端口数比对 | 同上 |
|
||||
| `LANRttChecker::SetEnabled` 的启停语义 | 同上 |
|
||||
| `LANRttChecker::RecordSample` 的滑窗 / 收敛 / 持续超阈逻辑 | 同上 |
|
||||
| `AuthTimeoutChecker` 的超时熔断分支 | 同上 |
|
||||
| `DateVerify::isTimeTampered` 的多 NTP 比对 | [`common/DateVerify.h`](../common/DateVerify.h) |
|
||||
| 服务端 `m_nMaxConnection` 在未授权 / 失效校验时回落到 2 的分支 | [`server/2015Remote/2015RemoteDlg.cpp`](../server/2015Remote/2015RemoteDlg.cpp) |
|
||||
| 服务端 `MasterSettings.Authorized` / `IsTrail` 字段的真实填充逻辑 | 同上 |
|
||||
| 客户端 `OnHeatbeatResponse` 中 `LANRttChecker` 的调用、`AuthTimeoutChecker` 的复位 | [`client/KernelManager.cpp`](../client/KernelManager.cpp) |
|
||||
| `TcpRttBreachDetector` 滑窗 / 收敛 / 持续超阈逻辑 | [`common/LANChecker.h`](../common/LANChecker.h) |
|
||||
| `IOCPServer::OnAccept` 末段试用模式下的入站 IP 段判定(PP2 真实 IP 优先) | [`server/2015Remote/IOCPServer.cpp`](../server/2015Remote/IOCPServer.cpp) |
|
||||
| `IOCPServer::RttPollThreadProc` 试用模式判定、SIO_TCP_INFO 探测、per-context 检测器喂样 | 同上 |
|
||||
| 主对话框 `OnTrialRttAbuse` / `OnTrialWanIpAbuse` 弹框 + 日志归档;`WM_TRIAL_RTT_ABUSE` / `WM_TRIAL_WAN_IP_ABUSE` 消息映射 | [`server/2015Remote/2015RemoteDlg.cpp`](../server/2015Remote/2015RemoteDlg.cpp) |
|
||||
| `LANChecker::IsPrivateIPv4Str` 字符串版私网段判定(被 §1.9 直接依赖) | [`common/LANChecker.h`](../common/LANChecker.h) |
|
||||
|
||||
允许的修改方向:
|
||||
|
||||
- 调整阈值参数(需在 PR 中给出新的统计依据);
|
||||
- 新增告警通道(如增加日志落盘 / 加 webhook 通知),但不得替换原有告警通道;
|
||||
- 提高检测精度(如本文档 §4 所列规划项);
|
||||
- 修复 OS 兼容性 bug,但不得以"OS 不支持"为由整体跳过试用档的检测。
|
||||
|
||||
---
|
||||
|
||||
## 6. 与政策文档的对照表
|
||||
|
||||
便于审查方在本仓库技术实现 ↔ `Compliance_AntiAbuse.md` 政策条款之间双向追溯。
|
||||
|
||||
| 政策条款 | 本文档节 |
|
||||
| --- | --- |
|
||||
| § 6.1 入站连接源 IP 段校验 | [1.1](#11-入站连接源-ip-段校验) |
|
||||
| § 6.2 监听端口数量上限 | [1.2](#12-监听端口数量上限) |
|
||||
| § 6.3 应用层 RTT 反代理 | [1.3](#13-应用层-rtt-反代理) + [1.9](#19-服务端入站-ip-段即时检测)(服务端即时 IP 段判定)+ [1.10](#110-服务端内核级-rtt-监测sio_tcp_info)(服务端内核级 RTT 补强) |
|
||||
| § 6.4 授权服务器周期心跳 | [1.4](#14-授权服务器周期心跳与超时熔断) |
|
||||
| § 6.5 措施的可被感知性 | 见各节"触发动作"列 |
|
||||
| § 6.6 措施的合理性声明 | 见各节"已知局限"列 |
|
||||
| § 7 授权分级与对应限制 | [2](#2-分档授权与对应限制) |
|
||||
|
||||
---
|
||||
|
||||
文档结束 / END OF DOCUMENT
|
||||
@@ -820,6 +820,8 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
|
||||
ON_MESSAGE(WM_SHOWMESSAGE, OnShowMessage)
|
||||
ON_MESSAGE(WM_SHOWNOTIFY, OnShowNotify)
|
||||
ON_MESSAGE(WM_SHOWERRORMSG, OnShowErrMessage)
|
||||
ON_MESSAGE(WM_TRIAL_RTT_ABUSE, OnTrialRttAbuse)
|
||||
ON_MESSAGE(WM_TRIAL_WAN_IP_ABUSE, OnTrialWanIpAbuse)
|
||||
ON_MESSAGE(WM_INJECT_SHELLCODE, InjectShellcode)
|
||||
ON_MESSAGE(WM_ANTI_BLACKSCREEN, AntiBlackScreen)
|
||||
ON_MESSAGE(WM_SHARE_CLIENT, ShareClient)
|
||||
@@ -1574,6 +1576,52 @@ VOID CMy2015RemoteDlg::ShowMessage(CString strType, CString strMsg)
|
||||
m_StatusBar.SetPaneText(0,strStatusMsg); //在状态条上显示文字
|
||||
}
|
||||
|
||||
// 试用版 IP 段触发:OnAccept 发现入站连接对端是公网 IP(已透过 Proxy Protocol v2 解出真实 IP)。
|
||||
// 与 OnTrialRttAbuse 共用 IOCPServer::s_TrialAbuseWarned latch,本函数每进程最多调一次。
|
||||
LRESULT CMy2015RemoteDlg::OnTrialWanIpAbuse(WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
CString* ip = (CString*)wParam;
|
||||
CString detail;
|
||||
detail.FormatL("入站公网 IP=%s (Proxy Protocol 真实 IP 或 raw TCP 对端)",
|
||||
ip ? (LPCTSTR)*ip : _T("?"));
|
||||
ShowMessage(_TR("入站告警"), detail);
|
||||
|
||||
CString msg;
|
||||
msg.FormatL(
|
||||
"检测到入站连接来自公网 IP:%s\r\n\r\n"
|
||||
"试用版仅供 LAN 内自用,跨网使用属于违反授权条款。\r\n"
|
||||
"如需跨网远控,请向发行方申请正式授权。\r\n\r\n"
|
||||
"详细记录见消息列表与运行日志。",
|
||||
ip ? (LPCTSTR)*ip : _T("?"));
|
||||
THIS_APP->MessageBox(msg, _TR("试用版 LAN-only 限制"), MB_OK | MB_ICONWARNING | MB_TOPMOST);
|
||||
if (ip) delete ip;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// 试用版反代理触发后的主窗口处理:写日志列表 + 弹一次模态框(前面已 latch,本函数每进程只会被调一次)。
|
||||
// 不在 IOCPServer 的 RTT 轮询线程里直接弹框,避免阻塞后续采样。
|
||||
LRESULT CMy2015RemoteDlg::OnTrialRttAbuse(WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
uint32_t clientIdLow = (uint32_t)wParam;
|
||||
int medianMs = (int)lParam;
|
||||
CString detail;
|
||||
// 纯英文格式串,不进翻译表
|
||||
detail.Format(_T("ClientID(low32)=%u median RTT=%d ms threshold=%d ms"),
|
||||
clientIdLow, medianMs, (int)TcpRttBreachDetector::RTT_THRESHOLD_MS);
|
||||
ShowMessage(_TR("反代理告警"), detail);
|
||||
|
||||
CString msg;
|
||||
msg.FormatL(
|
||||
"检测到可疑连接:内核 RTT 中位数 %d ms,超出阈值 %d ms。\r\n\r\n"
|
||||
"持续偏高的 RTT 提示该连接可能经由代理 / VPN / 隧道中转。\r\n"
|
||||
"试用版仅供 LAN 内自用,跨网使用属于违反授权条款。\r\n\r\n"
|
||||
"如需跨网远控,请向发行方申请正式授权。\r\n"
|
||||
"详细记录见消息列表与运行日志。",
|
||||
medianMs, (int)TcpRttBreachDetector::RTT_THRESHOLD_MS);
|
||||
THIS_APP->MessageBox(msg, _TR("试用版 LAN-only 限制"), MB_OK | MB_ICONWARNING | MB_TOPMOST);
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
LRESULT CMy2015RemoteDlg::OnShowErrMessage(WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
CString* text = (CString*)wParam;
|
||||
|
||||
@@ -536,6 +536,8 @@ public:
|
||||
afx_msg void OnNMCustomdrawOnline(NMHDR* pNMHDR, LRESULT* pResult);
|
||||
afx_msg void OnOnlineRunAsAdmin();
|
||||
afx_msg LRESULT OnShowErrMessage(WPARAM wParam, LPARAM lParam);
|
||||
afx_msg LRESULT OnTrialRttAbuse(WPARAM wParam, LPARAM lParam);
|
||||
afx_msg LRESULT OnTrialWanIpAbuse(WPARAM wParam, LPARAM lParam);
|
||||
afx_msg void OnMainWallet();
|
||||
afx_msg void OnMainNetwork();
|
||||
afx_msg void OnToolRcedit();
|
||||
|
||||
@@ -7,6 +7,68 @@
|
||||
#include <iostream>
|
||||
#include <ws2tcpip.h>
|
||||
|
||||
// 服务端 RTT 反代理(试用版执法)。声明在主对话框 cpp 中,无单独头文件。
|
||||
BOOL IsTrail(const std::string& passcode);
|
||||
|
||||
// ============================================================================
|
||||
// SIO_TCP_INFO 兼容性 shim
|
||||
//
|
||||
// SIO_TCP_INFO 自 Win10 1703 / Server 2016 起提供,对应的 SDK 头声明只在
|
||||
// NTDDI_VERSION >= NTDDI_WIN10_RS2 (0x0A000003) 时才可见。本项目当前
|
||||
// _WIN32_WINNT=0x0602 / NTDDI_VERSION=0x06020000(Win8),整体上调宏会
|
||||
// 波及其他模块,且会排除 Win8/8.1 用户。因此在此处本地声明常量与结构,
|
||||
// 运行时若 OS 不支持,WSAIoctl 会返回 WSAEOPNOTSUPP,由探测代码静默降级。
|
||||
//
|
||||
// 结构体字段顺序严格遵循 MS 公开的 TCP_INFO_v0 定义,不要随意调整。
|
||||
// ============================================================================
|
||||
#ifndef SIO_TCP_INFO
|
||||
#define SIO_TCP_INFO _WSAIORW(IOC_VENDOR, 39)
|
||||
#endif
|
||||
|
||||
typedef struct _TCP_INFO_v0_local {
|
||||
ULONG State; // TCPSTATE(枚举,按 4 字节读)
|
||||
ULONG Mss;
|
||||
ULONG64 ConnectionTimeMs;
|
||||
UCHAR TimestampsEnabled;
|
||||
UCHAR Pad_[3]; // 显式 padding,让 RttUs 落在 4 字节边界
|
||||
ULONG RttUs; // <-- 本文件唯一关心的字段
|
||||
ULONG MinRttUs;
|
||||
ULONG BytesInFlight;
|
||||
ULONG Cwnd;
|
||||
ULONG SndWnd;
|
||||
ULONG RcvWnd;
|
||||
ULONG RcvBuf;
|
||||
ULONG64 BytesOut;
|
||||
ULONG64 BytesIn;
|
||||
ULONG BytesReordered;
|
||||
ULONG BytesRetrans;
|
||||
ULONG FastRetrans;
|
||||
ULONG DupAcksIn;
|
||||
ULONG TimeoutEpisodes;
|
||||
UCHAR SynRetrans;
|
||||
} TCP_INFO_v0_local;
|
||||
|
||||
// 读取 socket 的内核测得 RTT。成功返回 0 并写入 *rttUs;失败返回 WSAGetLastError()。
|
||||
static int QuerySocketTcpRttUs(SOCKET s, uint32_t* rttUs)
|
||||
{
|
||||
TCP_INFO_v0_local info; ZeroMemory(&info, sizeof(info));
|
||||
DWORD ver = 0; // request v0
|
||||
DWORD bytesReturned = 0;
|
||||
int ret = WSAIoctl(s, SIO_TCP_INFO,
|
||||
&ver, sizeof(ver),
|
||||
&info, sizeof(info),
|
||||
&bytesReturned, NULL, NULL);
|
||||
if (ret == 0) {
|
||||
if (rttUs) *rttUs = info.RttUs;
|
||||
return 0;
|
||||
}
|
||||
return WSAGetLastError();
|
||||
}
|
||||
|
||||
// 全 server 进程级 latch,IP 段触发与 RTT 触发共用。多 server 实例(多端口监听)共享一份,
|
||||
// 任一先触发后其余 server 与其它触发路径不再重复弹框。
|
||||
std::atomic<bool> IOCPServer::s_TrialAbuseWarned{false};
|
||||
|
||||
// Proxy Protocol v2 签名 (12 字节)
|
||||
static const unsigned char PROXY_PROTOCOL_V2_SIGNATURE[12] = {
|
||||
0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A
|
||||
@@ -353,6 +415,13 @@ void IOCPServer::Destroy()
|
||||
|
||||
if (m_hKillEvent != NULL) {
|
||||
SetEvent(m_hKillEvent);
|
||||
// RTT 轮询线程要等它退出后再关 m_hKillEvent,否则线程仍在 WaitForSingleObject 上时
|
||||
// 关句柄是 UB。监听 / 工作线程是用 m_bTimeToKill 兜底的,原有时序不动。
|
||||
if (m_hRttThread != NULL) {
|
||||
WaitForSingleObject(m_hRttThread, 5000);
|
||||
SAFE_CLOSE_HANDLE(m_hRttThread);
|
||||
m_hRttThread = NULL;
|
||||
}
|
||||
SAFE_CLOSE_HANDLE(m_hKillEvent);
|
||||
m_hKillEvent = NULL;
|
||||
}
|
||||
@@ -414,7 +483,10 @@ UINT IOCPServer::StartServer(pfnNotifyProc NotifyProc, pfnOfflineProc OffProc, U
|
||||
m_nPort = uPort;
|
||||
m_NotifyProc = NotifyProc;
|
||||
m_OfflineProc = OffProc;
|
||||
m_hKillEvent = CreateEvent(NULL,FALSE,FALSE,NULL);
|
||||
// manual-reset:本进程内可能有多个等待者(ListenThread / RttPollThreadProc)。
|
||||
// 自动重置会让 SetEvent 只唤醒一个等待者,另一个要等自身 timeout(≤1s)。
|
||||
// 改 manual-reset 后所有等待者一次性醒来;本工程从无 ResetEvent 调用,无副作用。
|
||||
m_hKillEvent = CreateEvent(NULL,TRUE,FALSE,NULL);
|
||||
|
||||
if (m_hKillEvent==NULL) {
|
||||
return 1;
|
||||
@@ -507,6 +579,20 @@ UINT IOCPServer::StartServer(pfnNotifyProc NotifyProc, pfnOfflineProc OffProc, U
|
||||
|
||||
//启动工作线程 1 2
|
||||
InitializeIOCP();
|
||||
|
||||
// 试用版反代理 RTT 轮询(仅在主控自身为试用模式时启动)。
|
||||
// 检测信号来自内核 SIO_TCP_INFO,详见 IOCPServer.h 头部 / RttPollThreadProc 注释。
|
||||
{
|
||||
std::string pwd = THIS_CFG.GetStr("settings", "Password", "");
|
||||
m_bTrialMode = (IsTrail(pwd) == TRUE);
|
||||
}
|
||||
if (m_bTrialMode) {
|
||||
m_hRttThread = CreateThread(NULL, 0, RttPollThreadProc, (void*)this, 0, NULL);
|
||||
if (m_hRttThread == NULL) {
|
||||
Mprintf("[Compliance] RTT poll thread spawn failed (err=%lu); LANRttChecker (client-side) remains as fallback.\n",
|
||||
GetLastError());
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -930,6 +1016,97 @@ BOOL IOCPServer::OnClientPostSending(CONTEXT_OBJECT* ContextObject,ULONG ulCompl
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 试用版反代理 —— 服务端 RTT 轮询线程
|
||||
//
|
||||
// 仅在主控自身处于试用模式(IsTrail(passcode) == TRUE)时由 StartServer 启动。
|
||||
// 1 Hz 遍历 m_ContextConnectionList,对每个活跃连接调 WSAIoctl(SIO_TCP_INFO) 取
|
||||
// 内核测得的纯网络 RTT,喂给 ctx->m_RttDetector。任一 detector 首次触发 →
|
||||
// 通过 s_TrialAbuseWarned latch 抢一次 PostMessage 给主窗口弹框;其余 detector
|
||||
// 仍照常运转(继续记日志),但不再重复弹框。
|
||||
//
|
||||
// 并发模型:对齐既有 IoRefCount / IsRemoved 模式 —— 持 m_cs snapshot 指针并
|
||||
// 引用计数 ++,锁外做 WSAIoctl + 写 atomic,最后引用计数 --。RemoveStaleContext
|
||||
// 会等 IoRefCount==0 才回收,无悬空指针。
|
||||
//
|
||||
// 不支持 SIO_TCP_INFO 的 OS(Win8 / Server 2012 等):首次探测命中
|
||||
// WSAEOPNOTSUPP 时打日志后线程自行退出;客户端 LANRttChecker 仍作为兜底。
|
||||
// ============================================================================
|
||||
DWORD IOCPServer::RttPollThreadProc(LPVOID lParam)
|
||||
{
|
||||
IOCPServer* This = (IOCPServer*)lParam;
|
||||
|
||||
while (!This->m_bTimeToKill) {
|
||||
DWORD waitRet = WaitForSingleObject(This->m_hKillEvent, 1000);
|
||||
if (waitRet == WAIT_OBJECT_0 || waitRet == WAIT_FAILED) break;
|
||||
if (This->m_bTimeToKill) break;
|
||||
|
||||
// —— 步骤 1:持锁快照 + 占引用 —— 锁外才做 WSAIoctl,避免阻塞其他 I/O
|
||||
std::vector<PCONTEXT_OBJECT> snap;
|
||||
EnterCriticalSection(&This->m_cs);
|
||||
for (POSITION pos = This->m_ContextConnectionList.GetHeadPosition(); pos != NULL; ) {
|
||||
PCONTEXT_OBJECT ctx = This->m_ContextConnectionList.GetNext(pos);
|
||||
if (!ctx) continue;
|
||||
if (ctx->IsRemoved.load(std::memory_order_acquire)) continue;
|
||||
ctx->IoRefCount.fetch_add(1, std::memory_order_acq_rel);
|
||||
snap.push_back(ctx);
|
||||
}
|
||||
LeaveCriticalSection(&This->m_cs);
|
||||
|
||||
// —— 步骤 2:OS 兼容性探测(一次性,借第一个真实连接做) —— 探测失败的 OS
|
||||
// 上整个线程不必再活,本次循环把已占的引用还掉就退出。
|
||||
if (!This->m_bSioTcpInfoProbed.load(std::memory_order_acquire) && !snap.empty()) {
|
||||
uint32_t probeRtt = 0;
|
||||
int err = QuerySocketTcpRttUs(snap[0]->sClientSocket, &probeRtt);
|
||||
if (err == WSAEOPNOTSUPP) {
|
||||
Mprintf("[Compliance] SIO_TCP_INFO not supported by OS (WSAEOPNOTSUPP); "
|
||||
"server-side RTT monitoring disabled. Client-side LANRttChecker remains active.\n");
|
||||
This->m_bSioTcpInfoSupported.store(false, std::memory_order_release);
|
||||
This->m_bSioTcpInfoProbed.store(true, std::memory_order_release);
|
||||
for (auto* c : snap) c->IoRefCount.fetch_sub(1, std::memory_order_acq_rel);
|
||||
break;
|
||||
}
|
||||
// 其它错误(如 WSAENOTCONN 短连接刚断)不视为 OS 问题,下一轮再试
|
||||
if (err == 0) {
|
||||
This->m_bSioTcpInfoSupported.store(true, std::memory_order_release);
|
||||
This->m_bSioTcpInfoProbed.store(true, std::memory_order_release);
|
||||
Mprintf("[Compliance] SIO_TCP_INFO probe OK; server-side anti-proxy RTT monitor armed "
|
||||
"(threshold=%d ms, trigger after >=%d consecutive median breaches @1Hz).\n",
|
||||
TcpRttBreachDetector::RTT_THRESHOLD_MS, TcpRttBreachDetector::BREACH_PERSIST_COUNT);
|
||||
}
|
||||
}
|
||||
|
||||
// —— 步骤 3:逐 ctx 取 RTT + 喂检测器 —— 同步释放引用
|
||||
for (auto* ctx : snap) {
|
||||
uint32_t rttUs = 0;
|
||||
int err = QuerySocketTcpRttUs(ctx->sClientSocket, &rttUs);
|
||||
if (err == 0 && rttUs > 0) {
|
||||
ctx->SetRttUs(rttUs);
|
||||
// RttUs 单位是微秒,转毫秒喂检测器
|
||||
int rttMs = (int)((rttUs + 500) / 1000);
|
||||
if (ctx->m_RttDetector.Feed(rttMs)) {
|
||||
// 本 ctx 首次触发:记日志(每个 ctx 都记,便于排查 abusive 来源);
|
||||
// 全 server 一次性 latch 决定要不要弹框
|
||||
Mprintf("[Compliance] !!! Trial-mode anti-proxy triggered: client=%llu IP=%s "
|
||||
"median RTT=%d ms (threshold=%d ms).\n",
|
||||
ctx->ID, ctx->GetPeerName().c_str(),
|
||||
ctx->m_RttDetector.TriggerMedianMs(),
|
||||
TcpRttBreachDetector::RTT_THRESHOLD_MS);
|
||||
bool expected = false;
|
||||
if (s_TrialAbuseWarned.compare_exchange_strong(expected, true) && This->m_hMainWnd) {
|
||||
// WPARAM 携带 abusive ctx 的 ClientID 低 32 位(仅用于展示),LPARAM 携带 medianMs
|
||||
PostMessageA(This->m_hMainWnd, WM_TRIAL_RTT_ABUSE,
|
||||
(WPARAM)(ctx->ID & 0xFFFFFFFF),
|
||||
(LPARAM)ctx->m_RttDetector.TriggerMedianMs());
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx->IoRefCount.fetch_sub(1, std::memory_order_acq_rel);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
DWORD IOCPServer::ListenThreadProc(LPVOID lParam) //监听线程
|
||||
{
|
||||
IOCPServer* This = (IOCPServer*)(lParam);
|
||||
@@ -1012,6 +1189,29 @@ void IOCPServer::OnAccept()
|
||||
}
|
||||
RecordConnection(clientIP);
|
||||
|
||||
// 试用版反代理 —— 入站 IP 段检测(即时触发,对合作型代理透明)
|
||||
//
|
||||
// 与 RttPollThreadProc 的 SIO_TCP_INFO 检测互补:RTT 测的是"我↔直接 TCP 对端",
|
||||
// 任何 TCP 终结型代理都能欺骗它;本检测用 Proxy Protocol v2 解出的真实 IP(若有)
|
||||
// 或 getpeername 的 raw IP 直接判私网段。
|
||||
// - 覆盖:直连 WAN、PP2 透出真实 IP 是公网
|
||||
// - 不覆盖:socat / 不发 PP2 的中转 —— 那种场景仍由客户端 LANRttChecker 兜底
|
||||
//
|
||||
// 性能:每个新连接走一次 IsPrivateIPv4Str(几个位运算),不放心跳路径,可忽略。
|
||||
// 不主动断开连接(与 RTT 路径一致仅告警),由运营商看到弹框后自行处置。
|
||||
// 详见 docs/Compliance_TechnicalMeasures.md(口径文档,可能比这里更新)。
|
||||
if (m_bTrialMode && !LANChecker::IsPrivateIPv4Str(clientIP)) {
|
||||
Mprintf("[Compliance] !!! Trial-mode WAN inbound: IP=%s (resolved via %s).\n",
|
||||
clientIP.c_str(),
|
||||
ContextObject->GetPeerName().empty() ? "getpeername" : "Proxy Protocol v2 or getpeername");
|
||||
bool expected = false;
|
||||
if (s_TrialAbuseWarned.compare_exchange_strong(expected, true) && m_hMainWnd) {
|
||||
// CString* 由 OnTrialWanIpAbuse handler 负责 delete,与 OnShowErrMessage 一致
|
||||
PostMessageA(m_hMainWnd, WM_TRIAL_WAN_IP_ABUSE,
|
||||
(WPARAM)new CString(clientIP.c_str()), 0);
|
||||
}
|
||||
}
|
||||
|
||||
ContextObject->wsaInBuf.buf = (char*)ContextObject->szBuffer;
|
||||
ContextObject->wsaInBuf.len = sizeof(ContextObject->szBuffer);
|
||||
|
||||
|
||||
@@ -78,9 +78,19 @@ protected:
|
||||
void LoadIPWhitelist();
|
||||
void LoadIPBlacklist();
|
||||
|
||||
// RTT 反代理(试用版执法)相关。详见 IOCPServer.cpp 中的实现注释。
|
||||
HANDLE m_hRttThread = NULL;
|
||||
bool m_bTrialMode = false; // StartServer 时根据 IsTrail(passcode) 缓存
|
||||
std::atomic<bool> m_bSioTcpInfoProbed{false}; // 是否已完成 OS 兼容性探测
|
||||
std::atomic<bool> m_bSioTcpInfoSupported{false}; // 探测结果(不支持则 RTT 线程会自行退出)
|
||||
// 全 server 进程一次性 latch,IP 段触发与 RTT 触发共用。任一先触发后另一者只记日志不再弹框,
|
||||
// 避免对运营商反复打扰;不影响每条 abusive 连接的独立日志。
|
||||
static std::atomic<bool> s_TrialAbuseWarned;
|
||||
|
||||
private:
|
||||
static DWORD WINAPI ListenThreadProc(LPVOID lParam);
|
||||
static DWORD WINAPI WorkThreadProc(LPVOID lParam);
|
||||
static DWORD WINAPI RttPollThreadProc(LPVOID lParam);
|
||||
|
||||
BOOL InitializeIOCP(VOID);
|
||||
VOID OnAccept();
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "Buffer.h"
|
||||
#define XXH_INLINE_ALL
|
||||
#include "common/xxhash.h"
|
||||
#include "common/LANChecker.h"
|
||||
#include <WS2tcpip.h>
|
||||
#include <common/ikcp.h>
|
||||
#include <atomic>
|
||||
@@ -378,6 +379,14 @@ public:
|
||||
std::atomic<int> IoRefCount{0}; // I/O 处理引用计数
|
||||
std::atomic<bool> IsRemoved{false}; // 标记是否已被标记为移除
|
||||
|
||||
// 内核测得的纯网络 RTT(μs),由 IOCPServer 的 RTT 轮询线程通过
|
||||
// WSAIoctl(SIO_TCP_INFO) 周期性写入;任何线程可通过 GetRttUs() 安全读取。
|
||||
// m_LastRttSampleMs == 0 表示从未成功采样过(OS 不支持或连接太短未轮询到)。
|
||||
std::atomic<uint32_t> m_RttUs{0};
|
||||
std::atomic<uint64_t> m_LastRttSampleMs{0};
|
||||
// 试用版反代理逐连接检测器。仅由 IOCPServer 的 RTT 轮询线程访问,免锁。
|
||||
TcpRttBreachDetector m_RttDetector;
|
||||
|
||||
// 子连接身份校验:客户端发 TOKEN_CONN_AUTH 通过验证后置位。
|
||||
// 主连接(走 TOKEN_LOGIN 流程)不参与此机制。当前阶段宽容(未通过也接受),
|
||||
// 仅作为标记供后续命令处理 / 未来收紧策略使用。
|
||||
@@ -517,9 +526,28 @@ public:
|
||||
IoRefCount.store(0, std::memory_order_release);
|
||||
// 复用对象池时清空校验状态
|
||||
m_bAuthenticated.store(false, std::memory_order_release);
|
||||
// 复用对象池时重置 RTT 相关状态,避免上一个连接的数据污染
|
||||
m_RttUs.store(0, std::memory_order_release);
|
||||
m_LastRttSampleMs.store(0, std::memory_order_release);
|
||||
m_RttDetector.Reset();
|
||||
}
|
||||
void SetAuthenticated(bool v) { m_bAuthenticated.store(v, std::memory_order_release); }
|
||||
bool IsAuthenticated() const { return m_bAuthenticated.load(std::memory_order_acquire); }
|
||||
|
||||
// 由 RTT 轮询线程写入。rttUs 为内核测得的纯网络 RTT(微秒)。
|
||||
void SetRttUs(uint32_t rttUs)
|
||||
{
|
||||
m_RttUs.store(rttUs, std::memory_order_release);
|
||||
m_LastRttSampleMs.store((uint64_t)GetTickCount64(), std::memory_order_release);
|
||||
}
|
||||
uint32_t GetRttUs() const { return m_RttUs.load(std::memory_order_acquire); }
|
||||
// 供 UI 展示用:μs → ms;样本超过 10 秒未更新(连接刚断/未轮询到)视为不可用,返回 -1。
|
||||
int GetRttMsForDisplay() const
|
||||
{
|
||||
uint64_t last = m_LastRttSampleMs.load(std::memory_order_acquire);
|
||||
if (last == 0 || (uint64_t)GetTickCount64() - last > 10000) return -1;
|
||||
return (int)((m_RttUs.load(std::memory_order_acquire) + 500) / 1000);
|
||||
}
|
||||
uint64_t GetAliveTime()const
|
||||
{
|
||||
return time(0) - OnlineTime;
|
||||
|
||||
@@ -1847,3 +1847,10 @@ IOCP
|
||||
快照=Snapshot
|
||||
预览=Preview
|
||||
主机列表预览图=Host List Thumbnails
|
||||
|
||||
入站告警=Inbound Alert
|
||||
反代理告警=Anti-Proxy Alert
|
||||
试用版 LAN-only 限制=Trial Version - LAN Only Restriction
|
||||
入站公网 IP=%s (Proxy Protocol 真实 IP 或 raw TCP 对端)=Inbound public IP=%s (resolved via Proxy Protocol v2 real IP or raw TCP peer)
|
||||
检测到入站连接来自公网 IP:%s\r\n\r\n试用版仅供 LAN 内自用,跨网使用属于违反授权条款。\r\n如需跨网远控,请向发行方申请正式授权。\r\n\r\n详细记录见消息列表与运行日志。=Inbound connection from public IP: %s\r\n\r\nTrial version is restricted to LAN-only usage; cross-network use violates the license terms.\r\nFor cross-network remote control, please obtain a commercial license from the publisher.\r\n\r\nSee the message list and runtime log for full details.
|
||||
检测到可疑连接:内核 RTT 中位数 %d ms,超出阈值 %d ms。\r\n\r\n持续偏高的 RTT 提示该连接可能经由代理 / VPN / 隧道中转。\r\n试用版仅供 LAN 内自用,跨网使用属于违反授权条款。\r\n\r\n如需跨网远控,请向发行方申请正式授权。\r\n详细记录见消息列表与运行日志。=Suspicious connection detected: kernel-measured RTT median %d ms exceeds the threshold of %d ms.\r\n\r\nA persistently elevated RTT suggests the connection is being relayed through a proxy / VPN / tunnel.\r\nTrial version is restricted to LAN-only usage; cross-network use violates the license terms.\r\n\r\nFor cross-network remote control, please obtain a commercial license from the publisher.\r\nSee the message list and runtime log for full details.
|
||||
|
||||
@@ -1838,3 +1838,10 @@ IOCP
|
||||
快照=快照
|
||||
预览=預覽
|
||||
主机列表预览图=主機列表預覽圖
|
||||
|
||||
入站告警=入站告警
|
||||
反代理告警=反代理告警
|
||||
试用版 LAN-only 限制=試用版 LAN-only 限制
|
||||
入站公网 IP=%s (Proxy Protocol 真实 IP 或 raw TCP 对端)=入站公網 IP=%s (Proxy Protocol 真實 IP 或 raw TCP 對端)
|
||||
检测到入站连接来自公网 IP:%s\r\n\r\n试用版仅供 LAN 内自用,跨网使用属于违反授权条款。\r\n如需跨网远控,请向发行方申请正式授权。\r\n\r\n详细记录见消息列表与运行日志。=檢測到入站連線來自公網 IP:%s\r\n\r\n試用版僅供 LAN 內自用,跨網使用屬於違反授權條款。\r\n如需跨網遠控,請向發行方申請正式授權。\r\n\r\n詳細記錄請見訊息列表與執行日誌。
|
||||
检测到可疑连接:内核 RTT 中位数 %d ms,超出阈值 %d ms。\r\n\r\n持续偏高的 RTT 提示该连接可能经由代理 / VPN / 隧道中转。\r\n试用版仅供 LAN 内自用,跨网使用属于违反授权条款。\r\n\r\n如需跨网远控,请向发行方申请正式授权。\r\n详细记录见消息列表与运行日志。=檢測到可疑連線:核心 RTT 中位數 %d ms,超出閾值 %d ms。\r\n\r\n持續偏高的 RTT 提示該連線可能經由代理 / VPN / 隧道中轉。\r\n試用版僅供 LAN 內自用,跨網使用屬於違反授權條款。\r\n\r\n如需跨網遠控,請向發行方申請正式授權。\r\n詳細記錄請見訊息列表與執行日誌。
|
||||
|
||||
@@ -103,6 +103,8 @@
|
||||
#define WM_OPENTERMINALDIALOG WM_USER+3033
|
||||
#define WM_PREVIEW_RESPONSE WM_USER+3034
|
||||
#define WM_PREVIEW_LOOP_CLOSED WM_USER+3035
|
||||
#define WM_TRIAL_RTT_ABUSE WM_USER+3036 // 试用版 RTT 反代理:服务端检测到滥用,通知主窗口弹框
|
||||
#define WM_TRIAL_WAN_IP_ABUSE WM_USER+3037 // 试用版 IP 段检测:OnAccept 发现入站为公网 IP,通知主窗口弹框
|
||||
|
||||
#ifdef _UNICODE
|
||||
#if defined _M_IX86
|
||||
|
||||
Reference in New Issue
Block a user