Files
SimpleRemoter/docs/TransportTLS_Design.md
yuanyuanxianganddeepseek-v4-pro 96b9eea70a Feat: One-way TLS transport for the MFC server
Add optional one-way TLS to the MFC server (server/2015Remote) so the device
port can serve both TLS clients and legacy plaintext clients on the same port,
mirroring the Go server.

Server: on a connection's first bytes, sniff for a TLS ClientHello (0x16 0x03
+ handshake type 0x01). When [tls] Enabled=1 and the identity loads, hand the
socket to a short-lived blocking handshake thread (Option A) running Schannel
AcceptSecurityContext, then decrypt on recv and encrypt on send; plaintext
connections replay the sniffed bytes straight into the existing parse path, so
the plaintext code path is otherwise unchanged. The identity is loaded once
from [tls] CertPath/KeyPath as PEM + CNG (PKCS#8) — the same key pair the Go
server uses — and a missing [tls] section, an empty value, or a load failure
leaves plaintext serving intact and fail-closes any TLS ClientHello.

Also fix two transport-reliability bugs that surfaced end-to-end:
- send path: encrypt only the freshly appended plaintext tail and post a
  single IOWrite wake-up guarded by a bSendPending flag, so back-to-back small
  sends (terminal resize -> COMMAND_NEXT -> resize) no longer run concurrent
  WSASends over the same mutable buffer (client saw DecryptMessage failures,
  with WSAGetLastError() = 6001 / ERROR_DECRYPTION_FAILED);
- client Recv: keep the handshake's unconsumed tail bytes in m_raw and drain
  them before the next recv, and log the real SECURITY_STATUS on
  DecryptMessage failure instead of the stale Winsock error.

Co-Authored-By: deepseek-v4-pro
2026-10-15 13:48:27 +02:00

19 KiB
Raw Permalink Blame History

传输层单向 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 + 两轮伪随机交换,密钥是明文带内 12 字节)+ header.go 的 V0V6 头混淆——等于明文。这正是 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(仅明文,向后兼容):

TLSCertFile string // 服务端证书(PEM)
TLSKeyFile  string // 服务端私钥(PEM)

cmd/main.go 读取环境变量(沿用现有 YAMA_* 命名):

cfg.TLSCertFile = os.Getenv("YAMA_TLS_CERT")
cfg.TLSKeyFile  = os.Getenv("YAMA_TLS_KEY")

仅当两者都非空才构建 *tls.Config;否则 s.tlsCfg == nil,acceptLoop 走纯明文。

5.2 tls.Config

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 走明文,由读循环处理:

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):

openssl req -x509 -newkey rsa:2048 -nodes \
  -keyout yama-server.key -out yama-server.crt \
  -days 3650 -subj "/CN=yama-server"

或自建 CA + 签服务端证书(便于轮换,客户端信任锚为 ca.crt):

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 服务端

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),重启客户端:

$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 检测 + 解包接入。设计稿见 docs/TransportTLS_MFCServer_Design.md。
  • 回滚:服务端清空 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 接入排期。