RustDesk自建服务器报错reset by peer:从端口映射到密钥漂移的完整排查指南

发布时间:2026/9/17 3:23:46
RustDesk自建服务器报错reset by peer:从端口映射到密钥漂移的完整排查指南 昨天下午我在一台新开的云主机上捣鼓 RustDesk 自建服务器客户端往里一填服务器地址ID 列表倒是正常拉出来了可一点击连接没两秒就弹了一行字reset by peer 连接被对方关闭。这个报错看起来简单后面却把 Docker 端口映射、云安全组、NAT 打洞、密钥校验这几个环节全串了一遍。如果你也是用 Docker 或者 k8s 自建 RustDesk然后碰到了类似的连接被对方关闭这篇文章大概率能帮你少走几个小时的弯路。我会按照从现象还原到逐层排查再到Docker/k8s 部署的差异化处理来完整走一遍。1. 重置连接的现场还原报错出现的前后场景1.1 我当时用的部署方式先说环境。服务器是一台 Ubuntu 22.04 的云主机配置不高2C4G专门拿来跑内网穿透和自建远程桌面。RustDesk 的服务端我用的是官方镜像rustdesk/rustdesk-server:latest当时图省事写了一个非常简化的docker-compose.ymlservices: hbbs: image: rustdesk/rustdesk-server:latest container_name: hbbs command: hbbs ports: - 21115:21115 - 21116:21116 - 21117:21117 restart: unless-stopped注意这个版本有几个坑没有映射21116/udp没有挂载数据卷也没有单独的 hbbr 中继服务。我当时是想着先跑起来再说没想到后面这一个先跑起来直接坑了我一下午。1.2 报错的完整表现客户端我用的 Windows 桌面版。在设置里把 ID 服务器填成这台云主机的公网 IPKey 先随便填了个值保存后回到主界面设备列表里确实能看到服务器端的 ID也能看到在线状态。但问题来了双击连接进度条转了一下然后弹窗reset by peer 连接被对方关闭这里有个很重要的细节ID 能拉出来说明客户端和 hbbs 之间的21116/tcp是通的连接却失败说明问题出在更往后的环节比如密钥校验、中继握手、或者打洞协商。这个判断是整个排查过程的关键起点后面所有步骤都是围绕这句话展开的。2. 为什么 reset by peer 在 RustDesk 场景里高频出现TCP 的拒绝逻辑2.1 RST 不是对方关机而是对方挂了电话先说 TCP 层面的基础。TCP 连接断开有两种方式一种是正常的FIN大家挥挥手说再见各自关闭另一种是RST也就是 Reset 包它的含义是我不想跟你继续聊了而且不给你缓冲的余地直接挂断。拿打电话类比FIN是正常说完话那就这样拜拜RST是你话说到一半对方直接一句打错了然后挂断。客户端提示reset by peer本质就是它收到了服务端发来的 RST 包。也就是说连接建立请求已经到达了对端但对端主动选择拒绝或者更准确地说是某个网络节点主动把这个连接掐断了。理解这一点很重要因为很多人看到连接被对方关闭第一反应是服务器挂了其实恰恰相反服务器可能活得很好只是它不想让你连进来。2.2 RustDesk 自建服务器到底在哪些端口上干活RustDesk 服务端由两个角色组成hbbs是 ID/注册服务器负责设备 ID 分配、在线状态管理、NAT 类型探测hbbr是中继服务器负责在 P2P 打洞失败时转发流量。它们默认监听的端口如下端口协议用途21115TCPNAT 类型测试21116TCPID 注册与心跳21116UDPNAT 打洞21117TCP中继连接21118TCPWeb 客户端可选21119TCPWeb 客户端中继可选客户端连上服务器后整个流程大概是先用21115探测 NAT 类型再通过21116/tcp注册或登录拿到设备列表然后尝试用21116/udp打洞建立 P2P 连接如果打洞失败就退回到21117走中继。也就是说21116这个端口承载了 TCP 和 UDP 两种协议少一个都不行。2.3 服务端主动断开连接的四种典型原因结合我自己的踩坑和社区里的常见反馈RustDesk 场景下的reset by peer通常来自这四种原因端口没人监听比如客户端去连21117但 hbbr 根本没启动内核收到 TCP 握手后直接回 RST。这个是最常见也最好排查的。防火墙或安全组规则拒绝某些网络设备对未放行的端口会回 RST而不是静默丢包。表现同样是连接被对方关闭。密钥不匹配RustDesk 自建服务端会生成一对 ED25519 密钥客户端必须填对公钥。如果服务端持有的私钥和客户端填的公钥对不上服务端会直接断开握手。协议版本不兼容服务端升级了新版本但客户端还是旧版本协议握手失败后也会被服务端关闭连接。后面我的排查过程其实就是把上面四个原因从易到难挨个验证了一遍。3. 排查链路从 hbbs 端口开始逐层验证3.1 先确认服务在听用 ss 和 docker ps 兜底排错第一步永远是确认程序是不是真的在跑、端口是不是真的在监听。我直接在服务器上执行sudo ss -lntup | grep -E 2111(5|6|7)结果很尴尬只有 hbbs 在监听tcp LISTEN 0 4096 0.0.0.0:21115 0.0.0.0:* tcp LISTEN 0 4096 0.0.0.0:21116 0.0.0.0:*21117完全没有进程监听。因为我最初的 docker-compose 里压根没定义 hbbr 服务。客户端主界面能显示 ID是因为连的是 hbbs 的21116/tcp真正建立会话时客户端默认走21117中继结果这个端口没人听内核直接回 RST于是客户端就报了reset by peer。到这里其实已经抓到第一个根因了hbbr 没起来。补上 hbbr 服务后再测试连接发现依然是reset by peer这就说明还有别的问题。3.2 再查网络链路安全组、防火墙与 iptables服务起来了问题还在下一步就要排查网络链路。我先确认了服务器本机到端口的连通性telnet 127.0.0.1 21117本机可以通说明服务进程本身没问题。然后从我的另一台电脑去连公网 IP 的21117发现不通。这时候基本可以确定是中间链路的问题。我的第一反应是云安全组。登录云控制台一看安全组里确实放了21115/21116/21117但仔细再看所有规则都只勾了 TCP没有放行 UDP。很多人会在这里栽跟头21116端口同时承担 TCP 和 UDP安全组里只放 TCPUDP 打洞流量就直接被丢弃了。这里还要顺便说一句 iptables 的坑。某些服务器上如果开着iptables并且规则写的是REJECT那么被拒绝的连接会直接收到 RST如果写的是DROP客户端表现则是连接超时。所以如果你看到的是reset by peer反而说明中间设备在主动拒绝而不是默默丢弃。检查一下sudo iptables -L -n | grep 21116我遇到过一台机器被装了个安全防护脚本自动加了REJECT规则把非本机 IP 访问21115的包全部拒绝掉。如果你排查了很久都找不到原因记得看一眼 iptables 的全量链输出。3.3 抓包看 RST 从哪来tcpdump 的直观证据排查网络问题最直接的办法就是抓包。不要瞎猜直接上 tcpdump 看 RST 包的来源。我在服务器上执行sudo tcpdump -i any -nn tcp port 21117 -w /tmp/rst.pcap然后用客户端发起一次连接等报错出现后CtrlC停止抓包。把 pcap 文件拖到 Wireshark 里看过滤tcp.flags.reset 1就能看到 RST 包的完整链路。这里有一个判断技巧如果 RST 包的源 IP 是服务器自己的公网 IP说明是服务器上的某个组件内核或应用拒绝了连接如果源 IP 是云厂商网关或 NAT 设备的地址说明是中间网络设备干预。我抓包后发现RST 确实是从服务器本机发出的源端口是21117目标端口是客户端的随机端口。也就是说连接确实到达了服务器的21117端口但被服务端主动断开了。那么问题就非常明确地指向了应用层。3.4 检查 Key 文件容器重启导致的密钥漂移到了应用层首要怀疑对象就是密钥不匹配。RustDesk 服务端启动后会在数据目录下生成一对 ED25519 密钥默认文件名叫id_ed25519和id_ed25519.pub。客户端的Key设置里必须填服务端的公钥内容两边对不上服务端会在握手时直接把连接掐断。检查方法很简单docker exec hbbs cat /data/id_ed25519.pub然后把输出的公钥和客户端填的 Key 对比。我一看果然对不上。原因也不难猜我最初的 docker-compose 里没有给 hbbs 挂载数据卷容器一旦被删除或重建/data目录就会重新初始化生成全新的密钥对。之前调试时我重建过几次容器客户端填的 Key 早就变成了历史版本。这里要特别强调一下密钥漂移问题在 Docker 部署里有多常见很多人用docker run -d裸跑容器跑完就忘某天想升级镜像docker rm后重新 run容器数据全没了密钥也换了客户端自然连不上。表现就是这种诡异的能显示 ID但连接被对方关闭。3.5 看日志与版本隐蔽的兼容性陷阱密钥对不上修掉之后连接总算通了。但我在整理这篇文章的时候又单独遇到一个案例有一台机器服务端版本升级到了最新客户端却还是半年前的版本结果也是reset by peer。这种兼容性问题的排查方式最有效的是看日志docker logs hbbs --tail 100 docker logs hbbr --tail 100RustDesk 服务端日志通常会输出握手错误。如果你看到类似 TLS 握手失败、协议版本不支持的记录大概率就是版本不兼容。解法也不复杂要么升级客户端要么把服务端镜像标签固定到旧版本。顺便说一句自建服务这种长期跑的服务端不建议一直追latest标签最好固定一个明确版本号升级前先在测试环境验一遍。4. k8s 部署的差异点Service、PVC、多副本带来的新坑4.1 Service 的 UDP 暴露最容易漏的一条如果你把 RustDesk 搬进 k8sDocker 时代踩过的坑一个都不会少还会多出不少新的。排在最前面的就是 Service 的端口协议配置。在 Docker 里映射21116的 UDP 端口只需要写一行21116:21116/udp很多人记不住。到了 k8s如果 Service 里只写了 TCP 而漏了 UDP客户端能显示 ID 但打洞必然失败然后走中继中继如果也有问题最终报错同样是reset by peer。一个标准的 Service 定义应该是这样apiVersion: v1 kind: Service metadata: name: rustdesk-hbbs spec: selector: app: rustdesk component: hbbs ports: - name: nat-test port: 21115 targetPort: 21115 protocol: TCP - name: id-register-tcp port: 21116 targetPort: 21116 protocol: TCP - name: id-register-udp port: 21116 targetPort: 21116 protocol: UDP type: LoadBalancer注意21116同时占用 TCP 和 UDP 两个协议条目这是合法写法。但麻烦在后面很多云厂商的 LoadBalancer 对 UDP 的支持并不完善尤其是四层负载均衡器有些只转发 TCP。这种情况下也许更省心的方案就是用NodePort或者直接在节点上用hostNetwork。4.2 数据持久化Pod 重建后 Key 漂移如果你只是把 Docker 里的数据卷概念原样搬进 k8s用 Deployment 跑 hbbs那么恭喜你等待你的是一个经典故障Pod 每次重建容器文件系统都会被清空/data目录重新生成密钥对pubkey 变化所有客户端全部连不上。解法也很标准用 StatefulSet 加 PVC或者至少给 Deployment 挂一个 hostPath 类型的卷。我自己的实践更倾向于 StatefulSet不是为了它的有序部署主要是volumeClaimTemplates用起来省心不需要预先创建一大堆 PVC。apiVersion: apps/v1 kind: StatefulSet metadata: name: hbbs spec: serviceName: hbbs replicas: 1 selector: matchLabels: app: rustdesk component: hbbs template: metadata: labels: app: rustdesk component: hbbs spec: containers: - name: hbbs image: rustdesk/rustdesk-server:latest args: [hbbs, -r, relay.example.com:21117] ports: - containerPort: 21115 - containerPort: 21116 protocol: TCP - containerPort: 21116 protocol: UDP volumeMounts: - name: data mountPath: /data volumeClaimTemplates: - metadata: name: data spec: accessModes: [ReadWriteOnce] resources: requests: storage: 1Gi记住一个原则/data目录里的id_ed25519和id_ed25519.pub是比服务本身更重要的资产丢了它等于所有客户端都要重新配一遍。4.3 hbbs 与 hbbr 分开部署多副本与服务发现在 Docker Compose 里hbbs 和 hbbr 只是两个 service 的事。到了 k8s 里它们一般是两个独立的 workload因为一个是无状态的注册服务一个是要转发流量的中继服务扩缩容策略和资源需求都不一样。hbbr 的部署同样建议用 StatefulSet数据目录也用 PVC 挂载因为中继服务也要读取同一份密钥密钥不一致会导致中继握手失败。hbbs 启动参数里的-r要指向 hbbr 的地址在 k8s 里一般指向 hbbr 的 Service 名称hbbs -r hbbr.default.svc.cluster.local:21117这里有个多副本相关的坑hbbs 和 hbbr 如果都开了多个副本它们必须共享同一份密钥和数据。否则不同 Pod 各持一份密钥客户端这次连到 A 副本是好的下次调度到 B 副本就报 reset。所以实际生产里hbbs 和 hbbr 的副本数我基本都保持为 1性能不够就加机器而不是盲目扩副本。4.4 单节点 k8s 部署的简化建议如果你和我一样只是在一台云主机上搭了个单节点 k8s扛一个小团队的远程桌面使用我的建议是不要自己给自己加戏直接用hostNetwork: true。spec: hostNetwork: true containers: - name: hbbs image: rustdesk/rustdesk-server:latest args: [hbbs, -r, 公网IP:21117]hostNetwork让 Pod 直接复用宿主机网络端口监听在宿主机上公网 IP 直接就能访问绕开了 Service、NodePort、LoadBalancer 这一大堆中间层也避开了 UDP 转发这个最大的坑。代价是21115、21116、21117这些端口会被宿主机直接占用不能再用给别的服务。对于单节点部署来说这个代价完全值得。安全组放行时照旧要同时放 TCP 和 UDP。5. 修复后的配置基线照抄这份至少能用5.1 最终可用的 docker-compose 参考如果你不想上 k8s用 Docker Compose 跑通一套完整可用的 RustDesk 自建服务下面是我验证过的配置。它解决了三个问题hbbr 缺失、UDP 端口没映射、数据卷没挂载。services: hbbs: image: rustdesk/rustdesk-server:latest container_name: hbbs command: hbbs -r 你的公网IP或域名:21117 volumes: - ./data:/data ports: - 21115:21115 - 21116:21116 - 21116:21116/udp - 21117:21117 restart: unless-stopped hbbr: image: rustdesk/rustdesk-server:latest container_name: hbbr command: hbbr volumes: - ./data:/data ports: - 21117:21117 restart: unless-stopped等一下这里有细节要注意hbbs 和 hbbr 都映射了21117端口吗是的我把21117同时映射在两个容器上但宿主机上同一个端口不能同时被两个容器占用。正确做法是 hbbs 容器不要映射21117只让 hbbr 映射它。上面的配置里 hbbs 如果映射了21117会和 hbbr 冲突实际运行会报端口占用。我把 hbbs 的21117映射去掉保留 hbbr 的21117services: hbbs: image: rustdesk/rustdesk-server:latest container_name: hbbs command: hbbs -r 你的公网IP或域名:21117 volumes: - ./data:/data ports: - 21115:21115 - 21116:21116 - 21116:21116/udp restart: unless-stopped hbbr: image: rustdesk/rustdesk-server:latest container_name: hbbr command: hbbr volumes: - ./data:/data ports: - 21117:21117 restart: unless-stopped启动后用下面两条命令确认公钥docker exec hbbs cat /data/id_ed25519.pub这个输出就是要填到客户端里的 Key。5.2 客户端侧配置服务器地址与 Key 的一致性服务端不是启动完就万事大吉客户端侧的配置同样容易埋坑。桌面客户端打开设置 - 网络里面有三个字段字段填写内容ID 服务器填你的公网 IP 或域名不要带端口中继服务器填你的公网IP:21117端口一定要写Key填id_ed25519.pub文件内容注意不要带换行中继服务器这里很多人会漏掉端口号。如果不写端口客户端默认按21117走也行但显式写出来会避免意外。另外Key 的内容在 Docker 挂载数据卷后是稳定的但只要你换过机器、重建过容器、或者玩过 k8s 的 PVC都建议重新docker exec查看一遍不要想当然用几个月前的 Key。5.3 重启、迁移、升级时的验证清单我现在自己维护的 RustDesk 实例每次动完容器、迁移完机器、升级完版本都会按这个清单过一遍验证项方法预期结果端口监听sudo ss -lntupgrep -E 2111(5公钥获取docker exec hbbs cat /data/id_ed25519.pub能看到 ED25519 公钥客户端 ID 显示打开客户端看设备列表能看到本机 ID 和在线状态P2P 连接同一局域网内两台设备连接连接成功延迟低中继连接跨网络或禁掉 UDP 后连接连接成功走中继也能通重启容器docker compose restart后重连不需要重新填 Key如果你用的是 k8s把重启容器替换成重启 Pod和查看 PVC 是否挂载成功同样适用。5.4 我踩过后的几条补充建议最后再分享几个零散但实用的经验。第一容器启动参数里的-r一定要写。它告诉 hbbs 把中继地址下发给客户端。如果不写客户端会默认用 ID 服务器的 IP 作为中继地址。万一你的 hbbs 和 hbbr 不在同一台机器或者公网环境走了端口映射客户端就会拿着一个错误的中继地址去连结果自然是连接失败。第二备份数据目录里的id_ed25519和id_ed25519.pub。这两个文件是服务端的身份凭证。我习惯把 pub 文件单独复制出来放到一个固定目录下客户端配置时直接读这个文件省去反复docker exec的麻烦。第三升级镜像之前先看一下官方 Release Notes。RustDesk 服务端升级比较频繁有些版本改动过握手协议。我在一次升级后所有老版本客户端都连不上排了半天才发现是版本兼容问题。从那以后服务端镜像我固定具体版本号不再无脑追latest。第四如果已经走到抓包这一步Wireshark 里除了看 RST还要看 RST 之前的报文。如果客户端发了SYN服务端回了SYN-ACK客户端回了ACK然后服务端立刻RST说明三次握手本身是成功的问题在应用层重点查密钥和版本如果SYN发出去根本没人理才是防火墙和安全组的事。结尾经历这次排错之后我最大的感受是reset by peer这个报错就像一块万能砖哪里需要往哪里搬。它背后可能是 hbbr 没启动、安全组没放 UDP、密钥漂移、版本不兼容甚至是你根本没想到的中间设备。但只要按着端口监听 - 网络链路 - 抓包定位 - 应用层校验这条路一步步走再隐蔽的问题也能被揪出来。最后再分享一个小技巧在你的部署目录里建一个README.md把id_ed25519.pub的内容、服务端版本号、安全组放行列表全部记下来。下次你或者同事再遇到类似问题不用重新踩一遍坑翻一翻文件就知道当初是怎么配的。这比任何监控告警都实在。