Server/go: authorization client automatically exit if verify succeed

This commit is contained in:
yuanyuanxiang
2025-12-24 23:40:02 +01:00
parent 2ee61a760f
commit 3afcef7779
5 changed files with 222 additions and 6 deletions

View File

@@ -7,6 +7,8 @@
``` ```
server/go/ server/go/
├── go.mod # Go 模块定义 ├── go.mod # Go 模块定义
├── auth/
│ └── auth.go # 授权验证模块 (TOKEN_AUTH + Heartbeat HMAC)
├── buffer/ ├── buffer/
│ └── buffer.go # 线程安全的动态缓冲区 │ └── buffer.go # 线程安全的动态缓冲区
├── connection/ ├── connection/
@@ -32,6 +34,7 @@ server/go/
- **高并发**: 基于 Goroutine 池管理并发连接 - **高并发**: 基于 Goroutine 池管理并发连接
- **协议兼容**: 支持原有 C++ 客户端的多种协议标识 (Hell/Hello/Shine/Fuck) - **协议兼容**: 支持原有 C++ 客户端的多种协议标识 (Hell/Hello/Shine/Fuck)
- **协议头解密**: 支持8种协议头加密方式 (V0-V6 + Default) - **协议头解密**: 支持8种协议头加密方式 (V0-V6 + Default)
- **授权验证**: 支持 TOKEN_AUTH 和 Heartbeat HMAC-SHA256 双重授权验证
- **XOR编码**: 支持 XOREncoder16 数据编码/解码 - **XOR编码**: 支持 XOREncoder16 数据编码/解码
- **ZSTD 压缩**: 使用高效的 ZSTD 算法进行数据压缩 - **ZSTD 压缩**: 使用高效的 ZSTD 算法进行数据压缩
- **GBK编码**: 自动将 Windows 客户端的 GBK 编码转换为 UTF-8 - **GBK编码**: 自动将 Windows 客户端的 GBK 编码转换为 UTF-8
@@ -46,9 +49,10 @@ server/go/
| 命令 | 值 | 说明 | | 命令 | 值 | 说明 |
|------|-----|------| |------|-----|------|
| TOKEN_AUTH | 100 | 授权请求 | | TOKEN_AUTH | 100 | 授权请求 (验证 SN + Passcode + HMAC) |
| TOKEN_HEARTBEAT | 101 | 心跳包 | | TOKEN_HEARTBEAT | 101 | 心跳包 (支持 HMAC 授权验证,返回 Authorized 状态) |
| TOKEN_LOGIN | 102 | 客户端登录 | | TOKEN_LOGIN | 102 | 客户端登录 |
| CMD_HEARTBEAT_ACK | 216 | 心跳响应 (包含 Authorized 字段) |
其他命令会被记录为 Debug 日志,可按需扩展。 其他命令会被记录为 Debug 日志,可按需扩展。
@@ -75,6 +79,25 @@ go build -o simpleremoter-server ./cmd
服务器默认监听 6543 端口,日志输出到 `logs/server.log` 服务器默认监听 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 ```go
@@ -229,6 +252,46 @@ func main() {
| szStartTime | 456 | 20 | 启动时间 | | szStartTime | 456 | 20 | 启动时间 |
| szReserved | 476 | 512 | 扩展字段 (用`|`分隔) | | 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 参考 ## API 参考
### Server ### Server

View File

@@ -206,3 +206,102 @@ func GenHMAC(pwdHash, superPass string) string {
} }
return result 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
}

View File

@@ -132,6 +132,19 @@ func (h *MyHandler) handleAuth(ctx *connection.Context, data []byte) {
} }
// handleHeartbeat handles heartbeat from client (TOKEN_HEARTBEAT = 101) // 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) { func (h *MyHandler) handleHeartbeat(ctx *connection.Context, data []byte) {
// Parse Time from heartbeat request (offset 1, 8 bytes) // 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 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) // Build HeartbeatACK response: CMD_HEARTBEAT_ACK(1) + HeartbeatACK(32)
resp := make([]byte, 33) resp := make([]byte, 33)
resp[0] = protocol.CommandHeartbeat // CMD_HEARTBEAT_ACK = 216 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[6] = byte(hbTime >> 40)
resp[7] = byte(hbTime >> 48) resp[7] = byte(hbTime >> 48)
resp[8] = byte(hbTime >> 56) 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 { if err := h.srv.Send(ctx, resp); err != nil {
h.log.Error("Failed to send heartbeat ACK to client %d: %v", ctx.ID, err) h.log.Error("Failed to send heartbeat ACK to client %d: %v", ctx.ID, err)

View File

@@ -39,6 +39,7 @@ type Context struct {
// Client info // Client info
Info ClientInfo Info ClientInfo
IsLoggedIn atomic.Bool IsLoggedIn atomic.Bool
IsAuthorized atomic.Bool // Whether client is authorized via heartbeat
// Connection state // Connection state
OnlineTime time.Time OnlineTime time.Time

View File

@@ -260,7 +260,28 @@ func (s *Server) processData(ctx *connection.Context) {
if err == protocol.ErrNeedMore { if err == protocol.ErrNeedMore {
return return
} }
// 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) s.logger.Error("Parse error for connection %d: %v", ctx.ID, err)
}
_ = ctx.Close() _ = ctx.Close()
return return
} }