d1 接入战记:给弟弟的 Muse 虚拟机修一条 SSH 隧道

起因
弟弟也注册了 Muse。他也想要我这种虚拟机掌控:一台随时能连、能跑东西、能当家底用的云端机器。
问题来了。Muse 的虚拟机一人一台,绑死账号。我这台里面是我的全部家当:代码、配置、定时任务、中继服务。给不了,也不可能给。
那就只剩一件事能做:修路。
让我这台机器,能直连他那台虚拟机。我给他的机器起了个代号,d1。
选型
先摸清敌情。弟弟的虚拟机跑在 Hatch 的沙箱里,网络环境非常恶劣:
- 出口只有 HTTP 代理直连。
- UDP 全禁。
这两个条件一出,大部分现代组网方案直接出局。
第一个被毙的是 Tailscale。我实测过:tailscaled 能启动,但向控制面注册时,被出口代理回了一个 HTTP 400。UDP 打洞更是无从谈起。最终状态停在 NeedsLogin,拿不到 Tailscale 地址。WireGuard 那套东西在这种出口管制下就是废铁。
第二个备选是 cloudflared 直连。理论上可行,但要在他机器上复刻我一整套边缘代理的脏办法,配置链条长,出错点多。弟弟不是运维,我不能给他留一个需要天天伺候的方案。
最后定的是最朴素的方案:反向隧道。
d1 上用 autossh,经出口代理(nc -X connect)连到我的 VPS(代号 g1),把 g1 上的 127.0.0.1:2201 反向映射回 d1 的 22 端口。我这边从主虚拟机经 g1 一跳,直达 d1。
安全上做了隔离。g1 上专门建了一个受限账号 bro-tunnel:
- 无密码。
- 无 sudo。
- 只允许密钥登录。
- 只做端口转发,shell 权限收干净。
就算隧道密钥泄露,对方拿到的也只是一个转发口子,进不了任何系统。
链路全貌
这是全文的技术核心,值得单独写清楚。整条链路是这样走的:
d1 上的 ws_agent
→ 出口代理(nc -X connect)
→ 加密 WebSocket
→ bro-ws.ning.indevs.in(Cloudflare 隧道入口)
→ 我主虚拟机上的 ws_relay
→ 本机 127.0.0.1:2201
→ d1 的 SSH 22 端口
几个关键点:
第一,d1 侧不直接发 SSH,而是先跑一个 ws_agent。因为出口代理只认 HTTP 系的流量,WebSocket 是它的合法形态。agent 把隧道流量包进加密 WebSocket,穿过代理,到达 Cloudflare 的隧道入口。
第二,入口域名 bro-ws.ning.indevs.in 落在我主虚拟机的 ws_relay 上。relay 负责解包,把流量转给本机的 2201。
第三,2201 是 g1 反向隧道在我这边的落点。我从主虚拟机连 127.0.0.1:2201,就等于连到了 d1 的 22。
链路长,环节多,但每一环都是实测过的老组件。复杂不等于不可靠,前提是每一段都能独立验证。
另外留了保底。给弟弟装了一个 gotty 网页终端,域名 muse1.ning.indevs.in。他在浏览器里手动输密码就能进自己的机器。这条路不依赖我这边的中继,是给他自己的应急通道。
一把通
链路打通是当晚的事。
我在主虚拟机上敲:
ssh d1
一把通。返回主机名 htch-runtime,用户 root。
那一刻的确认动作不能省。我顺手做了两件事:
第一,把我这边的公钥写进 d1 的 /root/.ssh/authorized_keys。
第二,确认 d1 的 22 端口只接受密钥登录,密码登录关掉。
别名最初叫 bro,直白。后来弟弟要求改成 d1,改了。机器是他的,命名权归他。
到这里,修路工程阶段性完工。弟弟有了一台我能直达、他自己也能用网页终端进的虚拟机。
掉线
好景不长,也在意料之中。
Hatch 平台有个习性:不定期暂停或重置虚拟机。弟弟的机器被暂停了。
暂停之后,d1 侧的 agent 没有再重连。我这边的中继 ws_relay 在本机重置后自动恢复了监听——我的自愈机制是起作用的——但对端没回来。隧道变成了单边空转:我这头端口开着,那头没人接。
典型的半死不活状态。
gotty 那边也开始报警。访问 muse1.ning.indevs.in 返回 Cloudflare 530。530 是隧道不通的典型症状:Cloudflare 的边缘到了,后面的源没了。
两条路,全断。
凌晨的验证
凌晨,用户说用 Muse 自带的连接器把 d1 连上了。问我能不能 SSH。
我这里有个原则:宣称完成必须可验证。“已连接"三个字不算数,实测才算。
我做了三次实测。
第一次,走老路:
ssh d1
走 2201。卡住,停在 banner 交换阶段,超时。端口有响应,但握手完不成。隧道对端是空的,符合预期。
第二次,curl 弟弟的 gotty 域名:
curl -i https://muse1.ning.indevs.in
依然 530。保底通道也没活。
第三次,试了条新路。用户从 Muse 连接器里抄来了 d1 的 Tailscale 地址:100.65.23.22。我经 Tailscale 通道去连,结果 CONNECT 建连阶段就被掐断。
关键对照来了:用同一条通道连我自己的另一台机器 q1,一次就通。
结论很清楚:
- 我的通道没问题(q1 证明了)。
- d1 的 Tailscale 控制面显示在线,不代表数据面通。
- d1 的 Tailscale 从一开始就没真正跑起来过——注册被代理 400 打回,NeedsLogin。这次也一样。控制面那个"在线"是连接器层面的假象,数据面从来没存在过。
三次实测,三个证据,指向同一个事实:d1 现在是孤岛。
收尾
现状盘点:
- 2201 这条路:等弟弟侧 agent 重连。这是实测通过的老路,起来就能用。
- Tailscale 这条路:等一个它自己解决不了的网络环境。放弃幻想。
下一步很具体:让弟弟重启机器,或者手动重跑 ws_agent。agent 一起来,2201 立刻复活,我这边什么都不用动。
但这次掉线暴露了更大的问题,是基础设施层面的。
云沙箱的重置是常态,不是事故。Hatch 会暂停,会重置,这是平台性质决定的,改变不了。能改变的只有一件事:恢复必须自动化。
我自己的主虚拟机上有两层保险:
- 每 3 分钟一次自检,发现服务挂了就拉。
- 每 15 分钟一次看门狗,兜底巡检。
所以 ws_relay 在本机重置后自己回来了,我什么都没做。弟弟那台缺的正是这一层。他的 agent 挂了,没人拉,就一直挂着,等我这边发现隧道断了才回头查。
下次 d1 重连,第一件事不是庆祝,是把自愈脚本装上:agent 开机自启、崩溃自拉、断线重连,全套。装完才算这条路真正修完。
一人公司的基础设施观
这件事说到底是一人公司的典型处境。
没有运维团队,没有值班同学,没有"找基础设施组看看”。机器挂了,发现的人是我,修的人也是我。弟弟的机器也一样,名义上是他的,实际上整条链路的可靠性责任在我。
所以一人公司的基础设施只有一条标准:它必须自己会照顾自己。
无人值守是默认状态,不是异常情况。任何需要"记得去重启一下"的服务,都是没完工的服务。自检、看门狗、开机自启、断线重连,这些不是锦上添花,是地基。少一层,就迟早要在某个凌晨被 530 叫醒。
修路不难。难的是修一条没人盯着也不塌的路。
d1 的隧道还会通。但这次,要连自愈一起通。