背景

这次问题发生在一次测试环境迁移之后。自建 RustDesk 的 hbbs / hbbr 从旧服务器搬到新的腾讯云轻量服务器,数据目录、密钥文件、Docker Compose 都迁了过去。域名也改到了新机器。

按理说,这类迁移不复杂:容器跑起来,端口放开,客户端继续填同一个域名和 Key 就行。

问题偏偏出在半夜。服务已经迁完,别的服务也在陆续验证,RustDesk 这里却卡住了。更麻烦的是,它不是“所有地方都不通”,而是有的机器通,有的机器不通。半夜排这种网络问题很磨人,因为每一个方向看起来都像真凶。

现象

RustDesk 服务使用默认端口:

21115/tcp
21116/tcp
21116/udp
21117/tcp
21118/tcp
21119/tcp

迁移后看到的现象是:

  • hbbshbbr 容器都在运行。
  • 服务端 ss 能看到 RustDesk 端口都在监听。
  • 腾讯云防火墙里也放开了 21115-21119/tcp21116/udp
  • 一台 Mac 上 nc 测端口马上成功。
  • 另一台 Mac 上同样的命令访问 2111521117 一直超时。
  • RustDesk 客户端里域名换成 IP、Key 反复确认,还是提示未就绪。

最容易先怀疑的是安全组或 RustDesk Key。尤其 RustDesk 日志里曾经出现过 invalid key,这个线索很容易把人带偏。

排查过程

先从服务端开始确认。容器使用 host 网络,不存在 Docker 端口映射漏配的问题:

docker ps --filter "name=hbbs" --filter "name=hbbr"
ss -lntup | awk 'NR==1 || /:2111[5-9]/'
ss -lnup | awk 'NR==1 || /:21116/'

服务端确实在监听:

21115/tcp
21116/tcp
21116/udp
21117/tcp
21118/tcp
21119/tcp

Key 也和旧服务器一致。旧服务器上的 id_ed25519.pub 和新服务器上的公钥内容相同。这个方向暂时放下。

接着看服务器本机防火墙。ufw 没开,iptables 的 INPUT 默认是 ACCEPT,腾讯云主机安全加的链里只有一些明确的黑名单 IP,没有命中故障客户端的出口 IP。

从其它几台公网服务器测新机器的 RustDesk 端口,也都能通:

for p in 21115 21116 21117 21118 21119; do
  nc -vz -w 3 <server-ip> "$p"
done

nc -uzv -w 3 <server-ip> 21116

输出里的 inverse host lookup failed 一开始看着吓人,但它只是反向 DNS 没有 PTR 记录,不影响端口判断。真正要看的是后面的 open

这时第一个矛盾出现了:如果安全组或服务器防火墙没开,为什么其它公网机器都能连?如果 RustDesk 服务没起来,为什么 ss 和外部 nc 都正常?

然后排 DNS。某台 Mac 上 app.example.com 被解析到了 198.18.0.0/15 段,这是典型的 Clash/TUN fake-ip。这个发现一度让人以为问题就是代理或虚拟网卡。

但故障机器直接填 IP 后依然超时。DNS 也不是最终答案。

真正的转折点是抓包。

在服务器上抓 RustDesk 端口:

sudo timeout 90 tcpdump -ni eth0 \
  'host <client-public-ip> and (tcp port 21115 or tcp port 21117 or udp port 21116)' -vv

故障 Mac 上同时执行:

nc -vz -G 3 <server-ip> 21115
nc -vz -G 3 <server-ip> 21117

客户端超时。服务器抓包结果是:

0 packets captured
0 packets received by filter
0 packets dropped by kernel

这就很关键了。不是 RustDesk 拒绝了连接,也不是 Key 不对,而是请求根本没到服务器。

为了排除“这台 Mac 到服务器整体不通”,又让同一台 Mac 测了几个其它已开放端口:

