# 我用 tailcat 改造了 RustDesk，扫码就能直连

Source: https://blog.ferstar.org/posts/yet-another-remote-desktop-application-sealdesk/





昨天我把 SealDesk 开源了：RustDesk 的传输层换成 tailcat，被控端点一下分享，控制端扫个码就能连上。

RustDesk 的公共服务器不总是好用，自建又得买 VPS、起 `hbbs` 和 `hbbr`。SealDesk 把这套省掉了，账号也不用注册。

## 没有服务器，两端怎么找到对方

做这个项目的契机，是 Tailscale 开源了 [tailcat](https://github.com/tailscale/tailcat)。Tailscale 的穿透能力和 WireGuard 组网都好用，但标准用法离不开一个协调服务器（官方 SaaS 或自建 Headscale），身份认证、节点分发、ACL 都在那一层。tailcat 把数据面单独剥了出来：没有账号系统，没有控制平面，只剩下点对点通信本身。

`tc` 地址就是完整的连接凭证。服务端启动后生成一段 `tc` 开头的 Base64 字符串，150 字符左右，里面打包了 WireGuard 公钥、预共享密钥（PSK）和握手用的 DERP 节点区域，两端相认需要的密码学材料全在这一串里。

双方拿着这个地址在公共 DERP 节点上会合，STUN/UDP 打洞一旦成功，流量立刻切到两端直连的 WireGuard UDP 隧道；只有对称型 NAT 这类实在打不通的环境，才由 DERP 兜底转发密文，而它在数学上解不开 WireGuard 的载荷。这个会合用的中继就是 Tailscale 的公共 DERP，不用自己养，原来那套 `hbbs` / `hbbr` 就可以整个拿掉了。

## 扫码之后

握手不走中心服务器，配对码只负责把 `tc` 地址和临时密码带到控制端：

```mermaid
flowchart TD
    subgraph 被控端 Host
        H1[启动 tailcat 监听服务] --> H2[生成 tc... 地址 + 临时密码]
        H2 --> H3[渲染 SEALDESK: 配对二维码]
        H3 --> H4[等待 DERP 会合信标]
    end

    subgraph 控制端 Controller
        C1[扫码 / 拖入图片 / 粘贴文本] --> C2[解析 tc 地址与临时密码]
        C2 --> C3[连接 tailcat 传输层]
    end

    subgraph 握手与通道建立
        C3 & H4 --> D1[公共 DERP 节点会合握手]
        D1 --> D2{STUN/UDP 打洞检测}
        D2 -->|打洞成功| D3[切换为 P2P WireGuard 直连通道]
        D2 -->|严格 NAT 限制| D4[回退至 DERP 加密兜底转发]
    end

    D3 & D4 --> R[RustDesk 桌面会话层: 认证、音视频流、键鼠控制]
```

被控端打开界面点「Share this device」，二维码就在界面正中。控制端在手机上直接扫；桌面端可以用摄像头扫码、选择二维码图片文件，或者直接粘贴剪贴板里的截图和文本。两端在同一个局域网里时，不用过外网也能互相发现直连，第一次连进来要在被控端点一下同意，顺手勾上「信任此设备」，下次就直接进。

临时密码可以刷新，地址可以重置，换掉之后旧配对码就失效。用完在任一端停止共享，所有会话跟着断开，后台不留任何进程。

## Go 和 Rust 之间的那层 C-ABI

tailcat 是纯 Go，RustDesk 核心是 Rust。我们在 `libs/tailcat-ffi` 里用 cgo 封了一层极简 C-ABI：服务端走 `TcServe`，传入本地 RustDesk 的 loopback 端口，隧道上的每条连接都被转发到这个口，返回生成的 `tc...` 地址；控制端走 `TcForward`，传入对端地址，它在本地开一个转发口，客户端连上去就进了隧道。所有返回的字符串统一用 `TcFree` 释放，跨 FFI 传指针不会泄漏。

Android 上多绕了一步：Go 1.27.1 的 `-buildmode=c-archive` 在 android/arm64 上直接报不支持，构建只好换成 c-shared，Rust 这边跟着改成链接 `libtailcat.so`。

## 绕开 rendezvous_mediator

原版 RustDesk 的网络层和 `rendezvous_mediator` 耦合很重，连接前要先问中心服务器拿对端的直连 IP 列表或中继地址。我们在 `src/client.rs`、`src/tailcat.rs` 和 `src/client/io_loop.rs` 里加了一条独立的 tailcat 通道：只要对端是 `tc...` 地址，就跳过所有中心化解析，桌面流直接走 tailcat 的加密连接。RustDesk 成熟的编解码（H.264/H.265/VP9）和键鼠注入机制原样保留。

## Android 换网就断流

控制端经常在 Wi-Fi 和蜂窝网络之间切换，一换网，路径更新跟不上，会话就容易断。Android 又不给应用 UID 用 netlink，Go 侧的 netmon 自己列不出网络接口，只能靠上层把接口列表喂过来。我们在 `libs/tailcat-ffi/android_interfaces.go` 里加了 `TcSetAndroidInterfaces`：Kotlin 层用 `java.net.NetworkInterface` 读出接口的名字和地址，Connectivity 一变化就通过 JNI 把 JSON 推给它，注册进 netmon，免得移动端远控走两步就重连。

## 测过的组合

首发版本的这几条链路都测过：Android（arm64）控 Linux、Linux 控 Linux，局域网内外都稳，资源占用也低；同一个局域网里，还能和原版 RustDesk 双向互控。

## 平台和限制

平台支持还在 Alpha：Linux 当被控端和控制端都测过，Android 目前只能当控制端；Windows 和 macOS 继承的是上游代码，打包和驱动细节还没收口。

和原版 RustDesk 互控走的是它原来的直连协议，这一段不加密，连接前 SealDesk 会提示一下。

SealDesk 挂在 [runseal-labs](https://github.com/runseal-labs) 名下（Seal 取的是两端密封隔离的意思），仓库在 [github.com/runseal-labs/sealdesk](https://github.com/runseal-labs/sealdesk)，不想再伺候 `hbbs` / `hbbr` 的可以拉下来试试，Issue 和 PR 都欢迎。

