在家用 Ubuntu 开公网 Forge 整合包服务器:MCSManager + frp + ZeroTier 实战

在家用 Ubuntu 开公网 Forge 整合包服务器:MCSManager + frp + ZeroTier 实战

huangyixin447@gmail.com
2026-10-03 / 1 评论 / 11 阅读 / 正在检测是否收录...

本文记录我把一个约 300 个模组的 Forge 1.20.1 整合包(涟漪之篇·如涟漪之所见 0.2.23)开成公网服务器、和朋友联机的全过程,也记录了上线后踩过的坑和排查方法。

文中所有域名、IP、用户名都是示例值,照做时请换成你自己的。

0. 最终效果

  • 朋友在启动器里填一个域名就能进服,实测端到端延迟 p50 约 39ms,没有丢包。
  • 游戏跑在家里的 Ubuntu 主机上(Ryzen 9 9950X + 32GB 内存),存档留在自己手里。
  • 有一个网页管理面板:开关服、看实时控制台、传文件,全程 HTTPS,并开启两步验证。
  • 家里宽带没有公网 IP 也能用。

1. 整体架构:为什么用三台机器

家宽在 NAT 后面,外网连不进来,所以需要有公网 IP 的机器做入口。我把入口拆成了两台:

机器角色运行的服务
家里主机(Ubuntu 24)实际运行游戏Forge 服务端、MCSManager(面板 + 节点)、frpc
入口机 A(国内云服务器,我用的阿里云)游戏入口frps,对外开放 TCP 25565
入口机 B(另一台 VPS)网页入口Nginx + HTTPS 证书,反向代理面板

家里主机和入口机 B 通过 ZeroTier 组成虚拟局域网,面板流量走 ZeroTier;游戏流量走 frp 隧道(为什么不走 ZeroTier,见第 10.1 节)。

玩家的 Minecraft 客户端
  └─ mc.example.com:25565
       └─ 入口机 A:frps 监听 25565
            └─ frp 隧道(公网 TCP)
                 └─ 家里主机 frpc → 10.147.17.10:25565(Forge)

浏览器
  ├─ https://panel.example.com → 入口机 B Nginx → ZeroTier → 10.147.17.10:23333(面板 Web)
  └─ wss://node.example.com    → 入口机 B Nginx → ZeroTier → 10.147.17.10:24444(面板节点 Daemon)

为什么要拆成两台入口:

  • 游戏对延迟敏感,所以选离玩家近、延迟低的国内机器做游戏入口。
  • 面板需要 HTTPS 域名,还要上传 GB 级的整合包。放到另一台已经有 Nginx 和证书的 VPS 上,就不会挤占游戏入口的带宽。另外,国内云服务器用域名在 80/443 端口建站,一般需要先做 ICP 备案。
  • 只有一台云服务器也没问题:把 A、B 的工作都放在这一台上,原理完全一样。

为什么面板要用两个域名: MCSManager 分成「网页(Web)」和「节点(Daemon)」两个服务,浏览器会直接连接节点来上传文件、接收实时控制台。用 panel 和 node 两个域名,Nginx 就能在同一个 443 端口上区分后端,HTTPS 页面也能通过 WSS 连上节点,23333/24444 都不用对公网开放。

1.1 本文用到的示例值

名称示例值
家里主机 ZeroTier IP10.147.17.10
入口机 B ZeroTier IP10.147.17.30
入口机 A 公网 IP203.0.113.10
入口机 B 公网 IP198.51.100.20
游戏域名mc.example.com
面板域名panel.example.com
节点域名node.example.com

2. 准备工作

2.1 版本清单

项目版本
整合包涟漪之篇·如涟漪之所见 0.2.23
游戏Minecraft 1.20.1,Forge 47.4.16
Java64 位 Java 17(Forge 1.20.1 用 17,不要套用新版 MC 的 Java 21 要求)
面板MCSManager v10
内网穿透frp 0.52(TOML 配置格式)
组网ZeroTier
玩家启动器PCL(离线账号)

2.2 配置 DNS

以 Cloudflare 为例,添加三条 A 记录:

类型名称内容代理状态
Amc入口机 A 公网 IP仅 DNS(灰色云朵)
Apanel入口机 B 公网 IP仅 DNS(灰色云朵)
Anode入口机 B 公网 IP仅 DNS(灰色云朵)

三条都必须关闭代理(灰云):Cloudflare 的普通代理只转发 HTTP(S),不转发 Minecraft 的 TCP 25565;免费计划的代理还限制单次上传 100MB,GB 级的整合包会被拒绝。

添加后在本机核对:

dig +short mc.example.com panel.example.com node.example.com

应该输出你自己的公网 IP;如果出现 104.x、172.x 这类 Cloudflare 地址,说明代理没关。

2.3 ZeroTier 组网

