Feat: One-way TLS transport for the MFC server
Add optional one-way TLS to the MFC server (server/2015Remote) so the device port can serve both TLS clients and legacy plaintext clients on the same port, mirroring the Go server. Server: on a connection's first bytes, sniff for a TLS ClientHello (0x16 0x03 + handshake type 0x01). When [tls] Enabled=1 and the identity loads, hand the socket to a short-lived blocking handshake thread (Option A) running Schannel AcceptSecurityContext, then decrypt on recv and encrypt on send; plaintext connections replay the sniffed bytes straight into the existing parse path, so the plaintext code path is otherwise unchanged. The identity is loaded once from [tls] CertPath/KeyPath as PEM + CNG (PKCS#8) — the same key pair the Go server uses — and a missing [tls] section, an empty value, or a load failure leaves plaintext serving intact and fail-closes any TLS ClientHello. Also fix two transport-reliability bugs that surfaced end-to-end: - send path: encrypt only the freshly appended plaintext tail and post a single IOWrite wake-up guarded by a bSendPending flag, so back-to-back small sends (terminal resize -> COMMAND_NEXT -> resize) no longer run concurrent WSASends over the same mutable buffer (client saw DecryptMessage failures, with WSAGetLastError() = 6001 / ERROR_DECRYPTION_FAILED); - client Recv: keep the handshake's unconsumed tail bytes in m_raw and drain them before the next recv, and log the real SECURITY_STATUS on DecryptMessage failure instead of the stale Winsock error. Co-Authored-By: deepseek-v4-pro
This commit is contained in:
1 parent
ca0d7626d2
commit
96b9eea70a
16 files changed
+1298
-38
No files matched your search
@@ -350,6 +350,7 @@
|
||||
<ClInclude Include="ScreenSpyDlg.h" />
|
||||
<ClInclude Include="SearchBarDlg.h" />
|
||||
<ClInclude Include="Server.h" />
|
||||
<ClInclude Include="ServerTlsSession.h" />
|
||||
<ClInclude Include="ServicesDlg.h" />
|
||||
<ClInclude Include="ConnectionsDlg.h" />
|
||||
<ClInclude Include="SettingDlg.h" />
|
||||
@@ -457,6 +458,7 @@
|
||||
<ClCompile Include="ZstaPickerDlg.cpp" />
|
||||
<ClCompile Include="IOCPKCPServer.cpp" />
|
||||
<ClCompile Include="IOCPServer.cpp" />
|
||||
<ClCompile Include="ServerTlsSession.cpp" />
|
||||
<ClCompile Include="IOCPUDPServer.cpp" />
|
||||
<ClCompile Include="IPHistoryDlg.cpp" />
|
||||
<ClCompile Include="KeyBoardDlg.cpp" />
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<ClCompile Include="InputDlg.cpp" />
|
||||
<ClCompile Include="ZstaPickerDlg.cpp" />
|
||||
<ClCompile Include="IOCPServer.cpp" />
|
||||
<ClCompile Include="ServerTlsSession.cpp" />
|
||||
<ClCompile Include="KeyBoardDlg.cpp" />
|
||||
<ClCompile Include="Loader.c" />
|
||||
<ClCompile Include="proxy\ProxyConnectServer.cpp" />
|
||||
@@ -155,6 +156,7 @@
|
||||
</ClInclude>
|
||||
<ClInclude Include="IOCPUDPServer.h" />
|
||||
<ClInclude Include="Server.h" />
|
||||
<ClInclude Include="ServerTlsSession.h" />
|
||||
<ClInclude Include="CDrawingBoard.h" />
|
||||
<ClInclude Include="IOCPKCPServer.h" />
|
||||
<ClInclude Include="..\..\common\ikcp.h" />
|
||||
|
||||
@@ -337,3 +337,20 @@ VOID CBuffer::CommitWrite(ULONG writtenSize)
|
||||
m_Ptr += writtenSize;
|
||||
LeaveCriticalSection(&m_cs);
|
||||
}
|
||||
|
||||
// 截断缓冲区到指定有效长度(仅回退写指针,不移动/释放底层内存)。有效长度指「当前读取
|
||||
// 位置之后的字节数」,与 GetBufferLength 一致;请求值超出现有有效长度时收敛到现有长度。
|
||||
VOID CBuffer::TruncateBuffer(ULONG effectiveLen)
|
||||
{
|
||||
EnterCriticalSection(&m_cs);
|
||||
if (m_Base == NULL) {
|
||||
LeaveCriticalSection(&m_cs);
|
||||
return;
|
||||
}
|
||||
ULONG totalDataLen = (ULONG)(m_Ptr - m_Base);
|
||||
ULONG cur = (totalDataLen > m_ulReadOffset) ? (totalDataLen - m_ulReadOffset) : 0;
|
||||
if (effectiveLen > cur)
|
||||
effectiveLen = cur;
|
||||
m_Ptr = m_Base + m_ulReadOffset + effectiveLen;
|
||||
LeaveCriticalSection(&m_cs);
|
||||
}
|
||||
@@ -130,6 +130,11 @@ public:
|
||||
// 确认写入完成,更新内部指针
|
||||
VOID CommitWrite(ULONG writtenSize);
|
||||
|
||||
// 截断缓冲区到指定有效长度(仅回退写指针,不移动/释放底层内存)。用于「尾部明文替换为
|
||||
// 密文」:TLS 下 OutCompressedBuffer 追加明文后需换回密文,用此接口避免 ClearBuffer 的
|
||||
// DeAllocateBuffer 释放正在被异步 WSASend 引用的底层内存。
|
||||
VOID TruncateBuffer(ULONG effectiveLen);
|
||||
|
||||
protected:
|
||||
PBYTE m_Base;
|
||||
PBYTE m_Ptr;
|
||||
|
||||
@@ -124,7 +124,7 @@ void IOCPKCPServer::WorkerThread()
|
||||
}
|
||||
if (n > 0&& m_notify) {
|
||||
memcpy(ctx->szBuffer, recvbuf, n);
|
||||
BOOL ret = ParseReceivedData(ctx, n, m_notify);
|
||||
BOOL ret = ParseReceivedData(ctx, (PBYTE)ctx->szBuffer, n, m_notify);
|
||||
}
|
||||
} while (n>0);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "2015Remote.h"
|
||||
#include "common/IPWhitelist.h"
|
||||
#include "common/IPBlacklist.h"
|
||||
#include "ServerTlsSession.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <ws2tcpip.h>
|
||||
@@ -483,6 +484,11 @@ UINT IOCPServer::StartServer(pfnNotifyProc NotifyProc, pfnOfflineProc OffProc, U
|
||||
m_nPort = uPort;
|
||||
m_NotifyProc = NotifyProc;
|
||||
m_OfflineProc = OffProc;
|
||||
|
||||
// 单向 TLS:启动时加载服务端身份(证书+私钥)。加载失败时 IsLoadError()==true,
|
||||
// 明文客户端不受影响,TLS ClientHello 会被识别并 fail-closed 关闭(见设计文档 §5.2)。
|
||||
ServerTlsIdentity::Load(THIS_CFG);
|
||||
|
||||
// manual-reset:本进程内可能有多个等待者(ListenThread / RttPollThreadProc)。
|
||||
// 自动重置会让 SetEvent 只唤醒一个等待者,另一个要等自身 timeout(≤1s)。
|
||||
// 改 manual-reset 后所有等待者一次性醒来;本工程从无 ResetEvent 调用,无副作用。
|
||||
@@ -793,16 +799,16 @@ BOOL IOCPServer::OnClientInitializing(PCONTEXT_OBJECT ContextObject, DWORD dwTr
|
||||
}
|
||||
|
||||
// May be this function should be a member of `CONTEXT_OBJECT`.
|
||||
BOOL ParseReceivedData(CONTEXT_OBJECT * ContextObject, DWORD dwTrans, pfnNotifyProc m_NotifyProc, ZSTD_DCtx* m_Dctx, z_stream* z)
|
||||
BOOL ParseReceivedData(CONTEXT_OBJECT * ContextObject, PBYTE data, DWORD dataLen, pfnNotifyProc m_NotifyProc, ZSTD_DCtx* m_Dctx, z_stream* z)
|
||||
{
|
||||
AUTO_TICK(50, ContextObject->GetPeerName());
|
||||
BOOL ret = 1;
|
||||
try {
|
||||
if (dwTrans == 0) { //对方关闭了套接字
|
||||
if (dataLen == 0) { //对方关闭了套接字
|
||||
return FALSE;
|
||||
}
|
||||
//将接收到的数据拷贝到我们自己的内存中wsabuff 8192
|
||||
ContextObject->InCompressedBuffer.WriteBuffer((PBYTE)ContextObject->szBuffer,dwTrans);
|
||||
ContextObject->InCompressedBuffer.WriteBuffer(data, dataLen);
|
||||
//查看数据包的完整性
|
||||
while (true) {
|
||||
PR pr = ContextObject->Parse(ContextObject->InCompressedBuffer);
|
||||
@@ -889,18 +895,178 @@ BOOL ParseReceivedData(CONTEXT_OBJECT * ContextObject, DWORD dwTrans, pfnNotifyP
|
||||
return ret;
|
||||
}
|
||||
|
||||
BOOL IOCPServer::OnClientReceiving(PCONTEXT_OBJECT ContextObject, DWORD dwTrans, ZSTD_DCtx* ctx, z_stream* z)
|
||||
// 握手线程参数:把嗅探阶段已收到的 ClientHello 字节与连接上下文一起移交。
|
||||
struct TlsHandshakeParam {
|
||||
IOCPServer* server;
|
||||
CONTEXT_OBJECT* ctx;
|
||||
std::vector<BYTE> initial; // 嗅探阶段已攒下的首批字节(≥6,可能只是 ClientHello 分片)
|
||||
};
|
||||
|
||||
// 方案 A:专用阻塞握手线程。占线期间独占 socket(阻塞 recv/send,无未决 overlapped I/O),
|
||||
// 完成后把连接交还给 IOCP(PostRecv)。线程持有 ctx 的一枚 IoRefCount,结束时释放。
|
||||
DWORD WINAPI IOCPServer::TlsHandshakeThreadProc(LPVOID lParam)
|
||||
{
|
||||
if (FALSE == ParseReceivedData(ContextObject, dwTrans, m_NotifyProc, ctx, z)) {
|
||||
// 先减少引用计数,再调用 RemoveStaleContext (#215)
|
||||
// RemoveStaleContext 完成后对象会被移到空闲池,不能再访问
|
||||
ContextObject->IoRefCount.fetch_sub(1);
|
||||
RemoveStaleContext(ContextObject);
|
||||
return -2; // 特殊返回值:告诉 HandleIO 不要再减少引用计数
|
||||
TlsHandshakeParam* p = (TlsHandshakeParam*)lParam;
|
||||
IOCPServer* This = p->server;
|
||||
CONTEXT_OBJECT* ctx = p->ctx;
|
||||
std::vector<BYTE> initial = std::move(p->initial);
|
||||
delete p;
|
||||
|
||||
// 若上下文已被移除(服务端关停竞态),直接释放引用退出。
|
||||
if (ctx->IsRemoved.load(std::memory_order_acquire)) {
|
||||
ctx->IoRefCount.fetch_sub(1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
PostRecv(ContextObject); //投递新的接收数据的请求
|
||||
// 握手期间做同步 recv/send。注意:监听套接字经 WSAEventSelect 置为非阻塞后,accept 出的
|
||||
// 子套接字继承非阻塞态,FIONBIO=0 不一定能切回阻塞;ServerTlsSession::Handshake 内部已用
|
||||
// select 兜底 WSAEWOULDBLOCK,故这里仍设 FIONBIO=0 只是「尽量阻塞」的优化。
|
||||
u_long nb = 0;
|
||||
if (ioctlsocket(ctx->sClientSocket, FIONBIO, &nb) == SOCKET_ERROR)
|
||||
Mprintf("[TLS] handshake: ioctlsocket(FIONBIO=0) failed err=%d\n", WSAGetLastError());
|
||||
int tmo = 15000; // 15s 握手超时
|
||||
setsockopt(ctx->sClientSocket, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tmo, sizeof(tmo));
|
||||
setsockopt(ctx->sClientSocket, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tmo, sizeof(tmo));
|
||||
|
||||
bool ok = (ctx->tls != NULL) &&
|
||||
ctx->tls->Handshake(ctx->sClientSocket, initial.data(), (int)initial.size());
|
||||
|
||||
if (ok) {
|
||||
ctx->tlsState.store(TLS_Ready, std::memory_order_release);
|
||||
}
|
||||
|
||||
// 先释放握手线程持有的引用,再触发任何可能 RemoveStaleContext 的动作(#215 顺序)。
|
||||
ctx->IoRefCount.fetch_sub(1);
|
||||
|
||||
if (ok) {
|
||||
This->PostRecv(ctx); // 交还 IOCP 接收
|
||||
} else {
|
||||
Mprintf("[TLS] handshake failed for %s (fail-closed)\n", ctx->GetPeerName().c_str());
|
||||
This->RemoveStaleContext(ctx);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 判定为 TLS 后:创建会话、预支一枚 IoRefCount 给握手线程、启动握手线程。
|
||||
BOOL IOCPServer::StartTlsHandshake(CONTEXT_OBJECT* ContextObject)
|
||||
{
|
||||
if (!ServerTlsIdentity::IsReady()) {
|
||||
// 服务端身份不可用(未启用 TLS、或证书/私钥加载失败):fail-closed 关闭该连接。
|
||||
Mprintf("[TLS] client %s sent ClientHello but server identity not ready "
|
||||
"(TLS disabled or cert/key load failed); closing (fail-closed)\n",
|
||||
ContextObject->GetPeerName().c_str());
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
ContextObject->tls = new ServerTlsSession();
|
||||
TlsHandshakeParam* p = new TlsHandshakeParam;
|
||||
p->server = this;
|
||||
p->ctx = ContextObject;
|
||||
p->initial = std::move(ContextObject->tlsPreClassify);
|
||||
ContextObject->tlsPreClassify.clear();
|
||||
|
||||
// 预支引用:握手线程结束时释放。此引用保证握手期间对象不被回收。
|
||||
ContextObject->IoRefCount.fetch_add(1);
|
||||
|
||||
HANDLE h = CreateThread(NULL, 0, TlsHandshakeThreadProc, p, 0, NULL);
|
||||
if (h == NULL) {
|
||||
// 启动失败:回退预支引用,清理会话,按失败关闭。
|
||||
ContextObject->IoRefCount.fetch_sub(1);
|
||||
delete ContextObject->tls;
|
||||
ContextObject->tls = NULL;
|
||||
return FALSE;
|
||||
}
|
||||
CloseHandle(h);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
BOOL IOCPServer::OnClientReceiving(PCONTEXT_OBJECT ContextObject, DWORD dwTrans, ZSTD_DCtx* ctx, z_stream* z)
|
||||
{
|
||||
int st = ContextObject->tlsState.load(std::memory_order_acquire);
|
||||
|
||||
// ---- 未分类:按 ClientHello 头嗅探明文 / TLS(见设计文档 §5.2)----
|
||||
if (st == TLS_Unclassified) {
|
||||
if (dwTrans == 0) { // 首个包即对端关闭
|
||||
ContextObject->IoRefCount.fetch_sub(1);
|
||||
RemoveStaleContext(ContextObject);
|
||||
return -2;
|
||||
}
|
||||
ContextObject->tlsPreClassify.insert(ContextObject->tlsPreClassify.end(),
|
||||
(BYTE*)ContextObject->szBuffer,
|
||||
(BYTE*)ContextObject->szBuffer + dwTrans);
|
||||
if (ContextObject->tlsPreClassify.size() < 6) {
|
||||
PostRecv(ContextObject); // 头还没攒够,继续收
|
||||
return TRUE;
|
||||
}
|
||||
const BYTE* h = ContextObject->tlsPreClassify.data();
|
||||
bool isTls = (h[0] == 0x16 && h[1] == 0x03 && h[5] == 0x01);
|
||||
if (isTls) {
|
||||
ContextObject->tlsState.store(TLS_Handshaking, std::memory_order_release);
|
||||
if (!StartTlsHandshake(ContextObject)) {
|
||||
Mprintf("[TLS] StartTlsHandshake failed for %s (fail-closed)\n",
|
||||
ContextObject->GetPeerName().c_str());
|
||||
ContextObject->IoRefCount.fetch_sub(1);
|
||||
RemoveStaleContext(ContextObject);
|
||||
return -2;
|
||||
}
|
||||
return TRUE; // 不 PostRecv;握手线程完成后交还接收
|
||||
}
|
||||
|
||||
// 明文:把嗅探阶段攒下的字节回放给现有解析路径,然后清空缓冲。
|
||||
ContextObject->tlsState.store(TLS_Plain, std::memory_order_release);
|
||||
std::vector<BYTE> replay = std::move(ContextObject->tlsPreClassify);
|
||||
ContextObject->tlsPreClassify.clear();
|
||||
if (FALSE == ParseReceivedData(ContextObject, replay.data(), (DWORD)replay.size(), m_NotifyProc, ctx, z)) {
|
||||
ContextObject->IoRefCount.fetch_sub(1);
|
||||
RemoveStaleContext(ContextObject);
|
||||
return -2;
|
||||
}
|
||||
PostRecv(ContextObject);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// ---- 已分类 ----
|
||||
if (st == TLS_Ready) {
|
||||
if (dwTrans == 0) { // 明文 FIN:TLS 连接对端关闭
|
||||
ContextObject->IoRefCount.fetch_sub(1);
|
||||
RemoveStaleContext(ContextObject);
|
||||
return -2;
|
||||
}
|
||||
std::vector<BYTE> plain;
|
||||
if (ContextObject->tls->Decrypt((const BYTE*)ContextObject->szBuffer, dwTrans, plain) < 0) {
|
||||
ContextObject->IoRefCount.fetch_sub(1);
|
||||
RemoveStaleContext(ContextObject);
|
||||
return -2;
|
||||
}
|
||||
if (plain.empty()) {
|
||||
PostRecv(ContextObject); // 本轮无完整明文(TLS 记录分片/握手告警),继续收
|
||||
return TRUE;
|
||||
}
|
||||
if (FALSE == ParseReceivedData(ContextObject, plain.data(), (DWORD)plain.size(), m_NotifyProc, ctx, z)) {
|
||||
ContextObject->IoRefCount.fetch_sub(1);
|
||||
RemoveStaleContext(ContextObject);
|
||||
return -2;
|
||||
}
|
||||
PostRecv(ContextObject);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// TLS_Handshaking:握手线程独占 socket,正常不应出现 IORead;若出现属异常,fail-closed。
|
||||
if (st == TLS_Handshaking) {
|
||||
Mprintf("[TLS] unexpected IORead during handshake for %s (closing)\n",
|
||||
ContextObject->GetPeerName().c_str());
|
||||
ContextObject->IoRefCount.fetch_sub(1);
|
||||
RemoveStaleContext(ContextObject);
|
||||
return -2;
|
||||
}
|
||||
|
||||
// TLS_Plain:明文直通(原路径)。
|
||||
if (FALSE == ParseReceivedData(ContextObject, (PBYTE)ContextObject->szBuffer, dwTrans, m_NotifyProc, ctx, z)) {
|
||||
ContextObject->IoRefCount.fetch_sub(1);
|
||||
RemoveStaleContext(ContextObject);
|
||||
return -2;
|
||||
}
|
||||
PostRecv(ContextObject);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -967,15 +1133,49 @@ BOOL WriteContextData(CONTEXT_OBJECT* ContextObject, PBYTE szBuffer, size_t ulOr
|
||||
|
||||
BOOL IOCPServer::OnClientPreSending(CONTEXT_OBJECT* ContextObject, PBYTE szBuffer, size_t ulOriginalLength)
|
||||
{
|
||||
// TLS 下 OutCompressedBuffer 可能还残留上一轮尚未发完的密文(IOCP 线程异步 WSASend 中),
|
||||
// 新明文由 WriteContextData 追加在尾部。若把整个缓冲区重新加密,会把旧密文二次加密,
|
||||
// 客户端解密后得到的是「旧 TLS 记录」→ Unknown header 17 03 03。故只加密本次新追加的尾部。
|
||||
bool tlsReady = (ContextObject->tlsState.load(std::memory_order_acquire) == TLS_Ready);
|
||||
ULONG oldLen = tlsReady ? ContextObject->OutCompressedBuffer.GetBufferLength() : 0;
|
||||
|
||||
if (WriteContextData(ContextObject, szBuffer, ulOriginalLength, ContextObject->Zcctx)) {
|
||||
OVERLAPPEDPLUS* OverlappedPlus = new OVERLAPPEDPLUS(IOWrite);
|
||||
BOOL bOk = PostQueuedCompletionStatus(m_hCompletionPort, 0, (ULONG_PTR)ContextObject, &OverlappedPlus->m_ol);
|
||||
if ( (!bOk && GetLastError() != ERROR_IO_PENDING) ) { //如果投递失败
|
||||
int a = GetLastError();
|
||||
Mprintf("!!! OnClientPreSending 投递消息失败\n");
|
||||
RemoveStaleContext(ContextObject);
|
||||
SAFE_DELETE(OverlappedPlus);
|
||||
return FALSE;
|
||||
// 单向 TLS:只加密新追加的明文尾部,产出密文后与前缀旧密文拼接,使缓冲区始终保持
|
||||
// 一串合法 TLS 记录;之后 IOWrite / OnClientPostSending 完全不变(见设计文档 §5.5)。
|
||||
if (tlsReady) {
|
||||
ULONG totalLen = ContextObject->OutCompressedBuffer.GetBufferLength();
|
||||
ULONG plainLen = totalLen - oldLen;
|
||||
if (plainLen > 0) {
|
||||
std::vector<BYTE> wire;
|
||||
if (ContextObject->tls->Encrypt((const BYTE*)ContextObject->OutCompressedBuffer.GetBuffer(oldLen),
|
||||
(int)plainLen, wire) != (int)plainLen) {
|
||||
Mprintf("[TLS] Encrypt failed for %s (fail-closed)\n",
|
||||
ContextObject->GetPeerName().c_str());
|
||||
return FALSE; // 加密失败:丢弃本次发送
|
||||
}
|
||||
// 截断尾部明文,再追加密文。用 TruncateBuffer 而非 ClearBuffer:ClearBuffer 会
|
||||
// DeAllocateBuffer(可能 VirtualFree 正在被异步 WSASend 引用的底层内存)。
|
||||
ContextObject->OutCompressedBuffer.TruncateBuffer(oldLen);
|
||||
ContextObject->OutCompressedBuffer.WriteBuffer(wire.data(), (DWORD)wire.size());
|
||||
}
|
||||
}
|
||||
|
||||
// 仅当没有发送正在进行时才投递 IOWrite 唤醒发送。若已有 WSASend 在途,其完成回调
|
||||
// (OnClientPostSending)会看到排空后仍残留的新数据并继续发送,无需再投递;否则会对
|
||||
// 同一缓冲并发多个 WSASend,读到同一段可变内存,产出重复/错位密文(客户端 DecryptMessage
|
||||
// 失败,表现为 recv -1、GetLastError=6001)。
|
||||
if (!ContextObject->bSendPending) {
|
||||
ContextObject->bSendPending = true;
|
||||
OVERLAPPEDPLUS* OverlappedPlus = new OVERLAPPEDPLUS(IOWrite);
|
||||
BOOL bOk = PostQueuedCompletionStatus(m_hCompletionPort, 0, (ULONG_PTR)ContextObject, &OverlappedPlus->m_ol);
|
||||
if ( (!bOk && GetLastError() != ERROR_IO_PENDING) ) { //如果投递失败
|
||||
int a = GetLastError();
|
||||
Mprintf("!!! OnClientPreSending 投递消息失败\n");
|
||||
ContextObject->bSendPending = false;
|
||||
RemoveStaleContext(ContextObject);
|
||||
SAFE_DELETE(OverlappedPlus);
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
@@ -990,6 +1190,7 @@ BOOL IOCPServer::OnClientPostSending(CONTEXT_OBJECT* ContextObject,ULONG ulCompl
|
||||
|
||||
ContextObject->OutCompressedBuffer.RemoveCompletedBuffer(ulCompletedLength); //将完成的数据从数据结构中去除
|
||||
if (ContextObject->OutCompressedBuffer.GetBufferLength() == 0) {
|
||||
ContextObject->bSendPending = false; // 缓冲排空,允许下一次 OnClientPreSending 投递
|
||||
ContextObject->OutCompressedBuffer.ClearBuffer();
|
||||
return true; //走到这里说明我们的数据真正完全发送
|
||||
} else {
|
||||
@@ -1001,6 +1202,7 @@ BOOL IOCPServer::OnClientPostSending(CONTEXT_OBJECT* ContextObject,ULONG ulCompl
|
||||
NULL, ulFlags,&OverlappedPlus->m_ol, NULL);
|
||||
if ( iOk == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING ) {
|
||||
// 先减少引用计数,再调用 RemoveStaleContext (#215)
|
||||
ContextObject->bSendPending = false;
|
||||
ContextObject->IoRefCount.fetch_sub(1);
|
||||
if (RemoveStaleContext(ContextObject))
|
||||
Mprintf("!!! OnClientPostSending 投递消息失败: %d\n", WSAGetLastError());
|
||||
|
||||
@@ -91,6 +91,7 @@ private:
|
||||
static DWORD WINAPI ListenThreadProc(LPVOID lParam);
|
||||
static DWORD WINAPI WorkThreadProc(LPVOID lParam);
|
||||
static DWORD WINAPI RttPollThreadProc(LPVOID lParam);
|
||||
static DWORD WINAPI TlsHandshakeThreadProc(LPVOID lParam);
|
||||
|
||||
BOOL InitializeIOCP(VOID);
|
||||
VOID OnAccept();
|
||||
@@ -101,6 +102,7 @@ private:
|
||||
BOOL HandleIO(IOType PacketFlags, PCONTEXT_OBJECT ContextObject, DWORD dwTrans, ZSTD_DCtx* ctx, z_stream *z);
|
||||
BOOL OnClientInitializing(PCONTEXT_OBJECT ContextObject, DWORD dwTrans);
|
||||
BOOL OnClientReceiving(PCONTEXT_OBJECT ContextObject, DWORD dwTrans, ZSTD_DCtx* ctx, z_stream* z);
|
||||
BOOL StartTlsHandshake(CONTEXT_OBJECT* ContextObject);
|
||||
BOOL OnClientPreSending(CONTEXT_OBJECT* ContextObject, PBYTE szBuffer, size_t ulOriginalLength);
|
||||
BOOL OnClientPostSending(CONTEXT_OBJECT* ContextObject, ULONG ulCompressedLength);
|
||||
int AddWorkThread(int n)
|
||||
@@ -257,6 +259,8 @@ public:
|
||||
|
||||
typedef CDialogBase DialogBase;
|
||||
|
||||
BOOL ParseReceivedData(CONTEXT_OBJECT* ContextObject, DWORD dwTrans, pfnNotifyProc m_NotifyProc, ZSTD_DCtx *ctx=NULL, z_stream* z=NULL);
|
||||
// 解析一段已解密/直通的接收字节。data/dataLen 为数据源(TLS 连接传解密后的明文,
|
||||
// 明文连接传 szBuffer 原始字节);dataLen==0 视为对端关闭。
|
||||
BOOL ParseReceivedData(CONTEXT_OBJECT* ContextObject, PBYTE data, DWORD dataLen, pfnNotifyProc m_NotifyProc, ZSTD_DCtx *ctx=NULL, z_stream* z=NULL);
|
||||
|
||||
BOOL WriteContextData(CONTEXT_OBJECT* ContextObject, PBYTE szBuffer, size_t ulOriginalLength, ZSTD_CCtx *ctx=NULL, z_stream* z = NULL);
|
||||
@@ -107,7 +107,7 @@ void IOCPUDPServer::WorkerThread()
|
||||
|
||||
IO_CONTEXT* ioCtx = CONTAINING_RECORD(pOverlapped, IO_CONTEXT, ol);
|
||||
CONTEXT_UDP* ctx = ioCtx->pContext;
|
||||
BOOL ret = ParseReceivedData(ctx, bytes, m_notify);
|
||||
BOOL ret = ParseReceivedData(ctx, (PBYTE)ctx->szBuffer, bytes, m_notify);
|
||||
if (999 != ret)
|
||||
ctx->Destroy();
|
||||
|
||||
|
||||
@@ -329,6 +329,17 @@ public:
|
||||
#define PREALLOC_DECOMPRESS_SIZE (4 * 1024)
|
||||
|
||||
#include "context.h"
|
||||
|
||||
// 每连接传输层状态(单向 TLS,见 docs/TransportTLS_MFCServer_Design.md §5.2)。
|
||||
// 明文与 TLS 客户端在同一端口共存:首包按 ClientHello 头嗅探分派。
|
||||
enum ServerTlsState {
|
||||
TLS_Unclassified = 0, // 尚未收到足够字节判定(缓冲 tlsPreClassify)
|
||||
TLS_Plain = 1, // 明文客户端,直通现有解析路径
|
||||
TLS_Handshaking = 2, // TLS 客户端,握手线程阻塞 AcceptSecurityContext 中
|
||||
TLS_Ready = 3, // TLS 握手完成,收包解密 / 发包加密
|
||||
};
|
||||
class ServerTlsSession; // 定义见 ServerTlsSession.h(避免把 Schannel 头拖进本文件)
|
||||
|
||||
typedef class CONTEXT_OBJECT : public context
|
||||
{
|
||||
public:
|
||||
@@ -341,6 +352,7 @@ public:
|
||||
FreeDecompressBuffer();
|
||||
FreeCompressBuffer();
|
||||
FreeSendCompressBuffer();
|
||||
FreeTls();
|
||||
if (Zcctx) {
|
||||
ZSTD_freeCCtx(Zcctx);
|
||||
Zcctx = nullptr;
|
||||
@@ -377,6 +389,11 @@ public:
|
||||
ikcpcb* kcp = nullptr; // 新增,指向KCP会话
|
||||
std::string GroupName; // 分组名称
|
||||
CLock SendLock; // fix #214
|
||||
// 发送是否处于「进行中」状态。OnClientPreSending 每次 Send2Client 都会 Post 一个
|
||||
// IOWrite,若不加节制,OnClientPostSending(0) 会对同一 OutCompressedBuffer 并发发起
|
||||
// 多个 WSASend,读到同一段可变内存(背靠背小包最易触发:resize→COMMAND_NEXT→resize)。
|
||||
// 置位后抑制多余的 IOWrite,待缓冲排空(最后一个 WSASend 完成)再清零。仅受 SendLock 保护。
|
||||
bool bSendPending = false;
|
||||
time_t OnlineTime = 0; // 上线时间
|
||||
time_t LastHeartbeatTime = 0; // 最后心跳时间
|
||||
|
||||
@@ -392,6 +409,11 @@ public:
|
||||
// 试用版反代理逐连接检测器。仅由 IOCPServer 的 RTT 轮询线程访问,免锁。
|
||||
TcpRttBreachDetector m_RttDetector;
|
||||
|
||||
// ---- 单向 TLS 状态(跨线程:握手线程写入 TLS_Ready,工作线程读取;用 atomic 保序)----
|
||||
std::atomic<int> tlsState{ TLS_Unclassified }; // ServerTlsState
|
||||
ServerTlsSession* tls = nullptr; // 每连接 TLS 会话(握手线程独占→就绪后共用)
|
||||
std::vector<BYTE> tlsPreClassify; // 分类前缓冲的首批字节(嗅探 ClientHello 头)
|
||||
|
||||
// 子连接身份校验:客户端发 TOKEN_CONN_AUTH 通过验证后置位。
|
||||
// 主连接(走 TOKEN_LOGIN 流程)不参与此机制。当前阶段宽容(未通过也接受),
|
||||
// 仅作为标记供后续命令处理 / 未来收紧策略使用。
|
||||
@@ -495,6 +517,9 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
// 释放每连接 TLS 会话。定义在 ServerTlsSession.cpp(需 ServerTlsSession 完整类型才能 delete)。
|
||||
void FreeTls();
|
||||
|
||||
std::string GetProtocol() const override
|
||||
{
|
||||
return Parser.m_Masker && Parser.m_Masker->GetMaskType() == MaskTypeNone ? "TCP" : "HTTP";
|
||||
@@ -539,6 +564,12 @@ public:
|
||||
m_RttUs.store(0, std::memory_order_release);
|
||||
m_LastRttSampleMs.store(0, std::memory_order_release);
|
||||
m_RttDetector.Reset();
|
||||
// 复用对象池时重置 TLS 状态:释放旧会话、回到未分类、清空嗅探缓冲。
|
||||
FreeTls();
|
||||
tlsState.store(TLS_Unclassified, std::memory_order_release);
|
||||
tlsPreClassify.clear();
|
||||
// 复用对象池时清空发送进行中标志,避免上次连接残留的 bSendPending 抑制新连接的发送。
|
||||
bSendPending = false;
|
||||
}
|
||||
void SetAuthenticated(bool v) { m_bAuthenticated.store(v, std::memory_order_release); }
|
||||
bool IsAuthenticated() const { return m_bAuthenticated.load(std::memory_order_acquire); }
|
||||
|
||||
@@ -0,0 +1,604 @@
|
||||
// ServerTlsSession.cpp — 服务端单向 TLS(Schannel 入站)实现
|
||||
//
|
||||
// 与客户端 TlsChannel.cpp 对称,但走 AcceptSecurityContext 入站握手、DecryptMessage/
|
||||
// EncryptMessage 流式加解密。证书/私钥加载为 PEM + CNG(NCryptImportKey PKCS#8),
|
||||
// 以便与 Go 服务端(server/go 的 crypto/tls,YAMA_TLS_CERT/KEY)共用同一对密钥。
|
||||
#include "StdAfx.h"
|
||||
#include "ServerTlsSession.h"
|
||||
#include "Server.h" // CONTEXT_OBJECT::FreeTls()
|
||||
#include "common/iniFile.h" // config(GetInt/GetStr)
|
||||
|
||||
#include <ncrypt.h>
|
||||
#include <bcrypt.h>
|
||||
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <sstream>
|
||||
|
||||
#pragma comment(lib, "secur32.lib")
|
||||
#pragma comment(lib, "crypt32.lib")
|
||||
#pragma comment(lib, "ncrypt.lib")
|
||||
|
||||
namespace
|
||||
{
|
||||
// 进程级身份(进程启动加载一次;未来 UI/指令热切换时改为引用计数 + 惰性释放)。
|
||||
std::mutex g_identityMtx;
|
||||
bool g_enabled = false;
|
||||
bool g_ready = false;
|
||||
bool g_loadError = false;
|
||||
CredHandle g_cred = {};
|
||||
bool g_credValid = false;
|
||||
PCCERT_CONTEXT g_cert = NULL; // 与入站凭据同生命周期,FreeIdentity 时释放
|
||||
NCRYPT_KEY_HANDLE g_persistedKey = 0; // 已持久化到 CNG KSP 的私钥句柄,FreeIdentity 时删除
|
||||
|
||||
// 持久化私钥在 CNG KSP 中的名字。用固定名 + NCRYPT_OVERWRITE_KEY_FLAG,重启时覆盖旧键,
|
||||
// 避免残留;同名键的密钥材料与 server.key 相同,覆盖无副作用。
|
||||
const wchar_t* g_keyName = L"YamaServerTlsKey";
|
||||
|
||||
// 一次性读文件为 std::string(二进制安全)。
|
||||
bool ReadFileText(const std::string& path, std::string& out)
|
||||
{
|
||||
std::ifstream f(path, std::ios::binary);
|
||||
if (!f.is_open())
|
||||
return false;
|
||||
std::ostringstream ss;
|
||||
ss << f.rdbuf();
|
||||
if (f.bad())
|
||||
return false;
|
||||
out = ss.str();
|
||||
return true;
|
||||
}
|
||||
|
||||
// PEM(-----BEGIN ...----- 包裹的 base64)解码为 DER。失败返回 false。
|
||||
// CRYPT_STRING_ANY 会自动识别带头的 base64(PEM),对证书与 PKCS#8 私钥均适用。
|
||||
bool PemToDer(const std::string& pem, std::vector<BYTE>& der)
|
||||
{
|
||||
if (pem.empty())
|
||||
return false;
|
||||
DWORD need = 0;
|
||||
if (!CryptStringToBinaryA(pem.data(), (DWORD)pem.size(), CRYPT_STRING_ANY,
|
||||
NULL, &need, NULL, NULL))
|
||||
return false;
|
||||
der.resize(need);
|
||||
return CryptStringToBinaryA(pem.data(), (DWORD)pem.size(), CRYPT_STRING_ANY,
|
||||
der.data(), &need, NULL, NULL) == TRUE;
|
||||
}
|
||||
|
||||
// 从 PEM 文本里定位私钥块的类型,用于给出可操作的错误提示。
|
||||
enum KeyFmt { KF_PKCS8, KF_PKCS1, KF_UNKNOWN };
|
||||
KeyFmt DetectKeyFormat(const std::string& keyPem)
|
||||
{
|
||||
if (keyPem.find("-----BEGIN PRIVATE KEY-----") != std::string::npos)
|
||||
return KF_PKCS8;
|
||||
if (keyPem.find("-----BEGIN RSA PRIVATE KEY-----") != std::string::npos ||
|
||||
keyPem.find("-----BEGIN EC PRIVATE KEY-----") != std::string::npos)
|
||||
return KF_PKCS1;
|
||||
return KF_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// ServerTlsIdentity —— 进程级服务端身份(可重入加载,禁止 call_once,供未来热切换)
|
||||
// =====================================================================================
|
||||
|
||||
void ServerTlsIdentity::FreeIdentity()
|
||||
{
|
||||
// 先释放凭据,再释放证书上下文,最后删除持久化私钥(凭据/证书可能仍按名引用该键)。
|
||||
if (g_credValid)
|
||||
{
|
||||
FreeCredentialsHandle(&g_cred);
|
||||
g_credValid = false;
|
||||
}
|
||||
g_cred = {};
|
||||
if (g_cert)
|
||||
{
|
||||
CertFreeCertificateContext(g_cert);
|
||||
g_cert = NULL;
|
||||
}
|
||||
if (g_persistedKey)
|
||||
{
|
||||
NCryptDeleteKey(g_persistedKey, 0); // 删除并释放句柄
|
||||
g_persistedKey = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool ServerTlsIdentity::Load(config& cfg)
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_identityMtx);
|
||||
|
||||
bool enabled = cfg.GetInt("tls", "Enabled", 0) != 0;
|
||||
std::string certPath = cfg.GetStr("tls", "CertPath", "");
|
||||
std::string keyPath = cfg.GetStr("tls", "KeyPath", "");
|
||||
|
||||
g_enabled = enabled;
|
||||
|
||||
// 先释放旧身份(本轮只在启动时调用一次,不存在「旧身份仍被并发握手使用」的问题;
|
||||
// 未来热切换需先引用计数、再惰性释放旧凭据)。
|
||||
FreeIdentity();
|
||||
g_ready = false;
|
||||
g_loadError = false;
|
||||
|
||||
if (!enabled)
|
||||
{
|
||||
Mprintf("[TLS] server TLS disabled ([tls] Enabled=0) — plaintext only\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (certPath.empty() || keyPath.empty())
|
||||
{
|
||||
g_loadError = true;
|
||||
Mprintf("[TLS] FATAL: [tls] Enabled=1 but CertPath/KeyPath empty; "
|
||||
"refusing TLS ClientHello (fail-closed)\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!LoadIdentity(certPath, keyPath))
|
||||
{
|
||||
g_loadError = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
g_ready = true;
|
||||
Mprintf("[TLS] server TLS enabled: cert=%s key=%s\n", certPath.c_str(), keyPath.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ServerTlsIdentity::LoadIdentity(const std::string& certPath, const std::string& keyPath)
|
||||
{
|
||||
std::string certPem, keyPem;
|
||||
if (!ReadFileText(certPath, certPem))
|
||||
{
|
||||
Mprintf("[TLS] FATAL: cannot read cert file %s\n", certPath.c_str());
|
||||
return false;
|
||||
}
|
||||
if (!ReadFileText(keyPath, keyPem))
|
||||
{
|
||||
Mprintf("[TLS] FATAL: cannot read key file %s\n", keyPath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 1) 证书:PEM → DER → PCCERT_CONTEXT。
|
||||
std::vector<BYTE> certDer;
|
||||
if (!PemToDer(certPem, certDer))
|
||||
{
|
||||
Mprintf("[TLS] FATAL: cert file %s is not a valid PEM\n", certPath.c_str());
|
||||
return false;
|
||||
}
|
||||
PCCERT_CONTEXT cert = CertCreateCertificateContext(X509_ASN_ENCODING,
|
||||
certDer.data(), (DWORD)certDer.size());
|
||||
if (cert == NULL)
|
||||
{
|
||||
Mprintf("[TLS] FATAL: CertCreateCertificateContext failed (%u)\n", GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2) 私钥:PEM → DER → 持久化到 CNG KSP(仅支持 PKCS#8;PKCS#1 给出可操作的转换提示)。
|
||||
//
|
||||
// 关键:Schannel 入站凭据(AcquireCredentialsHandle)需要「能按名重开」的持久化私钥。
|
||||
// NCryptImportKey 产出的是匿名临时键,绑定 CERT_NCRYPT_KEY_HANDLE_PROP_ID 后 Schannel 仍
|
||||
// 拿不到(AcquireCredentialsHandle 返回 SEC_E_NO_CREDENTIALS 0x8009030e)。因此先把
|
||||
// PKCS#8 导入为临时键、导出为裸 RSA 私钥 BLOB(NCryptSetProperty 只认裸 BLOB,不认
|
||||
// PKCS#8),再写入持久化键并 finalize,最后用 CERT_KEY_PROV_INFO_PROP_ID 按名引用。
|
||||
std::vector<BYTE> keyDer;
|
||||
NCRYPT_KEY_HANDLE persistedKey = 0;
|
||||
if (DetectKeyFormat(keyPem) == KF_PKCS8)
|
||||
{
|
||||
if (!PemToDer(keyPem, keyDer))
|
||||
{
|
||||
Mprintf("[TLS] FATAL: key file %s is not a valid PEM\n", keyPath.c_str());
|
||||
CertFreeCertificateContext(cert);
|
||||
return false;
|
||||
}
|
||||
// 显式打开 Microsoft Software Key Storage Provider。hProvider 传 NULL 会让
|
||||
// NCryptImportKey 走「默认提供者」解析,实测返回 NTE_INVALID_HANDLE(0x80090026),
|
||||
// 故必须显式取得 KSP 句柄(临时键独立于 hProv,导入后即可释放)。
|
||||
NCRYPT_PROV_HANDLE hProv = 0;
|
||||
SECURITY_STATUS st = NCryptOpenStorageProvider(&hProv, MS_KEY_STORAGE_PROVIDER, 0);
|
||||
if (st != ERROR_SUCCESS)
|
||||
{
|
||||
Mprintf("[TLS] FATAL: NCryptOpenStorageProvider failed 0x%08x\n", (unsigned)st);
|
||||
CertFreeCertificateContext(cert);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2a) PKCS#8 → 临时键。
|
||||
NCRYPT_KEY_HANDLE tmp = 0;
|
||||
st = NCryptImportKey(hProv, NULL, NCRYPT_PKCS8_PRIVATE_KEY_BLOB, NULL,
|
||||
&tmp, keyDer.data(), (DWORD)keyDer.size(), 0);
|
||||
if (st == ERROR_SUCCESS)
|
||||
{
|
||||
// 2b) 允许明文导出,导出为裸 RSA 私钥 BLOB。
|
||||
DWORD policy = NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG | NCRYPT_ALLOW_EXPORT_FLAG;
|
||||
NCryptSetProperty(tmp, NCRYPT_EXPORT_POLICY_PROPERTY, (PBYTE)&policy, sizeof(policy), 0);
|
||||
|
||||
std::vector<BYTE> raw;
|
||||
DWORD rawLen = 0;
|
||||
st = NCryptExportKey(tmp, NULL, BCRYPT_RSAFULLPRIVATE_BLOB, NULL, NULL, 0, &rawLen, 0);
|
||||
if (st == ERROR_SUCCESS)
|
||||
{
|
||||
raw.resize(rawLen);
|
||||
st = NCryptExportKey(tmp, NULL, BCRYPT_RSAFULLPRIVATE_BLOB, NULL,
|
||||
raw.data(), rawLen, &rawLen, 0);
|
||||
}
|
||||
|
||||
// 2c) 写入持久化键并 finalize(落到 KSP,可被 Schannel 按名重开)。
|
||||
if (st == ERROR_SUCCESS)
|
||||
st = NCryptCreatePersistedKey(hProv, &persistedKey, BCRYPT_RSA_ALGORITHM,
|
||||
g_keyName, AT_KEYEXCHANGE, NCRYPT_OVERWRITE_KEY_FLAG);
|
||||
if (st == ERROR_SUCCESS)
|
||||
st = NCryptSetProperty(persistedKey, BCRYPT_RSAFULLPRIVATE_BLOB,
|
||||
raw.data(), (DWORD)raw.size(), 0);
|
||||
if (st == ERROR_SUCCESS)
|
||||
st = NCryptFinalizeKey(persistedKey, 0);
|
||||
|
||||
NCryptFreeObject(tmp);
|
||||
}
|
||||
NCryptFreeObject(hProv);
|
||||
|
||||
if (st != ERROR_SUCCESS)
|
||||
{
|
||||
Mprintf("[TLS] FATAL: failed to persist PKCS#8 key for %s (0x%08x)\n",
|
||||
keyPath.c_str(), (unsigned)st);
|
||||
if (persistedKey)
|
||||
NCryptFreeObject(persistedKey);
|
||||
CertFreeCertificateContext(cert);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// PKCS#1(RSA/EC PRIVATE KEY)或未知格式:本次不支持,给转换命令。
|
||||
Mprintf("[TLS] FATAL: key file %s must be PKCS#8 (-----BEGIN PRIVATE KEY-----). "
|
||||
"Convert with: openssl pkcs8 -topk8 -nocrypt -in %s -out %s\n",
|
||||
keyPath.c_str(), keyPath.c_str(), keyPath.c_str());
|
||||
CertFreeCertificateContext(cert);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3) 绑定私钥到证书上下文:按名引用持久化键。Schannel 经 CryptAcquireCertificatePrivateKey
|
||||
// 在 MS_KEY_STORAGE_PROVIDER 里用 pwszContainerName 重开该键。
|
||||
CRYPT_KEY_PROV_INFO provInfo = {};
|
||||
provInfo.pwszContainerName = (LPWSTR)g_keyName;
|
||||
provInfo.pwszProvName = (LPWSTR)MS_KEY_STORAGE_PROVIDER;
|
||||
provInfo.dwProvType = 0; // 0 = CNG(无 CAPI provider type)
|
||||
provInfo.dwKeySpec = AT_KEYEXCHANGE;
|
||||
if (!CertSetCertificateContextProperty(cert, CERT_KEY_PROV_INFO_PROP_ID, 0, &provInfo))
|
||||
{
|
||||
Mprintf("[TLS] FATAL: CertSetCertificateContextProperty(CERT_KEY_PROV_INFO_PROP_ID) "
|
||||
"failed (%u)\n", GetLastError());
|
||||
NCryptDeleteKey(persistedKey, 0); // 删除并释放句柄
|
||||
CertFreeCertificateContext(cert);
|
||||
return false;
|
||||
}
|
||||
// 绑定后持久化键句柄交进程级 g_persistedKey,随 FreeIdentity 删除(覆盖旧键,避免残留)。
|
||||
g_persistedKey = persistedKey;
|
||||
|
||||
// 4) 入站凭据:显式指定服务端证书,不请求客户端证书(单向 TLS)。
|
||||
SCHANNEL_CRED sc = {};
|
||||
sc.dwVersion = SCHANNEL_CRED_VERSION;
|
||||
sc.cCreds = 1;
|
||||
sc.paCred = &cert;
|
||||
sc.grbitEnabledProtocols = SP_PROT_TLS1_2_SERVER;
|
||||
// NO_DEFAULT_CREDS 避免回退到系统默认凭据,确保只用我们提供的证书/私钥。
|
||||
//(单向 TLS 不请求客户端证书,故无需 SCH_CRED_MANUAL_CRED_VALIDATION。)
|
||||
sc.dwFlags = SCH_CRED_NO_DEFAULT_CREDS;
|
||||
|
||||
TimeStamp tsExpiry = {};
|
||||
SECURITY_STATUS st = AcquireCredentialsHandleA(
|
||||
NULL, (SEC_CHAR*)"Microsoft Unified Security Protocol Provider", SECPKG_CRED_INBOUND,
|
||||
NULL, &sc, NULL, NULL, &g_cred, &tsExpiry);
|
||||
|
||||
if (st != SEC_E_OK)
|
||||
{
|
||||
Mprintf("[TLS] FATAL: AcquireCredentialsHandle(SECPKG_CRED_INBOUND) failed 0x%08x\n",
|
||||
(unsigned)st);
|
||||
CertFreeCertificateContext(cert); // 无凭据引用,安全释放
|
||||
g_cred = {};
|
||||
if (g_persistedKey) // 回滚:删除已持久化的私钥,避免残留
|
||||
{
|
||||
NCryptDeleteKey(g_persistedKey, 0); // 删除并释放句柄
|
||||
g_persistedKey = 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 成功:把证书上下文生命周期交给进程级 g_cert,随 FreeIdentity 释放,
|
||||
// 保证在 FreeCredentialsHandle 之前私钥一直可用。
|
||||
g_cert = cert;
|
||||
g_credValid = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ServerTlsIdentity::IsEnabled()
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_identityMtx);
|
||||
return g_enabled;
|
||||
}
|
||||
|
||||
bool ServerTlsIdentity::IsReady()
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_identityMtx);
|
||||
return g_ready && g_credValid;
|
||||
}
|
||||
|
||||
bool ServerTlsIdentity::IsLoadError()
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_identityMtx);
|
||||
return g_loadError;
|
||||
}
|
||||
|
||||
CredHandle ServerTlsIdentity::Cred()
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_identityMtx);
|
||||
return g_cred;
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// ServerTlsSession —— 单连接会话(握手 + 加解密)
|
||||
// =====================================================================================
|
||||
|
||||
void ServerTlsSession::CloseHandles()
|
||||
{
|
||||
if (m_ctxValid)
|
||||
{
|
||||
DeleteSecurityContext(&m_ctx);
|
||||
m_ctxValid = false;
|
||||
}
|
||||
m_ctx = {};
|
||||
m_ready = false;
|
||||
m_raw.clear();
|
||||
}
|
||||
|
||||
// 握手线程在 IOCP 的子套接字上做同步 I/O。该套接字可能是非阻塞的(监听套接字经
|
||||
// WSAEventSelect 置为非阻塞后,accept 出的子套接字继承非阻塞态;线程里 FIONBIO=0 也
|
||||
// 不一定即时生效)。故 recv/send 遇 WSAEWOULDBLOCK 时用 select 等可读/可写再重试,
|
||||
// 而非当致命错误,避免「客户端回包还没到就误判握手失败」。
|
||||
static bool WaitSocketReady(SOCKET sock, bool forRead, int timeoutMs)
|
||||
{
|
||||
fd_set fds;
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(sock, &fds);
|
||||
timeval tv;
|
||||
tv.tv_sec = timeoutMs / 1000;
|
||||
tv.tv_usec = (timeoutMs % 1000) * 1000;
|
||||
return select(0, forRead ? &fds : NULL, forRead ? NULL : &fds, NULL, &tv) > 0;
|
||||
}
|
||||
|
||||
bool ServerTlsSession::SendAll(SOCKET sock, const void* data, int len)
|
||||
{
|
||||
const BYTE* p = (const BYTE*)data;
|
||||
int sent = 0;
|
||||
while (sent < len)
|
||||
{
|
||||
int n = send(sock, (const char*)p + sent, len - sent, 0);
|
||||
if (n == SOCKET_ERROR && WSAGetLastError() == WSAEWOULDBLOCK)
|
||||
{
|
||||
if (!WaitSocketReady(sock, false, 15000))
|
||||
return false;
|
||||
continue; // 非阻塞套接字发送缓冲满:等可写后重试
|
||||
}
|
||||
if (n <= 0)
|
||||
return false;
|
||||
sent += n;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ServerTlsSession::Handshake(SOCKET sock, const BYTE* initial, int initialLen)
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_mtx);
|
||||
if (m_ctxValid)
|
||||
CloseHandles(); // 防御:会话应只握手一次
|
||||
|
||||
CredHandle cred = ServerTlsIdentity::Cred();
|
||||
|
||||
// 单向 TLS:服务端不请求客户端证书,故无需 ASC_REQ_MANUAL_CRED_VALIDATION
|
||||
//(该标志只在 mTLS 校验客户端证书时有意义;SDK 的 ASC 命名空间里亦无此常量)。
|
||||
DWORD req = ASC_REQ_SEQUENCE_DETECT | ASC_REQ_REPLAY_DETECT | ASC_REQ_CONFIDENTIALITY |
|
||||
ASC_REQ_ALLOCATE_MEMORY | ASC_REQ_STREAM;
|
||||
|
||||
std::vector<BYTE> hs(initial, initial + initialLen);
|
||||
CtxtHandle ctx = {};
|
||||
bool ctxValid = false;
|
||||
TimeStamp tsExpiry = {};
|
||||
ULONG attrs = 0;
|
||||
SECURITY_STATUS sc = SEC_I_CONTINUE_NEEDED;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
SecBuffer inBufs[2] = {
|
||||
{ (ULONG)hs.size(), SECBUFFER_TOKEN, hs.empty() ? NULL : hs.data() },
|
||||
{ 0, SECBUFFER_EMPTY, NULL },
|
||||
};
|
||||
SecBufferDesc inDesc = { SECBUFFER_VERSION, 2, inBufs };
|
||||
SecBuffer outBuf = { 0, SECBUFFER_TOKEN, NULL };
|
||||
SecBufferDesc outDesc = { SECBUFFER_VERSION, 1, &outBuf };
|
||||
|
||||
sc = AcceptSecurityContext(&cred, ctxValid ? &ctx : NULL, &inDesc, req,
|
||||
SECURITY_NATIVE_DREP, ctxValid ? NULL : &ctx,
|
||||
&outDesc, &attrs, &tsExpiry);
|
||||
ctxValid = true;
|
||||
|
||||
// 发出服务端握手记录(ServerHello/Certificate/... 或 Finished)。
|
||||
if (outBuf.cbBuffer > 0 && outBuf.pvBuffer)
|
||||
{
|
||||
bool ok = SendAll(sock, outBuf.pvBuffer, (int)outBuf.cbBuffer);
|
||||
FreeContextBuffer(outBuf.pvBuffer);
|
||||
if (!ok)
|
||||
{
|
||||
sc = SEC_E_INTERNAL_ERROR;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算已消费字节:SECBUFFER_EXTRA 里是「未消费的尾部」(下一条握手消息 / 首条应用数据)。
|
||||
// 必须在 SEC_E_OK / 失败判定之前处理:否则握手完成时 hs 里仍混着已消费的握手记录,
|
||||
// 紧跟 Finished 的首条应用数据(客户端登录包)会随 hs 一起被丢弃,导致随机丢包。
|
||||
if (inBufs[1].BufferType == SECBUFFER_EXTRA)
|
||||
{
|
||||
int extra = (int)inBufs[1].cbBuffer;
|
||||
hs.erase(hs.begin(), hs.begin() + (hs.size() - extra));
|
||||
}
|
||||
else if (sc != SEC_E_INCOMPLETE_MESSAGE && sc != SEC_I_INCOMPLETE_CREDENTIALS)
|
||||
{
|
||||
hs.clear(); // 已全部消费,继续等下一轮握手消息
|
||||
}
|
||||
// 否则:SEC_E_INCOMPLETE_MESSAGE —— 保留 hs(不完整消息),续收补齐。
|
||||
|
||||
if (sc == SEC_E_OK)
|
||||
break;
|
||||
if (FAILED(sc) && sc != SEC_E_INCOMPLETE_MESSAGE && sc != SEC_I_INCOMPLETE_CREDENTIALS)
|
||||
{
|
||||
Mprintf("[TLS] AcceptSecurityContext failed 0x%08x (in=%u consumed? extra=%u)\n",
|
||||
(unsigned)sc, inBufs[0].cbBuffer, inBufs[1].BufferType == SECBUFFER_EXTRA ? inBufs[1].cbBuffer : 0);
|
||||
break; // 真正的失败
|
||||
}
|
||||
|
||||
char raw[8192];
|
||||
int r;
|
||||
for (;;)
|
||||
{
|
||||
r = recv(sock, raw, sizeof(raw), 0);
|
||||
if (r == SOCKET_ERROR && WSAGetLastError() == WSAEWOULDBLOCK)
|
||||
{
|
||||
if (!WaitSocketReady(sock, true, 15000))
|
||||
{
|
||||
r = SOCKET_ERROR;
|
||||
break; // 等可读超时
|
||||
}
|
||||
continue; // 非阻塞套接字暂无数据:等可读后重试
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (r <= 0)
|
||||
{
|
||||
Mprintf("[TLS] handshake recv failed/EOF (r=%d err=%d)\n", r, WSAGetLastError());
|
||||
sc = SEC_E_INTERNAL_ERROR;
|
||||
break;
|
||||
}
|
||||
hs.insert(hs.end(), (BYTE*)raw, (BYTE*)raw + r);
|
||||
}
|
||||
|
||||
if (sc != SEC_E_OK)
|
||||
{
|
||||
Mprintf("[TLS] handshake failed: final status 0x%08x (fail-closed)\n", (unsigned)sc);
|
||||
if (ctxValid)
|
||||
DeleteSecurityContext(&ctx);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 握手成功后 hs 里残留的「未消费尾部」是紧跟 Finished 的首条应用数据(TLS 记录)。
|
||||
// 交给 Decrypt 处理,否则首包会丢(表现为随机登录超时/重连/协议错位)。
|
||||
if (!hs.empty())
|
||||
m_raw.insert(m_raw.end(), hs.begin(), hs.end());
|
||||
|
||||
// 查询流式记录尺寸(加密封装用)。
|
||||
SecPkgContext_StreamSizes sizes = {};
|
||||
if (QueryContextAttributes(&ctx, SECPKG_ATTR_STREAM_SIZES, &sizes) != SEC_E_OK)
|
||||
{
|
||||
DeleteSecurityContext(&ctx);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_ctx = ctx;
|
||||
m_ctxValid = true;
|
||||
m_sizes = sizes;
|
||||
m_ready = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
int ServerTlsSession::Decrypt(const BYTE* cipher, int len, std::vector<BYTE>& plain)
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_mtx);
|
||||
if (!m_ready)
|
||||
return -1;
|
||||
if (len <= 0)
|
||||
return 0;
|
||||
|
||||
m_raw.insert(m_raw.end(), cipher, cipher + len);
|
||||
|
||||
for (;;)
|
||||
{
|
||||
SecBuffer bufs[4] = {
|
||||
{ (ULONG)m_raw.size(), SECBUFFER_DATA, m_raw.empty() ? NULL : m_raw.data() },
|
||||
{ 0, SECBUFFER_EMPTY, NULL },
|
||||
{ 0, SECBUFFER_EMPTY, NULL },
|
||||
{ 0, SECBUFFER_EMPTY, NULL },
|
||||
};
|
||||
SecBufferDesc desc = { SECBUFFER_VERSION, 4, bufs };
|
||||
|
||||
SECURITY_STATUS sc = DecryptMessage(&m_ctx, &desc, 0, NULL);
|
||||
if (sc == SEC_E_INCOMPLETE_MESSAGE)
|
||||
break; // 记录不完整,等下一轮 WSARecv
|
||||
if (sc == SEC_I_CONTEXT_EXPIRED) {
|
||||
Mprintf("[TLS] DecryptMessage: peer close_notify (SEC_I_CONTEXT_EXPIRED)\n");
|
||||
return -1; // 对端 close_notify / 会话结束
|
||||
}
|
||||
if (sc == SEC_I_RENEGOTIATE || FAILED(sc)) {
|
||||
Mprintf("[TLS] DecryptMessage failed 0x%08x (raw=%u)\n", (unsigned)sc, (unsigned)m_raw.size());
|
||||
return -1;
|
||||
}
|
||||
|
||||
int decLen = (int)bufs[1].cbBuffer;
|
||||
const BYTE* p = (const BYTE*)bufs[1].pvBuffer;
|
||||
int extra = (bufs[3].BufferType == SECBUFFER_EXTRA) ? (int)bufs[3].cbBuffer : 0;
|
||||
int consumed = (int)m_raw.size() - extra;
|
||||
|
||||
if (decLen > 0 && p)
|
||||
plain.insert(plain.end(), p, p + decLen);
|
||||
|
||||
m_raw.erase(m_raw.begin(), m_raw.begin() + consumed);
|
||||
if (m_raw.empty())
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ServerTlsSession::Encrypt(const BYTE* plain, int len, std::vector<BYTE>& wire)
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_mtx);
|
||||
if (!m_ready)
|
||||
return -1;
|
||||
if (len <= 0)
|
||||
return 0;
|
||||
|
||||
ULONG maxMsg = m_sizes.cbMaximumMessage > 0 ? m_sizes.cbMaximumMessage : 16384;
|
||||
int total = 0;
|
||||
while (total < len)
|
||||
{
|
||||
int chunk = (len - total > (int)maxMsg) ? (int)maxMsg : (len - total);
|
||||
|
||||
std::vector<BYTE> rec(m_sizes.cbHeader + chunk + m_sizes.cbTrailer);
|
||||
BYTE* hdr = rec.data();
|
||||
BYTE* body = hdr + m_sizes.cbHeader;
|
||||
BYTE* trailer = body + chunk;
|
||||
memcpy(body, plain + total, chunk);
|
||||
|
||||
SecBuffer bufs[4] = {
|
||||
{ m_sizes.cbHeader, SECBUFFER_STREAM_HEADER, hdr },
|
||||
{ (ULONG)chunk, SECBUFFER_DATA, body },
|
||||
{ m_sizes.cbTrailer, SECBUFFER_STREAM_TRAILER, trailer },
|
||||
{ 0, SECBUFFER_EMPTY, NULL },
|
||||
};
|
||||
SecBufferDesc desc = { SECBUFFER_VERSION, 4, bufs };
|
||||
|
||||
SECURITY_STATUS sc = EncryptMessage(&m_ctx, 0, &desc, 0);
|
||||
if (FAILED(sc))
|
||||
return -1; // 握手已建立,此处失败视为致命(调用方关闭连接)
|
||||
|
||||
int wireLen = (int)(bufs[0].cbBuffer + bufs[1].cbBuffer + bufs[2].cbBuffer);
|
||||
wire.insert(wire.end(), rec.data(), rec.data() + wireLen);
|
||||
total += chunk;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// CONTEXT_OBJECT::FreeTls —— 释放每连接 TLS 会话(定义在此以便 delete 完整类型)
|
||||
// =====================================================================================
|
||||
|
||||
void CONTEXT_OBJECT::FreeTls()
|
||||
{
|
||||
delete tls;
|
||||
tls = NULL;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// ServerTlsSession.h — MFC 服务端单向 TLS(Schannel 入站)会话封装
|
||||
//
|
||||
// 与客户端 TlsChannel 对称:客户端走 InitializeSecurityContext(出站),本类走
|
||||
// AcceptSecurityContext(入站)。单向 TLS:不请求/校验客户端证书,客户端身份仍由
|
||||
// 隧道内登录(V1 PwdHmac / V2 ECDSA)承担。
|
||||
//
|
||||
// 分层:
|
||||
// ServerTlsIdentity —— 进程级「服务端身份」(证书 + 私钥 → 入站凭据)。可重入加载
|
||||
// (Load 禁止用 call_once,供未来 UI/指令热切换),本轮启动时加载一次。
|
||||
// ServerTlsSession —— 每条连接一个,持有握手/加解密所需的 Schannel 上下文与缓冲,
|
||||
// 随 CONTEXT_OBJECT 池化复用而重建。
|
||||
//
|
||||
// 线程模型:Handshake 在专用握手线程上阻塞执行(方案 A);握手完成后,Decrypt 由
|
||||
// IOCP 工作线程调用(每连接同一时刻只有一个未决 WSARecv,天然串行)、Encrypt 在
|
||||
// SendLock 内调用。Schannel 上下文非线程安全,故 Encrypt/Decrypt 内部以 m_mtx 互斥。
|
||||
//
|
||||
// 选型:Schannel(Windows 原生 SSPI),secur32.lib / crypt32.lib / ncrypt.lib 系统库。
|
||||
// 证书/私钥为 PEM(与 Go 服务端 YAMA_TLS_CERT/KEY 同构,可共用同一对密钥);私钥
|
||||
// 须为 PKCS#8(-----BEGIN PRIVATE KEY-----),见 ServerTlsSession.cpp 的加载实现。
|
||||
#pragma once
|
||||
|
||||
#include <WinSock2.h>
|
||||
|
||||
#ifndef SECURITY_WIN32
|
||||
#define SECURITY_WIN32 1
|
||||
#endif
|
||||
#include <security.h>
|
||||
#include <schannel.h>
|
||||
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class config; // common/iniFile.h(仅需引用,避免把 ini 头拖进 Server.h 的 include 图)
|
||||
|
||||
// ---- 进程级服务端身份(证书 + 私钥),可重入加载(禁止 call_once,供未来热切换)----
|
||||
class ServerTlsIdentity
|
||||
{
|
||||
public:
|
||||
// 从 cfg(THIS_CFG)读 [tls] 节:Enabled / CertPath / KeyPath,并加载身份。
|
||||
// 返回 true 表示「配置已生效」(开关关、或开关开且身份加载成功);开关开但加载
|
||||
// 失败时返回 false 且 IsLoadError()==true,服务端须对 TLS ClientHello fail-closed。
|
||||
static bool Load(config& cfg);
|
||||
|
||||
static bool IsEnabled(); // [tls] Enabled != 0
|
||||
static bool IsReady(); // 开关开 && 身份加载成功(有可用入站凭据)
|
||||
static bool IsLoadError(); // 开关开 && 身份加载失败(fail-closed)
|
||||
|
||||
// 入站凭据(AcquireCredentialsHandle 产出)。返回拷贝;句柄值本身可安全拷贝。
|
||||
static CredHandle Cred();
|
||||
|
||||
private:
|
||||
static void FreeIdentity();
|
||||
static bool LoadIdentity(const std::string& certPath, const std::string& keyPath);
|
||||
};
|
||||
|
||||
// ---- 单连接服务端 TLS 会话(握手 + 加解密)----
|
||||
class ServerTlsSession
|
||||
{
|
||||
public:
|
||||
ServerTlsSession() {}
|
||||
~ServerTlsSession() { CloseHandles(); }
|
||||
|
||||
ServerTlsSession(const ServerTlsSession&) = delete;
|
||||
ServerTlsSession& operator=(const ServerTlsSession&) = delete;
|
||||
|
||||
// 阻塞式服务端握手(方案 A:专用握手线程上调用)。initial/initialLen 是嗅探阶段已
|
||||
// 收到的 ClientHello 字节(可能只是分片;内部按 SEC_E_INCOMPLETE_MESSAGE 续收补齐)。
|
||||
// 成功返回 true,失败 false(调用方关闭连接,fail-closed)。
|
||||
bool Handshake(SOCKET sock, const BYTE* initial, int initialLen);
|
||||
|
||||
// 解密:把本次 WSARecv 收到的密文 cipher[0..len] 喂入,把能解出的明文追加到 plain。
|
||||
// 返回 0 = 成功(可能无明文);-1 = 错误 / 对端 close_notify(调用方关闭连接)。
|
||||
int Decrypt(const BYTE* cipher, int len, std::vector<BYTE>& plain);
|
||||
|
||||
// 加密:把明文 plain[0..len] 加密成完整 TLS 记录,追加到 wire。
|
||||
// 返回加密的明文字节数(== len);-1 = 错误。
|
||||
int Encrypt(const BYTE* plain, int len, std::vector<BYTE>& wire);
|
||||
|
||||
bool Ready() const { return m_ready; }
|
||||
|
||||
private:
|
||||
void CloseHandles();
|
||||
bool SendAll(SOCKET sock, const void* data, int len);
|
||||
|
||||
std::mutex m_mtx; // 序列化对 m_ctx 的 Encrypt/Decrypt(Schannel 非线程安全)
|
||||
CtxtHandle m_ctx = {};
|
||||
bool m_ctxValid = false;
|
||||
bool m_ready = false;
|
||||
SecPkgContext_StreamSizes m_sizes = {};
|
||||
std::vector<BYTE> m_raw; // 尚未解密 / 未消费完的 TLS 记录原始字节
|
||||
};
|
||||
Reference in new issue
Block a user