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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user