Feature: sub-connection auth (TOKEN_CONN_AUTH) with HMAC + clientID binding

Client first packet on every sub-connection signs (clientID || timestamp ||
nonce) and waits for server ack. Server verifies signature and pins clientID
on the sub-connection ctx, eliminating IP-reverse-lookup unreliability for
NAT/localhost scenarios. Sub-conn coverage: Win 12 sites, Linux/macOS 3-4
each. Main connection keeps existing TOKEN_LOGIN flow unchanged.

Includes:
- Protocol structs sized to 512/256 bytes with reserved space for future
  extensions (locale, OS info, session token, etc.)
- 5-min timestamp tolerance (Kerberos-grade replay window)
- 10-sec client wait for cross-pacific / weak-network tolerance
- Fix RemoveFromHostList side-effect ordering: MarkDeviceOffline and
  m_ActiveWndW.erase now only fire when ctx is actually removed from
  m_HostList, preventing sub-conn disconnects from misreporting main as
  offline (regression introduced by auth-set clientID on sub ctx)
- Fix latent bug: IOCPClient::m_conn was never assigned in ctor, leaving
  GetConnectionAddress() always NULL and FileManager V2 transfer's
  srcClientID always 0

Breaking change: new client cannot use sub-features against old server.
New server tolerates legacy clients (no auth). Future tightening can reject
unauthenticated sub-connections via IsAuthenticated() flag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
yuanyuanxiang
2026-05-06 23:55:34 +02:00
parent 2c5b5ad628
commit ef8165c3b4
11 changed files with 363 additions and 49 deletions

View File

