用于 XXTouch 1.3.8-20260122000000+ 的云控服务端(WebSocket + 静态前端)与管理面板。
设备端协议实现源码位于设备端 /var/mobile/Media/1ferver/bin/open-cloud-control-client.lua。
- 官方发布下载页(GitHub Pages):https://havonz.github.io/XXTCloudControl/
- 各平台发布版本列表:https://github.com/havonz/XXTCloudControl/releases
- 推荐优先下载 Release 中的
XXTCloudControl-<YYYYMMDDHHMM>.zip,解压后直接运行对应系统二进制即可。
server/- 后端 WebSocket/HTTP 服务(入口server/main.go)frontend/- 管理面板(SolidJS),源码在frontend/src/,构建产物在frontend/dist/device-client/- Lua WebSocket 客户端库XXT 云控设置.lua- 设备端配置脚本(写入云控地址)build.sh- 构建并打包多平台服务端 + 前端build/- 构建产物目录server/data/- 运行时数据目录(默认data_dir=./data,取决于启动目录)
- WebSocket 实时通信、设备状态同步
- 前端面板 + 后端一体化部署(服务端可直接托管静态前端)
- 设备批量控制:脚本、触控、按键、重启/注销、剪贴板
- WebRTC 实时桌面控制(可选内置 TURN 穿透)
- 设备分组与脚本配置(FormRunner 动态表单)
- 服务器端文件仓库(scripts/files/reports)+ 设备/服务器双向文件传输(小文件 WS,大文件 HTTP Token)
- control/http 代理到设备本地 HTTP(用于 WebRTC 等设备 API)
- 打开发布页并下载最新
XXTCloudControl-<YYYYMMDDHHMM>.zip:
https://github.com/havonz/XXTCloudControl/releases - 解压后进入目录,按系统运行对应二进制:
# macOS (Apple Silicon 示例) chmod +x ./xxtcloudserver-darwin-arm64 ./xxtcloudserver-darwin-arm64 # Linux (amd64 示例) chmod +x ./xxtcloudserver-linux-amd64 ./xxtcloudserver-linux-amd64 # Windows (PowerShell) .\xxtcloudserver-windows-amd64.exe
- 首次启动会在当前目录生成
xxtcloudserver.json并输出随机密码(只显示一次)。 - 浏览器访问
http://<服务器地址>:46980登录管理面板。 - 如果忘记密码,可在同一目录重置后重启服务:
# macOS (Apple Silicon 示例) ./xxtcloudserver-darwin-arm64 -set-password 12345678 # Linux (amd64 示例) ./xxtcloudserver-linux-amd64 -set-password 12345678 # Windows (PowerShell) .\xxtcloudserver-windows-amd64.exe -set-password 12345678
docker pull havonz/xxtcloudcontrol
docker run --rm \
-v "$PWD/xxtcc-data:/app/data" \
havonz/xxtcloudcontrol \
-config /app/data/xxtcloudserver.json -set-password 12345678
docker run -d --name xxtcloudcontrol \
-p 46980:46980 \
-p 43478:43478/tcp -p 43478:43478/udp \
-v "$PWD/xxtcc-data:/app/data" \
-e XXTCC_CONFIG=/app/data/xxtcloudserver.json \
-e XXTCC_TURN_PUBLIC_IP="" \
-e XXTCC_TURN_PUBLIC_ADDR="" \
havonz/xxtcloudcontrol提示:如果你不挂载数据目录,默认会在容器内
/app/data生成数据与配置。 服务启动时会按顺序读取:配置文件 → 环境变量覆盖。环境变量不会自动写回配置文件。 环境变量名称可参考 docker-compose.yml 示例
mkdir -p XXTCloudControl && cd XXTCloudControl
curl -L -o docker-compose.yml https://raw.githubusercontent.com/havonz/XXTCloudControl/main/docker-compose.yml
docker compose up -d依赖:
go、npm、zip
bash build.sh产物输出在 build/,包含各平台二进制与打包的 zip:
build/
├── xxtcloudserver-<os>-<arch>[.exe]
├── ...
└── XXTCloudControl-<YYYYMMDDHHMM>.zip
解压后目录结构如下:
XXTCloudControl/
├── frontend/
├── xxtcloudserver-darwin-arm64
├── xxtcloudserver-linux-amd64
└── xxtcloudserver-windows-amd64.exe
在该目录内选择与你系统匹配的二进制运行即可自动托管前端(默认 frontend_dir=./frontend)。
依赖:
docker(需启用 buildx)
bash build-docker.sh产物输出在 build/:
XXTCloudControl-docker-<YYYYMMDDHHMM>-linux-amd64.tar
XXTCloudControl-docker-<YYYYMMDDHHMM>-linux-arm64.tar
-
启动后端:
cd server go run .
首次启动会在当前目录生成
xxtcloudserver.json并输出随机密码(只显示一次)。 -
启动前端开发服务器:
cd frontend npm install npm run dev访问
http://localhost:3000,在登录页输入服务器地址与端口(默认46980)以及密码。提示:开发服务器默认绑定
127.0.0.1:3000,并将/api代理到http://127.0.0.1:46980。若后端不在本机,请调整frontend/vite.config.ts或使用反向代理。
注意:
go run .在server目录启动时,默认frontend_dir为./frontend,不会自动指向../frontend/dist。若希望后端托管前端,请在配置里设置frontend_dir,或使用打包后的目录结构。
./xxtcloudserver-<os>-<arch> -set-password 12345678或在源码模式:
cd server
go run . -set-password 12345678-config <path>:指定配置文件路径(默认使用启动目录的xxtcloudserver.json)-set-password <pwd>:修改控制端密码-set-turn-ip <ip>:设置 TURN 公网 IP 并启用-set-turn-port <port>:设置 TURN 监听端口并启用-v/-h:查看版本 / 帮助
默认配置文件:xxtcloudserver.json(在启动目录生成)
{
"port": 46980, // WebSocket 服务端口
"passhash": "hex-string", // 密码的 HMAC-SHA256 哈希值
"ping_interval": 15, // 服务端发送 WebSocket PING 心跳的间隔(秒)
"ping_timeout": 10, // 设备连续未响应次数阈值,超过则断开连接
"state_interval": 45, // 服务端请求设备状态 (app/state) 的间隔(秒)
"frontend_dir": "./frontend", // 前端文件目录
"data_dir": "./data", // 服务端数据目录
"tlsEnabled": false, // 是否启用 TLS(HTTPS/WSS)
"tlsCertFile": "./certs/server.crt", // TLS 证书文件路径
"tlsKeyFile": "./certs/server.key", // TLS 私钥文件路径
"turnEnabled": true, // 是否启用 TURN 服务器
"turnPort": 43478, // TURN 服务器监听端口(默认 43478)
"turnPublicIP": "你的公网IP", // 公网 IP(需验证格式)
"turnPublicAddr": "turn.example.com", // 公网地址(IP 或域名,无验证)
"turnRealm": "xxtcloud", // TURN realm
"turnSecretKey": "你的密钥", // TURN REST 密钥(留空会自动生成)
"turnCredentialTTL": 86400, // TURN 凭据有效期(秒)
"turnRelayPortMin": 49152, // TURN 服务器中继端口范围起始
"turnRelayPortMax": 65535, // TURN 服务器中继端口范围结束
"customIceServers": [] // 自定义 ICE 服务器列表(见下文)
}passhash为hmacSHA256("XXTouch", password)的结果,不是明文密码。ping_interval控制心跳检测频率,服务端每隔该时间向设备发送 WebSocket PING 帧,用于检测设备在线状态。state_interval控制状态刷新频率,服务端每隔该时间向设备发送app/state请求以获取最新设备状态。ping_timeout表示设备连续未响应的次数阈值(基于ping_interval的周期),超过后服务端断开该设备连接。data_dir默认生成scripts/、files/、reports/以及分组/脚本配置等持久化数据。- 配置中的路径均相对启动目录;在
server/目录启动时,默认data_dir=./data会落在server/data/。 turnEnabled默认为true,但仅在配置了turnPublicIP或turnPublicAddr时才会实际启动内置 TURN。
为了支持外网环境下的实时桌面控制,服务端内置了支持 UDP/TCP 的 TURN 服务器。
服务端支持两种公网地址配置方式:
| 字段 | 格式 | 验证 | 适用场景 |
|---|---|---|---|
turnPublicIP |
仅 IPv4 地址 | net.ParseIP() 验证 |
有固定公网 IP |
turnPublicAddr |
IPv4 或域名 | 域名自动 DNS 解析 | 使用域名访问 |
Important
仅支持 IPv4:TURN 服务器目前仅支持 IPv4 地址。IPv6 地址或仅有 AAAA 记录的域名会导致启动失败。
如果两者都配置,turnPublicIP 优先。只需配置其中一个即可启用内置 TURN。
配置示例:
// 方式 1: 使用 IP
{
"turnEnabled": true,
"turnPublicIP": "203.0.113.1"
}
// 方式 2: 使用域名
{
"turnEnabled": true,
"turnPublicAddr": "turn.example.com"
}除了使用内置 TURN 服务,你还可以配置外部 STUN/TURN 服务器。这在以下场景很有用:
- 不想在本地启用 TURN 服务,而是使用第三方 TURN 服务(如 Metered)
- 需要将本地 TURN 与外部服务合并使用,增强穿透能力
Warning
安全提示:customIceServers 中的配置(包括 username 和 credential)会在 WebRTC 连接时发送给设备端,不是保密信息。请使用支持临时凭据的 TURN 服务,或确保凭据可公开共享。
配置示例:
{
"turnEnabled": false,
"customIceServers": [
{
"urls": ["stun:stun.relay.metered.ca:80"]
},
{
"urls": ["turn:global.relay.metered.ca:80"],
"username": "your-username",
"credential": "your-credential"
},
{
"urls": ["turn:global.relay.metered.ca:80?transport=tcp"],
"username": "your-username",
"credential": "your-credential"
},
{
"urls": ["turn:global.relay.metered.ca:443"],
"username": "your-username",
"credential": "your-credential"
},
{
"urls": ["turns:global.relay.metered.ca:443?transport=tcp"],
"username": "your-username",
"credential": "your-credential"
}
]
}合并行为:
| 本地 TURN | 自定义 ICE Servers | 结果 |
|---|---|---|
| 启用 | 无 | 仅使用本地 TURN |
| 禁用 | 有 | 仅使用自定义 ICE Servers |
| 启用 | 有 | 合并:本地 TURN + 自定义 ICE Servers |
| 禁用 | 无 | 无 ICE 服务器,WebRTC 仅尝试直连 |
# 设置公网 IP 并启用
./xxtcloudserver -set-turn-ip 1.2.3.4
# (可选) 设置监听端口 (默认 43478)
./xxtcloudserver -set-turn-port 3478Tip
turnSecretKey 为空时会在启动时自动生成临时密钥(重启会变化),如需稳定的 TURN 凭据请手动配置。
服务器管理员需要在云安全组/防火墙中开放以下端口:
| 端口范围 | 协议 | 用途 |
|---|---|---|
46980 (或自定义) |
TCP | 云控本体服务 (API & WebSocket) |
43478 (或自定义) |
UDP & TCP | WebRTC [TURN] 控制、握手与回退 |
49152 - 65535 |
UDP | WebRTC [TURN] 实时媒体流中继 |
Tip
媒体中继优先使用 UDP。在 UDP 流量被严格限制的情况下,WebRTC 会自动回退到 TCP (端口 43478) 以确保桌面流能够正常传输。
服务端支持原生 HTTPS/WSS,无需反向代理即可启用加密连接。同时也兼容通过 Nginx/Caddy 等反向代理的方式。
在 xxtcloudserver.json 中设置:
{
"tlsEnabled": true,
"tlsCertFile": "./certs/server.crt",
"tlsKeyFile": "./certs/server.key"
}mkdir -p certs
openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes \
-keyout certs/server.key -out certs/server.crt \
-subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"Warning
自签名证书仅适用于本地测试。生产环境请使用 Let's Encrypt 或其他 CA 签发的证书。
如果使用 Nginx/Caddy 等反向代理,服务端可保持 HTTP 模式运行,由代理处理 TLS 终止。此时绑定脚本会通过 X-Forwarded-Proto 请求头自动检测协议并生成正确的 wss:// 地址。
Nginx 配置示例:
server {
listen 443 ssl;
server_name your-domain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://127.0.0.1:46980;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /api/ws {
proxy_pass http://127.0.0.1:46980;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-Proto $scheme;
}
}- 运行脚本
XXT 云控设置.lua,填写ws://<host>:46980/api/ws(TLS 或反向代理场景使用wss://)。 - 或下载自动生成的绑定脚本:
http://<host>:46980/api/download-bind-script?host=<host>&port=46980
可追加proto=https强制生成wss://地址;反向代理场景也可由X-Forwarded-Proto自动识别。 - 或手动调用设备本地接口:
PUT http://127.0.0.1:46952/api/config { "cloud": { "enable": true, "address": "ws://<host>:46980/api/ws" } }
关闭云控:将 enable 置为 false。
- WebSocket 地址:
ws://<host>:<port>/api/ws(TLS/反代场景使用wss://) - 控制端消息需包含
ts/nonce/sign,时间戳允许 ±60 秒漂移,nonce在 120 秒内不可重复。
本项目的鉴权不使用固定 token,而是使用「短时效动态签名」:客户端每次请求携带当前秒级时间戳 ts、随机 nonce 与签名 sign,服务端在允许的时间窗口内校验签名正确性,并做 nonce 去重。
服务端配置文件 xxtcloudserver.json 中保存的是 passhash(不是明文密码):
passhash = HMAC-SHA256(key="XXTouch", message=password),结果为 64 位十六进制字符串(hex)。
控制签名使用 passhash 作为 HMAC key,对规范化后的基串做 HMAC:
base = ts "\n" nonce "\n" METHOD "\n" PATH_AND_QUERY "\n" bodyHash
METHOD:请求方法(GET/POST/PUT/DELETE…)PATH_AND_QUERY:path+ 排序后的 query(去除ts/nonce/sign)bodyHash:- 普通请求体:
SHA-256(bodyBytes)的 hex - 空 body 或 multipart(
multipart/form-data)暂不参与:bodyHash = ""
- 普通请求体:
base = ts "\n" nonce "\n" type "\n" bodyHash
type:消息类型(如control/devices)bodyHash:对body的 JSON 结果做SHA-256(hex);没有 body 则为空字符串
最终签名:
sign = HMAC-SHA256(key=passhash, message=base),结果为 hex 字符串。
注意:这里的
key=passhash指的是 passhash 的 hex 字符串本身(按字符串字节参与 HMAC),不是把 hex 解码为 32 字节后再参与计算。
- 允许的时间漂移:
ts在服务端当前时间±60秒内才会继续校验。 nonce在120秒内不可重复(重复视为重放)。- 校验失败返回
401 Unauthorized(HTTP)或直接关闭连接(WebSocket 控制端消息)。
除 http 下载绑定脚本外,所有 HTTP API 都需要携带签名(与 WebSocket 相同的算法):
- 受保护路径:所有
/api/* - 放行:
/api/download-bind-script(按你的要求保留无需签名)/api/config(前端启动配置)/api/control/info(JSON 版配置输出)/api/ws(WebSocket 升级握手不做 HTTP 鉴权;控制端消息仍需签名)/api/transfer/download/:token(临时 token 下载)/api/transfer/upload/:token(临时 token 上传)OPTIONS预检请求(CORS)
HTTP 请求可用两种携带方式(二选一):
-
请求头(推荐)
X-XXT-TS: <ts>X-XXT-Nonce: <nonce>X-XXT-Sign: <sign>
-
Query 参数(适用于下载/
window.open/img等无法方便加自定义 header 的场景)?ts=<ts>&nonce=<nonce>&sign=<sign>
示例:
# 查询分组(header 方式)
curl -sS \
-H "X-XXT-TS: 1700000000" \
-H "X-XXT-Nonce: <nonce>" \
-H "X-XXT-Sign: <hex-sign>" \
http://127.0.0.1:46980/api/groups
# 下载服务器文件(query 方式)
curl -L -o out.bin \
"http://127.0.0.1:46980/api/server-files/download/scripts/demo.lua?ts=1700000000&nonce=<nonce>&sign=<hex-sign>"{
"ts": 1700000000,
"nonce": "<nonce>",
"sign": "hex-sign",
"type": "control/command|control/commands|control/devices|control/refresh",
"body": {}
}控制端可通过 WebSocket 发送 control/http,将 HTTP 请求转发到设备(设备侧以 http.request 执行),常用于 WebRTC 相关接口。
{
"ts": 1700000000,
"nonce": "<nonce>",
"sign": "hex-sign",
"type": "control/http",
"body": {
"devices": ["udid1"],
"requestId": "uuid",
"method": "POST",
"path": "/api/webrtc/start",
"query": {},
"headers": { "Content-Type": "application/json" },
"body": "base64-json",
"port": 46952
}
}说明:
body需要 base64 编码;当请求为/api/webrtc/start且 TURN 已启用时,服务端会自动注入iceServers。
设备端发送 app/state,并在 body.system.udid 中提供唯一标识。
服务端通知控制端:
{
"type": "device/disconnect",
"body": "udid"
}{
"ts": 1700000000,
"nonce": "<nonce>",
"sign": "hex-sign",
"type": "control/devices"
}响应:
{
"type": "control/devices",
"body": {
"udid1": {},
"udid2": {}
}
}{
"ts": 1700000000,
"nonce": "<nonce>",
"sign": "hex-sign",
"type": "control/refresh"
}服务端会向所有设备广播 app/state 请求。
订阅指定设备日志:
{
"ts": 1700000000,
"nonce": "<nonce>",
"sign": "hex-sign",
"type": "control/log/subscribe",
"body": { "devices": ["udid1"] }
}取消订阅:
{
"ts": 1700000000,
"nonce": "<nonce>",
"sign": "hex-sign",
"type": "control/log/unsubscribe",
"body": { "devices": ["udid1"] }
}设备端若支持日志推送,会发送:
{
"type": "system/log/push",
"udid": "udid1",
"body": { "chunk": "log line..." }
}{
"ts": 1700000000,
"nonce": "<nonce>",
"sign": "hex-sign",
"type": "control/commands",
"body": {
"devices": ["udid1", "udid2"],
"commands": [
{ "type": "script/run", "body": { "name": "demo.lua" } },
{ "type": "screen/snapshot", "body": { "format": "png", "scale": 30 } }
]
}
}{
"ts": 1700000000,
"nonce": "<nonce>",
"sign": "hex-sign",
"type": "control/command",
"body": {
"devices": ["udid1"],
"type": "file/put",
"body": {
"path": "/scripts/xxx.lua",
"data": "Base64数据"
}
}
}{
"ts": 1700000000,
"nonce": "<nonce>",
"sign": "hex-sign",
"type": "control/command",
"body": {
"devices": ["udid1"],
"type": "file/put",
"body": {
"path": "/scripts/dir",
"directory": true
}
}
}{
"ts": 1700000000,
"nonce": "<nonce>",
"sign": "hex-sign",
"type": "control/command",
"body": {
"devices": ["udid1"],
"type": "file/list",
"body": {
"path": "/scripts"
}
}
}{
"ts": 1700000000,
"nonce": "<nonce>",
"sign": "hex-sign",
"type": "control/command",
"body": {
"devices": ["udid1"],
"type": "file/get",
"body": {
"path": "/scripts/xxx.lua"
}
}
}{
"ts": 1700000000,
"nonce": "<nonce>",
"sign": "hex-sign",
"type": "control/command",
"body": {
"devices": ["udid1"],
"type": "file/copy",
"body": {
"from": "/scripts/xxx.lua",
"to": "/scripts/yyy.lua"
}
}
}{
"ts": 1700000000,
"nonce": "<nonce>",
"sign": "hex-sign",
"type": "control/command",
"body": {
"devices": ["udid1"],
"type": "file/move",
"body": {
"from": "/scripts/xxx.lua",
"to": "/scripts/yyy.lua"
}
}
}{
"ts": 1700000000,
"nonce": "<nonce>",
"sign": "hex-sign",
"type": "control/command",
"body": {
"devices": ["udid1"],
"type": "file/delete",
"body": {
"path": "/scripts/xxx.lua"
}
}
}{
"ts": 1700000000,
"nonce": "<nonce>",
"sign": "hex-sign",
"type": "control/command",
"body": {
"devices": ["udid1", "udid2"],
"type": "system/respring"
}
}{
"ts": 1700000000,
"nonce": "<nonce>",
"sign": "hex-sign",
"type": "control/command",
"body": {
"devices": ["udid1", "udid2"],
"type": "system/reboot"
}
}{
"ts": 1700000000,
"nonce": "<nonce>",
"sign": "hex-sign",
"type": "control/command",
"body": {
"devices": ["udid1", "udid2"],
"type": "touch/down|touch/move|touch/up",
"body": {
"x": 100,
"y": 200
}
}
}{
"ts": 1700000000,
"nonce": "<nonce>",
"sign": "hex-sign",
"type": "control/command",
"body": {
"devices": ["udid1", "udid2"],
"type": "key/down|key/up",
"body": {
"code": "HOMEBUTTON"
}
}
}{
"ts": 1700000000,
"nonce": "<nonce>",
"sign": "hex-sign",
"type": "control/command",
"body": {
"devices": ["udid1"],
"type": "screen/snapshot",
"body": {
"format": "png",
"scale": 30
}
}
}{
"ts": 1700000000,
"nonce": "<nonce>",
"sign": "hex-sign",
"type": "control/command",
"body": {
"devices": ["udid1"],
"type": "pasteboard/read"
}
}{
"ts": 1700000000,
"nonce": "<nonce>",
"sign": "hex-sign",
"type": "control/command",
"body": {
"devices": ["udid1"],
"type": "pasteboard/write",
"body": {
"uti": "public.plain-text",
"data": "UTF8 文本或 Base64 图片"
}
}
}{
"ts": 1700000000,
"nonce": "<nonce>",
"sign": "hex-sign",
"type": "control/command",
"body": {
"devices": ["udid1"],
"type": "proc-value/put",
"body": {
"key": "foo",
"value": "bar"
}
}
}{
"ts": 1700000000,
"nonce": "<nonce>",
"sign": "hex-sign",
"type": "control/command",
"body": {
"devices": ["udid1"],
"type": "proc-queue/push",
"body": {
"key": "queue",
"value": "item"
}
}
}{
"ts": 1700000000,
"nonce": "<nonce>",
"sign": "hex-sign",
"type": "control/command",
"body": {
"devices": ["udid1"],
"type": "script/selected/put",
"body": {
"name": "demo.lua"
}
}
}{
"ts": 1700000000,
"nonce": "<nonce>",
"sign": "hex-sign",
"type": "control/command",
"body": {
"devices": ["udid1", "udid2"],
"type": "script/run",
"body": {
"name": "脚本名称.lua" // 这里的 name 如果为 "" 表示启动设备端已经选中的脚本
}
}
}{
"ts": 1700000000,
"nonce": "<nonce>",
"sign": "hex-sign",
"type": "control/command",
"body": {
"devices": ["udid1", "udid2"],
"type": "script/stop"
}
}- 所有控制命令(WebSocket)与除绑定脚本下载外的 HTTP API 都需要使用 HMAC-SHA256 动态签名验证
- 首次启动会生成随机密码(只显示一次),建议及时修改
- 大文件建议使用
/api/transfer/*走 HTTP 临时 token(WebSocket 仅适合小文件/控制消息)
