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

+2 -1
View File
@@ -665,7 +665,8 @@ VOID IOCPClient::OnServerReceiving(CBuffer* m_CompressedBuffer, char* szBuffer,
for (int i = 0; i < 16 && i < (int)bufLen; ++i) {
char tmp[12]; sprintf(tmp, "%02X ", (unsigned char)src[i]); buf += tmp;
}
Mprintf("[ERROR] Unknown header! this=%p bufLen=%lu, first 16 bytes: %s\n", this, bufLen, buf.c_str());
Mprintf("[ERROR] Unknown header! this=%p bufLen=%lu, first 16 bytes: %s (TlsEnabled=%d tlsReady=%d)\n",
this, bufLen, buf.c_str(), (int)TlsEnabled(), (int)m_tls.IsReady());
m_CompressedBuffer->ClearBuffer();
break;
}
+26 -12
View File
@@ -368,6 +368,11 @@ bool TlsChannel::Handshake(SOCKET sock)
return false;
}
// 握手成功后 hs 里可能残留「未消费的尾部」= 服务端 Finished 后紧跟的首条应用数据
// (TLS 记录)。若不下发,首包会被丢弃,导致随机丢包/协议错位。交给 Recv 解。
if (!hs.empty())
m_raw.insert(m_raw.end(), hs.begin(), hs.end());
if (!VerifyServerCert()) {
Mprintf("[TLS] server certificate not trusted against yama-ca.pem (fail-closed)\n");
CloseHandles();
@@ -404,14 +409,10 @@ int TlsChannel::Recv(SOCKET sock, char* out, int outLen)
char raw[16384];
for (;;) {
int r = recv(sock, raw, sizeof(raw), 0);
if (r == SOCKET_ERROR)
return SOCKET_ERROR;
if (r == 0)
return 0; // 对端关闭(此时无 pending)
m_raw.insert(m_raw.end(), (BYTE*)raw, (BYTE*)raw + r);
for (;;) {
// 优先解密 m_raw 里已缓冲的字节(握手时随 Finished 收下的首条应用数据,或上轮
// 一次 recv 到多条记录后剩下的记录)。select() 只看底层 socket,看不到这些字节,
// 若先 recv 就会把它们卡住(随机延迟/死锁/协议错位)。
while (!m_raw.empty()) {
SecBuffer bufs[4] = {
{ (ULONG)m_raw.size(), SECBUFFER_DATA, m_raw.data() },
{ 0, SECBUFFER_EMPTY, NULL },
@@ -422,11 +423,17 @@ int TlsChannel::Recv(SOCKET sock, char* out, int outLen)
SECURITY_STATUS sc = DecryptMessage(&m_ctx, &desc, 0, NULL);
if (sc == SEC_E_INCOMPLETE_MESSAGE)
break; // 记录不完整,再 recv
break; // 记录不完整,去 recv 补字节
if (sc == SEC_I_CONTEXT_EXPIRED)
return 0; // close_notify
if (sc == SEC_I_RENEGOTIATE || FAILED(sc))
if (sc == SEC_I_RENEGOTIATE || FAILED(sc)) {
// 客户端解密失败(服务端发来的密文损坏/错位)。这里只返回 SOCKET_ERROR,
// 而 WSAGetLastError() 会残留上一次 Winsock 调用的旧值(如 6001),
// 掩盖真实原因,故把真实的 SECURITY_STATUS 记下来。
Mprintf("[TLS] client DecryptMessage failed 0x%08x (raw=%u pending=%u)\n",
(unsigned)sc, (unsigned)m_raw.size(), (unsigned)m_pending.size());
return SOCKET_ERROR;
}
// 流模式 DecryptMessage 会把输入 SECBUFFER_DATA 重组为 4 段:
// bufs[0]=STREAM_HEADER(记录头+nonce)、bufs[1]=DATA(明文)、
@@ -445,9 +452,16 @@ int TlsChannel::Recv(SOCKET sock, char* out, int outLen)
m_pending.erase(m_pending.begin(), m_pending.begin() + n);
return n;
}
if (m_raw.empty())
break; // 无明文(如 alert/CCS 记录),继续 recv
// 无明文的记录(如 alert/CCS):继续循环解下一条
}
int r = recv(sock, raw, sizeof(raw), 0);
if (r == SOCKET_ERROR)
return SOCKET_ERROR;
if (r == 0)
return 0; // 对端关闭(此时无 pending,且 m_raw 已解完/为空)
m_raw.insert(m_raw.end(), (BYTE*)raw, (BYTE*)raw + r);
// 循环回顶部,解密刚收到的字节
}
}
+8 -2
View File
@@ -60,8 +60,14 @@ public:
// 前缀字节数(>0)或 SOCKET_ERROR(一个字节都没发出去时)。
int Send(SOCKET sock, const char* buf, int len);
// 是否有已解密但尚未交付给应用层的明文(供接收线程在 select 前优先消费)。
bool HasPending() const { return !m_pending.empty(); }
// 是否有待交付的数据:已解密明文(m_pending)或已缓冲但尚未解密的 TLS 记录(m_raw)。
// 供接收线程在 select 前优先消费——select 只看底层 socket,看不到 Schannel 缓冲的
// m_raw(握手时随 Finished 收下的首条应用数据、或一次 recv 到多条记录后的剩余),
// 若这里不把 m_raw 也算作「有数据」,这些字节会被 select 卡住。
bool HasPending() const { return !m_pending.empty() || !m_raw.empty(); }
// 握手是否已完成(Send/Recv 可用)。诊断用:区分「本连接已走 TLS」与「裸连接」。
bool IsReady() const { return m_ready; }
private:
void CloseHandles();