@@ -5494,6 +5494,53 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
g_2015RemoteDlg->SendMessage(WM_USERTOONLINELIST, 0, (LPARAM)ContextObject);
break;
}
case TOKEN_CONN_AUTH: { // 子连接身份校验【L】
// 设计取舍:
// - 主连接走 TOKEN_LOGIN不进入此分支。
// - 当前阶段宽容:未通过的子连接仍允许后续命令(依靠 IP 反查兜底),
// 仅把 IsAuthenticated 标志记下来,便于后续命令优先用。
// - 失败时回应 ConnAuthAck 让客户端 fail fast但不主动断连接
// (客户端拒绝继续 = 自己关闭,等价效果,少一次 RST 噪音)。
ConnAuthAck ack = {};
ack.token = TOKEN_CONN_AUTH;
ack.serverTime = (uint64_t)time(0);
ack.status = CONN_AUTH_INTERNAL_ERROR;
if (len < (int)sizeof(ConnAuthPacket)) {
ack.status = CONN_AUTH_BAD_SIZE;
Mprintf("[ConnAuth] %s: 包长度不足 (%d < %zu)\n",
ContextObject->GetPeerName().c_str(), len, sizeof(ConnAuthPacket));
} else {
const ConnAuthPacket* pkt = (const ConnAuthPacket*)szBuffer;
int64_t skew = std::abs((int64_t)time(0) - (int64_t)pkt->timestamp);
if (skew > CONN_AUTH_TIMESTAMP_TOLERANCE_SEC) {
ack.status = CONN_AUTH_CLOCK_SKEW;
Mprintf("[ConnAuth] %s: 时钟偏差 %lld 秒,拒绝\n",
ContextObject->GetPeerName().c_str(), skew);
} else {
BYTE sigInput[8 + 8 + 16];
memcpy(sigInput, &pkt->clientID, 8);
memcpy(sigInput + 8, &pkt->timestamp, 8);
memcpy(sigInput + 16, pkt->nonce, 16);
bool verifyMessage(const std::string& publicKey, BYTE* msg, int len, const std::string& signature);
std::string sig(pkt->signature, pkt->signature + 64);
if (verifyMessage("", sigInput, sizeof(sigInput), sig)) {
// 通过:把 clientID 钉在子连接 ctx 上
ContextObject->SetID(pkt->clientID);
ContextObject->SetAuthenticated(true);
ack.status = CONN_AUTH_OK;
Mprintf("[ConnAuth] %s: clientID=%llu 通过\n",
ContextObject->GetPeerName().c_str(), pkt->clientID);
} else {
ack.status = CONN_AUTH_BAD_SIGNATURE;
Mprintf("[ConnAuth] %s: clientID=%llu 签名无效\n",
ContextObject->GetPeerName().c_str(), pkt->clientID);
}
}
}
ContextObject->Send2Client((PBYTE)&ack, sizeof(ack));
break;
}
case TOKEN_BITMAPINFO: { // 远程桌面【x】
ContextObject->SetNoDelay(TRUE);
ContextObject->EnableZstdContext(-1);
@@ -5694,17 +5741,17 @@ context* CMy2015RemoteDlg::GetContextByListIndex(int iItem)
return m_HostList[realIdx];
}
// 从 m_HostList 中移除 context 并更新索引映射
// 从 m_HostList 中移除 context 并更新索引映射
//
// 重要MarkDeviceOffline / m_ActiveWndW.erase 等"主机下线"副作用必须**只在
// 确实从 m_HostList 移除后**才触发。否则在子连接auth 通过后 ctx->GetClientID()
// 等于主连接 ID正常断开时会误把主连接当成下线造成 Web 端"假下线"和
// 活动窗口缓存被清空。
bool CMy2015RemoteDlg::RemoveFromHostList(context* ctx)
{
if (!ctx) return false;
uint64_t clientID = ctx->GetClientID();
// 通知 Web 服务(批量通知,由定时器触发)
if (WebService().IsRunning()) {
WebService().MarkDeviceOffline(clientID);
}
// 清理"活动窗口"列的宽字符旁路表
m_ActiveWndW.erase(clientID);
bool removed = false;
// 方案1通过索引快速查找如果索引有效且匹配
auto indexIt = m_ClientIndex.find(clientID);
@@ -5721,29 +5768,40 @@ bool CMy2015RemoteDlg::RemoveFromHostList(context* ctx)
m_ClientIndex[c->GetClientID()] = i;
}
}
return true;
removed = true;
}
}
// 方案2索引不存在或不匹配遍历查找处理重复 ID 的情况)
for (size_t i = 0; i < m_HostList.size(); ++i) {
if (m_HostList[i] == ctx) {
m_HostList.erase(m_HostList.begin() + i);
// 如果索引指向的是被删除的元素,也删除索引
if (indexIt != m_ClientIndex.end() && indexIt->second == i) {
m_ClientIndex.erase(indexIt);
}
// 更新后续元素的索引
for (size_t j = i; j < m_HostList.size(); ++j) {
context* c = m_HostList[j];
if (c) {
m_ClientIndex[c->GetClientID()] = j;
if (!removed) {
for (size_t i = 0; i < m_HostList.size(); ++i) {
if (m_HostList[i] == ctx) {
m_HostList.erase(m_HostList.begin() + i);
// 如果索引指向的是被删除的元素,也删除索引
if (indexIt != m_ClientIndex.end() && indexIt->second == i) {
m_ClientIndex.erase(indexIt);
}
// 更新后续元素的索引
for (size_t j = i; j < m_HostList.size(); ++j) {
context* c = m_HostList[j];
if (c) {
m_ClientIndex[c->GetClientID()] = j;
}
}
removed = true;
break;
}
return true;
}
}
return false;
// 副作用:仅在主连接真的从列表中移除时才触发,避免子连接断开误伤主连接的状态。
if (removed) {
if (WebService().IsRunning()) {
WebService().MarkDeviceOffline(clientID);
}
m_ActiveWndW.erase(clientID);
}
return removed;
}
LRESULT CMy2015RemoteDlg::OnUserOfflineMsg(WPARAM wParam, LPARAM lParam)