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:
yuanyuanxianganddeepseek-v4-pro committed 2026-10-15 13:48:27 +02:00
1 parent ca0d7626d2
commit 96b9eea70a
16 files changed
+1298 -38

No files matched your search

+221 -19
View File
@@ -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());