Files
yuanyuanxianganddeepseek-v4-pro 96b9eea70a 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
2026-10-15 13:48:27 +02:00

93 lines
4.2 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 记录原始字节
};