让朋友只记住一个域名:我的家庭 Minecraft 开服手记
这篇文章的目录
在家里开一个 Minecraft 服务器,启动游戏进程只是开头。真正要一直处理的是后面的事:公网端口变了,朋友该连哪里;域名还没更新,页面为什么说正常;网页上的延迟很好看,进游戏却完全不是那个感觉。
我想把朋友需要知道的事情尽量缩到一句话:Java 版多人游戏里填 mc.inklazy.com,想看情况,就打开同一个域名的状态页。地址和端口怎么变,留给自己维护。
最后留下的结构是:游戏在家里跑,Lucky 提供公网入口,Cloudflare 负责域名发现,香港服务器放状态页和地址更新服务,浏览器另外直测家里的延迟。 看起来用了好几个组件,但它们各自只回答一个问题。
这篇文章整理于 2026 年 10 月 25 日,依据本地保留的代码、部署快照和 10 月 11—12 日验收记录。文中的“验收时”都指那次检查,不代表今天的在线状态。配图是按实践资料绘制的结构示意,不是运行截图;示例地址也不能直接拿来连接。
家里负责游戏,香港负责把情况说清楚
家里这边,OpenWrt 负责出口、NAT、防火墙和端口转发。一台 Debian 虚拟机运行 Lucky,另一台 Linux 主机运行 Minecraft,由 MCSManager 管理。验收时,Minecraft 状态协议返回的版本是 Java 版 1.20.1;留下的材料不足以确认最终 Mod 加载器和整合包,我就不替它补一个名字了。
香港服务器没有加入游戏转发链路。我让它提供固定的 HTTPS 页面、接收 Lucky 的 Webhook、更新 DNS,再从外部做一些连通性诊断。
这样安排的理由很实际:家庭入口变动时,朋友仍有一个固定的地方看地址和故障信息。网页也不用跟着家里的动态端口搬来搬去。至于进游戏的流量,客户端解析完地址,就直接连接家庭入口。
四条链路各有终点。Cloudflare 在这里负责 DNS,香港负责网页和控制;两者都不承担这套方案的游戏流量中转。
这也决定了它的限制。香港能正常打开页面,不等于家里的 Minecraft 正常;家里网络不通,香港也不会自动接管游戏。把这两件事分开以后,页面才有机会解释“哪里出了问题”。
Lucky 给了入口,但 STUN 不是连通性的保证
Lucky 这部分,我保留了两条独立的 IPv4 TCP 映射。游戏通道和网页延迟探测各占一条,公网端口分别动态获取。
| 通道 | OpenWrt 转发到 Lucky | Lucky 再转到 |
|---|---|---|
| Minecraft 游戏 | 家庭通道 TCP 5554 | MC 主机的实际监听端口 |
| WSS 延迟探测 | 家庭通道 TCP 5556 | Lucky 主机的 127.0.0.1:18081 |
表里的 5554、5556 是家庭侧通道端口,不是让朋友固定填写的公网端口。MC 自己的监听端口又是另一回事。早期日志里出现过 25565 和别的目标端口,但没有最终快照的值,我不会把它写成现在照抄就能用的配置。
端口有好几层,排错时必须说清是哪一层。 我现在会沿着“公网地址与端口 → OpenWrt → Lucky 监听 → 转发目标 → MC 服务”逐段核对。只确认其中一段,很容易把问题归错地方。
STUN 在这里帮助 Lucky 发现 NAT 映射后的公网地址,映射的维持还依赖相应的保持机制。STUN 服务器不负责把每个游戏包转回家。上游 NAT 不配合、防火墙没放行、Lucky 没监听,或者游戏服务根本没有响应,都不是“拿到一个公网地址”就能解决的。
我也尝试过在腾讯云上用 Docker 跑 Coturn。历史日志里有配置文件、TLS 证书相关警告,但没有留全最终服务器地址和 Lucky 采用的配置。因此这段只能算尝试记录,不能写成已经稳定接入的自建 STUN 服务。Tailscale 能直连,同样不能替 Lucky 的公网被动入站做证明。它们经过的链路和条件并不一样。
一个域名,浏览器和 Minecraft 各走各的路
让 mc.inklazy.com 同时用于网页和进服,靠的是客户端查询方式不同。
浏览器打开 HTTPS 时,查询这个名字的 A 记录,去香港的 443 端口。Minecraft Java 客户端只填写域名时,则可以查询 _minecraft._tcp.mc.inklazy.com 的 SRV 记录,从里面拿到真正的游戏目标和端口。
A mc.inklazy.com → <香港固定 IPv4>A home.inklazy.com → <家庭当前公网 IPv4>SRV _minecraft._tcp.mc.inklazy.com → home.inklazy.com:<动态游戏端口>这套分工里,mc 是朋友记住的入口,home 是实际指向家庭的名字。游戏 SRV 的优先级和权重在验收配置里是 0 / 5,真正需要随 Lucky 变化的是家庭 A 记录和 SRV 端口。
我用的是 DNS-only 记录。普通 Cloudflare HTTP 代理不能直接代替 Minecraft TCP 入口;SRV 也只是告诉客户端“去哪里连”,不会搬运游戏流量,更不会让游戏改走 HTTPS 443。 DNS 简化的是朋友输入地址的过程,没有把真实地址变成秘密。
这里还有一个容易误用的地方:其他不支持 Minecraft SRV 的游戏,不能直接填 mc.inklazy.com:家庭映射端口。因为 mc 的 A 指向香港,那样实际上连错了机器。它们应该使用家庭域名,例如 home.inklazy.com:对应公网端口。
写成两三条 DNS 记录不难,难的是下一次地址变化后,这些记录能不能可靠地跟上。
Webhook 返回成功以后,事情还没有结束
Lucky 的游戏映射上报到 /api/webhook,探测映射上报到 /api/probe-webhook。两者沿用相同的认证格式,但职责分开。
POST /api/webhook HTTP/1.1Host: mc.inklazy.comAuthorization: Bearer <WEBHOOK_TOKEN>Content-Type: application/jsonLucky 里配置的请求体模板是下面这样。#{ip} 和 #{port} 由 Lucky 替换,它不是一份可以直接发出去的普通 JSON。
{"ip":"#{ip}","port":#{port}}这里的 Webhook Token 用于验证地址上报,Cloudflare API Token 则留在香港服务端,用于修改 DNS。把两种凭据分开,Lucky 不需要直接拿到 Cloudflare 的权限,浏览器也不需要接触其中任何一个。
最需要想清楚的是:接口返回 "success":true,究竟承诺了什么?
如果把“收到回调”和“Cloudflare 已更新”绑在同一个 HTTP 请求里,外部 API 一慢,Lucky 的请求就会被拖住。更麻烦的是,若只在内存里记下地址便返回成功,进程恰好重启,Lucky 以为报过了,服务端却可能再也找不到那次任务。
我把这个承诺收窄为:通过认证和公网地址校验后,地址与待同步任务已经持久化。
认证、校验地址 → SQLite 事务:最新地址 + 变化时间 + 待同步任务 → 提交成功,返回 success:true → 后台线程比较并更新 Cloudflare A / SRV写入失败就不能返回成功。DNS 的失败则留给后台重试,不能指望 Lucky 一定再报一遍相同地址。这两个故障由不同的一方负责,接口的语义才算说清楚。
后台只有一个 DNS 工作线程,按 5、10、20、40、80、160、300 秒 退避,之后以 300 秒为上限继续重试。进程或容器重启后,从数据库恢复未完成任务。
我还需要处理两种不太显眼的情况。第一种是同一个地址被重复上报:最近真实回调时间可以更新,但 IP、端口各自的变化时间不该跟着重置,也不该刷出一串“地址变化”事件。第二种是地址接连变化:任务尽量合并到最新期望值,并带版本号核对。旧请求回来时,不能因为它成功了,就把已经变化的新地址标记成同步完成。
Cloudflare 的 A 和 SRV 更新也不是一个原子操作。A 改成了、SRV 失败了,状态必须留下这半步进度,下一轮逐项比较、继续重试。它解决的是最终追上最新地址的问题,没有让整个 DNS 切换瞬间完成;递归解析器的缓存还会带来另一段等待。
探测 Webhook 只保存独立入口,不修改游戏 SRV。探测上报 IP 与游戏 IP 不一致时,页面也不会直接把它当成有效探测地址。否则两条映射各自上报,很容易互相覆盖,最后页面看起来有个地址,实际却不是同一条家庭链路。
为什么最后用了 SQLite
对这个规模的服务,我没有引入 Redis、Celery 或消息队列。后端是 Python 标准库 HTTP 服务,前端用普通 HTML、CSS 和 JavaScript,持久化用开启 WAL 模式的 SQLite,Docker Compose 负责部署。
SQLite 对我最有用的地方,是能把“当前想要什么地址”和“还欠着一次什么同步”放进同一个事务。这里只要一个后台线程,没必要为了很小的任务量,再维护一套额外的队列服务。
数据库里还保存探测入口、重试状态,以及地址变化、DNS 失败与恢复、MC 失败与恢复等事件。事件默认最多保留 2000 条、90 天,在写入新事件时清理;同类高频错误合并计数,正常轮询不反复记日志。内部用 UTC,页面统一显示北京时间。
这样查看历史时,我能分清“半小时前端口变过”和“半小时前收到了一次相同回调”。两者看起来都像有新活动,对排错的含义却完全不同。
当然,WAL 不等于备份。它也不会替我处理磁盘损坏和错误回滚。JSON 迁移到 SQLite 前保留了原文件,部署前另做归档;回滚脚本会先备份 SQLite、导出最新地址,再恢复旧服务。这个顺序很重要:代码可以退回旧版,游戏入口不能顺手退回一个早就过期的地址。
旧版没有可靠的异步 DNS 重试能力。即使旧页面能启动,也不能当作运维能力完全恢复。回滚前需要知道自己放弃了什么;那次验收没有在生产反向执行回滚,我也不把脚本检查写成完整生产回滚实测。
状态页最有用的地方,是承认几种“正常”并不相同
香港的 HTTPS 由宿主机已有的 Caddy 接入,状态容器只发布到回环地址:
公网 HTTPS 443 → Caddy → 127.0.0.1:18080 → 状态容器 :8080后端的 8080 没有直接暴露到公网。页面提供固定进服域名、当前实际入口与复制按钮,显示地址变化、真实 Webhook 接收时间、在线人数、版本、诊断和分页事件。它没有 TPS、MSPT,也没有通过 MCSManager API 读取 MC 主机 CPU 或内存。
我更在意诊断能不能帮助缩小范围,所以保留了几种不同的信号:
| 信号 | 它实际能说明什么 |
|---|---|
| 地址上报 | 最近何时收到正式游戏 Webhook;不是 Lucky 心跳 |
| DNS 同步 | 后台与 Cloudflare API 的同步进度 |
| 公开 DNS | Cloudflare、Google 两个公共解析器是否返回期望 A/SRV |
| 公网 TCP | 香港能否与游戏公网入口完成握手 |
| Minecraft | 是否收到了有效的 Java 版状态协议响应 |
| 访客 WSS | 当前浏览器到家庭探测服务的测量情况 |
“很久没收到回调”不能直接翻译成“Lucky 离线”,因为地址可能一直没有变化。TCP 能握手,也不能直接翻译成“MC 可玩”,后面可能没有有效的游戏协议响应。两个公开解析器结果一致,仍不能保证每位朋友的运营商缓存都已经更新。
页面可见时大约每 5 秒读取一次状态,外部 DNS、TCP、MC 诊断则由后台约每 30 秒独立执行,访客读取缓存。这样多开几个网页不会把外部探测也放大几倍。默认超过 120 秒的诊断标记为过期,避免拿着很久以前的绿色结果继续报平安。
这些信号没有被压成一个万能的“在线”。我宁愿多给一点解释:Cloudflare 已更新但公开解析还没跟上,或香港能查到 MC、当前访客却连不上 WSS。至少下一步该查什么,会更明确。
网页上的延迟,我希望它确实从访客出发
如果让香港服务器 Ping 家里,再把结果放在页面上,数字反映的是香港到家里的路径。朋友坐在另一条运营商网络里,读到这个数字,容易把它当成自己的游戏体验。
浏览器不能直接发 ICMP Ping,也不能把原始 Minecraft TCP 连接当普通网页请求来测。我另外在家里的 Lucky 主机上运行了一个 WSS 回显服务:Python 配合固定的 websockets==15.0.1,监听 127.0.0.1:18081,路径为 /ping,由第二条 Lucky 映射提供公网入口。
当前浏览器 → wss://home.inklazy.com:<动态探测端口>/ping → OpenWrt → Lucky → 家里的 WSS 回显服务香港不代理这段连接。测量先建立 TCP、TLS 和 WebSocket,做一次预热,再连续测五次消息往返,取中位数。连接建立与预热都不计入数字,我希望它表达的是连接建立之后的应用层 RTT,而不是把握手成本混在里面。
但它仍然不是精确的游戏内 Ping:终点在 Lucky 主机,不在 MC 进程;它不包含 Minecraft Tick 耗时,也不包含后续到 MC 主机的额外局域网转发和处理。它比“香港替你测”更贴近访客路径,但还不能凭一个数字判断游戏卡不卡。
网页切到后台时,我会立即关闭探测连接、取消等待和计时器,不把浏览器暂停记成网络故障。恢复后先刷新入口,再开始新测量。入口变化或旧连接迟到的结果,要靠测量代次隔开,不能覆盖新连接的状态。
页面可见时大约每 30 秒重测,失败或过期就清掉旧数字,也不悄悄回退成香港延迟。一个暂时没有结果的空位,比一个来历已经变了的数字更诚实。
证书解决了一个问题,转发又带来另一个问题
WSS 使用 home.inklazy.com 的证书,通过 Cloudflare DNS-01 申请,不依赖家庭公网固定的 80/443。续期 timer 每天检查,带随机延迟;WSS 进程每 60 秒检查并热加载证书。那次验收确认了有效证书、timer 和凭据文件权限,但没有等到证书真正过期再观察续期。
探测入口也有资源限制:指定 Origin、TLS 至少 1.2、全局最多 64 个并发、消息帧最多 256 字节、只回显最多 64 字符的文本,每连接最多 12 条消息、15 秒,并限制突发消息。Origin 检查有助于约束网页调用来源,不能当成公开服务的身份认证。
这里有个实测之后才不能含糊的边界:经过 Lucky 转发,WSS 看到的连接来源是 127.0.0.1。
代码虽然支持按真实 TCP 来源 IP 限制并发和新连接次数,但在这条公网路径上拿不到访客真实 IP。若对回环地址照常执行低阈值,所有朋友就会被当成同一个人。因此回环来源被刻意排除在单 IP 阈值之外,全局、消息和时长限制仍然执行。
不能靠让客户端随便加一个 X-Forwarded-For 修复它,那样来源可以伪造,而且当前是原始 TCP 转发、端到端 TLS。若以后真要做公网访客粒度限流,需要可信的源地址传递,例如收发双方都支持并限定可信转发端的 PROXY protocol,或者保留来源的透明转发。当前方案没有实现有效的公网访客单 IP 限流。 这条限制值得写在正文里,而不是藏在配置后面。
留下验收依据,也留下没有验证的部分
这次整理核对的是后端 app.py、前端 app.js、家里探测服务 probe.py、Compose,以及 OPERATIONS.md、ACCEPTANCE.md 和当时的状态快照。公开文章没有附带凭据、证书私钥、数据库或管理面板入口。
10 月 11—12 日的验收记录记载 53 项测试通过:后端 27 项、前端控制器 12 项、WSS 14 项。其中后端与前端有本地文本结果;WSS 的依据主要是当时工具输出和验收记录,留存的 wss-tests.txt 为空,不能算完整日志。这些数字是历史验收记录,不是写这篇文章时重新跑出来的结果。
当时还做过真实环境检查:迁移后关键状态保留、状态容器重启后数据仍在、两个 /test 接口无数据副作用、后台核对 Cloudflare A/SRV、两个公开解析器与 TCP/MC 响应,以及香港经公网映射完成 WSS 回显。正式页面也检查过浏览器直测和隐藏、恢复行为,公共页面与 API 做过实际 Token 的精确泄露比对。
磁盘写入失败、DNS 失败、连续地址变化等异常是在隔离环境或故障注入中验证的,没有为了测试故意破坏生产。手机布局检查用的是浏览器模拟,不能等同于朋友拿实体手机在移动网络上进服。自动续期、生产反向回滚,以及未留全的 Lucky 镜像、最终 STUN 配置,都不能因为写了一篇文章就自动变成“已验证”。
整理到这里,我觉得这次开服最有价值的部分,是以后遇到“连不上”时,手里终于有几条可以顺着查的线:先确认最新地址有没有可靠落库,再看 DNS 同步和缓存,接着查公网 TCP、MC 协议,最后分清香港的诊断和朋友自己的路径。
朋友仍然只需要记住 mc.inklazy.com。我多维护了一点东西,换来的是地址变化以后少靠手动通知,出问题时也少猜一点。