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
@@ -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