飞牛NAS白嫖H5棋牌游戏:openinggame/qp 从安全审计到完整部署实录
写在前头:这套东西是 2021 年的闭源商业演示版,放出来主要是引流到作者 Telegram 群的。部署玩玩可以,当生产平台用就算了。本文记录完整过程,重点是各种坑和修补方案,给同样爱折腾的朋友一个参考。
一、项目是什么
GitHub 上的 openinggame/qp,一套商用 H5 网页版棋牌游戏平台(麻将/斗地主/百家乐/捕鱼/彩票等 30+ 游戏),Go 微服务架构 + Egret 引擎前端,支持千万级用户是宣传话术,实际就是个小集群。
关键点:仓库里没有服务端源码,只有 docker-compose.yml + 数据库备份(mysqldb.tar.gz / mongodb.tar.gz),游戏逻辑全在 Docker Hub 的闭源镜像里:
openinggame/web:v1—— nginx + H5 前端openinggame/server:v1—— 游戏服务端(gs 游戏服 / gt 网关 / p 代理,三个 Go 进程挤在一个容器里)
二、先审计,再部署
部署前先把两个镜像从 Docker Hub 拉下来拆包做了静态分析(我部署的机器是飞牛 NAS,4 核 / 16G):
审计结论:没发现传统后门。
- 三个 Go 二进制 strings 扫描:无公网 IP、无外部回连、无挖矿/反弹 shell 特征
- 依赖全是正常库(sarama/zap/gokrb5 等)
- 数据库备份是 2021 年空库初始数据,无隐藏管理员账号
但有几个真实风险点:
- kafka 容器挂载了宿主机
/var/run/docker.sock—— 容器能控制宿主机 Docker,等于给容器开了宿主机的门(已删) - 镜像用可变 tag(
:v1),作者随时能推送恶意新版本(已锁本地) - 弱口令一堆:MySQL root/root、Redis 123456、MongoDB 123456,JWT 密钥硬编码
- 原版端口 80/81 直接暴露公网、无 TLS
另外这游戏有个"区块链开奖"功能(水果机/百人牛牛),开奖走 ginar.io 区块链服务、可跳以太坊浏览器验证——这就是它宣传的"公平公正",本质是赌场玩法背书,心里有数就行。
三、部署流程
3.1 环境
- 飞牛 NAS(x86_64,4核/16G),Docker 29.x + Compose v5
- 本机 80/81/443 被飞牛系统 nginx 占用,改用高位端口 8081/8082 只绑内网
3.2 第一个坑:镜像拉不下来
docker pull 走了代理环境变量完全无效——因为拉镜像是 dockerd 守护进程干的,客户端设的 https_proxy 它根本不理,直连 Docker Hub 基本卡死。
修补方案:手动从 registry 下载镜像层 + 构造 docker save 格式 + docker load 导入。
# 1. 拿 token
TOKEN=$(curl -s -x socks5h://旁路由:7893 \
"https://auth.docker.io/token?service=registry.docker.io&scope=repository:openinggame/server:pull" \
| jq -r .token)
# 2. 拿 manifest(注意 docker hub 返回的 digest 在响应头里)
curl -s -D headers.txt -H "Authorization: Bearer $TOKEN" \
-H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
"https://registry-1.docker.io/v2/openinggame/server/manifests/v1"
# 3. 下载每层(⚠️ blob 下载返回 307 跳 CloudFront,curl 必须加 -L)
curl -sL -H "Authorization: Bearer $TOKEN" \
"https://registry-1.docker.io/v2/openinggame/server/blobs/sha256:<layer-digest>"
# 4. 解压 gzip 层 → 按 config.json 的 rootfs.diff_ids 命名目录 → 写 manifest.json/repositories
# → tar 打包 → docker load -i xxx.tar要点:
- 多架构镜像(mysql/mongo 等)返回的是 manifest list,要按 platform=linux/amd64 再取一次单架构 manifest
- 层目录名用
config.json里rootfs.diff_ids(解压后 digest),不是下载时的压缩 digest - docker load 需要
manifest.json(含 RepoTags/Layers)+repositories文件,缺一不可
3.3 加固版 docker-compose
原版 compose 直接改造成这样(完整文件我放在文末链接,关键修改):
services:
web:
image: openinggame/web:v1 # 已本地锁死
ports:
- "192.168.2.21:8081:80" # 只绑内网,不用 80
environment:
- API_HOST=192.168.2.21
volumes:
- ./hall-config/HALL_config_53ef0ae.json:/HALL_config_53ef0ae.json:ro
server:
image: openinggame/server:v1
ports:
- "192.168.2.21:8082:81"
volumes:
- ./run.sh:/run.sh:ro # 修补后的启动脚本
mysql:
image: mysql:8.0.23
volumes:
- qp-mysql:/var/lib/mysql # named volume,不暴露端口
mongodb:
image: mongo:4.4.4
command: --auth
volumes:
- qp-mongo:/data/db
# redis0/1/2、kafka、zookeeper、etcd 全部不暴露端口,仅 game 网络内部删除/修改的关键项:
- kafka 去掉
- /var/run/docker.sock:/var/run/docker.sock - etcd 的
--data-dir改到卷路径(原版写容器层,重启丢数据) - kafka 加
KAFKA_HEAP_OPTS=-Xmx256m -Xms256m省内存 - 前端配置 HALL_config 的 ipList 预填
127.0.0.1:8082,web 启动时 entrypoint 会自动把127.0.0.1替换成 API_HOST
数据库数据:把仓库里的 mysqldb/mongodb 备份解压,用临时 alpine 容器灌进 named volume,注意 chown -R 999:999(mysql/mongo 的 uid 都是 999)。
四、排坑实录(重点)
坑 3:gs/gt 端口竞争,游戏服起不来
启动后登录接口一直连不上,查日志发现:
listen tcp 0.0.0.0:6071: bind: address already in use原因:gs(游戏服)和 gt(网关)跑在同一个容器里,两者都会绑定固定端口 6060/6071/6072。run.sh 里 gs 先启动,但 gs 二进制 55MB 加载慢,被后启动的 gt 抢先绑了 6071 → gs 的 RPC 模块初始化失败 → Web 服务(8080)不监听 → p 网关连不上游戏服 → 全链路挂。
修补:写自己的 run.sh 挂载覆盖容器里的 /run.sh,gs 启动后等 12 秒再启动 gt/p:
cd /server/gs && ./gs -profile=game >/dev/null 2>&1 &
sleep 12s # 等 gs 完成固定端口绑定
cd /server/gt && ./gt -profile=gate >/dev/null 2>&1 &
cd /server/p && ./p -profile=proxy >/dev/null 2>&1 &坑 4:浏览器报"所有线路不可用"
部署好了但浏览器打开一直提示线路不可用。排查过程非常曲折,结论是前端连的不是普通 WebSocket,而是定制 MQTT 协议:
- 前端线路检测是 HTTP 请求
/1,响应必须是字符串"2"(这个接口正常) - 登录接口
POST /game/login/by/guest正常,返回 JWT - 真正的坑在登录后的 WS 连接:前端用 Paho MQTT 客户端,连接地址是
ws://IP:8082/ws(Paho 定制版默认 path),必须带正确的 Origin 头(http://IP:8081),否则 403 握手成功后是定制 MQTT 握手流程:
- 客户端发标准 MQTT CONNECT(MQIsdp v3)
- 服务器回 PRE_CONNECT(0xF0 类型,带 messageIdentifier)
- 客户端计算
hash = CRC32(messageIdentifier + password + "!@#$%^&*IH")再发一次 CONNECT - 服务器校验通过才回 CONNACK
- 密码必须是 exchange 后的 token:登录返回的初始 token 还要再调一次
POST /game/login/exchange/token(form 格式token=xxx)换新 token,用初始 token 连 MQTT 必被拒
排查手段记录一下:headless Chrome CDP(--remote-debugging-port=9222)开页面抓 console + WebSocket 帧(Network.webSocketFrameSent/Received),把协议栈一层层扒出来的。附 Python 手工握手脚本:
import websocket, struct, zlib
ws = websocket.create_connection("ws://192.168.2.21:8082/ws",
header={"Origin": "http://192.168.2.21:8081"},
subprotocols=["mqttv3.1"])
# 第一次 CONNECT
ws.send_binary(mqtt_connect("test01", "user", token_exchanged))
pre = ws.recv() # 0xF0 02 xx xx = PRE_CONNECT
msg_id = pre[2] << 8 | pre[3]
hashv = str(zlib.crc32(f"{msg_id}{token_exchanged}!@#$%^&*IH".encode()) & 0xFFFFFFFF)
ws.send_binary(mqtt_connect("test01", "user", token_exchanged, game_version="1.0.000", hashv=hashv))
resp = ws.recv() # 0x20 02 00 00 = CONNACK 成功!如果你也遇到"所有线路不可用":先确认上面 5 步,再检查浏览器缓存——我这边服务端一切正常,最后发现是浏览器缓存了旧配置,清缓存 + 无痕窗口就好了。
坑 5:游戏服每 10 分钟报 ginar 超时
CallInitialize [http://172.16.236.22:7090/rng/initialize/...] i/o timeout原因:gs 硬编码了原作者内网 RNG 服务器地址(172.16.236.22:7090,区块链开奖服务),我们的网络里不存在,每 10 分钟重试超时一次。普通游戏(斗地主/麻将)不受影响,只影响"区块链开奖"类游戏初始化。闭源改不了,忽略即可。
坑 6:出牌假死、卡顿
排查发现是 NAS 负载高(4 核机器 load 5+):原有十几个容器 + 游戏集群 10 个容器 + 调试用的 headless Chrome 后台页面在抢 CPU。清理掉调试进程后负载从 5.7 降到 1.3,流畅多了。
另外这个游戏本身是 2021 年演示版,出牌逻辑 bug 多、体验不完善是常态,别指望生产级品质。
五、总结
- 这套棋牌能跑、能玩,游客免注册,每个注册号送 100 万游戏币,当玩具/研究用可以
- 闭源 + 演示版 = 别上线、别充钱、别当真
- 最大收获其实是排坑过程:手动下载 Docker 镜像、定制 MQTT 协议逆向、CDP 调试前端——折腾本身就是乐趣
部署目录结构:
qp/
├── docker-compose.yml # 加固版编排
├── run.sh # 修补版启动脚本(gs 先跑 12 秒)
├── hall-config/
│ └── HALL_config_53ef0ae.json # 定制前端配置(ipList 指向 8082)
└── repo/ # 原仓库(数据库备份等)记录于折腾之夜。如果这篇文章对你有帮助,欢迎交流;如果发现哪步有更好的解法,也请告诉我。