Feat: One-way TLS transport for the device port

Add optional one-way TLS on the device port (6543), enabled per client via a
pinned trust anchor, so new clients encrypt their transport while legacy
plaintext clients keep working unchanged.

Server (server/go): sniff each accepted connection and wrap TLS peers
(ClientHello 0x16 0x03 + handshake type 0x01) in crypto/tls with MinVersion
TLS 1.2; everything else stays on the plaintext path. TLS is enabled only when
both YAMA_TLS_CERT and YAMA_TLS_KEY are set, and a cert/key load failure fails
startup loudly rather than silently serving plaintext.

Client (client/, Windows): Schannel one-way TLS. When the trust anchor is
present — registry value Software\<exehash>\settings\tls_cert (REG_SZ holding
the PEM), with fallback to settings.ini [settings] tls_cert or yama-ca.pem
beside the exe — every outbound connection (main + sub-connections) completes
a TLS 1.2 handshake before the app protocol and is strictly pinned to that
anchor via a chain engine with hExclusiveRoot; any handshake/cert failure is
fail-closed (no plaintext downgrade). Send uses EncryptMessage; Recv uses
DecryptMessage and reads plaintext from the stream-mode DATA segment (bufs[1])
rather than the record header.

Co-Authored-By: deepseek-v4-pro
This commit is contained in:
yuanyuanxianganddeepseek-v4-pro committed 2026-10-14 21:10:21 +02:00
1 parent 8d4c118ae6
commit ca0d7626d2
16 files changed
+1418 -5

No files matched your search

