From 3afcef7779cc90f38554b336d540f7cb59fa3729 Mon Sep 17 00:00:00 2001 From: yuanyuanxiang <962914132@qq.com> Date: Wed, 24 Dec 2025 23:40:02 +0100 Subject: [PATCH] Server/go: authorization client automatically exit if verify succeed --- server/go/README.md | 67 +++++++++++++++++++++- server/go/auth/auth.go | 99 +++++++++++++++++++++++++++++++++ server/go/cmd/main.go | 34 ++++++++++- server/go/connection/context.go | 5 +- server/go/server/server.go | 23 +++++++- 5 files changed, 222 insertions(+), 6 deletions(-) diff --git a/server/go/README.md b/server/go/README.md index 3266762..fb8c6cd 100644 --- a/server/go/README.md +++ b/server/go/README.md @@ -7,6 +7,8 @@ ``` server/go/ ├── go.mod # Go 模块定义 +├── auth/ +│ └── auth.go # 授权验证模块 (TOKEN_AUTH + Heartbeat HMAC) ├── buffer/ │ └── buffer.go # 线程安全的动态缓冲区 ├── connection/ @@ -32,6 +34,7 @@ server/go/ - **高并发**: 基于 Goroutine 池管理并发连接 - **协议兼容**: 支持原有 C++ 客户端的多种协议标识 (Hell/Hello/Shine/Fuck) - **协议头解密**: 支持8种协议头加密方式 (V0-V6 + Default) +- **授权验证**: 支持 TOKEN_AUTH 和 Heartbeat HMAC-SHA256 双重授权验证 - **XOR编码**: 支持 XOREncoder16 数据编码/解码 - **ZSTD 压缩**: 使用高效的 ZSTD 算法进行数据压缩 - **GBK编码**: 自动将 Windows 客户端的 GBK 编码转换为 UTF-8 @@ -46,9 +49,10 @@ server/go/ | 命令 | 值 | 说明 | |------|-----|------| -| TOKEN_AUTH | 100 | 授权请求 | -| TOKEN_HEARTBEAT | 101 | 心跳包 | +| TOKEN_AUTH | 100 | 授权请求 (验证 SN + Passcode + HMAC) | +| TOKEN_HEARTBEAT | 101 | 心跳包 (支持 HMAC 授权验证,返回 Authorized 状态) | | TOKEN_LOGIN | 102 | 客户端登录 | +| CMD_HEARTBEAT_ACK | 216 | 心跳响应 (包含 Authorized 字段) | 其他命令会被记录为 Debug 日志,可按需扩展。 @@ -75,6 +79,25 @@ go build -o simpleremoter-server ./cmd 服务器默认监听 6543 端口,日志输出到 `logs/server.log`。 +### 环境变量 + +| 变量 | 说明 | 示例 | +|------|------|------| +| `YAMA_PWDHASH` | 密码的 SHA256 哈希值 (64位十六进制) | `61f04dd6...` | +| `YAMA_PWD` | 超级密码,用于 HMAC 签名验证 | `your_super_password` | + +```bash +# Linux/macOS +export YAMA_PWDHASH="61f04dd637a74ee34493fc1025de2c131022536da751c29e3ff4e9024d8eec43" +export YAMA_PWD="your_super_password" +./simpleremoter-server + +# Windows PowerShell +$env:YAMA_PWDHASH="61f04dd637a74ee34493fc1025de2c131022536da751c29e3ff4e9024d8eec43" +$env:YAMA_PWD="your_super_password" +.\simpleremoter-server.exe +``` + ## 使用示例 ```go @@ -229,6 +252,46 @@ func main() { | szStartTime | 456 | 20 | 启动时间 | | szReserved | 476 | 512 | 扩展字段 (用`|`分隔) | +### Heartbeat 结构 + +客户端心跳包结构 (1024 字节): + +| 字段 | 偏移 | 大小 | 说明 | +|------|------|------|------| +| Time | 0 | 8 | 时间戳 (uint64) | +| ActiveWnd | 8 | 512 | 当前活动窗口 | +| Ping | 520 | 4 | 延迟 (int) | +| HasSoftware | 524 | 4 | 软件标识 (int) | +| SN | 528 | 20 | 序列号 (用于授权验证) | +| Passcode | 548 | 44 | 授权码 (格式: v0-v1-v2-v3-v4-v5) | +| PwdHmac | 592 | 8 | HMAC 签名 (uint64) | +| Reserved | 600 | 424 | 保留字段 | + +### HeartbeatACK 结构 + +服务端心跳响应结构 (32 字节): + +| 字段 | 偏移 | 大小 | 说明 | +|------|------|------|------| +| Time | 0 | 8 | 原始时间戳 (uint64) | +| Authorized | 8 | 1 | 授权状态 (1=已授权, 0=未授权) | +| Reserved | 9 | 23 | 保留字段 | + +### 授权验证流程 + +``` +客户端 Heartbeat 服务端 + │ │ + │ SN + Passcode + PwdHmac │ + │ ────────────────────────────────► │ + │ │ 1. 验证 Passcode 格式 + │ │ 2. 验证 Passcode 哈希 + │ │ 3. 验证 HMAC 签名 + │ HeartbeatACK │ + │ ◄──────────────────────────────── │ + │ (Authorized=1 或 0) │ +``` + ## API 参考 ### Server diff --git a/server/go/auth/auth.go b/server/go/auth/auth.go index f798e79..19889f1 100644 --- a/server/go/auth/auth.go +++ b/server/go/auth/auth.go @@ -206,3 +206,102 @@ func GenHMAC(pwdHash, superPass string) string { } return result } + +// HeartbeatAuthResult contains the result of heartbeat authentication +type HeartbeatAuthResult struct { + Authorized bool + SN string + Passcode string + PwdHmac uint64 +} + +// AuthenticateHeartbeat validates authorization info from a Heartbeat message +// Data format (after TOKEN_HEARTBEAT byte): +// - offset 0: Time (8 bytes, uint64) +// - offset 8: ActiveWnd (512 bytes) +// - offset 520: Ping (4 bytes, int) +// - offset 524: HasSoftware (4 bytes, int) +// - offset 528: SN (20 bytes) +// - offset 548: Passcode (44 bytes) +// - offset 592: PwdHmac (8 bytes, uint64) +func (a *Authenticator) AuthenticateHeartbeat(data []byte) *HeartbeatAuthResult { + result := &HeartbeatAuthResult{ + Authorized: false, + } + + // Minimum length check: need at least SN + Passcode + PwdHmac + // Offset 528 + 20 (SN) + 44 (Passcode) + 8 (PwdHmac) = 600 bytes + if len(data) < 600 { + return result + } + + // Extract SN (offset 528, 20 bytes) + snBytes := data[528:548] + // Find null terminator + snEnd := bytes.IndexByte(snBytes, 0) + if snEnd == -1 { + snEnd = len(snBytes) + } + sn := string(snBytes[:snEnd]) + result.SN = sn + + // Extract Passcode (offset 548, 44 bytes) + passcodeBytes := data[548:592] + passcodeEnd := bytes.IndexByte(passcodeBytes, 0) + if passcodeEnd == -1 { + passcodeEnd = len(passcodeBytes) + } + passcode := string(passcodeBytes[:passcodeEnd]) + result.Passcode = passcode + + // Extract PwdHmac (offset 592, 8 bytes) + pwdHmac := binary.LittleEndian.Uint64(data[592:600]) + result.PwdHmac = pwdHmac + + // If SN, Passcode, or PwdHmac is empty/zero, not authorized + if sn == "" || passcode == "" || pwdHmac == 0 { + return result + } + + // Split passcode by '-' + parts := strings.Split(passcode, "-") + if len(parts) != 6 && len(parts) != 7 { + return result + } + + // Get last 4 parts as subvector + subvector := parts[len(parts)-4:] + + // Build password string: v[0] + " - " + v[1] + ": " + PwdHash + (optional: ": " + v[2]) + password := parts[0] + " - " + parts[1] + ": " + a.config.PwdHash + if len(parts) == 7 { + password += ": " + parts[2] + } + + // Derive key from password and SN + finalKey := DeriveKey(password, sn) + + // Get fixed length ID + hash256 := strings.Join(subvector, "-") + fixedKey := GetFixedLengthID(finalKey) + + // Compare passcode + if hash256 != fixedKey { + return result + } + + // Passcode validation successful, now verify HMAC + superPass := os.Getenv("YAMA_PWD") + if superPass == "" { + superPass = a.config.SuperPass + } + + if superPass != "" { + verified := VerifyMessage(superPass, []byte(passcode), pwdHmac) + if verified { + result.Authorized = true + } + } + + return result +} diff --git a/server/go/cmd/main.go b/server/go/cmd/main.go index 1812325..eece7d5 100644 --- a/server/go/cmd/main.go +++ b/server/go/cmd/main.go @@ -132,6 +132,19 @@ func (h *MyHandler) handleAuth(ctx *connection.Context, data []byte) { } // handleHeartbeat handles heartbeat from client (TOKEN_HEARTBEAT = 101) +// Heartbeat structure (after command byte): +// - offset 0: Time (8 bytes, uint64) +// - offset 8: ActiveWnd (512 bytes) +// - offset 520: Ping (4 bytes, int) +// - offset 524: HasSoftware (4 bytes, int) +// - offset 528: SN (20 bytes) +// - offset 548: Passcode (44 bytes) +// - offset 592: PwdHmac (8 bytes, uint64) +// +// HeartbeatACK structure: +// - offset 0: Time (8 bytes, uint64) +// - offset 8: Authorized (1 byte, char) +// - offset 9: Reserved (23 bytes) func (h *MyHandler) handleHeartbeat(ctx *connection.Context, data []byte) { // Parse Time from heartbeat request (offset 1, 8 bytes) @@ -141,6 +154,23 @@ func (h *MyHandler) handleHeartbeat(ctx *connection.Context, data []byte) { uint64(data[5])<<32 | uint64(data[6])<<40 | uint64(data[7])<<48 | uint64(data[8])<<56 } + // Authenticate heartbeat if it contains authorization info + // data[1:] skips the command byte to get the raw Heartbeat structure + var authorized byte = 0 + if len(data) > 1 { + authResult := h.auth.AuthenticateHeartbeat(data[1:]) + if authResult.Authorized { + authorized = 1 + // Log authorization success (only log once per connection to avoid spam) + if !ctx.IsAuthorized.Load() { + ctx.IsAuthorized.Store(true) + info := ctx.GetInfo() + h.log.Info("Heartbeat auth success: clientID=%s computer=%s ip=%s sn=%s passcode=%s pwdHmac=%d", + info.ClientID, info.ComputerName, ctx.GetPeerIP(), authResult.SN, authResult.Passcode, authResult.PwdHmac) + } + } + } + // Build HeartbeatACK response: CMD_HEARTBEAT_ACK(1) + HeartbeatACK(32) resp := make([]byte, 33) resp[0] = protocol.CommandHeartbeat // CMD_HEARTBEAT_ACK = 216 @@ -153,7 +183,9 @@ func (h *MyHandler) handleHeartbeat(ctx *connection.Context, data []byte) { resp[6] = byte(hbTime >> 40) resp[7] = byte(hbTime >> 48) resp[8] = byte(hbTime >> 56) - // Reserved[24] at offset 9 is already zero + // Authorized at offset 9 (1 byte) + resp[9] = authorized + // Reserved[23] at offset 10 is already zero if err := h.srv.Send(ctx, resp); err != nil { h.log.Error("Failed to send heartbeat ACK to client %d: %v", ctx.ID, err) diff --git a/server/go/connection/context.go b/server/go/connection/context.go index 8ca3dce..db70977 100644 --- a/server/go/connection/context.go +++ b/server/go/connection/context.go @@ -37,8 +37,9 @@ type Context struct { OutBuffer *buffer.Buffer // Decompressed data for processing // Client info - Info ClientInfo - IsLoggedIn atomic.Bool + Info ClientInfo + IsLoggedIn atomic.Bool + IsAuthorized atomic.Bool // Whether client is authorized via heartbeat // Connection state OnlineTime time.Time diff --git a/server/go/server/server.go b/server/go/server/server.go index f8e6c13..a5bf118 100644 --- a/server/go/server/server.go +++ b/server/go/server/server.go @@ -260,7 +260,28 @@ func (s *Server) processData(ctx *connection.Context) { if err == protocol.ErrNeedMore { return } - s.logger.Error("Parse error for connection %d: %v", ctx.ID, err) + // Log more details for unsupported protocol errors + if err == protocol.ErrUnsupported { + // Get first 32 bytes for debugging + peekLen := 32 + if ctx.InBuffer.Len() < peekLen { + peekLen = ctx.InBuffer.Len() + } + rawData := ctx.InBuffer.Peek(peekLen) + // Sanitize for safe logging (replace non-printable chars) + ascii := make([]byte, len(rawData)) + for i, b := range rawData { + if b >= 32 && b < 127 { + ascii[i] = b + } else { + ascii[i] = '.' + } + } + s.logger.Warn("Unsupported protocol from ip=%s conn=%d raw=%x ascii=%s", + ctx.GetPeerIP(), ctx.ID, rawData, string(ascii)) + } else { + s.logger.Error("Parse error for connection %d: %v", ctx.ID, err) + } _ = ctx.Close() return }