Add a dedicated security audit trail to the Go server. Login attempts, authorization denials, WebSocket lifecycle, remote screen and terminal sessions, clipboard operations, device authentication and admin mutations now emit stable JSON-line events (audit=true, event=<name>) to a separate logs/audit.log, mirrored to server.log. The path is configurable through YAMA_AUDIT_LOG and the separate file can be switched off. Records never contain passcodes, password hashes, tokens or clipboard contents, and untrusted string fields are stripped of control characters and truncated so they cannot forge log lines. Expose the trail to administrators through a query_audit WebSocket command and an Audit Log page in the web UI. The page filters by time range, event (exact or prefix), actor, IP, device id, keyword and warning level, pages newest-first, and stays hidden unless the server advertises the auditLog capability, so older C++ servers degrade cleanly. Each query scans at most the newest 32 MB of the current file to bound latency, records itself as admin.audit.query, and tolerates torn or non-audit lines. Add unit tests for event emission and redaction, query filtering, paging, malformed lines, missing or disabled audit files, and handler authorization. Co-Authored-By: deepseek-flash
SimpleRemoter Go TCP Server Framework
基于 Go 语言实现的高性能 TCP 服务端框架,用于替代原有的 C++ IOCP 服务端。
项目结构
server/go/
├── go.mod # Go 模块定义
├── auth/
│ └── auth.go # 授权验证模块 (TOKEN_AUTH + Heartbeat HMAC)
├── buffer/
│ └── buffer.go # 线程安全的动态缓冲区
├── connection/
│ ├── context.go # 连接上下文
│ ├── errors.go # 错误定义
│ └── manager.go # 连接管理器
├── protocol/
│ ├── parser.go # 协议解析器
│ ├── codec.go # 编解码和压缩 (ZSTD)
│ ├── header.go # 协议头解密 (8种加密方式)
│ └── commands.go # 命令常量和LOGIN_INFOR解析
├── server/
│ ├── server.go # TCP 服务器核心
│ └── pool.go # Goroutine 工作池
├── logger/
│ └── logger.go # 日志模块 (基于 zerolog)
├── hub/
│ └── hub.go # 在线设备注册表 + 事件订阅
├── wsauth/
│ └── wsauth.go # Web 鉴权 (challenge-response + 不透明 token)
├── web/
│ ├── embed.go # //go:embed 嵌入 HTML/xterm.js 等 web 资源
│ ├── server.go # HTTP server (静态页面 + REST + WS 路由)
│ ├── ws.go # WebSocket 连接生命周期
│ ├── ws_handlers.go # WS 消息分发与处理
│ └── assets/
│ ├── index.html # 从 ../../web/index.html sync 而来 (gitignored)
│ └── static/ # 第三方 xterm.js 资源 (checked in)
├── licensing/
│ ├── signer.go # Signer interface (Sign / Mode / Close)
│ ├── local.go # LocalSigner — operator 部署,HMAC 直连
│ ├── remote.go # RemoteSigner — 客户部署,HTTPS + singleflight + 24h cache + heartbeat ticker
│ ├── noop.go # NoOpSigner — free tier,返回空签名
│ ├── token.go # JWT RS256 校验 + RSA 公钥加载
│ ├── quota.go # 配额追踪 (Reserve / RefreshExisting / 5min eviction)
│ ├── server.go # License Server HTTP handlers (/license/sign, /license/heartbeat) + Issue()
│ ├── factory.go # NewFromEnv / LicenseServerFromEnv + 模式选择
│ └── licensing_test.go # 17 个单元测试
└── cmd/
└── main.go # 程序入口
核心特性
底层基础设施:
- 高并发: 基于 Goroutine 池管理并发连接
- 协议兼容: 支持原有客户端的多种协议标识 (Hell/Hello/Shine/Fuck)
- 协议头解密: 支持 8 种协议头加密方式 (V0-V6 + Default)
- 授权验证: TOKEN_AUTH 和 Heartbeat HMAC-SHA256 双重授权
- XOR 编码 / ZSTD 压缩: 与客户端完全兼容
- 字符编码自适应: 根据客户端能力位选择 UTF-8 直通或 GBK→UTF-8 转换
- 线程安全 / 优雅关闭 / 多端口监听 / 结构化日志
Web 应用能力 (Phase 3-7):
- Web 鉴权: challenge-response 登录 + 不透明 token,与 users.json schema 互通
- 登录加固: 双维度速率限制(10 次/分钟·IP + 5 次/15 分钟·用户名)+ 失败固定延迟,防口令枚举;
/get_salt用确定性假盐响应未知用户,杜绝用户名探测;WebSocket Origin 同源校验 + 显式白名单;/api/devicesBearer Token 鉴权 - 设备列表与监控: 在线设备 / RTT / 活动窗口 / 分辨率 实时下发
- Web 远程桌面: 浏览器 WebCodecs 解码 H.264,二进制 WS 帧低延迟中继;late-join 自动重发最近 IDR;优雅 BYE 关闭防止客户端无意义重连
- 鼠标 / 键盘输入: Win32 消息映射 (
WM_*/VK_*/MK_*),MSG64 48 字节布局直传客户端 - Web 终端: xterm.js + Windows ConPTY / 旧 cmd 管道双模式;二进制 "TRM1" 帧分流;尺寸自适应;单设备单 viewer
- 用户与分组: admin 可创建/删除 viewer 账号、配置 allowed_groups,users.json 原子写入
支持的命令
客户端 → 服务端
| Token | 值 | 用途 |
|---|---|---|
TOKEN_AUTH |
100 | 授权请求(SN + Passcode + HMAC) |
TOKEN_HEARTBEAT |
101 | 心跳包(携带 ActiveWnd / Ping / SN) |
TOKEN_LOGIN |
102 | 主连接登录 |
TOKEN_BITMAPINFO |
115 | 屏幕子连接首包,含分辨率 + clientID |
TOKEN_FIRSTSCREEN |
116 | 原始 BGRA 首帧(Go 侧丢弃) |
TOKEN_NEXTSCREEN |
117 | H.264 屏幕帧 |
TOKEN_SHELL_START |
128 | 旧 cmd-pipe 终端子连接首包 |
TOKEN_KEYFRAME |
134 | GOP 关键帧(DEFAULT_GOP 无限大,实际未用) |
TOKEN_TERMINAL_START |
232 | PTY 终端子连接首包 |
TOKEN_TERMINAL_CLOSE |
233 | 终端关闭通知 |
TOKEN_CONN_AUTH |
246 | 子连接身份握手,含 clientID |
| (raw bytes) | — | 终端 sub-conn 绑定后裸字节即 shell 输出 |
服务端 → 客户端
| Command | 值 | 用途 |
|---|---|---|
COMMAND_SCREEN_SPY |
16 | 启动屏幕捕获 |
COMMAND_SCREEN_CONTROL |
20 | 鼠标 / 键盘输入(MSG64 批次) |
COMMAND_NEXT |
30 | 解除客户端读线程阻塞 |
COMMAND_SHELL |
40 | 请求开启 shell 子连接 |
CMD_TERMINAL_RESIZE |
81 | PTY 尺寸 (cols / rows int16 LE) |
COMMAND_BYE |
204 | 优雅断开屏幕 / 终端 |
CMD_MASTERSETTING |
215 | 主控配置 + HMAC 签名 (1000B) |
CMD_HEARTBEAT_ACK |
216 | 心跳响应(携带 Authorized 字段) |
TOKEN_CONN_AUTH |
246 | 子连接身份握手响应 (256B) |
未列出的命令字节会被记录为 Debug 日志,按需扩展。
快速开始
安装依赖
cd server/go
go mod tidy
编译
推荐用 Makefile,编译前会自动从 server/web/ 同步 HTML 到 web/assets/:
make build # 当前平台
make windows # Windows amd64
make linux # Linux amd64
也可以直接用 go build,但要先手动 sync:
make sync && go build -o simpleremoter-server ./cmd
VSCode F5 调试时由 sync-web-assets preLaunchTask 自动同步。
运行
./simpleremoter-server
默认监听 TCP 端口 6543(被控设备),HTTP 端口 8080(浏览器 Web UI)。日志写到 logs/server.log,安全审计记录另写 logs/audit.log。
命令行参数
| 参数 | 默认值 | 说明 |
|---|---|---|
-port / -p |
6543 |
TCP 监听端口,分号分隔可多端口(如 6543;6544) |
-http-port |
8080 |
HTTP 监听端口(Web UI),传 0 禁用 |
-no-console |
false |
关闭控制台输出(守护进程模式) |
环境变量
| 变量 | 说明 | 示例 |
|---|---|---|
YAMA_PWDHASH |
密码的 SHA256 哈希值 (64位十六进制) | 61f04dd6... |
YAMA_PWD |
超级密码,用于 HMAC 签名验证;也作为 Web admin 密码的默认来源 | your_super_password |
YAMA_WEB_ADMIN_PASS |
Web UI 的 admin 密码(明文);优先于 YAMA_PWD。两者都未设置时 Web 登录禁用 |
your_admin_password |
YAMA_SIGN_PASSWORD |
[LocalSigner 模式] HMAC-SHA256 master key,直接给 CMD_MASTERSETTING 签名。Operator 自己的部署用。设置此变量后进入 LocalSigner 模式(见下方"签名模式")。 | <deployment-shared-secret> |
YAMA_LICENSE_SERVER |
[RemoteSigner / Trial 模式] License Server 公开 URL。不设置则用 DefaultLicenseServerURL (https://web.just-do-it.icu:8080)。客户部署设置此变量后进入 RemoteSigner 模式 —— 每次新设备登录会 HTTPS POST 给 License Server 拿签名,本机永远看不到 HMAC master key。 |
https://license.example.com |
YAMA_LICENSE_TOKEN |
[RemoteSigner 模式] Operator 颁发的客户 JWT(RS256),作为 Bearer token 鉴权。每个客户一份。未设置则进入 TRIAL 模式(匿名试用,按出口 IP 配额 2 台)。 | eyJhbGciOiJSUzI1NiI... |
YAMA_LICENSE_DISABLED |
设为 1 强制 NoOp 模式(既不读 token 也不连 License Server,客户端会拒绝屏幕/文件功能)。给本地开发 / 离线测试用。 |
1 |
YAMA_LICENSE_OFFLINE_HRS |
[RemoteSigner / Trial 模式] License Server 短暂不可达时,本地缓存签名的宽限期(小时)。默认 24。0 → 不缓存,每次新登录必须联网。 | 24 |
YAMA_LICENSE_PRIVATE_KEY |
[issue-token 子命令] RSA 私钥 PEM 路径,用于离线签发客户 JWT。与 YAMA_LICENSE_PUBLIC_KEY 配对。设置后 issue-token 子命令无需 -key 参数。 |
/opt/yama/license_priv.pem |
YAMA_LICENSE_PUBLIC_KEY |
[License Server 模式] Operator 自己(已经是 LocalSigner)想顺便对外提供 License Server 时,用来验证客户提交的 JWT 的 RSA 公钥 PEM 路径。必须与 YAMA_LICENSE_HTTP_ADDR 同时设置。 |
./license_pub.pem |
YAMA_LICENSE_HTTP_ADDR |
[License Server 模式] License Server HTTP 监听地址。仅在 LocalSigner 模式下生效(RemoteSigner 客户不能反向当 license server)。建议挂 nginx/Caddy 加 TLS 后对外。 | :8443 |
YAMA_LICENSE_STATE_PATH |
[License Server 模式] Quota 状态持久化文件路径。设置后每次新设备入队或 slot 被驱逐时原子写入磁盘(tmp + rename),License Server 重启时从此文件恢复设备列表,消除重启期间因 tracker 清空导致的配额绕过窗口。不设则仅内存状态,重启后 tracker 清零。 | /var/lib/yama/quota.json |
YAMA_USERS_FILE |
Path to the JSON file that persists non-admin web users (allowed_groups, password hash, salt). Default is users.json in the working directory. |
users.json |
YAMA_WEB_ALLOWED_ORIGINS |
Comma-separated WebSocket Origin allowlist for cross-origin upgrades. Empty (default) → only same-origin upgrades are accepted, which is correct when the web UI and /ws share a host. Add an entry per trusted PWA / dev origin. |
https://yama.example.com,https://yama-mobile.example.com |
YAMA_WEB_TRUST_PROXY |
Set to 1 only when running behind a reverse proxy you control (caddy / nginx / cloudflare). Switches client-IP extraction to use the last entry of X-Forwarded-For instead of RemoteAddr, so per-IP login rate limit sees the real client. Direct-exposure deployments MUST leave this unset — otherwise attackers can spoof the header to evade rate limits. |
1 |
YAMA_AUDIT_LOG |
安全审计日志路径(JSON lines)。默认 logs/audit.log;设为 off / none / 0 / - 关闭独立文件(审计记录仍会写入 logs/server.log)。审计记录带 audit=true 和稳定的 event 名,便于告警与检索。 |
/var/log/yama/audit.log |
YAMA_SSO_JWT_SECRET |
[SSO] Shared HS256 secret for bridging the parent portal's auth_token JWT into a YAMA web session via /sso. Must equal the upstream portal's signing key (FMS: SERVER_PRIVATE_KEY). Only admin portal users are auto-logged-in; other roles still need YAMA's own web login. Unset → SSO disabled, /sso returns 401. |
<shared-secret> |
# 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
签名模式(CMD_MASTERSETTING signer)
单个 Go 二进制按启动时的环境变量自动选择四种签名模式之一。同一个 master HMAC key 永远不会出现在客户机器上 —— 这是把 Go server 商业化部署给付费客户的核心安全前提。
| 模式 | 触发条件 | 用途 |
|---|---|---|
| LocalSigner | YAMA_SIGN_PASSWORD 已设 |
Operator 自己的部署。master HMAC key 在本机内存,签名直连 HMAC,微秒级延迟。可选:再设 YAMA_LICENSE_PUBLIC_KEY + YAMA_LICENSE_HTTP_ADDR 让本进程同时对外提供 License Server HTTP 服务。 |
| RemoteSigner (paid) | YAMA_LICENSE_TOKEN 已设(YAMA_LICENSE_SERVER 可选,未设则用 DefaultLicenseServerURL) |
付费客户部署。本机永远看不到 master HMAC key —— 每次新设备登录会 HTTPS POST 到 operator 的 License Server,带 Bearer JWT 鉴权,拿到签名后塞进 CMD_MASTERSETTING。同 (clientID, startTime) 元组的签名缓存 24h(可调,YAMA_LICENSE_OFFLINE_HRS),用于扛短暂网络故障。 |
| RemoteSigner (trial) | YAMA_LICENSE_TOKEN 未设、且未显式 YAMA_LICENSE_DISABLED=1 |
匿名试用模式。没有 JWT,连默认 License Server URL;服务端按下级出口 IP 识别身份,配额 FreeMaxDevices (2 台),且匿名 /license/sign 受 IP 限流(10 req/min)。零配置直接跑就在这个模式。 |
| NoOpSigner | YAMA_LICENSE_DISABLED=1 |
显式离线/开发模式。不连任何 License Server,返回空签名 → 客户端私有库拒绝启动 screen/file 功能。设备列表仍然可用。 |
注:YAMA_SIGN_PASSWORD 与 YAMA_LICENSE_* 同时设置时 LocalSigner 优先(operator 自己的 server 不应该回连自己)。
License Server endpoints(仅 LocalSigner 暴露)
设了 YAMA_LICENSE_PUBLIC_KEY + YAMA_LICENSE_HTTP_ADDR 后,本进程会监听 YAMA_LICENSE_HTTP_ADDR 提供两个端点:
POST /license/sign— body{"client_id":"...","start_time":"..."},headerAuthorization: Bearer <customer-JWT>,回{"signature":"<64-hex>"}。强制按 JWT 的tier+max_devices配额限制。POST /license/heartbeat— body{"active_device_count":N,"active_device_ids":["..."]},回{"server_view_count":M,"drift":N-M}。Drift 大于阈值时记日志,供 operator 反作弊审核。
部署建议:本进程只跑 plain HTTP,前面挂 nginx/Caddy/cloudflare 加 TLS。JWT 校验已经把 alg 锁死成 RS256,杜绝 alg:none 攻击。
颁发客户 JWT
第一步:一次性生成 RSA 密钥对(只在授权中心执行一次,私钥永久保管)
openssl genrsa -out license_priv.pem 2048
openssl rsa -in license_priv.pem -pubout -out license_pub.pem
license_priv.pem— 私钥,设为YAMA_LICENSE_PRIVATE_KEY,仅存于授权中心,绝不外发license_pub.pem— 公钥,设为YAMA_LICENSE_PUBLIC_KEY,授权中心 License Server 用于验证客户 JWT
第二步:颁发 JWT
server 二进制内置 issue-token 子命令。授权中心已配置 YAMA_LICENSE_PRIVATE_KEY 时,只需要提供客户标识:
# 最简调用(私钥路径从 $YAMA_LICENSE_PRIVATE_KEY 读取)
server issue-token -sub customer-acme
# 完整参数
server issue-token \
-sub customer-acme \ # 客户唯一标识(必填)
-tier paid \ # paid 或 trial(默认 paid)
-devices 20 \ # 最大并发设备数(默认 10)
-ttl 8760h # 有效期(默认 8760h = 1 年)
命令将 JWT 字符串输出到 stdout,将其作为 YAMA_LICENSE_TOKEN 交给客户。
| 参数 | 默认值 | 说明 |
|---|---|---|
-key |
$YAMA_LICENSE_PRIVATE_KEY |
RSA 私钥 PEM 路径;env 已设则无需重复指定 |
-sub |
(必填) | 客户唯一标识,建议用 company-id 格式 |
-tier |
paid |
paid 或 trial |
-devices |
10 |
并发设备上限;paid 必须显式设置合理值 |
-ttl |
8760h |
Token 有效期,支持 Go duration 语法(h/m/s) |
使用示例
完整的 TCP + Hub + Web 集成示例就是 cmd/main.go,那是程序入口本身、也是最权威的范例 —— 包含 handler 装配、hub 注册、web HTTP/WS 服务、信号优雅关闭等。
如果只想用 TCP 框架做自定义服务端(不要 Web/Hub),最小示例如下:
package main
import (
"os"
"os/signal"
"syscall"
"github.com/yuanyuanxiang/SimpleRemoter/server/go/connection"
"github.com/yuanyuanxiang/SimpleRemoter/server/go/logger"
"github.com/yuanyuanxiang/SimpleRemoter/server/go/protocol"
"github.com/yuanyuanxiang/SimpleRemoter/server/go/server"
)
type MyHandler struct{ log *logger.Logger }
func (h *MyHandler) OnConnect(ctx *connection.Context) {}
func (h *MyHandler) OnDisconnect(ctx *connection.Context) {}
func (h *MyHandler) OnReceive(ctx *connection.Context, data []byte) {
if len(data) == 0 {
return
}
if data[0] == protocol.TokenLogin {
info, _ := protocol.ParseLoginInfo(data)
h.log.Info("login: %s (%s)", info.PCName, info.OsVerInfo)
}
}
func main() {
log := logger.New(logger.DefaultConfig())
srv := server.New(server.DefaultConfig())
srv.SetLogger(log.WithPrefix("Server"))
srv.SetHandler(&MyHandler{log: log})
if err := srv.Start(); err != nil {
log.Fatal("start: %v", err)
}
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
srv.Stop()
}
配置选项
| 配置项 | 默认值 | 说明 |
|---|---|---|
| Port | 8080 | 监听端口 |
| MaxConnections | 10000 | 最大连接数 |
| MinWorkers | 4 | 最小工作协程数 |
| MaxWorkers | 100 | 最大工作协程数 |
| ReadBufferSize | 8192 | 读缓冲区大小 |
| WriteBufferSize | 8192 | 写缓冲区大小 |
| KeepAliveTime | 5min | 连接保活时间 |
| ReadTimeout | 2min | 读超时时间 |
| WriteTimeout | 30s | 写超时时间 |
日志配置
| 配置项 | 默认值 | 说明 |
|---|---|---|
| Level | Info | 日志级别 (Debug/Info/Warn/Error/Fatal) |
| Console | true | 是否输出到控制台 |
| File | "" | 日志文件路径 (空则不写文件) |
| MaxSize | 100 | 单个日志文件最大 MB |
| MaxBackups | 3 | 保留的旧日志文件数量 |
| MaxAge | 30 | 旧日志保留天数 |
| Compress | true | 是否压缩轮转的日志 |
日志示例输出:
{"level":"info","module":"Server","time":"2025-12-19T13:17:32+01:00","message":"Server started on port 6543"}
{"level":"info","module":"Handler","event":"login","client_id":1,"ip":"192.168.0.92","computer":"DESKTOP-BI6RGEJ","os":"Windows 10","version":"Dec 19 2025","time":"2025-12-19T13:17:32+01:00"}
{"level":"debug","module":"Handler","time":"2025-12-19T13:17:47+01:00","message":"Heartbeat from client 1 (DESKTOP-BI6RGEJ)"}
安全审计日志
Web 端与设备端的认证、授权、远程控制操作会额外写入独立的 logs/audit.log(可由 YAMA_AUDIT_LOG 改路径或关闭),同时镜像到 logs/server.log。每行是一个 JSON 对象,固定带 audit=true 与 event,其余字段按事件携带 actor / role / ip / peer_ip / conn / device_id / reason 等。
网页暴露到公网后,优先关注这些事件:
| 事件 | 含义 |
|---|---|
web.ws.connect / web.ws.reject |
WebSocket 建立 / 被拒(Origin 不允许、升级失败) |
web.login.success / web.login.failure / web.login.throttled |
登录成功、失败(含 reason、user_exists)、触发限流 |
web.salt.unknown_user |
get_salt 探测不存在的用户名(用户名枚举信号,每连接记一次) |
web.auth.invalid_token / web.authz.denied |
无效 token、权限不足(非 admin 调用管理命令) |
web.session.resume |
浏览器用缓存的 token 在重连后恢复会话 |
web.ws.close |
连接关闭;带 duration_ms、commands 以及 invalid_tokens / login_failures / admin_denied 等可疑计数 |
remote.screen.start / .stop / .denied |
远程桌面开始 / 结束(reason:disconnect、device_offline、ws_close、replaced)/ 被拒 |
remote.terminal.start / .stop / .denied |
远程终端会话生命周期 |
remote.clipboard.get / .set |
剪贴板读取 / 写入(只记字节数,不记内容) |
remote.rdp_reset |
远程 RDP 会话归位操作 |
admin.user.create / admin.user.delete / admin.device.delete |
管理操作(target / device_id / mode) |
admin.audit.query |
管理员在网页查询审计日志(记录筛选条件与命中数) |
device.online / device.offline |
受控设备上线 / 下线 |
device.auth.success / .failure / .throttled / .heartbeat.success |
设备 TCP 授权结果(只记 passcode 长度,不记 passcode) |
device.connauth.rejected / device.subconn.foreign_ip |
子连接签名失败 / 子连接来源 IP 与主连接不一致 |
管理员可以在网页里直接查询:设备页工具栏的 Audit Log 按钮(admin 角色且服务端在 caps 中声明 auditLog 时显示)打开审计页面,支持按时间范围、事件(精确值或前缀,如 web.*)、actor、IP、device_id、关键字筛选以及“仅警告”、分页。页面读取当前 logs/audit.log(超大文件只扫描末尾 32 MB,并会提示;已轮转的历史文件仍用下面的 grep/jq 排查),查询动作本身记为 admin.audit.query。
常用排查命令:
# 最近的登录失败与被拒操作
grep -E '"event":"web\.(login\.failure|login\.throttled|authz\.denied|salt\.unknown_user)"' logs/audit.log | tail -50
# 按来源 IP 聚合失败次数(找爆破源)
grep '"event":"web.login.failure"' logs/audit.log | jq -r '.ip' | sort | uniq -c | sort -rn | head
# 谁在什么时候开了远程桌面
grep '"event":"remote.screen.start"' logs/audit.log | jq -c '{time,actor,role,ip,device_id}'
注意:鼠标 / 键盘事件不逐条记录(频率太高,会把审计日志刷爆);审计以会话粒度(remote.screen.*、remote.terminal.*)体现操作。审计记录中的字符串字段来自客户端(用户名、User-Agent、Origin、设备名等),已做控制字符清理与长度截断,避免伪造日志行。
协议格式
数据包格式与 C++ 版本兼容:
+----------+------------+------------+------------------+
| Flag | TotalLen | OrigLen | Compressed Data |
| (N bytes)| (4 bytes) | (4 bytes) | (variable) |
+----------+------------+------------+------------------+
协议标识
| 标识 | Flag长度 | 压缩方式 | 说明 |
|---|---|---|---|
| HELL | 8 bytes | ZSTD | 主要协议 |
| Hello? | 8 bytes | None | 无压缩协议 |
| Shine | 5 bytes | ZSTD | 备用协议 |
| <> | 11 bytes | ZSTD | 备用协议 |
协议头加密
支持8种加密方式,服务端自动检测并解密:
- V0 (Default): 动态密钥,4种操作
- V1: 交替加减
- V2: 带旋转的异或
- V3: 带位置的动态密钥
- V4: 对称的伪随机异或
- V5: 带位移的动态密钥
- V6: 带位置的伪随机
- V7: 纯异或
LOGIN_INFOR 结构
客户端登录信息结构体 (考虑 C++ 内存对齐):
| 字段 | 偏移 | 大小 | 说明 |
|---|---|---|---|
| bToken | 0 | 1 | 命令标识 (102) |
| OsVerInfoEx | 1 | 156 | 操作系统版本 |
| (padding) | 157 | 3 | 对齐填充 |
| dwCPUMHz | 160 | 4 | CPU 频率 |
| moduleVersion | 164 | 24 | 模块版本 |
| szPCName | 188 | 240 | 计算机名 |
| szMasterID | 428 | 20 | 主控 ID |
| bWebCamExist | 448 | 4 | 是否有摄像头 |
| dwSpeed | 452 | 4 | 网速 |
| 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
// 创建服务器
srv := server.New(config)
// 设置日志
srv.SetLogger(log)
// 设置事件处理器
srv.SetHandler(handler)
// 启动服务器
srv.Start()
// 停止服务器
srv.Stop()
// 发送数据到指定连接
srv.Send(ctx, data)
// 广播数据到所有连接
srv.Broadcast(data)
// 获取当前连接数
count := srv.ConnectionCount()
Connection Context
// 发送数据
ctx.Send(data)
// 关闭连接
ctx.Close()
// 获取客户端 IP
ip := ctx.GetPeerIP()
// 检查连接状态
closed := ctx.IsClosed()
// 获取/更新最后活跃时间 (线程安全)
lastActive := ctx.LastActive()
ctx.UpdateLastActive()
duration := ctx.TimeSinceLastActive()
// 设置/获取客户端信息
ctx.SetInfo(clientInfo)
info := ctx.GetInfo()
// 设置/获取用户数据
ctx.SetUserData(myData)
data := ctx.GetUserData()
Protocol
// 解析登录信息
info, err := protocol.ParseLoginInfo(data)
if err == nil {
fmt.Println(info.PCName) // 计算机名
fmt.Println(info.OsVerInfo) // 操作系统
fmt.Println(info.ModuleVersion) // 版本
fmt.Println(info.WebCamExist) // 是否有摄像头
}
// 获取扩展字段
reserved := info.ParseReserved() // 返回 []string
clientType := info.GetReservedField(0) // 客户端类型
cpuCores := info.GetReservedField(2) // CPU 核数
filePath := info.GetReservedField(4) // 文件路径
publicIP := info.GetReservedField(11) // 公网 IP
与 C++ 版本对比
| 特性 | C++ (IOCP) | Go |
|---|---|---|
| 并发模型 | IOCP + 线程池 | Goroutine 池 |
| 跨平台 | Windows | 全平台 |
| 内存管理 | 手动 | GC |
| 代码复杂度 | 高 | 低 |
| 压缩 / XOR / 头加密 | 完整 8 套加密方式 + XOREncoder16 + ZSTD | 完全对齐 |
| 字符编码 | GBK | UTF-8 直通 / GBK→UTF-8 (按客户端能力位) |
| 设备列表与监控 | MFC 列表控件 | Web UI |
| Web 远程桌面 | 内嵌浏览器 + H.264 | 完全对齐(WebCodecs 解码) |
| 鼠标键盘转发 | 已实现 | 完全对齐 |
| Web 终端 | 内嵌 xterm.js + ConPTY | 完全对齐(含旧 cmd-pipe 兼容) |
| 用户 / 分组管理 | 已实现 | users.json schema 互通 |
| 文件传输 / 摄像头 / 录音 等 | 已实现 | 暂未实现(按需扩展) |
依赖
- github.com/klauspost/compress/zstd - ZSTD 压缩
- github.com/rs/zerolog - 高性能日志
- gopkg.in/natefinch/lumberjack.v2 - 日志轮转
- golang.org/x/text - GBK 编码转换
配置和启动参数
启动参数说明
-p/--port:受管设备 TCP 监听端口(默认 6543,可分号分隔多端口,如6543;6544;6545)--http-port:Web 管理 HTTP 端口(默认 8080,设为 0 则禁用 Web 管理)--no-console:守护进程模式,不输出到控制台(日志仍写入logs/server.log)
(1)授权中心(由我运行)
模式判定:设置了 YAMA_SIGN_PASSWORD,本进程以 LocalSigner 持有主 HMAC 密钥;可选地开启
License Server HTTP(YAMA_LICENSE_PUBLIC_KEY + YAMA_LICENSE_HTTP_ADDR),向下级服务签发带 24h 缓存的授权。
export YAMA_PWD="授权码 HMAC 校验密钥(TCP 端 passcode 签名验证)"
export YAMA_WEB_ADMIN_PASS="Web 端登录密码"
export YAMA_SIGN_PASSWORD="主控签名 HMAC 主密钥(受管设备验证服务端身份)"
export YAMA_WEB_TRUST_PROXY=1
export YAMA_WEB_ALLOWED_ORIGINS="https://web.just-do-it.icu:8080"
export YAMA_LICENSE_PRIVATE_KEY="/opt/yama/license_priv.pem"
export YAMA_LICENSE_PUBLIC_KEY="/opt/yama/license_pub.pem"
export YAMA_LICENSE_HTTP_ADDR="127.0.0.1:8443"
nohup ./server_linux_amd64 -p 8000 --http-port=9001 --no-console &
由前端代理将公网流量(受管设备 8000、Web 9001、License Server 8443)转发到本进程。受管设备通过 8000 端口直连服务端;
下级服务(运行 RemoteSigner)通过代理对外暴露的 YAMA_LICENSE_SERVER 公网 URL 取授权签名,代理后端转发到本进程
绑定的 YAMA_LICENSE_HTTP_ADDR (127.0.0.1:8443)。
可选环境变量:
YAMA_PWDHASH:TCP 授权码的 SHA256 哈希(未设置则使用代码内置默认值,启动 banner 会有警告)YAMA_USERS_FILE:额外 Web 用户列表 JSON 路径(默认users.json)
(2)下级服务(运营商部署)
下级二进制把"小白用户开箱即用"作为目标——理想情况下,什么都不配,直接 ./server_linux_amd64 就能跑起来。
启动后默认行为:
- License Server:默认连到
https://web.just-do-it.icu:8080(DefaultLicenseServerURL) - 模式:无
YAMA_LICENSE_TOKEN→ 进入试用模式(TRIAL),授权中心按下级出口 IP 识别身份,最多 2 台 受管设备(FreeMaxDevices) - Web 管理:无
YAMA_WEB_ADMIN_PASS→ 使用默认账号admin/admin,启动日志会大字警告 - 监听端口:受管设备 TCP 6543、Web 8080
最简启动(零配置试用,2 台设备上限)
nohup ./server_linux_amd64 --no-console &
启动日志会显示:
WARN ⚠ YAMA_WEB_ADMIN_PASS / YAMA_PWD 均未设置,Web 管理使用默认密码 admin/admin — 生产环境务必覆盖
INFO Signer mode: TRIAL (anonymous试用模式, license server=https://web.just-do-it.icu:8080,
最多 2 台受管设备; 设置 YAMA_LICENSE_TOKEN 解锁付费配额)
推荐生产配置(覆盖默认密码 + 付费 token)
export YAMA_WEB_ADMIN_PASS="自定义 Web 登录密码"
export YAMA_LICENSE_TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3M..." # 向授权中心申请
nohup ./server_linux_amd64 -p 6543 --http-port=8080 --no-console &
设置 YAMA_LICENSE_TOKEN 后切换为 REMOTE 模式(付费),配额由 JWT 的 max_devices 决定。
如果同时改 YAMA_LICENSE_SERVER 可以接到自部署的授权中心,否则继续走默认 URL。
其他可选
YAMA_LICENSE_DISABLED=1:完全禁用 License Server 通信(离线 / 内网测试),客户端会拒绝屏幕/文件功能但设备列表仍能用YAMA_LICENSE_OFFLINE_HRS=24:本地签名缓存的 TTL,默认 24h- 受管设备的
client.exe由 Windows 主控端生成(已绑定服务端地址和公钥),下级运营商把它分发给终端用户。 client 通过 6543 端口连接到本服务端;用户通过 8080 端口登录 Web 管理页面。
验证与排障
下级服务是否真的拿到付费授权,两侧各看一行日志即可定位:
| 现象 | 授权中心日志(logs/server.log) |
下级启动日志 | 结论 |
|---|---|---|---|
| 付费生效 | License Server: signed for sub=<客户名> ... active=N/M ttl=... |
Signer mode: REMOTE (paid customer, ...) |
✅ 正常 |
| 匿名试用 | License Server: signed for sub=trial:<下级出口IP> ... |
Signer mode: TRIAL (...) |
⚠️ 下级没读到 YAMA_LICENSE_TOKEN |
| token 无效 | nginx access.log 里 /license/sign 回 401 |
Signer mode: REMOTE 但设备连不上 |
⚠️ token 过期 / 密钥不匹配 / 被篡改 |
关键区分:sub=trial:<IP> 表示下级请求根本没带 Authorization 头(token 为空,等于没读到),
而不是 token 错 —— token 错会返回 401。两者根因不同,别误判成「token 配错了」。
systemd 改 env 文件后不生效(最常见坑)
EnvironmentFile= 只在服务启动那一刻读一次,改文件不会热更新,必须重启服务。典型表现就是
改了 YAMA_LICENSE_TOKEN 后日志仍显示 Signer mode: TRIAL。
# 1. 确认运行中的进程到底有没有读到该变量(这才是真相)
PID=$(systemctl show <服务名> -p MainPID --value)
tr '\0' '\n' < /proc/$PID/environ | grep -i YAMA_LICENSE_TOKEN || echo "进程里没有该变量"
# 2. 重启后再验一次 + 看启动日志
systemctl daemon-reload && systemctl restart <服务名>
journalctl -u <服务名> --no-pager -n 40 | grep -iE "signer mode"
下级日志出现 Signer mode: REMOTE 说明 token 已读入;授权中心日志出现 sub=<客户名> 即全链路闭环。
SSO(单点登录,可选)
Web UI 挂在已有域名(如 FMS 网关)的 /remoter 子路径下时,可让已登录门户的管理员免二次登录直接进远程桌面管理页。设 YAMA_SSO_JWT_SECRET(= 上游门户的 HS256 签名密钥,FMS 即 SERVER_PRIVATE_KEY)后启用:
export YAMA_SSO_JWT_SECRET="与上游门户一致的共享密钥"
行为:
/sso校验浏览器携带的auth_tokencookie(或Authorization: Bearer),通过后铸一枚 YAMA 自己的会话 token 返回给前端。- 只有
admin角色的门户用户会被自动登录;其他角色(viewer / 未知)即使已登录门户,/sso仍返回 401,前端回退到 YAMA 自带的 Web 登录表单,必须再输一遍 YAMA 的登录密码。 - 未配置
YAMA_SSO_JWT_SECRET时/sso恒 401,行为等同纯本地登录。
注意:SSO 只桥接「识别门户身份」,YAMA 本地登录(账号 + 密码)始终保留作为兜底;自动登录后的角色权限仍由 YAMA 自己的账号体系决定。
License
MIT License