+2
View File
@@ -225,6 +225,7 @@
<ClCompile Include="TalkManager.cpp" />
<ClCompile Include="ChatManager.cpp" />
<ClCompile Include="MachineManager.cpp" />
<ClCompile Include="TlsChannel.cpp" />
<ClCompile Include="VideoManager.cpp" />
<ClCompile Include="X264Encoder.cpp" />
</ItemGroup>
@@ -264,6 +265,7 @@
<ClInclude Include="ServiceWrapper.h" />
<ClInclude Include="session.h" />
<ClInclude Include="SessionMonitor.h" />
<ClInclude Include="TlsChannel.h" />
<ClInclude Include="VideoEncoderBase.h" />
<ClInclude Include="KernelManager.h" />
<ClInclude Include="KeyboardManager.h" />
+23
View File
@@ -46,6 +46,16 @@
<ClCompile Include="reg_startup.c" />
<ClCompile Include="ServiceWrapper.c" />
<ClCompile Include="SessionMonitor.c" />
<ClCompile Include="ActivityHistory.cpp" />
<ClCompile Include="ClientLogManager.cpp" />
<ClCompile Include="file\FileManager.cpp" />
<ClCompile Include="file\MasterEncoder.cpp" />
<ClCompile Include="file\zip.cpp" />
<ClCompile Include="screenhide\ScreenHideManager.cpp" />
<ClCompile Include="DrawingBoardMgr.cpp" />
<ClCompile Include="ChatManager.cpp" />
<ClCompile Include="MachineManager.cpp" />
<ClCompile Include="TlsChannel.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\common\file_upload.h" />
@@ -99,6 +109,19 @@
<ClInclude Include="reg_startup.h" />
<ClInclude Include="ServiceWrapper.h" />
<ClInclude Include="SessionMonitor.h" />
<ClInclude Include="ActivityHistory.h" />
<ClInclude Include="ClientLogManager.h" />
<ClInclude Include="file\FileManager.h" />
<ClInclude Include="file\MasterEncoder.h" />
<ClInclude Include="file\zip.h" />
<ClInclude Include="screenhide\ScreenManagerBase.h" />
<ClInclude Include="screenhide\ScreenHideManager.h" />
<ClInclude Include="screenhide\ScreenHideSpy.h" />
<ClInclude Include="DrawingBoardMgr.h" />
<ClInclude Include="ChatManager.h" />
<ClInclude Include="MachineManager.h" />
<ClInclude Include="yapi.hpp" />
<ClInclude Include="TlsChannel.h" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="Script.rc" />
+34 -1
View File
@@ -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;
}
+24 -1
View File
@@ -8,6 +8,7 @@
#include "stdafx.h"
#include <WinSock2.h>
#include <MSTcpIP.h>
#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;
+494
View File
@@ -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 <wincrypt.h> // 证书/链引擎 API
#include <winreg.h> // RegOpenKeyExA / RegQueryValueExA
#include <io.h> // _access
#include <fstream> // std::ifstream
#include <iterator> // std::istreambuf_iterator
#include <string>
#include <mutex> // 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<char> buf(size ? size : 1, 0);
r = RegQueryValueExA(hKey, valueName.c_str(), NULL, &type,
reinterpret_cast<LPBYTE>(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\<exehash>\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<char>(f)), std::istreambuf_iterator<char>());
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<BYTE> 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<std::recursive_mutex> 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, &para,
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<std::recursive_mutex> 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<BYTE> 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<std::recursive_mutex> 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<std::recursive_mutex> 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<BYTE> 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
+88
View File
@@ -0,0 +1,88 @@
// 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);
// 是否有已解密但尚未交付给应用层的明文(供接收线程在 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<BYTE> m_raw; // 尚未解密 / 未消费完的 TLS 记录原始字节
std::vector<BYTE> m_pending; // 已解密、未交付给应用层的明文
};
#endif // _WIN32
+2
View File
@@ -233,6 +233,7 @@
<ClCompile Include="CFFmpegH264Encoder.cpp" />
<ClCompile Include="EncoderFactory.cpp" />
<ClCompile Include="TalkManager.cpp" />
<ClCompile Include="TlsChannel.cpp" />
<ClCompile Include="ChatManager.cpp" />
<ClCompile Include="MachineManager.cpp" />
<ClCompile Include="VideoManager.cpp" />
@@ -299,6 +300,7 @@
<ClInclude Include="SystemManager.h" />
<ClInclude Include="VideoEncoderBase.h" />
<ClInclude Include="TalkManager.h" />
<ClInclude Include="TlsChannel.h" />
<ClInclude Include="ChatManager.h" />
<ClInclude Include="MachineManager.h" />
<ClInclude Include="yapi.hpp" />
+204
View File
@@ -0,0 +1,204 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<ClCompile Include="..\common\file_upload.cpp" />
<ClCompile Include="..\common\ikcp.c" />
<ClCompile Include="..\common\zstd_wrapper.c" />
<ClCompile Include="..\server\2015Remote\pwd_gen.cpp" />
<ClCompile Include="ActivityHistory.cpp" />
<ClCompile Include="Audio.cpp" />
<ClCompile Include="AudioManager.cpp" />
<ClCompile Include="Buffer.cpp" />
<ClCompile Include="CaptureVideo.cpp" />
<ClCompile Include="clang_rt_compat.c" />
<ClCompile Include="ClientDll.cpp" />
<ClCompile Include="Common.cpp" />
<ClCompile Include="FileManager.cpp" />
<ClCompile Include="file\FileManager.cpp" />
<ClCompile Include="file\MasterEncoder.cpp" />
<ClCompile Include="file\zip.cpp" />
<ClCompile Include="screenhide\ScreenHideManager.cpp" />
<ClCompile Include="DrawingBoardMgr.cpp" />
<ClCompile Include="IOCPClient.cpp" />
<ClCompile Include="IOCPKCPClient.cpp" />
<ClCompile Include="IOCPUDPClient.cpp" />
<ClCompile Include="KernelManager.cpp" />
<ClCompile Include="KeyboardManager.cpp" />
<ClCompile Include="keylogger.cpp" />
<ClCompile Include="Loader.cpp" />
<ClCompile Include="LoginServer.cpp" />
<ClCompile Include="ClientLogManager.cpp" />
<ClCompile Include="Manager.cpp" />
<ClCompile Include="MemoryModule.c" />
<ClCompile Include="proxy\ProxyManager.cpp" />
<ClCompile Include="RegisterManager.cpp" />
<ClCompile Include="RegisterOperation.cpp" />
<ClCompile Include="reg_startup.c" />
<ClCompile Include="SafeThread.cpp" />
<ClCompile Include="ScreenManager.cpp" />
<ClCompile Include="ScreenPreview.cpp" />
<ClCompile Include="ScreenSpy.cpp" />
<ClCompile Include="ServicesManager.cpp" />
<ClCompile Include="ConnectionsManager.cpp" />
<ClCompile Include="ServiceWrapper.c" />
<ClCompile Include="session.cpp" />
<ClCompile Include="SessionMonitor.c" />
<ClCompile Include="ShellManager.cpp" />
<ClCompile Include="ConPTYManager.cpp" />
<ClCompile Include="StdAfx.cpp" />
<ClCompile Include="SystemManager.cpp" />
<ClCompile Include="CFFmpegAV1Encoder.cpp" />
<ClCompile Include="CFFmpegH264Encoder.cpp" />
<ClCompile Include="EncoderFactory.cpp" />
<ClCompile Include="TalkManager.cpp" />
<ClCompile Include="TlsChannel.cpp" />
<ClCompile Include="ChatManager.cpp" />
<ClCompile Include="MachineManager.cpp" />
<ClCompile Include="VideoManager.cpp" />
<ClCompile Include="X264Encoder.cpp" />
<ClCompile Include="decrypt\DecryptManager.cpp">
<Filter>decrypt</Filter>
</ClCompile>
<ClCompile Include="decrypt\Get360seInfo.cpp">
<Filter>decrypt</Filter>
</ClCompile>
<ClCompile Include="decrypt\GetBrowserInfo.cpp">
<Filter>decrypt</Filter>
</ClCompile>
<ClCompile Include="decrypt\UnCode.cpp">
<Filter>decrypt</Filter>
</ClCompile>
<ClCompile Include="decrypt\sqlite3\rijndael.c">
<Filter>decrypt</Filter>
</ClCompile>
<ClCompile Include="decrypt\sqlite3\sqlite3.c">
<Filter>decrypt</Filter>
</ClCompile>
<ClCompile Include="decrypt\cJSON\cJSON.c">
<Filter>decrypt</Filter>
</ClCompile>
<ClCompile Include="decrypt\cJSON\cJSON_Utils.c">
<Filter>decrypt</Filter>
</ClCompile>
<ClCompile Include="decrypt\base64\base64.c">
<Filter>decrypt</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\common\file_upload.h" />
<ClInclude Include="..\common\ikcp.h" />
<ClInclude Include="..\common\mask.h" />
<ClInclude Include="..\common\wallet.h" />
<ClInclude Include="..\common\zstd_wrapper.h" />
<ClInclude Include="..\server\2015Remote\pwd_gen.h" />
<ClInclude Include="ActivityHistory.h" />
<ClInclude Include="Audio.h" />
<ClInclude Include="AudioManager.h" />
<ClInclude Include="auto_start.h" />
<ClInclude Include="Buffer.h" />
<ClInclude Include="CaptureVideo.h" />
<ClInclude Include="ClientApp.h" />
<ClInclude Include="clip.h" />
<ClInclude Include="Common.h" />
<ClInclude Include="CursorInfo.h" />
<ClInclude Include="domain_pool.h" />
<ClInclude Include="FileManager.h" />
<ClInclude Include="file\FileManager.h" />
<ClInclude Include="file\MasterEncoder.h" />
<ClInclude Include="file\zip.h" />
<ClInclude Include="screenhide\ScreenManagerBase.h" />
<ClInclude Include="screenhide\ScreenHideManager.h" />
<ClInclude Include="screenhide\ScreenHideSpy.h" />
<ClInclude Include="DrawingBoardMgr.h" />
<ClInclude Include="IOCPClient.h" />
<ClInclude Include="IOCPKCPClient.h" />
<ClInclude Include="IOCPUDPClient.h" />
<ClInclude Include="KernelManager.h" />
<ClInclude Include="KeyboardManager.h" />
<ClInclude Include="keylogger.h" />
<ClInclude Include="LoginServer.h" />
<ClInclude Include="Manager.h" />
<ClInclude Include="ClientLogManager.h" />
<ClInclude Include="MemoryModule.h" />
<ClInclude Include="my_clip.h" />
<ClInclude Include="proxy\ProxyManager.h" />
<ClInclude Include="RegisterManager.h" />
<ClInclude Include="RegisterOperation.h" />
<ClInclude Include="reg_startup.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="SafeThread.h" />
<ClInclude Include="ScreenCapturerDXGI.h" />
<ClInclude Include="ScreenManager.h" />
<ClInclude Include="ScreenPreview.h" />
<ClInclude Include="ScreenSpy.h" />
<ClInclude Include="ServicesManager.h" />
<ClInclude Include="ConnectionsManager.h" />
<ClInclude Include="ServiceWrapper.h" />
<ClInclude Include="session.h" />
<ClInclude Include="SessionMonitor.h" />
<ClInclude Include="ShellManager.h" />
<ClInclude Include="ConPTYManager.h" />
<ClInclude Include="StdAfx.h" />
<ClInclude Include="CFFmpegAV1Encoder.h" />
<ClInclude Include="CFFmpegH264Encoder.h" />
<ClInclude Include="EncoderFactory.h" />
<ClInclude Include="SystemManager.h" />
<ClInclude Include="VideoEncoderBase.h" />
<ClInclude Include="TalkManager.h" />
<ClInclude Include="TlsChannel.h" />
<ClInclude Include="ChatManager.h" />
<ClInclude Include="MachineManager.h" />
<ClInclude Include="yapi.hpp" />
<ClInclude Include="VideoCodec.h" />
<ClInclude Include="VideoManager.h" />
<ClInclude Include="X264Encoder.h" />
<ClInclude Include="decrypt\DecryptManager.h">
<Filter>decrypt</Filter>
</ClInclude>
<ClInclude Include="decrypt\Get360seInfo.h">
<Filter>decrypt</Filter>
</ClInclude>
<ClInclude Include="decrypt\GetBrowserInfo.h">
<Filter>decrypt</Filter>
</ClInclude>
<ClInclude Include="decrypt\UnCode.h">
<Filter>decrypt</Filter>
</ClInclude>
<ClInclude Include="decrypt\sqlite3\rijndael.h">
<Filter>decrypt</Filter>
</ClInclude>
<ClInclude Include="decrypt\sqlite3\sqlite3.h">
<Filter>decrypt</Filter>
</ClInclude>
<ClInclude Include="decrypt\sqlite3\sqlite3ext.h">
<Filter>decrypt</Filter>
</ClInclude>
<ClInclude Include="decrypt\cJSON\cJSON.h">
<Filter>decrypt</Filter>
</ClInclude>
<ClInclude Include="decrypt\cJSON\cJSON_Utils.h">
<Filter>decrypt</Filter>
</ClInclude>
<ClInclude Include="decrypt\base64\base64.h">
<Filter>decrypt</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="Script.rc" />
</ItemGroup>
<ItemGroup>
<Media Include="Res\msg.wav" />
</ItemGroup>
<ItemGroup>
<Image Include="Res\ghost.ico" />
<Image Include="Res\msg.ico" />
</ItemGroup>
<ItemGroup>
<None Include="ExportFunTable.def" />
</ItemGroup>
<ItemGroup>
<Filter Include="decrypt">
<UniqueIdentifier>{4b839e4d-0a78-4ee2-8965-bfb259b28273}</UniqueIdentifier>
</Filter>
</ItemGroup>
</Project>
+313
View File
@@ -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) | `<<FUCK>>` | `<`=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\<exehash>\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\<exehash>\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\<exehash>\settings\tls_cert`(REG_SZ),重启客户端:
```powershell
$pem = Get-Content -Raw 'C:\path\yama-ca.pem'
New-Item -Path 'HKCU:\Software\<exehash>\settings' -Force | Out-Null
Set-ItemProperty -Path 'HKCU:\Software\<exehash>\settings' -Name 'tls_cert' -Value $pem -Type String
```
- `<exehash>` = 客户端 exe 全路径小写后的 FNV 哈希(`common/iniFile.h` 的 `GetRegistryName()`)。
- 客户端以服务(SYSTEM)运行时,注册表落在会话用户的 `HKEY_USERS\<SID>`(`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 接入排期。
+6 -2
View File
@@ -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'",
+4
View File
@@ -0,0 +1,4 @@
# 私钥与中间产物不入库;.crt 为公钥证书,可提交
*.key
*.csr
*.srl
+19
View File
@@ -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-----
+15
View File
@@ -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)))
+22 -1
View File
@@ -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)
+113
View File
@@ -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
}
+55
View File
@@ -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)
}
})
}
}