家里主机和入口机 B 都加入同一个 ZeroTier 网络(安装与入网见 ZeroTier 官方文档)。在 ZeroTier Central 里给每台机器固定分配 IP,后面的监听地址和反向代理都写死了这些 IP,变了就会失效。

在每台机器上检查:

sudo zerotier-cli listnetworks
sudo zerotier-cli peers

状态应为 OK。在入口机 B 上确认能连到家里主机:

ip route get 10.147.17.10
ping -c 3 10.147.17.10
在 frp 方案下,入口机 A 不需要加入 ZeroTier;如果你打算用第 10.1 节提到的 Nginx stream 方案,A 才需要加入。

3. 把客户端整合包做成服务端

整合包是给客户端用的,不能直接拿来开服。它的 ZIP 里通常有一个 overrides/ 目录,里面才是 mods/、config/ 等内容。我的做法:

  1. 从 overrides/ 取出 mods/、config/、defaultconfigs/、kubejs/、tacz/、resourcepacks/(以你的整合包实际内容为准)。
  2. 排除三类模组:作者在文件名里标了「客户端」的、META-INF/mods.toml 里声明 clientSideOnly = true 的,以及整合包里的可选模组。
  3. 剩下的模组不代表都能在专用服务端上跑。 我实测后还需要再排除 5 个没有标记的客户端模组,否则服务端启动时会崩溃:
模组 JAR崩溃原因
ruok-forge_1.20.1_Pre-Release_6-1.7.4.jar通用 mixin 引用了 ClientLevel,在 Mixin 应用阶段崩溃
MaFgLib-0.1.14-mc1.20.1.jar加载了客户端类 client/gui/screens/Screen
oculus-mc1.20.1-1.8.0.jar同上(光影模组)
Fastquit-3.0.1+1.20.1forge.jar同上
IMBlocker-5.5.4-forge+1.17-1.20.4.jar(输入法冲突修复)加载了客户端类 client/KeyMapping
  1. 修改 config/QuantifiedAPI/quantified_config.json:把 "enableGpuAcceleration" 改为 false,"preferredGpuBackend" 改为 "CPU_ONLY"。如果主机有 NVIDIA 显卡,这个模组会在服务端探测 OpenCL/Vulkan,每次停服都会在 libEGL_nvidia.so 里段错误退出(存档已保存,但看着很吓人)。
  2. 加入启动用的文件:install-forge.sh、start-server.sh、user_jvm_args.txt、server.properties、eula.txt。

最终原包 297 个模组,服务端保留 264 个,打包后约 1.35GB。我把这些步骤写成了一个 Python 小脚本,排除模组时按 modId 而不是文件名匹配,这样整合包更新、JAR 文件名变了也能命中。客户端不需要删除这些模组,大家照常用原整合包进服即可。

install-forge.sh 下载 Forge 官方安装器、校验 SHA-1 后安装服务端:

#!/usr/bin/env bash
set -euo pipefail
cd -- "$(dirname -- "$0")"
MC_JAVA="${MC_JAVA:-/usr/lib/jvm/java-17-openjdk-amd64/bin/java}"
if [[ -e libraries/net/minecraftforge/forge/1.20.1-47.4.16/unix_args.txt ]]; then
  echo 'Forge is already installed; no change made.'
  exit 0
fi
installer=forge-1.20.1-47.4.16-installer.jar
curl --fail --location --retry 3 --connect-timeout 20 \
  "https://maven.minecraftforge.net/net/minecraftforge/forge/1.20.1-47.4.16/$installer" \
  --output "$installer"
printf '%s  %s\n' 0d4bdf4f4d8558c789df7313aeaf02e2c8f9211d "$installer" | sha1sum --check -
"$MC_JAVA" -jar "$installer" --installServer

start-server.sh 固定使用 Java 17 启动(系统默认 Java 是 21 也不影响):

#!/usr/bin/env bash
set -euo pipefail
cd -- "$(dirname -- "$0")"
MC_JAVA="${MC_JAVA:-/usr/lib/jvm/java-17-openjdk-amd64/bin/java}"
forge_args=libraries/net/minecraftforge/forge/1.20.1-47.4.16/unix_args.txt
if [[ ! -f "$forge_args" ]]; then
  echo 'Forge is missing. First run: bash install-forge.sh' >&2
  exit 1
fi
exec "$MC_JAVA" @user_jvm_args.txt "@$forge_args" nogui "$@"

4. 家里主机:安装 Java 17 和 MCSManager

sudo apt update
sudo apt install -y openjdk-17-jre-headless curl wget vim python3 unzip
/usr/lib/jvm/java-17-openjdk-amd64/bin/java -version
curl -fL https://script.mcsmanager.com/setup_cn.sh -o /tmp/mcsm-setup-cn.sh
vim /tmp/mcsm-setup-cn.sh            # 执行前先看一遍脚本
sudo bash /tmp/mcsm-setup-cn.sh --user mcsm
sudo systemctl enable --now mcsm-daemon mcsm-web
  • --user mcsm 不要省略,否则面板会默认以 root 运行。也可以从 MCSManager 的 GitHub Releases 下载官方包、校验 sha256 后手动安装。
  • 如果机器上装了 Docker,安装器可能会把 mcsm 用户加进 docker 组。docker 组几乎等于 root 权限,不用 Docker 实例的话,建议移出:
