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
95 lines
4.5 KiB
C++
95 lines
4.5 KiB
C++
// TlsChannel.h — Windows 客户端单向 TLS(Schannel)会话封装
|
||
//
|
||
// 用途:为 YAMA 客户端(被控端)提供「按需开启」的传输层加密。当信任锚证书存在时
|
||
// (注册表 Software\<exehash>\settings\tls_cert,或 yama-ca.pem 文件,见 TlsChannel.cpp
|
||
// 的 InitTls),所有出站 TCP 连接在应用协议首包前先完成 TLS 握手;握手失败一律断开
|
||
// (fail-closed,不回退明文)。
|
||
//
|
||
// 选型:Schannel(Windows 原生 SSPI),零第三方依赖;secur32.lib / crypt32.lib 为
|
||
// 系统库。单向 TLS:客户端只校验服务端证书链到固定的信任锚,不提供客户端证书,
|
||
// 客户端身份仍由隧道内登录(V1 PwdHmac / V2 ECDSA)承担。
|
||
//
|
||
// 线程模型:Handshake 在连接建立线程上、m_bConnected 置位前调用;Send/Recv 分别在
|
||
// 发送线程 / 接收线程上调用。上下文生命周期(Handshake/CloseHandles 释放 m_ctx)与
|
||
// Send/Recv 对 m_ctx 的使用(EncryptMessage/DecryptMessage)由 m_ctxMtx 互斥,避免
|
||
// 重连线程释放上下文时与使用线程产生 use-after-free。本对象作为 IOCPClient 的值成员
|
||
// 存在,析构时释放上下文。
|
||
//
|
||
// 注意:本文件仅在 _WIN32 下有效,非 Windows 构建(Linux/macOS/Android)不含任何内容,
|
||
// 客户端维持明文(TLS 接入按设计文档延后)。
|
||
#pragma once
|
||
|
||
#ifdef _WIN32
|
||
|
||
#include <WinSock2.h>
|
||
|
||
#ifndef SECURITY_WIN32
|
||
#define SECURITY_WIN32 1
|
||
#endif
|
||
#include <security.h>
|
||
#include <schannel.h>
|
||
|
||
#include <mutex>
|
||
#include <vector>
|
||
|
||
// 进程级 TLS 开关:首次调用时加载信任锚(注册表或文件),返回是否启用。线程安全、幂等。
|
||
bool TlsEnabled();
|
||
|
||
// 信任锚已配置但加载失败(损坏/格式错误)时为 true:表示管理员已「颁发」证书,
|
||
// 客户端必须拒绝连接(fail-closed),不得静默回退明文。线程安全、幂等。
|
||
bool TlsLoadError();
|
||
|
||
class TlsChannel
|
||
{
|
||
public:
|
||
TlsChannel() {}
|
||
~TlsChannel() { CloseHandles(); }
|
||
|
||
// 持 Schannel 句柄,禁止拷贝/赋值(避免句柄被二次释放)。
|
||
TlsChannel(const TlsChannel&) = delete;
|
||
TlsChannel& operator=(const TlsChannel&) = delete;
|
||
|
||
// 在已连通的 socket 上完成 TLS 客户端握手并校验服务端证书链。失败返回 false,
|
||
// 调用方必须关闭 socket(fail-closed)。可重复调用(重连时复用同一对象)。
|
||
bool Handshake(SOCKET sock);
|
||
|
||
// 解密读取:返回 >0 的明文字节数;0 表示对端正常关闭;SOCKET_ERROR(-1) 出错。
|
||
int Recv(SOCKET sock, char* buf, int len);
|
||
|
||
// 加密发送 len 字节:成功返回已完整发送的明文字节数(== len);失败返回已发送的
|
||
// 前缀字节数(>0)或 SOCKET_ERROR(一个字节都没发出去时)。
|
||
int Send(SOCKET sock, const char* buf, int len);
|
||
|
||
// 是否有待交付的数据:已解密明文(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();
|
||
bool SendAll(SOCKET sock, const void* data, int len);
|
||
bool VerifyServerCert();
|
||
|
||
CredHandle m_cred = {};
|
||
CtxtHandle m_ctx = {};
|
||
bool m_credValid = false; // m_cred 已分配,需 FreeCredentialsHandle
|
||
bool m_ctxValid = false; // m_ctx 已分配,需 DeleteSecurityContext
|
||
bool m_ready = false; // 握手完成,Send/Recv 可用
|
||
|
||
SecPkgContext_StreamSizes m_sizes = {};
|
||
|
||
// 保护上下文生命周期:重连线程的 Handshake/CloseHandles(释放 m_ctx)与工作线程的
|
||
// Recv(DecryptMessage)、发送线程的 Send(EncryptMessage)互斥,避免 use-after-free。
|
||
// recursive 是因为 Handshake 内部会再调 CloseHandles。
|
||
std::recursive_mutex m_ctxMtx;
|
||
|
||
std::vector<BYTE> m_raw; // 尚未解密 / 未消费完的 TLS 记录原始字节
|
||
std::vector<BYTE> m_pending; // 已解密、未交付给应用层的明文
|
||
};
|
||
|
||
#endif // _WIN32
|