# 传输层单向 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 检测 + 解包接入。设计稿见 `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 接入排期。