id mcsm
sudo gpasswd -d mcsm docker
sudo systemctl restart mcsm-daemon mcsm-web

4.1 让面板只监听 ZeroTier 地址

先停服务再改配置,避免运行中的服务把修改覆盖回去:

sudo systemctl stop mcsm-web mcsm-daemon
sudo cp -a /opt/mcsmanager/web/data/SystemConfig/config.json{,.bak}
sudo cp -a /opt/mcsmanager/daemon/data/Config/global.json{,.bak}
sudo vim /opt/mcsmanager/web/data/SystemConfig/config.json
sudo vim /opt/mcsmanager/daemon/data/Config/global.json

只改对应字段,其他字段和节点密钥都保留:

// web/data/SystemConfig/config.json
"httpIp": "10.147.17.10",
"httpPort": 23333

// daemon/data/Config/global.json
"ip": "10.147.17.10",
"port": 24444

(上面的 // 只是说明,JSON 文件里不能写注释。)改完后先校验 JSON,两个都通过再启动:

python3 -m json.tool /opt/mcsmanager/web/data/SystemConfig/config.json > /dev/null
python3 -m json.tool /opt/mcsmanager/daemon/data/Config/global.json > /dev/null
sudo systemctl start mcsm-daemon mcsm-web
sudo ss -lntp | grep -E '23333|24444'
坑: 改完监听地址后,面板会显示「节点未连接」。这是因为默认节点地址还是 localhost:24444,需要在面板的节点管理里改成 10.147.17.10:24444。

4.2 防火墙:只允许入口机 B 访问面板端口

我的主机上还跑着别的服务,ufw 也没启用,贸然开启「默认拒绝入站」容易误伤,所以单独建了一条 iptables 链,只管这几个端口:

#!/bin/sh
# /usr/local/sbin/mc-firewall.sh
# 面板/节点端口只允许入口机 B 的 ZeroTier 地址访问;
# 游戏端口只允许本机访问(frpc 在本机连接,走回环接口,不受这条链限制)。
set -e
iptables -N MC_ZT 2>/dev/null || iptables -F MC_ZT
iptables -A MC_ZT -p tcp -s 10.147.17.30 -m multiport --dports 23333,24444 -j RETURN
iptables -A MC_ZT -p tcp -m multiport --dports 23333,24444,25565 -j DROP
iptables -C INPUT ! -i lo -j MC_ZT 2>/dev/null || iptables -I INPUT 1 ! -i lo -j MC_ZT

如果你用第 10.1 节的 Nginx stream 方案(入口机 A 经 ZeroTier 连游戏端口),还要在 DROP 那行之前加一行,放行入口机 A 的 ZeroTier IP 访问 25565。

用 systemd 让它开机时在面板之前加载:

# /etc/systemd/system/mc-firewall.service
[Unit]
Description=Restrict MCSManager/Minecraft ports
Before=mcsm-web.service mcsm-daemon.service
After=network-pre.target
Wants=network-pre.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/mc-firewall.sh

[Install]
WantedBy=multi-user.target
sudo chmod 755 /usr/local/sbin/mc-firewall.sh
sudo systemctl daemon-reload
sudo systemctl enable --now mc-firewall.service
sudo iptables -S MC_ZT

4.3 开机时等 ZeroTier 地址就绪

面板监听的是 ZeroTier IP,如果开机时网卡还没拿到地址,面板会绑定失败。光写 After=zerotier-one.service 不够,因为 ZeroTier 服务启动了不代表 IP 已经分配好。我加了一个等待脚本:

#!/bin/sh
# /usr/local/sbin/wait-zt-ip.sh:最多等 120 秒
i=0
while [ "$i" -lt 120 ]; do
  ip -4 addr show | grep -q 'inet 10.147.17.10/' && exit 0
  i=$((i + 1))
  sleep 1
done
echo "ZeroTier address not ready after 120s" >&2
exit 1

再给 mcsm-web 和 mcsm-daemon 各加一个 drop-in 文件 /etc/systemd/system/mcsm-web.service.d/10-wait-zerotier.conf(daemon 同理):

[Unit]
After=zerotier-one.service mc-firewall.service
Wants=zerotier-one.service mc-firewall.service

[Service]
ExecStartPre=/usr/local/sbin/wait-zt-ip.sh
RestartSec=10
sudo chmod 755 /usr/local/sbin/wait-zt-ip.sh
sudo systemctl daemon-reload

4.4 先私下初始化管理员,再发布到公网

不要把还没设置管理员的安装页面暴露到公网。 在自己电脑上开一个 SSH 隧道连到家里主机:

ssh -N -L 127.0.0.1:12333:10.147.17.10:23333 -L 127.0.0.1:12444:10.147.17.10:24444 你的用户名@家里主机

浏览器打开 http://127.0.0.1:12333,创建管理员并设置强密码,然后在账户设置里开启 TOTP 两步验证,退出后重新登录确认一遍。管理员账号不要共享给朋友;朋友需要帮忙管理时,单独建普通账号,只分配这一个实例。

5. 入口机 B:用 Nginx 反向代理面板(HTTPS + WSS)

5.1 申请证书

panel 和 node 两条 DNS 记录生效后,用 certbot 申请一张同时覆盖两个域名的证书:

sudo certbot certonly --nginx -d panel.example.com -d node.example.com

证书在 /etc/letsencrypt/live/panel.example.com/,certbot 的定时任务会自动续期。

5.2 Nginx 配置

新建站点配置(Debian/Ubuntu 习惯放在 sites-available/ 再链接到 sites-enabled/):

map $http_upgrade $mcsm_connection_upgrade {
    default upgrade;
    '' close;
}

server {
    listen 443 ssl;
    server_name panel.example.com;

    ssl_certificate     /etc/letsencrypt/live/panel.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/panel.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    client_max_body_size 2g;
    client_body_timeout 3600s;

    location / {
        proxy_pass http://10.147.17.10:23333;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $mcsm_connection_upgrade;
        proxy_connect_timeout 10s;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
        proxy_request_buffering off;
        proxy_buffering off;
    }
}

server {
    listen 443 ssl;
    server_name node.example.com;

    ssl_certificate     /etc/letsencrypt/live/panel.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/panel.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    client_max_body_size 2g;
    client_body_timeout 3600s;

    location / {
        proxy_pass http://10.147.17.10:24444;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $mcsm_connection_upgrade;
        proxy_connect_timeout 10s;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
        proxy_request_buffering off;
        proxy_buffering off;
    }
}

# 可选:HTTP 跳转 HTTPS。跳转写在 location 里,不影响证书续期验证
server {
    listen 80;
    server_name panel.example.com node.example.com;
    location / {
        return 301 https://$host$request_uri;
    }
}

几个关键点:

  • client_max_body_size 2g:上传超过这个大小会返回 413,按整合包大小调整。
  • proxy_request_buffering off:不让 Nginx 先把大文件写到临时目录再转发。
  • Upgrade/Connection 头和长超时:实时控制台走 WebSocket,必须有。
  • X-Real-IP 用 $remote_addr 覆盖:客户端没法伪造自己的 IP(配合第 4.2 节的防火墙,只有入口机 B 能访问面板)。

检查通过再重载:

sudo nginx -t && sudo systemctl reload nginx

入口机 B 的防火墙/安全组需要放行 TCP 443(以及可选的 80)、ZeroTier 的 UDP 9993。

5.3 在面板里配置节点地址映射

打开 https://panel.example.com 登录后:

  1. 节点地址保持 10.147.17.10:24444 不改。 面板后端在家里主机本地直连节点,不绕经入口机 B;这样入口机 B 挂了,还能用 SSH 隧道管理。
  2. 在节点编辑页的「节点直连地址映射」里添加一条:
地址端口前缀
来源(浏览器访问面板用的地址)panel.example.com443留空
目标(浏览器连接节点用的地址)node.example.com443留空

前端会按当前页面的 host:port 匹配来源,命中后改连目标;HTTPS 页面会自动使用 WSS。

  1. 再加一条应急映射 127.0.0.1:12333 → 127.0.0.1:12444,配合第 4.4 节的 SSH 隧道使用。
  2. 在「设置 → 安全设置」开启反向代理模式。不开的话,面板看到的所有登录都来自入口机 B 的 IP,「同 IP 登录失败次数限制」被任何人触发后,所有人都会被锁在外面。
坑: 在界面里编辑映射时,保存会整体覆盖映射列表。改完后检查两条映射是不是都还在。

最后实际测试一次上传和实时控制台。能打开登录页,不代表节点能用。

6. 入口机 A:用 frp 转发游戏端口

6.1 入口机 A 运行 frps

从 frp 的 GitHub Releases 下载对应版本(我用的 0.52.3),解压到 /opt/frp/。配置 frps.toml:

bindPort = 7000
auth.method = "token"
auth.token = "换成一串足够长的随机字符"
# 只允许 frpc 申请 25565,避免 token 泄露后被用来开任意端口
allowPorts = [{ single = 25565 }]

systemd 服务:

# /etc/systemd/system/frps.service
[Unit]
Description=frp server
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/opt/frp/frps -c /opt/frp/frps.toml
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now frps

阿里云安全组放行:

协议/端口用途来源
TCP 25565玩家连接游戏最好只放行你和朋友的公网 IP(理由见第 8 节)
TCP 7000家里 frpc 连接 frps家宽 IP 会变,可以放开来源,靠 token 鉴权
TCP SSH 端口管理仅自己的 IP

6.2 家里主机运行 frpc

frpc.toml:

serverAddr = "203.0.113.10"     # 入口机 A 公网 IP
serverPort = 7000
auth.method = "token"
auth.token = "和 frps 相同的 token"

[[proxies]]
name = "mc"
type = "tcp"
localIP = "10.147.17.10"        # 与 server.properties 的 server-ip 一致
localPort = 25565
remotePort = 25565
# /etc/systemd/system/frpc.service
[Unit]
Description=frp client
After=network-online.target
Wants=network-online.target

[Service]
User=你的普通用户名
ExecStart=/opt/frp/frpc -c /opt/frp/frpc.toml
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
/opt/frp/frpc verify -c /opt/frp/frpc.toml
sudo systemctl daemon-reload
sudo systemctl enable --now frpc
journalctl -u frpc -n 30 --no-pager

注意几点:

  • frpc 重启时,所有玩家都会掉线一次,最好趁没人在线时改配置。
  • 经过 frp 转发后,服务端日志里所有玩家的来源 IP 都是家里主机自己的地址。所以登录模组「按 IP 自动登录」、「单 IP 注册上限」、按 IP 封禁这类功能都会失效,要关掉。
  • 我的 Forge 绑在 ZeroTier IP 上,是因为最早用的是 ZeroTier 转发方案。如果你只用 frp,可以把 server-ip 和 frpc 的 localIP 都改成 127.0.0.1,更简单。
  • 游戏能承载多少人,取决于入口机 A 的公网带宽,见第 10.2 节。

7. 导入服务端并首次启动

7.1 上传

在面板里新建「本机节点上的自定义 Minecraft Java 实例」,选一个空的工作目录,通过文件管理上传服务端 ZIP 并解压。

1GB 以上的包走网页上传比较慢,也可以在自己电脑上用 scp 经 ZeroTier 直接传到家里主机(我实测 1~3 MB/s),再在面板里解压。

解压后,工作目录根部应该直接是:

mods/  config/  defaultconfigs/  kubejs/  tacz/  resourcepacks/
install-forge.sh  start-server.sh  user_jvm_args.txt
server.properties  eula.txt

不要多套一层文件夹。

7.2 安装 Forge,再改启动命令

  1. 实例启动命令先设为 bash install-forge.sh,点「启动」,等它装完。
  2. 把启动命令改成 bash start-server.sh 并保存。(如果启动时又跑了一遍安装,就是这一步没保存。)

7.3 server.properties

只改下面这些字段,其他自动生成的字段保留:

server-ip=10.147.17.10
server-port=25565
level-name=world
online-mode=false            # 离线账号(PCL 离线登录)需要
enforce-secure-profile=false
white-list=true
enforce-whitelist=true
hide-online-players=true     # 状态查询只显示人数、不显示玩家名
allow-flight=true            # 见第 10.4 节
enable-rcon=false
enable-query=false
view-distance=8
simulation-distance=6
max-players=8
max-tick-time=180000         # 大整合包保存/加载慢,放宽看门狗,避免被误判卡死而强制关服

阅读并同意 Minecraft EULA 后,把 eula.txt 改成 eula=true。

7.4 内存

user_jvm_args.txt:

-Xms4G
-Xmx12G
-Dfile.encoding=UTF-8

32GB 的机器,开服后实际占用(RSS)约 6~9GB。不要把 -Xmx 设得太大,要给系统、面板和磁盘缓存留出余量。

7.5 首次启动

先用一个临时新世界测试,等控制台出现 Done (...)!。我去掉第 3 节那 5 个模组后,首次启动约 76 秒出现 Done (35.355s)!。

启动失败时:

  • 看 logs/latest.log 和 crash-reports/,先处理第一处有效异常。
  • MixinApplyError 或致命的 ... for invalid dist DEDICATED_SERVER:多半是还有客户端模组混在里面。把对应 JAR 移到 disabled-mods/(不要直接删)。只有 [mixin/]: Error loading class 这种 WARN 一般无害。
  • 模组加载失败后,JVM 可能因为残留线程不退出:面板显示「运行中」,日志却不再更新。这时点「强制终止」,再去看 crash-reports/ 里的 -fml.txt。
  • 不要为了「能开起来」大量删除内容模组,那样会改变世界内容。

确认能稳定运行后,在实例的事件任务里开启「自动启动」和「自动重启」(我设了最多 3 次)。测试阶段不要开无限重启。

8. 离线账号与白名单(一定要看)

online-mode=false 允许离线账号进服,但服务器不会验证这个名字是不是你的。白名单只按名字筛选,知道或猜到白名单里玩家名的人,可以用同名离线账号冒名进服。

8.1 白名单操作

在面板控制台执行,一次一行、不加 /(游戏内才加 /):

whitelist add Steve
whitelist add Alex
whitelist list
  • 多条命令一次性粘贴进去,会被当成一条执行,日志里会出现 <--[HERE]。
  • 玩家名要求 3~16 个字符,只能用英文字母、数字和下划线,区分大小写。名字超过 16 个字符,客户端会报 String too big (was N characters, max 16);中文名也会被拒绝。

8.2 核对离线 UUID

有时 whitelist add 会查到同名正版账号,写进 whitelist.json 的是正版 UUID,于是离线玩家进不来,提示「你不在此服务器的白名单中」。离线 UUID 的算法是对 OfflinePlayer:玩家名 做 MD5,再转成 v3 UUID,可以这样算:

import hashlib, uuid

def offline_uuid(name):
    h = bytearray(hashlib.md5(f"OfflinePlayer:{name}".encode()).digest())
    h[6] = h[6] & 0x0F | 0x30   # version 3
    h[8] = h[8] & 0x3F | 0x80   # IETF variant
    return str(uuid.UUID(bytes=bytes(h)))

print(offline_uuid("Steve"))

如果 whitelist.json 里的 UUID 和算出来的不一样,改成离线 UUID 后在控制台执行 whitelist reload。

8.3 降低冒名风险

能做的缓解措施:

  • hide-online-players=true:别人查询服务器状态时看不到在线玩家的名字。
  • 不给任何人 OP,管理命令(比如 gamemode creative Steve)都在面板控制台执行。
  • 不玩的时候在面板里关服。

这些只能降低风险。真正解决有两种办法:

  1. 只放行固定来源 IP:安全组里 25565 只允许你和朋友的公网 IP。
  2. 装登录验证模组:注意 Bukkit 的 AuthMe 插件不能用在 Forge 上。可以评估 Nedologin(有 Forge 1.20.1 版本),它服务端和所有玩家的客户端都要装。上线前先在测试副本上验证和整合包的兼容性,然后让自己人先登录、绑定凭据,别让陌生人抢先注册你的名字。

9. 备份与开机自启

9.1 备份

这个整合包自带 FTB Backups 2,默认每 30 分钟备份一次、有玩家在线才执行、保留 5 份,备份在实例的 backups/ 目录。要调整保留份数,改 config/ftbbackups2.json 里的 max_backups(模组会自动重载配置)。

  • 配置写了不等于备份真的成功。第一份备份出来后,用 unzip -t 检查一下,确认里面有 level.dat、playerdata/、region/。
  • 5 份每 30 分钟一次,最多只能回退约 2.5 小时的游戏进度;而且备份和存档在同一块硬盘上,硬盘坏了会一起丢。建议定期复制一份到别的机器。
  • 千万不要把备份文件放进 MCSManager 的数据目录(web/data/RemoteServiceConfig/、daemon/data/InstanceConfig/ 等)。面板会把目录里的每个文件当成配置加载,然后崩溃,报错 Cannot read properties of null (reading 'ip')。

回档步骤:关服 → 把 world/ 改名留底 → 把 backups/ 里需要的 zip(里面有 world/)解压到实例根目录 → 开服。这些都能在面板的「文件管理」里完成。

9.2 开机自启链路

家里主机重启后,各服务的启动顺序是:ZeroTier → 防火墙规则 → 等待 ZeroTier IP → 面板 Web/Daemon → 实例自动启动 → frpc。重启后检查:

sudo zerotier-cli listnetworks
sudo systemctl status mcsm-daemon mcsm-web frpc --no-pager
sudo ss -lntp | grep -E '23333|24444|25565'

注意,面板服务开机自启和游戏实例开机自启是两回事,后者要在面板的实例设置里单独开启。

10. 踩坑实录

10.1 几个人同时 Timed out:ZeroTier 丢包,改走 frp

最初的方案是在入口机 A 上用 Nginx stream 转发 TCP,经 ZeroTier 连到家里主机:

# 放在 nginx.conf 的主层级(http {} 和 events {} 外面),需要 stream 模块
stream {
    server {
        listen 25565;
        proxy_connect_timeout 10s;
        proxy_timeout 1h;
        proxy_socket_keepalive on;
        proxy_pass 10.147.17.10:25565;   # 家里主机的 ZeroTier IP
    }
}

(stream {} 不能放进 conf.d/,那个目录是在 http {} 里加载的。也不要开 PROXY protocol,Forge 默认不认这个协议头。)

现象: 几个玩家在几秒内一起 lost connection: Timed out,重连时卡在登录界面。

排查: 所有玩家共用的只有「入口机 A ↔ 家里主机」这一段,于是我在家里主机上分别测量到入口机 A 的 ZeroTier 地址和公网地址的 TCP 建连耗时(各测 150 次):

python3 - <<'EOF'
import socket, time

def probe(host, port, n=150):
    ok, fail = [], 0
    for _ in range(n):
        t = time.perf_counter()
        try:
            socket.create_connection((host, port), timeout=3).close()
            ok.append((time.perf_counter() - t) * 1000)
        except OSError:
            fail += 1
        time.sleep(0.1)
    ok.sort()
    if ok:
        print(f"{host}: fail={fail} p50={ok[len(ok)//2]:.0f}ms p95={ok[int(len(ok)*0.95)]:.0f}ms max={ok[-1]:.0f}ms")
    else:
        print(f"{host}: all {fail} failed")

probe("入口机A的ZeroTier IP", 22)   # 换成入口机 A 上一个对你开放的 TCP 端口
probe("203.0.113.10", 22)          # 同一台机器的公网地址
EOF

TCP 建连时丢一个包会触发约 1 秒的重传,所以丢包会表现为 p95/max 出现 1000ms 以上的尖峰。

结果: ZeroTier(UDP 隧道)丢包 4%~14%,延迟尖峰超过 1 秒;同一时间公网 TCP 0 丢包,稳定在 27~35ms。

处理: 游戏改走 frp(第 6 节),并在入口机 A 上注释掉 Nginx stream 的 include(两者不能同时占用 25565)。改完后端到端 p50 39ms、p95 48ms、0 丢包。

如果你的 ZeroTier 是直连、不丢包,Nginx stream 方案更简单;但上线前最好先用上面的脚本测一下。

10.2 所有人一起卡:入口机带宽只有 3 Mbps

现象: 三个人同时玩、特别是跑图时,所有人的延迟一起升高;但这时单独测新连接的延迟是正常的。

原因: 跑图时服务器要给玩家发 2~4 Mbit/s 的区块和实体数据,超过了入口机 A 约 3 Mbps 的出口带宽,数据都堵在入口机 A 上排队。家里主机到入口机 A 的隧道 RTT 只有 17ms、没有积压,瓶颈不在家里这一侧。

处理:

  • 降低流量:view-distance 从 8 降到 6,entity-broadcast-range-percentage 从 100 降到 75,估计能少四成左右的区块流量。
  • 或者给入口机 A 提升带宽,比如阿里云改成「按使用流量计费」并调高峰值带宽。

经验: 选游戏入口机时,除了看延迟,还要看出口带宽。多人跑图的模组服,每人按 1 Mbps 以上估算比较稳妥。

10.3 一按 R 换弹就被踢:terra_entity 的「骑乘」键

这是排查最久的一个问题。

现象:

  • 只有一个玩家(Mac 客户端)用枪按 R 换弹时被踢,每次都能复现;其他两人换弹完全正常。
  • 客户端提示:

    Internal Exception: java.lang.RuntimeException: Attempted to load class
    net/minecraft/client/player/LocalPlayer for invalid dist DEDICATED_SERVER
  • 服务端日志里,报错出现在 Netty 网络线程上,不是主线程:

    [Netty Epoll Server IO #N/ERROR] [RuntimeDistCleaner/DISTXFORM]: Attempted to load class net/minecraft/client/player/LocalPlayer for invalid dist DEDICATED_SERVER
  • 服务器不崩,其他人不受影响。

排查过程:

步骤做法结论
1看断开前的日志:大量 RS 网络统计弹药的诊断输出时间上相关,但那段代码在主线程运行,报错却在网络线程,不是直接原因
2静态扫描所有服务端模组里引用 LocalPlayer 的数据包类,一共 116 个大多是正常的仅客户端代码,静态分析定位不出来
3用 javap 检查枪械模组 TACZ 的相关消息类都标了 @OnlyIn(Dist.CLIENT),排除 TACZ
4检查几个和换弹有关的附属模组及其 mixin写法规范,排除
5分析报错形态:原始 RuntimeException,没有被编解码异常包装说明发生在服务端处理客户端发来的模组数据包时
6对比这个玩家的 mods/ 和整合包:297 个完全一致不是模组差异,问题出在客户端发出的数据包本身
7开启 JVM 类加载日志,复现一次,看报错前加载了哪些类报错前 3ms,同一个网络线程上加载的唯一一个模组类是 terra_entity 的 CustomPacketPayload$IPayloadContext
8对比这个玩家的 options.txt 和整合包默认值「泰拉生物·骑乘」被绑到了 R,和换弹同一个键;整合包默认是不绑定

第 7 步是破案的关键。在 user_jvm_args.txt 里临时加一行:

-Xlog:class+load=info:file=logs/classload.log:time,tid

重启后复现一次被踢,然后在 logs/classload.log 里找报错时间点前后、和报错同一个线程 id(tid)加载的类,模组的类名会直接暴露是谁。查完记得把这一行删掉,否则日志会一直增长。

原因:

  1. 模组 bug: terra_entity 1.2.2 的网络上下文类里有一个 public Player player() 方法,客户端分支返回 LocalPlayer,但这个方法没有用 @OnlyIn(Dist.CLIENT) 隔离。JVM 校验这个类时,需要确认 LocalPlayer 是 Player 的子类,于是去加载 LocalPlayer;专用服务端上的 Forge 禁止加载客户端类,直接抛异常。这个类只在服务器第一次收到 terra_entity 的客户端数据包时才会加载,所以开服、跑图都正常,一触发就踢人。
  2. 触发条件: 这个玩家改过很多按键,「骑乘」恰好绑在了 R 上。每次按 R 换弹,都会同时发出一个骑乘数据包。

处理:

  • 在「选项 → 控制 → 按键绑定」里,把「骑乘」(泰拉生物)设为未绑定(选中后按 Esc)。也可以在游戏完全关闭时,把 options.txt 里的 key_key.terra_entity.ride:key.keyboard.r 改成 key.keyboard.unknown。
  • 顺便检查一下 R 上有没有绑其他功能(比如光影重载、法术轮盘),一起恢复默认。
  • 在模组作者修复之前,terra_entity 的骑乘功能在服务器上不可用:任何人用任何键触发都会被踢。建议提醒所有玩家不要绑定这个键。

通用方法: 凡是 Netty 线程报 ... for invalid dist DEDICATED_SERVER、而且只踢单个玩家的,都可以用「类加载日志 + 同线程追溯」来定位是哪个模组。

10.4 Flying is not enabled on this server

现象: was kicked for floating too long 或 Flying is not enabled on this server。

原因: allow-flight=false。模组的喷气背包、翅膀、法术浮空,以及网络抖动造成的位置不同步,都会触发原版的防飞行检测。

处理: 模组服把 allow-flight 设为 true。

10.5 常见问题速查

现象原因 / 处理
面板 502入口机 B 到家里主机 23333 不通:检查 ZeroTier、监听地址、防火墙
能登录面板,但控制台/上传失败节点映射、node 域名证书、Nginx 的 Upgrade 头
上传返回 413Nginx 的 client_max_body_size 太小
改完监听地址后「节点未连接」节点地址还是 localhost:24444,改成 ZeroTier IP
面板 Web 一启动就退出,报 reading 'ip'RemoteServiceConfig/ 里混进了多余文件,移出去
登录日志里的 IP 全是入口机 B「反向代理模式」没开或没保存
服务无法绑定 ZeroTier IP开机时 ZeroTier 地址还没分配,见第 4.3 节
启动后日志不动,面板显示运行中模组加载失败但 JVM 没退出,强制终止后看 crash-reports/
停服时 SIGSEGV ... libEGL_nvidia.soQuantified 探测了显卡,关掉 GPU 加速
启动时又跑了一遍 Forge 安装启动命令没改成 bash start-server.sh,或者改了没保存
进服提示不在白名单名字大小写不对,或者 whitelist.json 里是正版 UUID
<--[HERE]多条控制台命令被一次粘贴进去了
进服提示模组不匹配双方整合包版本、模组或配置不一致

11. 日常运维速查

要做的事怎么做
开服/关服面板实例页的「启动」/「关闭」(关闭会发送 stop,先保存世界再退出)
加/删白名单控制台:whitelist add 名字 / whitelist remove 名字
管理命令在面板控制台执行,相当于最高权限,不用给 OP
看日志实例内 logs/latest.log;崩溃看 crash-reports/;面板自身看 journalctl -u mcsm-web -u mcsm-daemon
看隧道journalctl -u frpc
回档见第 9.1 节
入口机 B 挂了用第 4.4 节的 SSH 隧道,打开 http://127.0.0.1:12333 管理
换别的整合包新建实例,同样用 25565;先关旧实例再开新实例,「自动启动」只开给当前在玩的实例

12. 进一步加固(建议)

  • 离线服务器装登录验证模组,或者只放行固定 IP(第 8.3 节)。
  • 定期把备份复制到另一台机器。
  • 找个没人玩的时间,实际重启一次家里主机,验证整条开机链路能自动恢复。
  • 在副本上做一次回档演练,确认备份真的能用。

参考资料

2

评论 (1)

取消
  1. 头像
    huangyixin447@gmail.com 作者
    MacOS · Google Chrome
    @

    注意本文精妙的地方在于巧妙的利用了各个服务器的优点,例如阿里云低的延迟,frp的稳定不丢包用于实时传输游戏服务,dimt域名自由和zerotier对于dimt有更大的带宽用于后续上传整合包,杜甫spark的计算能力强大。非常巧妙的利用了现有的环境搞定了大部分问题,内心十分快乐。美中不足的是阿里云的带宽自由5mb,只能勉强支持三人在线游玩,后面看来需要更强的200mb带宽的阿里云vps。

    回复