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:
@@ -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,包透传给 manager(manager 一般也不识别此 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);
|
||||
|
||||
Reference in New Issue
Block a user