diff --git a/client/ClientDll_vs2015.vcxproj b/client/ClientDll_vs2015.vcxproj
index 34e7c79..09145ea 100644
--- a/client/ClientDll_vs2015.vcxproj
+++ b/client/ClientDll_vs2015.vcxproj
@@ -225,6 +225,7 @@
+
@@ -264,6 +265,7 @@
+
diff --git a/client/ClientDll_vs2015.vcxproj.filters b/client/ClientDll_vs2015.vcxproj.filters
index 373048a..ff7eabf 100644
--- a/client/ClientDll_vs2015.vcxproj.filters
+++ b/client/ClientDll_vs2015.vcxproj.filters
@@ -46,6 +46,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -99,6 +109,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/IOCPClient.cpp b/client/IOCPClient.cpp
index 1402e70..6f6ed70 100644
--- a/client/IOCPClient.cpp
+++ b/client/IOCPClient.cpp
@@ -227,7 +227,11 @@ IOCPClient::IOCPClient(const State&bExit, bool exit_while_disconnect, int mask,
m_DataProcess = NULL;
m_ReconnectFunc = NULL;
m_masker = mask ? new HttpMask(DEFAULT_HOST) : new PkgMask();
+#ifdef _DEBUG
+ auto enc = GetHeaderEncoder(HeaderEncType(time(nullptr) % (HeaderEncV6 + 1)));
+#else
auto enc = GetHeaderEncoder(HeaderEncType(time(nullptr) % HeaderEncNum));
+#endif
m_EncoderType = encoder;
m_Encoder = encoder ? new HellEncoder(enc, new XOREncoder16()) : new ProtocolEncoder();
#ifdef _WIN32
@@ -463,6 +467,25 @@ BOOL IOCPClient::ConnectServer(const char* szServerIP, unsigned short uPort)
SetKeepAliveOptions(m_sClientSocket);
#endif
}
+#ifdef _WIN32
+ // 单向 TLS:证书存在(TlsEnabled())时,在应用协议首包前完成握手;握手失败即断开,
+ // 不回退明文(fail-closed,防降级)。证书存在但加载失败(TlsLoadError())同样拒绝
+ // 连接。握手须在 m_bConnected 置位前,确保工作线程不会在握手尚未完成时就开始 recv。
+ if (TlsLoadError()) {
+ Mprintf("[TLS] 证书加载失败,拒绝连接(fail-closed)\n");
+ closesocket(m_sClientSocket);
+ m_sClientSocket = INVALID_SOCKET;
+ return FALSE;
+ }
+ if (TlsEnabled()) {
+ if (!m_tls.Handshake(m_sClientSocket)) {
+ Mprintf("[TLS] 握手失败,断开连接(fail-closed)\n");
+ closesocket(m_sClientSocket);
+ m_sClientSocket = INVALID_SOCKET;
+ return FALSE;
+ }
+ }
+#endif
m_bConnected = TRUE;
Mprintf("连接服务端成功: %s:%d.\n", m_sCurIP.c_str(), (int)port);
@@ -526,6 +549,16 @@ DWORD WINAPI IOCPClient::WorkThreadProc(LPVOID lParam)
Sleep(50);
continue;
}
+#ifdef _WIN32
+ // TLS 会话里可能有已解密、尚未交付的明文(select 只看底层 socket,看不到
+ // Schannel 内部缓冲),优先消费,避免被 select 卡住。
+ if (This->HasPendingData()) {
+ if (!This->ProcessRecvData(&m_CompressedBuffer, szBuffer, MAX_RECV_BUFFER - 1, 0)) {
+ break;
+ }
+ continue;
+ }
+#endif
FD_ZERO(&fd);
FD_SET(This->m_sClientSocket, &fd);
// Linux select() 会修改 timeval,必须每次重置
@@ -632,7 +665,7 @@ 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! bufLen=%lu, first 16 bytes: %s\n", bufLen, buf.c_str());
+ Mprintf("[ERROR] Unknown header! this=%p bufLen=%lu, first 16 bytes: %s\n", this, bufLen, buf.c_str());
m_CompressedBuffer->ClearBuffer();
break;
}
diff --git a/client/IOCPClient.h b/client/IOCPClient.h
index 8415ae0..8aa6a61 100644
--- a/client/IOCPClient.h
+++ b/client/IOCPClient.h
@@ -8,6 +8,7 @@
#include "stdafx.h"
#include
#include
+#include "TlsChannel.h"
#pragma comment(lib,"ws2_32.lib")
#endif
@@ -289,14 +290,33 @@ public:
protected:
virtual int ReceiveData(char* buffer, int bufSize, int flags)
{
+#ifdef _WIN32
+ if (TlsEnabled())
+ return m_tls.Recv(m_sClientSocket, buffer, bufSize - 1);
+#endif
// TCP版本调用 recv
- return recv(m_sClientSocket, buffer, bufSize - 1, 0);
+ int n = recv(m_sClientSocket, buffer, bufSize - 1, 0);
+ return n;
}
virtual bool ProcessRecvData(CBuffer* m_CompressedBuffer, char* szBuffer, int len, int flag);
virtual int SendTo(const char* buf, int len, int flags)
{
+#ifdef _WIN32
+ if (TlsEnabled())
+ return m_tls.Send(m_sClientSocket, buf, len);
+#endif
return ::send(m_sClientSocket, buf, len, flags);
}
+ // TLS 会话里可能有已解密但尚未交付的明文(select 只能看到底层 socket 可读性,
+ // 看不到 Schannel 内部缓冲),接收线程在 select 前须优先消费,避免其被卡住。
+ bool HasPendingData() const
+ {
+#ifdef _WIN32
+ return m_tls.HasPending();
+#else
+ return false;
+#endif
+ }
BOOL OnServerSending(const char* szBuffer, ULONG ulOriginalLength, PkgMask* mask);
static DWORD WINAPI WorkThreadProc(LPVOID lParam);
VOID OnServerReceiving(CBuffer *m_CompressedBuffer, char* szBuffer, ULONG ulReceivedLength);
@@ -305,6 +325,9 @@ protected:
protected:
sockaddr_in m_ServerAddr;
SOCKET m_sClientSocket;
+#ifdef _WIN32
+ TlsChannel m_tls; // TLS 会话(证书存在时才启用,见 TlsChannel.h)
+#endif
BOOL m_bWorkThread;
HANDLE m_hWorkThread;
BOOL m_bIsRunning;
diff --git a/client/TlsChannel.cpp b/client/TlsChannel.cpp
new file mode 100644
index 0000000..b978f1c
--- /dev/null
+++ b/client/TlsChannel.cpp
@@ -0,0 +1,494 @@
+// TlsChannel.cpp — 见 TlsChannel.h 头注释。仅 _WIN32 编译。
+#ifdef _WIN32
+
+#include "stdafx.h"
+#include "TlsChannel.h"
+
+#include "common/iniFile.h" // config(settings.ini 文件回退) + GetRegistryName/GetCurrentUserRegistryKey(注册表正文)
+
+#include // 证书/链引擎 API
+#include // RegOpenKeyExA / RegQueryValueExA
+#include // _access
+#include // std::ifstream
+#include // std::istreambuf_iterator
+#include
+#include // std::call_once
+
+#pragma comment(lib, "secur32.lib")
+#pragma comment(lib, "crypt32.lib")
+
+namespace {
+
+std::once_flag g_tlsOnce;
+bool g_tlsEnabled = false; // TLS 会话可用:send/recv 走加密
+bool g_tlsError = false; // 证书存在但加载失败:拒绝连接(fail-closed)
+HCERTSTORE g_tlsStore = NULL;
+HCERTCHAINENGINE g_tlsEngine = NULL; // 只信任 g_tlsStore 的链引擎(严格钉扎)
+
+// 解析信任锚证书路径:优先 settings.ini 的 [settings] tls_cert,其次可执行文件同目录
+// yama-ca.pem。
+std::string ResolveCertPath()
+{
+ std::string certPath;
+ {
+ config cfg;
+ certPath = cfg.GetStr("settings", "tls_cert", "");
+ }
+ if (certPath.empty()) {
+ char path[MAX_PATH];
+ if (!GetModuleFileNameA(NULL, path, sizeof(path)))
+ return std::string(); // 拿不到 exe 路径,视为未配置
+ char* p = path + strlen(path);
+ while (p > path && *p != '\\') --p;
+ strcpy(p + 1, "yama-ca.pem");
+ certPath = path;
+ }
+ return certPath;
+}
+
+// 读取注册表 REG_SZ(动态扩容)。iniFile::GetStr 用 512 字节固定缓冲,会把 PEM 正文
+// (约 1.3~1.8KB)截断成空,故此处自行两段式 RegQueryValueEx 读取。
+// 返回 true 表示值存在且为 REG_SZ,out 为去掉末尾 '\0' 后的内容;否则 false。
+bool ReadRegistrySz(HKEY root, const std::string& subKey, const std::string& valueName, std::string& out)
+{
+ HKEY hKey = NULL;
+ if (RegOpenKeyExA(root, subKey.c_str(), 0, KEY_READ, &hKey) != ERROR_SUCCESS)
+ return false;
+
+ DWORD type = 0, size = 0;
+ LONG r = RegQueryValueExA(hKey, valueName.c_str(), NULL, &type, NULL, &size);
+ if (r != ERROR_SUCCESS || type != REG_SZ) {
+ RegCloseKey(hKey);
+ return false;
+ }
+ std::vector buf(size ? size : 1, 0);
+ r = RegQueryValueExA(hKey, valueName.c_str(), NULL, &type,
+ reinterpret_cast(buf.data()), &size);
+ RegCloseKey(hKey);
+ if (r != ERROR_SUCCESS)
+ return false;
+ if (size > 0 && buf[size - 1] == '\0')
+ --size; // REG_SZ 的 size 含末尾 '\0'
+ out.assign(buf.data(), size);
+ return true;
+}
+
+// 取信任锚 PEM 正文。优先注册表 Software\\settings\tls_cert(REG_SZ 直接存
+// PEM 正文,便于后续服务端下发/撤销),其次 settings.ini [settings] tls_cert 路径、
+// exe 同目录 yama-ca.pem 文件(向后兼容旧的文件投递方式)。
+// 返回 true 表示已配置(pem 可能为空:值/文件存在但内容为空,由 LoadTrustAnchor 判为
+// fail-closed);false 表示完全未配置(走明文)。source 描述来源,仅用于日志。
+bool ReadTrustAnchor(std::string& pem, std::string& source)
+{
+ // 1) 注册表正文(首选)。值存在(即便为空)即视为「已配置」,交由 LoadTrustAnchor
+ // 对空/损坏内容判 fail-closed,与「空文件 → fail-closed」语义保持一致,避免空值
+ // 被静默当成未配置而回退明文。
+ std::string regSubKey = GetRegistryName() + "\\settings";
+ std::string regVal;
+ if (ReadRegistrySz(GetCurrentUserRegistryKey(), regSubKey, "tls_cert", regVal)) {
+ pem = regVal;
+ source = "registry " + regSubKey + "\\tls_cert";
+ return true;
+ }
+
+ // 2) 文件回退:settings.ini [settings] tls_cert 路径 → exe 同目录 yama-ca.pem
+ std::string certPath = ResolveCertPath();
+ if (certPath.empty() || _access(certPath.c_str(), 0) != 0)
+ return false; // 无证书 → 明文
+
+ std::ifstream f(certPath.c_str(), std::ios::binary);
+ if (!f) {
+ Mprintf("[TLS] cannot open %s\n", certPath.c_str());
+ return false;
+ }
+ pem.assign((std::istreambuf_iterator(f)), std::istreambuf_iterator());
+ source = certPath;
+ return true;
+}
+
+// 解码 PEM/DER 信任锚正文为 DER,并加入内存信任锚存储。
+bool LoadTrustAnchor(const std::string& pem, const std::string& source, HCERTSTORE* outStore)
+{
+ if (pem.empty()) {
+ Mprintf("[TLS] empty certificate %s\n", source.c_str());
+ return false;
+ }
+
+ // PEM(-----BEGIN CERTIFICATE----- base64)或 DER → DER。CRYPT_STRING_ANY 两者都认,
+ // 失败再退回 CRYPT_STRING_BASE64HEADER(纯 PEM)。
+ DWORD derLen = 0;
+ if (!CryptStringToBinaryA(pem.data(), (DWORD)pem.size(), CRYPT_STRING_ANY,
+ NULL, &derLen, NULL, NULL) || derLen == 0) {
+ if (!CryptStringToBinaryA(pem.data(), (DWORD)pem.size(), CRYPT_STRING_BASE64HEADER,
+ NULL, &derLen, NULL, NULL) || derLen == 0) {
+ Mprintf("[TLS] failed to decode certificate %s\n", source.c_str());
+ return false;
+ }
+ }
+ std::vector der(derLen);
+ if (!CryptStringToBinaryA(pem.data(), (DWORD)pem.size(), CRYPT_STRING_ANY,
+ der.data(), &derLen, NULL, NULL)) {
+ if (!CryptStringToBinaryA(pem.data(), (DWORD)pem.size(), CRYPT_STRING_BASE64HEADER,
+ der.data(), &derLen, NULL, NULL)) {
+ Mprintf("[TLS] failed to decode certificate %s\n", source.c_str());
+ return false;
+ }
+ }
+
+ PCCERT_CONTEXT cert = CertCreateCertificateContext(X509_ASN_ENCODING, der.data(), derLen);
+ if (!cert) {
+ Mprintf("[TLS] invalid certificate %s\n", source.c_str());
+ return false;
+ }
+
+ HCERTSTORE store = CertOpenStore(CERT_STORE_PROV_MEMORY, X509_ASN_ENCODING,
+ 0, CERT_STORE_CREATE_NEW_FLAG, NULL);
+ if (!store) {
+ CertFreeCertificateContext(cert);
+ Mprintf("[TLS] CertOpenStore failed\n");
+ return false;
+ }
+ if (!CertAddCertificateContextToStore(store, cert, CERT_STORE_ADD_REPLACE_EXISTING, NULL)) {
+ CertCloseStore(store, 0);
+ CertFreeCertificateContext(cert);
+ Mprintf("[TLS] CertAddCertificateContextToStore failed\n");
+ return false;
+ }
+ CertFreeCertificateContext(cert);
+
+ *outStore = store;
+ return true;
+}
+
+void InitTls()
+{
+ std::string pem, source;
+ if (!ReadTrustAnchor(pem, source)) {
+ // 无证书 → 明文(默认,与老客户端一致)
+ g_tlsEnabled = false;
+ g_tlsError = false;
+ Mprintf("[TLS] disabled: no trust anchor configured, plaintext mode\n");
+ return;
+ }
+
+ // 证书已配置:管理员已「颁发」,必须走 TLS;加载失败即拒绝连接(fail-closed),
+ // 不能静默回退明文。
+ HCERTSTORE store = NULL;
+ if (!LoadTrustAnchor(pem, source, &store)) {
+ g_tlsEnabled = false;
+ g_tlsError = true;
+ Mprintf("[TLS] FATAL: trust anchor (%s) present but failed to load; refusing connections (fail-closed)\n",
+ source.c_str());
+ return;
+ }
+
+ // 严格钉扎:链引擎只信任该 store 里的根证书,排除系统根库。否则攻击者拿一张任意
+ // 公共 CA 签发的证书就能通过 CertGetCertificateChain(默认引擎信任系统根库)。
+ CERT_CHAIN_ENGINE_CONFIG ec = {};
+ ec.cbSize = sizeof(ec);
+ ec.hExclusiveRoot = store;
+ HCERTCHAINENGINE engine = NULL;
+ if (!CertCreateCertificateChainEngine(&ec, &engine)) {
+ CertCloseStore(store, 0);
+ g_tlsEnabled = false;
+ g_tlsError = true;
+ Mprintf("[TLS] FATAL: CertCreateCertificateChainEngine failed; refusing connections (fail-closed)\n");
+ return;
+ }
+
+ g_tlsStore = store;
+ g_tlsEngine = engine;
+ g_tlsEnabled = true;
+ g_tlsError = false;
+ Mprintf("[TLS] enabled: trust anchor loaded (%s), all connections use TLS\n", source.c_str());
+}
+
+} // namespace
+
+bool TlsEnabled()
+{
+ std::call_once(g_tlsOnce, InitTls);
+ return g_tlsEnabled;
+}
+
+bool TlsLoadError()
+{
+ std::call_once(g_tlsOnce, InitTls);
+ return g_tlsError;
+}
+
+// ---- TlsChannel ----
+
+void TlsChannel::CloseHandles()
+{
+ std::lock_guard lk(m_ctxMtx);
+ if (m_ctxValid) { DeleteSecurityContext(&m_ctx); m_ctx = {}; m_ctxValid = false; }
+ if (m_credValid) { FreeCredentialsHandle(&m_cred); m_cred = {}; m_credValid = false; }
+ m_ready = false;
+ m_raw.clear();
+ m_pending.clear();
+}
+
+bool TlsChannel::SendAll(SOCKET sock, const void* data, int len)
+{
+ const char* p = (const char*)data;
+ int sent = 0;
+ while (sent < len) {
+ int n = ::send(sock, p + sent, len - sent, 0);
+ if (n == SOCKET_ERROR || n == 0)
+ return false;
+ sent += n;
+ }
+ return true;
+}
+
+bool TlsChannel::VerifyServerCert()
+{
+ PCCERT_CONTEXT remote = NULL;
+ SECURITY_STATUS sc = QueryContextAttributesA(&m_ctx, SECPKG_ATTR_REMOTE_CERT_CONTEXT, &remote);
+ if (sc != SEC_E_OK || !remote)
+ return false;
+
+ if (!g_tlsStore || !g_tlsEngine) {
+ CertFreeCertificateContext(remote);
+ return false;
+ }
+
+ CERT_CHAIN_PARA para = {};
+ para.cbSize = sizeof(para);
+ PCCERT_CHAIN_CONTEXT chain = NULL;
+ // g_tlsEngine 的 hExclusiveRoot 已把信任锚限制为 g_tlsStore;hAdditionalStore 再传一次
+ // 该 store 供链引擎查找 CA/中间证书。只接受「无任何错误位」的链(含时间有效性)。
+ // 不设置任何 CERT_CHAIN_REVOCATION_CHECK_* 位 = 不做吊销检查(私有 CA 无 CRL/OCSP,
+ // 且这些位会触发联网,反而失败);只保留 CERT_CHAIN_DISABLE_AIA 禁止联网取中间证书。
+ BOOL ok = CertGetCertificateChain(g_tlsEngine, remote, NULL, g_tlsStore, ¶,
+ CERT_CHAIN_DISABLE_AIA,
+ NULL, &chain);
+ bool trusted = false;
+ if (ok && chain) {
+ trusted = (chain->TrustStatus.dwErrorStatus == CERT_TRUST_NO_ERROR);
+ }
+ if (chain) CertFreeCertificateChain(chain);
+ CertFreeCertificateContext(remote);
+ return trusted;
+}
+
+bool TlsChannel::Handshake(SOCKET sock)
+{
+ std::lock_guard lk(m_ctxMtx);
+ CloseHandles(); // 重连时清理上一次会话(recursive 锁,此处可重入)
+
+ SCHANNEL_CRED cred = {};
+ cred.dwVersion = SCHANNEL_CRED_VERSION;
+ cred.dwFlags = SCH_CRED_MANUAL_CRED_VALIDATION | SCH_CRED_NO_DEFAULT_CREDS;
+ cred.grbitEnabledProtocols = SP_PROT_TLS1_2_CLIENT; // TLS 1.2(与 Go 端 MinVersion 对齐)
+
+ TimeStamp tsExpiry = {};
+ SECURITY_STATUS sc = AcquireCredentialsHandleA(
+ NULL, (SEC_CHAR*)"Microsoft Unified Security Protocol Provider",
+ SECPKG_CRED_OUTBOUND, NULL, &cred, NULL, NULL, &m_cred, &tsExpiry);
+ if (sc != SEC_E_OK) {
+ Mprintf("[TLS] AcquireCredentialsHandle failed: 0x%08X\n", (unsigned)sc);
+ return false;
+ }
+ m_credValid = true;
+
+ DWORD flags = ISC_REQ_SEQUENCE_DETECT | ISC_REQ_REPLAY_DETECT | ISC_REQ_CONFIDENTIALITY
+ | ISC_REQ_ALLOCATE_MEMORY | ISC_REQ_STREAM | ISC_REQ_MANUAL_CRED_VALIDATION;
+
+ SecBuffer outBuf = { 0, SECBUFFER_TOKEN, NULL };
+ SecBufferDesc outDesc = { SECBUFFER_VERSION, 1, &outBuf };
+ ULONG attrs = 0;
+
+ // 首次调用产生 ClientHello
+ sc = InitializeSecurityContextA(&m_cred, NULL, NULL, flags, 0, SECURITY_NATIVE_DREP,
+ NULL, 0, &m_ctx, &outDesc, &attrs, &tsExpiry);
+ m_ctxValid = true; // 上下文已分配,之后必须 DeleteSecurityContext
+ if (sc != SEC_I_CONTINUE_NEEDED && sc != SEC_E_OK) {
+ Mprintf("[TLS] InitializeSecurityContext(initial) failed: 0x%08X\n", (unsigned)sc);
+ CloseHandles();
+ return false;
+ }
+ if (outBuf.cbBuffer > 0 && outBuf.pvBuffer) {
+ if (!SendAll(sock, outBuf.pvBuffer, (int)outBuf.cbBuffer)) {
+ FreeContextBuffer(outBuf.pvBuffer);
+ CloseHandles();
+ return false;
+ }
+ FreeContextBuffer(outBuf.pvBuffer);
+ }
+
+ std::vector hs;
+ char raw[8192];
+ while (sc == SEC_I_CONTINUE_NEEDED || sc == SEC_E_INCOMPLETE_MESSAGE ||
+ sc == SEC_I_INCOMPLETE_CREDENTIALS)
+ {
+ int r = recv(sock, raw, sizeof(raw), 0);
+ if (r == SOCKET_ERROR || r == 0) {
+ Mprintf("[TLS] handshake recv failed/EOF\n");
+ CloseHandles();
+ return false;
+ }
+ hs.insert(hs.end(), (BYTE*)raw, (BYTE*)raw + r);
+
+ SecBuffer inBufs[2] = {
+ { (ULONG)hs.size(), SECBUFFER_TOKEN, hs.data() },
+ { 0, SECBUFFER_EMPTY, NULL },
+ };
+ SecBufferDesc inDesc = { SECBUFFER_VERSION, 2, inBufs };
+
+ outBuf = { 0, SECBUFFER_TOKEN, NULL };
+ outDesc = { SECBUFFER_VERSION, 1, &outBuf };
+
+ sc = InitializeSecurityContextA(&m_cred, &m_ctx, NULL, flags, 0, SECURITY_NATIVE_DREP,
+ &inDesc, 0, NULL, &outDesc, &attrs, &tsExpiry);
+
+ // 裁剪已消费的输入,仅保留 SECBUFFER_EXTRA 指向的未消费尾部
+ if (inBufs[1].BufferType == SECBUFFER_EXTRA) {
+ int extra = inBufs[1].cbBuffer;
+ int consumed = (int)hs.size() - extra;
+ hs.erase(hs.begin(), hs.begin() + consumed);
+ } else {
+ hs.clear();
+ }
+
+ if (outBuf.cbBuffer > 0 && outBuf.pvBuffer) {
+ if (!SendAll(sock, outBuf.pvBuffer, (int)outBuf.cbBuffer)) {
+ FreeContextBuffer(outBuf.pvBuffer);
+ CloseHandles();
+ return false;
+ }
+ FreeContextBuffer(outBuf.pvBuffer);
+ }
+ }
+
+ if (sc != SEC_E_OK) {
+ Mprintf("[TLS] handshake failed: 0x%08X\n", (unsigned)sc);
+ CloseHandles();
+ return false;
+ }
+
+ if (!VerifyServerCert()) {
+ Mprintf("[TLS] server certificate not trusted against yama-ca.pem (fail-closed)\n");
+ CloseHandles();
+ return false;
+ }
+
+ sc = QueryContextAttributesA(&m_ctx, SECPKG_ATTR_STREAM_SIZES, &m_sizes);
+ if (sc != SEC_E_OK) {
+ Mprintf("[TLS] QueryContextAttributes(StreamSizes) failed: 0x%08X\n", (unsigned)sc);
+ CloseHandles();
+ return false;
+ }
+
+ m_ready = true;
+ Mprintf("[TLS] handshake OK (TLS 1.2, server cert trusted)\n");
+ return true;
+}
+
+int TlsChannel::Recv(SOCKET sock, char* out, int outLen)
+{
+ std::lock_guard lk(m_ctxMtx);
+ if (!m_ready || outLen <= 0) {
+ Mprintf("[TLS] Recv called but m_ready=%d (outLen=%d), returning SOCKET_ERROR\n",
+ (int)m_ready, outLen);
+ return SOCKET_ERROR;
+ }
+
+ if (!m_pending.empty()) {
+ int n = (int)m_pending.size() < outLen ? (int)m_pending.size() : outLen;
+ memcpy(out, m_pending.data(), n);
+ m_pending.erase(m_pending.begin(), m_pending.begin() + n);
+ return n;
+ }
+
+ 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 (;;) {
+ SecBuffer bufs[4] = {
+ { (ULONG)m_raw.size(), SECBUFFER_DATA, m_raw.data() },
+ { 0, SECBUFFER_EMPTY, NULL },
+ { 0, SECBUFFER_EMPTY, NULL },
+ { 0, SECBUFFER_EMPTY, NULL },
+ };
+ SecBufferDesc desc = { SECBUFFER_VERSION, 4, bufs };
+ SECURITY_STATUS sc = DecryptMessage(&m_ctx, &desc, 0, NULL);
+
+ if (sc == SEC_E_INCOMPLETE_MESSAGE)
+ break; // 记录不完整,再 recv
+ if (sc == SEC_I_CONTEXT_EXPIRED)
+ return 0; // close_notify
+ if (sc == SEC_I_RENEGOTIATE || FAILED(sc))
+ return SOCKET_ERROR;
+
+ // 流模式 DecryptMessage 会把输入 SECBUFFER_DATA 重组为 4 段:
+ // bufs[0]=STREAM_HEADER(记录头+nonce)、bufs[1]=DATA(明文)、
+ // bufs[2]=STREAM_TRAILER(标签)、bufs[3]=EXTRA(下一条记录)。
+ // 明文在 bufs[1].pvBuffer,不在 m_raw 首字节。
+ int decLen = bufs[1].cbBuffer;
+ const BYTE* plain = (const BYTE*)bufs[1].pvBuffer;
+ int extra = (bufs[3].BufferType == SECBUFFER_EXTRA) ? bufs[3].cbBuffer : 0;
+ int consumed = (int)m_raw.size() - extra;
+ m_pending.insert(m_pending.end(), plain, plain + decLen);
+ m_raw.erase(m_raw.begin(), m_raw.begin() + consumed);
+
+ if (!m_pending.empty()) {
+ int n = (int)m_pending.size() < outLen ? (int)m_pending.size() : outLen;
+ memcpy(out, m_pending.data(), n);
+ m_pending.erase(m_pending.begin(), m_pending.begin() + n);
+ return n;
+ }
+ if (m_raw.empty())
+ break; // 无明文(如 alert/CCS 记录),继续 recv
+ }
+ }
+}
+
+int TlsChannel::Send(SOCKET sock, const char* data, int len)
+{
+ std::lock_guard lk(m_ctxMtx);
+ if (!m_ready)
+ return SOCKET_ERROR;
+ if (len <= 0)
+ return 0;
+
+ ULONG maxMsg = m_sizes.cbMaximumMessage > 0 ? m_sizes.cbMaximumMessage : 16384;
+ int total = 0;
+ while (total < len) {
+ int chunk = (len - total > (int)maxMsg) ? (int)maxMsg : (len - total);
+
+ std::vector rec(m_sizes.cbHeader + chunk + m_sizes.cbTrailer);
+ BYTE* hdr = rec.data();
+ BYTE* body = hdr + m_sizes.cbHeader;
+ BYTE* trailer = body + chunk;
+ memcpy(body, data + total, chunk);
+
+ SecBuffer bufs[4] = {
+ { m_sizes.cbHeader, SECBUFFER_STREAM_HEADER, hdr },
+ { (ULONG)chunk, SECBUFFER_DATA, body },
+ { m_sizes.cbTrailer, SECBUFFER_STREAM_TRAILER, trailer },
+ { 0, SECBUFFER_EMPTY, NULL },
+ };
+ SecBufferDesc desc = { SECBUFFER_VERSION, 4, bufs };
+ SECURITY_STATUS sc = EncryptMessage(&m_ctx, 0, &desc, 0);
+ if (FAILED(sc)) {
+ return total > 0 ? total : SOCKET_ERROR;
+ }
+
+ int wireLen = bufs[0].cbBuffer + bufs[1].cbBuffer + bufs[2].cbBuffer;
+ if (!SendAll(sock, rec.data(), wireLen)) {
+ return total > 0 ? total : SOCKET_ERROR;
+ }
+ total += chunk;
+ }
+ return total;
+}
+
+#endif // _WIN32
diff --git a/client/TlsChannel.h b/client/TlsChannel.h
new file mode 100644
index 0000000..0c6fafc
--- /dev/null
+++ b/client/TlsChannel.h
@@ -0,0 +1,88 @@
+// TlsChannel.h — Windows 客户端单向 TLS(Schannel)会话封装
+//
+// 用途:为 YAMA 客户端(被控端)提供「按需开启」的传输层加密。当信任锚证书存在时
+// (注册表 Software\\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
+
+#ifndef SECURITY_WIN32
+#define SECURITY_WIN32 1
+#endif
+#include
+#include
+
+#include
+#include
+
+// 进程级 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);
+
+ // 是否有已解密但尚未交付给应用层的明文(供接收线程在 select 前优先消费)。
+ bool HasPending() const { return !m_pending.empty(); }
+
+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 m_raw; // 尚未解密 / 未消费完的 TLS 记录原始字节
+ std::vector m_pending; // 已解密、未交付给应用层的明文
+};
+
+#endif // _WIN32
diff --git a/client/ghost_vs2015.vcxproj b/client/ghost_vs2015.vcxproj
index 46f8598..70713e1 100644
--- a/client/ghost_vs2015.vcxproj
+++ b/client/ghost_vs2015.vcxproj
@@ -233,6 +233,7 @@
+
@@ -299,6 +300,7 @@
+
diff --git a/client/ghost_vs2015.vcxproj.filters b/client/ghost_vs2015.vcxproj.filters
new file mode 100644
index 0000000..3759f1b
--- /dev/null
+++ b/client/ghost_vs2015.vcxproj.filters
@@ -0,0 +1,204 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ decrypt
+
+
+ decrypt
+
+
+ decrypt
+
+
+ decrypt
+
+
+ decrypt
+
+
+ decrypt
+
+
+ decrypt
+
+
+ decrypt
+
+
+ decrypt
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ decrypt
+
+
+ decrypt
+
+
+ decrypt
+
+
+ decrypt
+
+
+ decrypt
+
+
+ decrypt
+
+
+ decrypt
+
+
+ decrypt
+
+
+ decrypt
+
+
+ decrypt
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {4b839e4d-0a78-4ee2-8965-bfb259b28273}
+
+
+
\ No newline at end of file
diff --git a/docs/TransportTLS_Design.md b/docs/TransportTLS_Design.md
new file mode 100644
index 0000000..37cb670
--- /dev/null
+++ b/docs/TransportTLS_Design.md
@@ -0,0 +1,313 @@
+# 传输层单向 TLS 加密设计(按需加密)
+
+> 版本:v0.5 · 状态:**Go 服务端 + Windows 客户端已实现并验证;信任锚改存注册表;MFC 主控端(阶段 3)未施工** · 日期:2026-10-14
+> 目标:让 YAMA 设备端口(TCP 6543)在继续兼容老客户端(明文/XOR)的同时,允许新客户端在拿到证书后按需开启**单向 TLS** 加密传输。
+> 本期范围:**Go 服务端(`server/go`)+ Windows 客户端**;MFC 服务端(`server/2015Remote`)延后。
+>
+> 需求原文(三点,逐一对应下文):
+> 1. 新客户端**默认不加密**,行为与老客户端完全一致;
+> 2. 某些高要求场景,管理员「颁发一个证书给该客户端 + 重启」,该客户端即改走加密;
+> 3. 服务端同一端口(6543)同时兼容明文与 TLS 两种客户端,老客户端零改动。
+
+## 1. 目标与范围
+
+### 目标
+
+- 同一 `6543` 端口上,服务端自动区分「明文客户端」与「TLS 客户端」,按连接路由。
+- 加密是**按部署、按客户端开关**的:证书(注册表值或文件)在 → TLS;证书不在 → 明文(与老客户端一致)。
+- 老客户端(无证书概念)行为不变、零改动。
+- 满足 Measure 1 的「传输层真加密 + 抗重放」目标(对 TLS 客户端而言)。
+
+### 非目标(Non-goals,本次不做)
+
+- 不做 **mTLS / 客户端证书**:单向 TLS 不验证客户端证书,客户端身份仍由隧道内的现有登录(`PwdHmac` V1 / `ECDSA` V2)承担。
+- 不做传输层 PSK(`YAMA_TRANSPORT_KEY`)方案——本设计改用证书,更贴合「颁发证书」的运维模型。
+- 不改应用层协议(flag 帧格式 / XOR / zstd),不做「TLS 内去掉 XOR」的二次精简(后续可选)。
+- 不做加密开关的运行时热切换(证书只在客户端启动时读取一次,改配置需重启——这正是需求)。
+- 不做 Linux / macOS / Android 客户端的 TLS 接入(本文以 Windows MFC 客户端为主,其余平台同样式跟进)。
+- 不做 **MFC 服务端(`server/2015Remote`)的 TLS 接入**——本期只做 Go 服务端(`server/go`),MFC 主控端延后到阶段 3。
+
+## 2. 现状与复用
+
+### 2.1 当前传输(已核实)
+
+服务端 TCP 传输链路:`server/go/server/server.go` `acceptLoop()`(329–360 行)`Accept` 后 `go handleConnection(conn)`;`handleConnection()`(363–425 行)读入 `ctx.InBuffer`,交给 `protocol.Parser`。
+
+`server/go/protocol/parser.go` `Parse()`(51–85 行):
+
+1. 可选剥离 HTTP-mask(`POST /...\r\n\r\n` 外壳,`common/mask.h` 的 `HttpMask`);
+2. `parseHeader()`(88–140 行)调用 `CheckHead()` 识别协议;
+3. `parsePacket()`(143–225 行)解头 + XOR 解码 + zstd 解压。
+
+`server/go/protocol/header.go` `CheckHead()`(149–165 行)对前 12 字节依次尝试 8 种头混淆 × 4 种魔数,命中即得 `FlagType`:
+
+| FlagType | 魔数 | 首字节 |
+|---|---|---|
+| FlagHell(4) | `HELL` | `H`=0x48 |
+| FlagShine(1) | `Shine` | `S`=0x53 |
+| FlagFuck(2) | `<>` | `<`=0x3C |
+| FlagHello(3) | `Hello?` | `H`=0x48 |
+| HTTP-mask | `POST ` | `P`=0x50 |
+
+> 注意:上表「首字节」是**解密后**的魔数首字节。实际线上,客户端(`common/header.h` 的 `GetHead`)会用 `time() % 12` 随机选一个混淆方法对前 4 字节魔数加密(仅在 `superAdmin` 命中的 `HellEncoder` 路径;默认 `ProtocolEncoder` 直接发明文 `Shine`)。因此**原始首字节可能是任意值**,包括 `0x16`。这正是 TLS 检测不能只看前两字节、必须再加一字节判据的原因(见 §3.1)。
+
+当前「加密」为 `codec.go` 的 `xorDecoder16`(XOR + 两轮伪随机交换,密钥是明文带内 1~2 字节)+ `header.go` 的 V0~V6 头混淆——**等于明文**。这正是 Measure 1 要解决的。
+
+### 2.2 客户端登录(已核实,隧道内照常)
+
+- V1:`client/KernelManager.cpp:1857` 从配置读 `settings/PwdHmac`(预计算摘要),服务端 `auth.go:240` `VerifyMessage(YAMA_PWD, passcode, hmacSig)` 校验。`YAMA_PWD` 只存在服务端,客户端只有摘要,不持有 `YAMA_PWD`。
+- V2:`auth.go:188` ECDSA P-256 验签(`v2:BASE64`),客户端持私钥、服务端持公钥。
+
+### 2.3 关键结论(本设计的基础)
+
+**TLS 作为「透明传输包装」,应用层协议一行不改。** 即:
+
+```
+明文客户端: [TCP] = flag 帧 + XOR payload + zstd
+TLS 客户端: [TCP][TLS] = { flag 帧 + XOR payload + zstd }
+```
+
+服务端 `parser.go`/`codec.go`/`header.go`/handler 全部不动;TLS 客户端解密后的字节流就是同一套协议。唯一新增的是**连接建立时的 TLS 检测与解包**。XOR 套在 TLS 里冗余但无害,保留它换来「应用协议零改动、老新客户端共用同一套代码」。
+
+## 3. 总体方案
+
+### 3.1 同端口共存 + 首字节检测
+
+服务端在 `handleConnection` 开头(`Accept` 之后)偷看连接开头 6 字节:
+
+- 命中 `0x16 0x03 … 0x01`(TLS 握手记录 + TLS 版本号 + 握手类型 ClientHello)→ 交给 `tls.Server` 解包;
+- 其它 → 走现有明文路径。
+
+**为何要看第 6 字节**:客户端会随机混淆魔数头(见 §2.1),所以明文连接的前两字节**可能**恰好是 `0x16 0x03`。但混淆只作用于前 4 字节魔数,字节 5 对混淆后的 `HELL` 头恒为 `0x00`,对明文 `Shine`/`POST` 变体是 ASCII 字符——**永远不会是 `0x01`**;而任何 TLS 握手记录里,字节 5 就是握手消息类型,ClientHello 恒为 `0x01`。于是 `0x16 0x03` 加 `b[5]==0x01` 三重判据让误判概率趋近于零。
+
+```
+accept
+ ├─ 现有 MaxConnections / per-IP 限流(保持,先于 TLS 握手,廉价拒绝 + 抗握手 DoS)
+ ├─ peek 3 字节
+ │ ├─ 0x16 0x03 → tls.Server(conn, tlsCfg)(惰性握手,首次 Read 触发)
+ │ └─ 其它 → 明文
+ └─ handleConnection(conn) // 不区分明文/TLS,tls.Conn 实现 net.Conn
+```
+
+### 3.2 客户端行为
+
+客户端启动时检查「证书文件是否存在」:
+
+- 不在 → 明文(与老客户端完全一致,默认即此);
+- 在 → 所有出站 TCP 连接(主连接 + screen/terminal/file 子连接)统一走 TLS。
+
+### 3.3 架构图
+
+```
+┌──────────── 被控端(客户端,无证书) ────────────┐
+│ TCP ── flag+XOR+zstd ─────────────────────────► │ 明文
+└───────────────────────────────────────────────────┘
+┌──────────── 被控端(客户端,有证书) ────────────┐
+│ TCP ── TLS ── { flag+XOR+zstd } ──────────────► │ 加密
+└───────────────────────────────────────────────────┘
+ │
+ ▼
+ ┌──────────────────────┐
+ │ 服务端 6543 │
+ │ peek 首字节路由 │
+ │ ├─ 0x16 0x03 → TLS │
+ │ └─ 其它 → 明文 │
+ │ ── 同一套 parser/handler │
+ └──────────────────────┘
+```
+
+## 4. 证书模型(单向 TLS)
+
+- 服务端持有一张**服务端证书 + 私钥**(可自签名,或由自建 CA 签发)。
+- 客户端持有**信任锚证书**(CA 证书,或自签名服务端证书本身)用于在握手时校验服务端。
+- 「颁发证书给某客户端」= 把信任锚证书投递到该客户端(**注册表值**,见 §6.1/§9.3)。证书是**共享的信任锚**(不是每个客户端一张唯一证书),「颁发」的本质是**决定给哪个客户端开加密**。
+- 单向 TLS **不认证客户端身份**;客户端身份仍由隧道内的登录(V1 `PwdHmac` / V2 ECDSA)承担。
+- 信任锚首选存注册表 `Software\\settings\tls_cert`(REG_SZ 直接存 PEM 正文);兼容旧方式:`settings.ini` `[settings] tls_cert` 路径,或 exe 同目录 `yama-ca.pem` 文件。
+
+## 5. 服务端改动
+
+### 5.1 配置(`server/go/server/server.go` `Config`)
+
+新增两个字段,均为证书/私钥文件路径;空 = 禁用 TLS(仅明文,向后兼容):
+
+```go
+TLSCertFile string // 服务端证书(PEM)
+TLSKeyFile string // 服务端私钥(PEM)
+```
+
+`cmd/main.go` 读取环境变量(沿用现有 `YAMA_*` 命名):
+
+```go
+cfg.TLSCertFile = os.Getenv("YAMA_TLS_CERT")
+cfg.TLSKeyFile = os.Getenv("YAMA_TLS_KEY")
+```
+
+仅当两者都非空才构建 `*tls.Config`;否则 `s.tlsCfg == nil`,`acceptLoop` 走纯明文。
+
+### 5.2 `tls.Config`
+
+```go
+tlsCfg := &tls.Config{
+ Certificates: []tls.Certificate{cert},
+ MinVersion: tls.VersionTLS12,
+ // 单向 TLS:不设 ClientAuth / ClientCAs
+}
+```
+
+### 5.3 `handleConnection` 检测 + 回放
+
+标准 peek 回放:`bufio.Reader` 预读 6 字节,封装成 `bufferedConn`(实现 `net.Conn`,`Read` 委托给 `Reader`),判断后把已读字节一并交给目标。嗅探本身设了 `ReadTimeout` 截止,超时/EOF 则退回原始 conn 走明文,由读循环处理:
+
+```go
+func (s *Server) wrapTLS(conn net.Conn) net.Conn {
+ br := bufio.NewReader(conn)
+ _ = conn.SetReadDeadline(time.Now().Add(s.config.ReadTimeout)) // 嗅探受超时约束
+ hdr, err := br.Peek(6)
+ _ = conn.SetReadDeadline(time.Time{})
+ if err != nil {
+ return conn // 超时/EOF:走明文,交由读循环处理
+ }
+ bc := &bufferedConn{Conn: conn, r: br}
+ if isTLSClientHello(hdr) { // 0x16 0x03 且 b[5]==0x01
+ return tls.Server(bc, s.tlsCfg) // 握手惰性,首次 Read 触发
+ }
+ return bc
+}
+```
+
+### 5.4 握手超时
+
+`handleConnection` 首次 `conn.Read` 会触发 TLS 握手,而它已按 `ReadTimeout` 设了 `SetReadDeadline`,因此握手天然受 `ReadTimeout` 约束,无需额外机制。per-IP 限流(现有)继续在握手前生效,压制握手 DoS。
+
+### 5.5 明确不动的部分
+
+- `parser.go` / `codec.go` / `header.go`:**零改动**。
+- `handleConnection` / `processData` / handler 栈:**零改动**(`tls.Conn` 满足 `net.Conn`)。
+
+## 6. 客户端改动
+
+### 6.1 证书检测
+
+启动时(连接前)按优先级找信任锚:① 注册表 `Software\\settings\tls_cert`(REG_SZ 存 PEM 正文)→ ② `settings.ini` `[settings] tls_cert` 路径 → ③ exe 同目录 `yama-ca.pem`。找到即走 TLS,否则明文。实现:`client/TlsChannel.cpp` 的 `ReadTrustAnchor()` + `TlsEnabled()`(`std::call_once` 惰性加载、线程安全、幂等)。
+
+### 6.2 TLS 栈选型(已定:Schannel)
+
+- **已选 Schannel(Windows 原生 SSPI)**:零第三方依赖/零额外 DLL 部署,`secur32.lib`/`crypt32.lib` 均为系统库(`secur32.lib` 已在 `CFFmpegH264Encoder.cpp:38` 链接)。
+- 信任锚(PEM/DER 正文,来自注册表或文件)经 `CryptStringToBinaryA` 解码 → `CertCreateCertificateContext` → 内存 `HCERTSTORE`;握手后 `CertGetCertificateChain` 把服务端证书链到该信任锚,`dwErrorStatus == 0` 才放行(含时间有效性)。**严格钉扎**:链引擎用 `hExclusiveRoot` 只信任该 store,排除系统根库,否则公共 CA 签发的证书也能通过。**fail-closed**。
+- 握手凭据:`SCH_CRED_MANUAL_CRED_VALIDATION | SCH_CRED_NO_DEFAULT_CREDS`,`grbitEnabledProtocols = SP_PROT_TLS1_2_CLIENT`(与 Go 端 `MinVersion: TLS 1.2` 对齐)。
+- 单向 TLS:不提供客户端证书;客户端身份仍由隧道内登录承担。
+
+### 6.3 连接层封装
+
+在 socket 建立后、应用协议首包前插入 TLS 握手(`IOCPClient::ConnectServer`,`m_bConnected` 置位前);`ReceiveData`/`SendTo` 改走 `TlsChannel` 会话。所有出站连接(主连接 + 子连接)统一遵循 TLS 开关,因此子连接同样被加密——服务端按连接检测,天然支持。实现:`client/TlsChannel.{h,cpp}`,接入点 `client/IOCPClient.{h,cpp}`。
+
+### 6.4 关键:无静默降级(fail-closed)
+
+**证书文件存在但握手失败(证书不匹配 / 中间人)时,客户端必须报错并放弃连接,禁止回退到明文。** 否则攻击者可制造降级。这必须写进实现与测试。
+
+### 6.5 实现要点:Schannel 流模式 `DecryptMessage` 的缓冲重组
+
+Schannel 流模式(`ISC_REQ_STREAM`)下,`DecryptMessage` 会把输入的那段 `SECBUFFER_DATA`
+**重组**为 4 段输出,而不是就地解密到原偏移:
+
+| 索引 | 类型 | 内容 |
+|---|---|---|
+| `bufs[0]` | `SECBUFFER_STREAM_HEADER` | TLS 记录头(5) + 显式 nonce(8) |
+| `bufs[1]` | `SECBUFFER_DATA` | **明文** |
+| `bufs[2]` | `SECBUFFER_STREAM_TRAILER` | 认证标签 |
+| `bufs[3]` | `SECBUFFER_EXTRA` | 下一条记录的剩余密文(如有) |
+
+因此客户端 `Recv` 必须从 `bufs[1].pvBuffer`/`bufs[1].cbBuffer` 取明文、`EXTRA` 从 `bufs[3]`
+取。若误用 `bufs[0]`(记录头+nonce)或从原缓冲首字节取数,会把 TLS 记录头 `17 03 03 …`
+当作明文交给协议解析器(表现为 `Unknown header`)。`Send` 方向相反:`EncryptMessage` 用
+`STREAM_HEADER/DATA/TRAILER` 三段布局,本就正确。这一处是联调阶段实际踩到的、唯一的方向性
+不对称 bug,已修复。
+
+## 7. 兼容矩阵
+
+| 客户端 | 服务端行为 | 结果 |
+|---|---|---|
+| 老客户端(无证书概念) | 明文路径 | 与现状完全一致 |
+| 新客户端 · 无证书 | 明文路径 | 与老客户端行为一致(默认) |
+| 新客户端 · 有证书 | TLS 路径 | 全量加密(主 + 子连接) |
+
+服务端 TLS 未配置(`YAMA_TLS_CERT/KEY` 为空)时,一切与现状一致,TLS 客户端握手自然失败(fail-closed)。
+
+## 8. 安全考量
+
+- **TLS 提供**:机密性、完整性、服务端身份(客户端校验)、抗重放(TLS record 序号 + nonce)。即对 TLS 客户端而言,Measure 1(真加密 + 防重放)达成。
+- **TLS 不提供**(单向):客户端证书身份——仍由隧道内登录承担;对「未颁证客户端」不强制加密(这是本设计刻意保留的兼容性,也是薄弱点:部署安全性取决于最弱客户端)。
+- **明文前导**:TLS ClientHello 前几字节明文(协议固有),仅暴露「这是 TLS 客户端」,不含业务数据;客户端不设 SNI(连接目标是 IP,无域名)。
+- **子连接**:客户端统一走 TLS,screen/terminal/file 子连接一并加密;对应 Measure 2 的「源 IP 绑定」降级为纵深防御,不再是唯一防线。
+- **证书轮换**:重签服务端证书后,需向已颁证客户端重新投递新信任锚(或在证书到期前提前过渡)。自签名建议 `-days 3650`,到期前统一更换。
+- **协议强度**:`MinVersion` 固定 TLS 1.2+,套件用 Go 默认安全集(`tls.Config` 默认已排除弱套件)。
+
+## 9. 部署与运维
+
+### 9.1 生成证书
+
+自签名(最简,客户端信任锚即 `yama-server.crt`):
+
+```bash
+openssl req -x509 -newkey rsa:2048 -nodes \
+ -keyout yama-server.key -out yama-server.crt \
+ -days 3650 -subj "/CN=yama-server"
+```
+
+或自建 CA + 签服务端证书(便于轮换,客户端信任锚为 `ca.crt`):
+
+```bash
+openssl req -x509 -newkey rsa:2048 -nodes -keyout ca.key -out ca.crt -days 3650 -subj "/CN=yama-ca"
+openssl req -newkey rsa:2048 -nodes -keyout server.key -out server.csr -subj "/CN=yama-server"
+openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 3650
+```
+
+### 9.2 服务端
+
+```bash
+YAMA_TLS_CERT=/etc/yama/server.crt \
+YAMA_TLS_KEY=/etc/yama/server.key \
+./server_linux_amd64 ...
+```
+
+### 9.3 客户端「颁发 + 重启」
+
+首选:把信任锚 PEM 正文写入注册表值 `Software\\settings\tls_cert`(REG_SZ),重启客户端:
+
+```powershell
+$pem = Get-Content -Raw 'C:\path\yama-ca.pem'
+New-Item -Path 'HKCU:\Software\\settings' -Force | Out-Null
+Set-ItemProperty -Path 'HKCU:\Software\\settings' -Name 'tls_cert' -Value $pem -Type String
+```
+
+- `` = 客户端 exe 全路径小写后的 FNV 哈希(`common/iniFile.h` 的 `GetRegistryName()`)。
+- 客户端以服务(SYSTEM)运行时,注册表落在会话用户的 `HKEY_USERS\`(`GetCurrentUserRegistryKey()`)。
+- REG_SZ 直接存 PEM 正文(含换行);客户端动态读取,不受 `iniFile::GetStr` 512 字节固定缓冲限制。
+
+兼容旧方式:把信任锚复制到客户端目录并命名为 `yama-ca.pem`(或经 `settings.ini` `[settings] tls_cert` 指向),重启客户端。
+
+移除注册表值(或删除文件)并重启即退回明文。**远程下发/撤销信任锚属后续工作,本期仅支持本地写注册表。**
+
+## 10. 测试计划
+
+1. **检测单元**:`isTLSClientHello` 对 `0x16 0x03 … 0x01`(TLS 1.2/1.3 ClientHello)/ `POST ` / `Shine` / 混淆 `HELL`(首两字节恰好 `0x16 0x03` 但 `b[5]==0x00`)/ 短于 6 字节 等判定的单测(已实现 `server/go/server/tls_test.go`)。
+2. **TLS 未配置**:不设 `YAMA_TLS_*` → 纯明文,行为与现状一致。
+3. **明文回归**:老客户端 + 新客户端(无证书)均走明文,登录/屏幕/终端/文件全链路不回归。
+4. **TLS 加密**:新客户端(有证书)→ 握手成功,登录/屏幕/终端/文件子连接全部在 TLS 内跑通;抓包确认 payload 不可读。
+5. **混合并发**:明文与 TLS 客户端同时在线,互不干扰。
+6. **fail-closed**:客户端有证书但服务端证书不匹配 / 中间人 → 客户端报错断开,**不**回退明文。
+7. **握手超时 / 限流**:恶意慢握手被 `ReadTimeout` + per-IP 限流压制。
+
+## 11. 分期与回滚
+
+- **阶段 1(Go 服务端,本期优先)**:`Config` + `tls.Config` + `acceptLoop` 检测回放;明文路径零改动。可独立上线、独立验证——用 `openssl s_client` 或 Go 测试客户端即可联调 TLS 链路,无需真实客户端配合。
+- **阶段 2(Windows 客户端)**:证书检测 + TLS 栈 + fail-closed。
+- **阶段 3(MFC 服务端 `server/2015Remote`,延后)**:与 Go 服务端同款的 TLS 检测 + 解包接入。
+- **回滚**:服务端清空 `YAMA_TLS_CERT/KEY` 即回到纯明文;客户端删除注册表 `tls_cert` 值(或 `yama-ca.pem` 文件)即回明文。
+
+## 12. 待定问题
+
+1. ~~客户端 TLS 栈最终选型~~ → **已定 Schannel**(见 §6.2,2026-10-13 落地)。
+2. ~~信任锚文件固定名(`yama-ca.pem`)是否需可配置~~ → **已改为注册表 `tls_cert` 值优先、文件回退**(见 §6.1/§9.3,2026-10-14)。
+3. 是否同步把「TLS 内去掉 XOR」列为后续精简项(当前保留 XOR,换取零协议改动)。
+4. Linux / macOS / Android 客户端的 TLS 接入排期。
diff --git a/server/go/.vscode/launch.json b/server/go/.vscode/launch.json
index 5af3e26..bb490fe 100644
--- a/server/go/.vscode/launch.json
+++ b/server/go/.vscode/launch.json
@@ -13,7 +13,9 @@
"--http-port=8080"
],
"env": {
- "YAMA_WEB_ADMIN_PASS": "3.14159"
+ "YAMA_WEB_ADMIN_PASS": "3.14159",
+ "YAMA_TLS_CERT": "${workspaceFolder}/certs/server.crt",
+ "YAMA_TLS_KEY": "${workspaceFolder}/certs/server.key"
},
"console": "integratedTerminal",
"preLaunchTask": "sync-web-assets"
@@ -30,7 +32,9 @@
"--http-port=8080"
],
"env": {
- "YAMA_WEB_ADMIN_PASS": "3.14159"
+ "YAMA_WEB_ADMIN_PASS": "3.14159",
+ "YAMA_TLS_CERT": "${workspaceFolder}/certs/server.crt",
+ "YAMA_TLS_KEY": "${workspaceFolder}/certs/server.key"
},
"console": "integratedTerminal",
"buildFlags": "-gcflags='all=-N -l'",
diff --git a/server/go/certs/.gitignore b/server/go/certs/.gitignore
new file mode 100644
index 0000000..b8d37b9
--- /dev/null
+++ b/server/go/certs/.gitignore
@@ -0,0 +1,4 @@
+# 私钥与中间产物不入库;.crt 为公钥证书,可提交
+*.key
+*.csr
+*.srl
diff --git a/server/go/certs/server.crt b/server/go/certs/server.crt
new file mode 100644
index 0000000..e0a66e8
--- /dev/null
+++ b/server/go/certs/server.crt
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDLDCCAhSgAwIBAgIUPkDGqH/CbV5Qgy3mFPOJ4VwdZq0wDQYJKoZIhvcNAQEL
+BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MTAxNDEzMDMwMVoXDTM2MTAx
+MTEzMDMwMVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
+AAOCAQ8AMIIBCgKCAQEAh+nNFRRn9pK94OfgUv7hqVPI774HyUoMEzgiAx/XB3Mf
+UdEkJExvtMnHGT4FSlQ80oi1cIUuh/9svYCyPVwGuY1Bd5Jt8me9pN9CY7kJmFl+
+MlBv8mz34e5gNwrybd2orn/2RATmVPCgQ82nXRmY8vFqs6KA/Eqh5Sd8S+b2rUs7
+wzlg80lDQtH/7Bf7egsQ+cYS3Xeex6H82Q9AwVpvljjrst7HI+kxKEifHRsE/amW
+Bx88evqx8keE0AQ30gyXsbIRgID+LF8jcFf3WzMx+cqYStn9JJG+SlakaIlKw2I6
+FCES/UTSYoFsk06EaOy+od2+wo6jpfkJCGdEItlJlwIDAQABo3YwdDAgBgNVHREE
+GTAXgglsb2NhbGhvc3SHBH8AAAGHBAAAAAAwDAYDVR0TAQH/BAIwADAOBgNVHQ8B
+Af8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHQYDVR0OBBYEFBoxa/G+knDe
+DBs5vl+Nrdxy0uJpMA0GCSqGSIb3DQEBCwUAA4IBAQCBE/Xj13AzPhxZacbdAvpn
+xNxfF+3wJs9j0qo8w2LcB+x10pe8/ay2Gs0ThZc63o/LqDxlS6LrRvkjRlypM+Ls
+gahajW0hJs+sXfa+EhcJx6c1IR/uolSpRYuel/Ui6Xvhsr11BTpsgrHZdcYlKZqf
+75mX0byKRoPVkG/dIVPWpnoFkm8BLJdlZ0OpgGL9iyBVqgH0rw5hejnDZVVH2K4g
+0ZeE9QUbNl0oS01bpBJfCJfZT70+38yU61TTegpZEzCTBTMKccNU3hKnX7n9C7ZM
+JMb0lEZ/IuCaOxmpz5zmHPa4bibkAPSbpozOKP3S48zQPDEa58CvSUlVIrmo7+d+
+-----END CERTIFICATE-----
diff --git a/server/go/cmd/main.go b/server/go/cmd/main.go
index 9f541a1..af6a717 100644
--- a/server/go/cmd/main.go
+++ b/server/go/cmd/main.go
@@ -920,6 +920,19 @@ func main() {
}
}()
+ // Optional one-way TLS on the device port. Both vars must be set to enable;
+ // a cert without its key (or vice versa) is treated as disabled with a
+ // warning. The cert is loaded (and errors surfaced) inside Server.Start().
+ tlsCert := os.Getenv("YAMA_TLS_CERT")
+ tlsKey := os.Getenv("YAMA_TLS_KEY")
+ if tlsCert != "" && tlsKey != "" {
+ log.Info("TLS enabled for device port (one-way): cert=%s", tlsCert)
+ } else if tlsCert != "" || tlsKey != "" {
+ log.Warn("TLS disabled: both YAMA_TLS_CERT and YAMA_TLS_KEY must be set")
+ } else {
+ log.Info("TLS disabled (YAMA_TLS_CERT/YAMA_TLS_KEY not set) — device port is plaintext")
+ }
+
// Create servers for each port
var servers []*server.Server
for _, port := range ports {
@@ -928,6 +941,8 @@ func main() {
config.MaxConnections = 9999
config.PerIPMaxConnections = envInt("YAMA_PERIP_MAX_CONNS", config.PerIPMaxConnections)
config.PerIPConnRateLimit = envInt("YAMA_PERIP_CONN_RATE", config.PerIPConnRateLimit)
+ config.TLSCertFile = tlsCert
+ config.TLSKeyFile = tlsKey
srv := server.New(config)
srv.SetLogger(log.WithPrefix(fmt.Sprintf("Server:%d", port)))
diff --git a/server/go/server/server.go b/server/go/server/server.go
index e71f6a6..6b51696 100644
--- a/server/go/server/server.go
+++ b/server/go/server/server.go
@@ -2,6 +2,7 @@ package server
import (
"context"
+ "crypto/tls"
"io"
"net"
"sync"
@@ -40,6 +41,13 @@ type Config struct {
PerIPMaxConnections int // max concurrent connections per source IP (0 = unlimited)
PerIPConnRateLimit int // max new connections per PerIPConnRateWindow per IP (0 = unlimited)
PerIPConnRateWindow time.Duration // window for PerIPConnRateLimit
+
+ // TLSCertFile / TLSKeyFile enable optional one-way TLS on the device port.
+ // When both are set the server loads the cert and auto-detects TLS clients
+ // by their ClientHello; plaintext clients keep working unchanged. Both empty
+ // (the default) disables TLS and behaves exactly as before.
+ TLSCertFile string // server certificate (PEM)
+ TLSKeyFile string // server private key (PEM)
}
// DefaultConfig returns default configuration
@@ -75,6 +83,10 @@ type Server struct {
parser *protocol.Parser
ipLimit *ipLimiter
+ // tlsCfg is non-nil when one-way TLS is enabled (cert + key configured).
+ // wrapTLS consults it to sniff and wrap incoming connections.
+ tlsCfg *tls.Config
+
running atomic.Bool
wg sync.WaitGroup
ctx context.Context
@@ -234,6 +246,12 @@ func (s *Server) SetLogger(l *logger.Logger) {
// Start starts the server
func (s *Server) Start() error {
+ // Load optional one-way TLS cert up front so a bad path/key fails startup
+ // loudly rather than silently serving plaintext.
+ if err := s.loadTLSConfig(); err != nil {
+ return err
+ }
+
addr := net.JoinHostPort("0.0.0.0", itoa(s.config.Port))
listener, err := net.Listen("tcp", addr)
if err != nil {
@@ -360,7 +378,10 @@ func (s *Server) acceptLoop() {
}
// handleConnection handles a single connection
-func (s *Server) handleConnection(conn net.Conn) {
+func (s *Server) handleConnection(rawConn net.Conn) {
+ // Sniff and (when TLS is enabled) wrap TLS peers; plaintext passes through.
+ conn := s.wrapTLS(rawConn)
+
// Create context
ctx := connection.NewContext(conn, nil)
diff --git a/server/go/server/tls.go b/server/go/server/tls.go
new file mode 100644
index 0000000..4a9ee13
--- /dev/null
+++ b/server/go/server/tls.go
@@ -0,0 +1,113 @@
+package server
+
+import (
+ "bufio"
+ "crypto/tls"
+ "fmt"
+ "net"
+ "time"
+)
+
+// loadTLSConfig builds the server's one-way TLS configuration from the
+// configured cert/key paths, leaving s.tlsCfg nil when TLS is disabled. A cert
+// path set without its key (or vice versa) is a misconfiguration: TLS is left
+// disabled and a warning is logged rather than guessing. A load failure is
+// returned to the caller so startup fails loudly instead of silently serving
+// plaintext.
+func (s *Server) loadTLSConfig() error {
+ if s.config.TLSCertFile == "" && s.config.TLSKeyFile == "" {
+ return nil
+ }
+ if s.config.TLSCertFile == "" || s.config.TLSKeyFile == "" {
+ s.logger.Warn("TLS disabled: both YAMA_TLS_CERT and YAMA_TLS_KEY must be set (cert=%q key=%q)",
+ s.config.TLSCertFile, s.config.TLSKeyFile)
+ return nil
+ }
+
+ cert, err := tls.LoadX509KeyPair(s.config.TLSCertFile, s.config.TLSKeyFile)
+ if err != nil {
+ return fmt.Errorf("load TLS cert/key: %w", err)
+ }
+
+ s.tlsCfg = &tls.Config{
+ Certificates: []tls.Certificate{cert},
+ MinVersion: tls.VersionTLS12,
+ // One-way TLS: no ClientAuth / ClientCAs. Client identity is still
+ // established by the in-tunnel login (V1 PwdHmac / V2 ECDSA).
+ }
+ s.logger.Info("TLS enabled (one-way): cert=%s", s.config.TLSCertFile)
+ return nil
+}
+
+// isTLSClientHello reports whether b begins with a TLS handshake record whose
+// first message is a ClientHello:
+//
+// 0x16 — handshake record type
+// 0x03 — TLS major version (all TLS 1.x / 1.3 records)
+// 0x01 (b[5])— handshake message type ClientHello
+//
+// The first two bytes alone are insufficient: the C++ client obfuscates its
+// "HELL" header (common/header.h GetHead) with a per-connection key, so a
+// plaintext client can in principle emit 0x16 0x03 as its first two bytes. For
+// that obfuscated header byte 5 is always 0x00, and for the plain "Shine"/
+// "POST" variants it is an ASCII byte — never 0x01. Requiring b[5]==0x01 makes
+// a false positive effectively impossible.
+func isTLSClientHello(b []byte) bool {
+ return len(b) >= 6 && b[0] == 0x16 && b[1] == 0x03 && b[5] == 0x01
+}
+
+// bufferedConn replays bytes already peeked from a net.Conn, so the caller can
+// sniff a prefix and then hand the connection (peeked bytes included) to the
+// TLS or plaintext read loop. Read delegates to the buffered reader; everything
+// else delegates to the embedded net.Conn.
+type bufferedConn struct {
+ net.Conn
+ r *bufio.Reader
+}
+
+func (c *bufferedConn) Read(p []byte) (int, error) {
+ return c.r.Read(p)
+}
+
+// wrapTLS sniffs the first bytes of a freshly accepted connection and, when TLS
+// is enabled and the peer is speaking TLS, returns a tls.Conn wrapper. The TLS
+// handshake is lazy: it runs on the first Read, which handleConnection already
+// bounds with its ReadTimeout. Plaintext peers are returned unchanged (wrapped
+// in a bufferedConn so the sniffed prefix is replayed).
+func (s *Server) wrapTLS(conn net.Conn) net.Conn {
+ // NewContext only disables Nagle on a *net.TCPConn; once we wrap the conn
+ // (bufferedConn / tls.Conn) that assertion no longer fires, so disable it
+ // here on the raw conn to preserve screen-handshake responsiveness.
+ if tcp, ok := conn.(*net.TCPConn); ok {
+ _ = tcp.SetNoDelay(true)
+ }
+
+ if s.tlsCfg == nil {
+ return conn
+ }
+
+ br := bufio.NewReader(conn)
+ if s.config.ReadTimeout > 0 {
+ _ = conn.SetReadDeadline(time.Now().Add(s.config.ReadTimeout))
+ }
+ hdr, err := br.Peek(6)
+ _ = conn.SetReadDeadline(time.Time{})
+
+ // Peek failed (timeout / EOF): the peer never delivered enough bytes to
+ // identify itself. Fall through with the raw conn so the read loop surfaces
+ // EOF/timeout exactly as it does for the plaintext path today. Any <6
+ // partial bytes already buffered are discarded, which only affects peers
+ // that were already broken (real clients send ≥13 bytes immediately).
+ if err != nil {
+ s.logger.Warn("wrapTLS peek failed for %s: %v", conn.RemoteAddr(), err)
+ return conn
+ }
+
+ bc := &bufferedConn{Conn: conn, r: br}
+ if isTLSClientHello(hdr) {
+ s.logger.Info("wrapTLS: TLS peer %s first6=%x", conn.RemoteAddr(), hdr)
+ return tls.Server(bc, s.tlsCfg)
+ }
+ s.logger.Info("wrapTLS: plaintext peer %s first6=%x", conn.RemoteAddr(), hdr)
+ return bc
+}
diff --git a/server/go/server/tls_test.go b/server/go/server/tls_test.go
new file mode 100644
index 0000000..8a4ae2a
--- /dev/null
+++ b/server/go/server/tls_test.go
@@ -0,0 +1,55 @@
+package server
+
+import "testing"
+
+func TestIsTLSClientHello(t *testing.T) {
+ tests := []struct {
+ name string
+ b []byte
+ want bool
+ }{
+ {
+ name: "TLS 1.2 ClientHello",
+ b: []byte{0x16, 0x03, 0x03, 0x00, 0x01, 0x01, 0x00},
+ want: true,
+ },
+ {
+ name: "TLS 1.3 ClientHello (legacy record version)",
+ b: []byte{0x16, 0x03, 0x01, 0x00, 0x01, 0x01, 0x00},
+ want: true,
+ },
+ {
+ name: "plaintext Shine flag",
+ b: []byte{'S', 'h', 'i', 'n', 'e', 0x00, 0x00},
+ want: false,
+ },
+ {
+ name: "obfuscated HELL header that happens to start 0x16 0x03 but byte5=0x00",
+ b: []byte{0x16, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00},
+ want: false,
+ },
+ {
+ name: "HTTP-mask POST envelope",
+ b: []byte{'P', 'O', 'S', 'T', ' ', '/', 'x'},
+ want: false,
+ },
+ {
+ name: "0x16 0x03 but handshake type ServerHello (0x02), not ClientHello",
+ b: []byte{0x16, 0x03, 0x03, 0x00, 0x01, 0x02, 0x00},
+ want: false,
+ },
+ {
+ name: "too short",
+ b: []byte{0x16, 0x03, 0x01},
+ want: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := isTLSClientHello(tt.b); got != tt.want {
+ t.Errorf("isTLSClientHello(% x) = %v, want %v", tt.b, got, tt.want)
+ }
+ })
+ }
+}