nc -vz -G 3 <server-ip> 22
nc -vz -G 3 <server-ip> 8848
nc -vz -G 3 <server-ip> 10848
nc -vz -G 3 <server-ip> 6000
nc -vz -G 3 <server-ip> 21115

这一次抓包很说明问题:

<client-ip> > <server-private-ip>.22:    SYN
<server-private-ip>.22 > <client-ip>:    SYN, ACK

<client-ip> > <server-private-ip>.8848:  SYN
<server-private-ip>.8848 > <client-ip>:  SYN, ACK

<client-ip> > <server-private-ip>.10848: SYN
<server-private-ip>.10848 > <client-ip>: SYN, ACK

<client-ip> > <server-private-ip>.6000:  SYN
<server-private-ip>.6000 > <client-ip>:  SYN, ACK

21115 没有任何 SYN 到达。

同一个客户端,同一个目标服务器,228848108486000 都到,偏偏 RustDesk 默认端口不到。这个时候再去改 Docker、重启容器、换 Key,基本就是在消耗时间了。

根因

根因是:故障客户端所在的四川电信网络,到原公网 IP 的 RustDesk 默认端口流量被运营商侧阻断了。连接请求在到达腾讯云轻量服务器实例之前就被丢掉。

判断依据有四个:

  • 服务端监听正常,hbbs / hbbr 都在。
  • 多台其它公网服务器访问 RustDesk 端口正常。
  • 故障客户端访问同一服务器的其它端口正常。
  • 服务端抓包看不到故障客户端到 21115 / 21117 的 SYN。

后来参考了 V2EX 上的一个讨论:自建 Rustdesk 服务端口被阻断。里面也有人提到类似现象:安全组看起来没问题,服务端收不到请求,换 IP 或换端口后恢复。这个方向和现场证据对上了。

解决方案

最终选择更换腾讯云轻量服务器公网 IP。腾讯云后台可以申请更换轻量服务器 IP,本次处理时一个轻量服务器可以换一次。这个规则以后可能调整,实际以控制台为准。

换 IP 后做了几件事:

  1. 确认新公网 IP 已绑定到同一台服务器。
  2. 用新 IP 从本机和旧服务器测试 RustDesk 默认端口。
  3. 更新域名 A 记录到新 IP。
  4. 如果 hbbs 显式配置了 relay 地址,同步改成新 IP:
services:
  hbbs:
    command: hbbs -r <new-server-ip>:21117
  1. 重启 RustDesk 服务:
docker compose up -d --force-recreate
  1. 客户端先用 IP 直连验证,再等域名解析生效。

客户端配置保持原来的形式,只是服务器地址换成新 IP 或解析到新 IP 的域名:

ID 服务器:<new-server-ip>
中继服务器:<new-server-ip>
Key:<server-public-key>

验证结果

更换公网 IP 后,同一组 RustDesk 默认端口恢复可达:

for p in 21115 21116 21117 21118 21119; do
  nc -vz -w 3 <new-server-ip> "$p"
done

nc -uzv -w 3 <new-server-ip> 21116

hbbs 日志也显示 relay 地址已经更新:

relay-servers=["<new-server-ip>:21117"]

等域名解析到新 IP 后,客户端连接恢复。这个结果也反过来证明:原来的服务配置、Key、容器迁移并不是根因。

经验沉淀

这类问题最消耗人的地方,是它看起来像安全组,也像 DNS,也像客户端配置,尤其发生在半夜,很容易在几个方向之间来回切。

以后遇到“端口开了但某个网络就是连不上”,我会更早做这两件事:

  1. 从故障客户端和正常客户端分别测同一组端口。
  2. 在服务端抓包,看 SYN 到底有没有进实例。

只要服务端抓不到目标端口的 SYN,就不要继续纠结应用日志。应用层还没收到包,日志再干净也说明不了什么。

还有一个小教训:一台机器上的 nc 成功不代表全网成功。代理、fake-ip、运营商路由、区域性阻断都会让结果分裂。半夜排障时,最省力的办法不是多重启几次服务,而是早点把网络路径切开看。