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

@@ -100,6 +100,77 @@ VOID IOCPClient::setManagerCallBack(void* Manager, DataProcessCB dataProcess, O
m_ReconnectFunc = m_exit_while_disconnect ? reconnect : NULL;
}
// 子连接身份校验:发 TOKEN_CONN_AUTH 包后阻塞等服务端响应。
// signMessage 由私有库提供(与 KernelManager.cpp 验证主控签名同款),
// 空 publicKey/privateKey 走内置 HMAC。
extern std::string signMessage(const std::string& privateKey, BYTE* msg, int len);
bool IOCPClient::PerformConnAuth(uint64_t clientID, int timeoutMs)
{
ConnAuthPacket pkt = {};
pkt.token = TOKEN_CONN_AUTH;
pkt.clientID = clientID;
pkt.timestamp = (uint64_t)time(NULL);
// 16 字节 nonce用 rand() + 时间扰动,强度够用(重放保护主要靠时间戳)
for (int i = 0; i < 16; ++i) {
pkt.nonce[i] = (uint8_t)((rand() ^ (clock() >> i)) & 0xFF);
}
BYTE sigInput[8 + 8 + 16];
memcpy(sigInput, &pkt.clientID, 8);
memcpy(sigInput + 8, &pkt.timestamp, 8);
memcpy(sigInput + 16, pkt.nonce, 16);
auto sig = signMessage("", sigInput, sizeof(sigInput));
size_t sigLen = sig.size() < 64 ? sig.size() : 64;
memcpy(pkt.signature, sig.data(), sigLen);
// 设置等待状态
{
std::lock_guard<std::mutex> lk(m_authMtx);
m_authStatus = -1;
m_authPending = true;
}
// 发包;用 HttpMask 包装与其它子连接首包风格一致
HttpMask mask(DEFAULT_HOST, GetClientIPHeader());
int sent = Send2Server((char*)&pkt, sizeof(pkt), &mask);
if (sent <= 0) {
std::lock_guard<std::mutex> lk(m_authMtx);
m_authPending = false;
Mprintf("[ConnAuth] 发送失败\n");
return false;
}
// 等响应或超时
std::unique_lock<std::mutex> lk(m_authMtx);
bool got = m_authCv.wait_for(lk, std::chrono::milliseconds(timeoutMs),
[this]{ return !m_authPending; });
int status = m_authStatus;
m_authPending = false;
if (!got) {
Mprintf("[ConnAuth] 等待响应超时 (%d ms),判定失败\n", timeoutMs);
return false;
}
bool ok = (status == CONN_AUTH_OK);
Mprintf("[ConnAuth] %s (status=%d)\n", ok ? "通过" : "失败", status);
return ok;
}
bool IOCPClient::TryHandleAuthResponse(PBYTE buf, ULONG len)
{
if (!buf || len < sizeof(ConnAuthAck)) return false;
if (buf[0] != TOKEN_CONN_AUTH) return false;
{
std::lock_guard<std::mutex> lk(m_authMtx);
if (!m_authPending) return false; // 没在等 → 不消费,让 manager 处理(理论不会发生)
const ConnAuthAck* ack = (const ConnAuthAck*)buf;
m_authStatus = ack->status;
m_authPending = false;
}
m_authCv.notify_all();
return true;
}
IOCPClient::IOCPClient(const State&bExit, bool exit_while_disconnect, int mask, CONNECT_ADDRESS* conn,
const std::string& pubIP, void* main) : g_bExit(bExit)
@@ -119,6 +190,8 @@ IOCPClient::IOCPClient(const State&bExit, bool exit_while_disconnect, int mask,
}
m_main = main;
m_conn = conn; // 保存 CONNECT_ADDRESS 指针。子连接 auth 在每次连接时通过
// m_conn->clientID 现取主连接 ID同一指针主连接登录后填好的最新值
int encoder = conn ? conn->GetHeaderEncType() : 0;
m_sLocPublicIP = pubIP;
m_ServerAddr = {};
@@ -380,6 +453,27 @@ BOOL IOCPClient::ConnectServer(const char* szServerIP, unsigned short uPort)
#endif
}
// 子连接身份校验opt-in 通过 EnableSubConnAuth 开启):
// - WorkThread 已经启动,能接收 ack 包并通过 TryHandleAuthResponse 唤醒等待。
// - clientID 优先用 EnableSubConnAuth 显式传入的值Linux/macOS 客户端走此路径),
// 未显式传入时从 m_conn 现取Windows 客户端走此路径)。
// - 校验失败Disconnect 并返回 FALSE让上层走重连或放弃逻辑。
if (m_subConnAuthEnabled) {
uint64_t cid = m_subConnAuthClientID;
if (cid == 0 && m_conn) cid = m_conn->clientID;
if (cid == 0) {
Mprintf("[ConnAuth] 跳过校验clientID 尚未就绪(主连接还没拿到 ID\n");
// 没拿到 ID 就别盲发,等下一次 Reconnect 时再试。视为本次连接失败。
Disconnect();
return FALSE;
}
if (!PerformConnAuth(cid, CONN_AUTH_CLIENT_WAIT_MS)) {
Mprintf("[ConnAuth] 校验失败,断开连接\n");
Disconnect();
return FALSE;
}
}
return TRUE;
}
@@ -549,11 +643,16 @@ VOID IOCPClient::OnServerReceiving(CBuffer* m_CompressedBuffer, char* szBuffer,
size_t iRet = uncompress(DeCompressedBuffer, &ulOriginalLength, CompressedBuffer, ulCompressedLength);
if (Z_SUCCESS(iRet)) { //如果解压成功
//解压好的数据和长度传递给对象Manager进行处理 注意这里是用了多态
//由于m_pManager中的子类不一样造成调用的OnReceive函数不一样
int ret = DataProcessWithSEH(m_DataProcess, m_Manager, DeCompressedBuffer, ulOriginalLength);
if (ret) {
Mprintf("[ERROR] DataProcessWithSEH return exception code: [0x%08X]\n", ret);
// 优先看是不是 TOKEN_CONN_AUTH 响应;只有当 PerformConnAuth 正在等待时才消费。
// 不在等待状态时返回 false包透传给 managermanager 一般也不识别此 token
// 走 default 路径忽略,无副作用)。
if (!TryHandleAuthResponse(DeCompressedBuffer, ulOriginalLength)) {
//解压好的数据和长度传递给对象Manager进行处理 注意这里是用了多态
//由于m_pManager中的子类不一样造成调用的OnReceive函数不一样
int ret = DataProcessWithSEH(m_DataProcess, m_Manager, DeCompressedBuffer, ulOriginalLength);
if (ret) {
Mprintf("[ERROR] DataProcessWithSEH return exception code: [0x%08X]\n", ret);
}
}
} else {
Mprintf("[ERROR] uncompress fail: dstLen %lu, srcLen %lu\n", ulOriginalLength, ulCompressedLength);