UniClipboard
在线试用博客对比使用场景文档更新日志赞助
下载
UniClipboard

在你自己的设备之间同步剪贴板,开源开发。

链接
仓库IssuesReleases文档赞助支持
技术栈
RustTauriirohshadcn/ui
联系
X · @uniclipboardX · @mkdir700B站 · @mkdir700[email protected]
© 2026 mkdir700made with Rust 🦀 · AGPL-3.0
UniClipboard
在线试用博客对比使用场景文档更新日志赞助
下载
UniClipboard

在你自己的设备之间同步剪贴板,开源开发。

链接
仓库IssuesReleases文档赞助支持
技术栈
RustTauriirohshadcn/ui
联系
X · @uniclipboardX · @mkdir700B站 · @mkdir700[email protected]
© 2026 mkdir700made with Rust 🦀 · AGPL-3.0
UniClipboard
在线试用博客对比使用场景文档更新日志赞助
下载
首页博客为什么选 iroh,而不是 libp2p 或 WebRTC

最后更新·2026-05-05

为什么选 iroh,而不是 libp2p 或 WebRTC:构建一个 P2P 剪贴板

做一个无服务器跨设备剪贴板一整年,最有意思的决定就一个:P2P 栈选谁。这是那个问题的长版回答。

过去一年我一直在做 UniClipboard —— 一个不依赖服务器的跨平台剪贴板同步工具。大部分工作其实都很无聊:操作系统之间剪贴板 API 的差异、加密人体工学的各种坑、Tauri 应用打包到三个平台的痛苦。

但有一个决定确实有意思,而且总是被人问到:为什么 P2P 层选了 iroh, 而不是看起来更显然的 libp2p、WebRTC、syncthing 的 BEP,或者 magic-wormhole?

这篇文章是那个回答的长版本。如果你也在做需要两台终端设备直接对话——跨越互联网、 穿过 NAT、还不想自己跑基础设施——的事情,这里的取舍大概率也适用于你的项目。

问题的形状

一个剪贴板同步工具对网络层有四个要求:

  1. 长连接、已认证的通道,在一小撮已知设备之间(通常是同一个人名下的 2–5 台), 并且能挺过网络切换。
  2. 小载荷的低延迟。绝大多数剪贴板内容小于 1 KB。所谓"好"的体验,意味着新条目 要在用户手还没移开之前就出现在另一台设备上——大概 200ms 的端到端预算。
  3. 偶发的较大载荷——图片(几 MB)和偶尔的文件(~100 MB)。
  4. NAT 穿透要在消费级 ISP、强制门户登录、运营商级 NAT、企业代理这种乱糟糟的现实里 都能用,fallback 还不能让我(开发者)自己跑一套随用户量线性扩张的基础设施。

我不需要的:

  • DHT 或内容寻址存储。这里没有"剪贴板网络"可发现——设备是显式配对的。
  • Pubsub。fan-out 是个位数。
  • 浏览器支持。UniClipboard 是桌面应用。

最后一条悄悄判了 WebRTC 死刑,这个稍后再说。

候选 1:libp2p

最显然的选择。libp2p 成熟、有 Rust 实现(rust-libp2p)、 驱动着 IPFS 和加密生态的一大块,而且明确把自己定位成"P2P 工具箱"。

我先用 libp2p 做了原型。能跑。但 libp2p 的形状对一个小应用而言是错的:

// 一个 libp2p hello-world 大致长这样
let local_key = identity::Keypair::generate_ed25519();
let local_peer_id = PeerId::from(local_key.public());

let transport = tcp::tokio::Transport::default()
    .upgrade(Version::V1)
    .authenticate(noise::Config::new(&local_key)?)
    .multiplex(yamux::Config::default())
    .boxed();

let behaviour = MyBehaviour {
    kademlia: kad::Behaviour::new(local_peer_id, MemoryStore::new(local_peer_id)),
    identify: identify::Behaviour::new(...),
    ping: ping::Behaviour::new(...),
    relay_client: relay::client::Behaviour::new(local_peer_id, relay_client_transport),
    dcutr: dcutr::Behaviour::new(local_peer_id),
};

let mut swarm = SwarmBuilder::with_existing_identity(local_key)
    .with_tokio()
    .with_other_transport(|key| ...)
    .with_behaviour(|_| behaviour)?
    .build();

// ……到这里,你才终于可以开始写应用代码

这正是 libp2p 设计的样子——让你自由组装一个传输层、一个安全通道、一个多路复用器、 一个 peer 识别协议、一个用于路由的 DHT、一个用于 NAT 穿透的 relay client、一个 hole-punching behaviour。可组合性是它的卖点。

但对一个剪贴板应用而言,我是在发出第一个字节之前先组装并配置一个小型操作系统。 每一块都有自己的版本兼容性、自己的 bug、自己的配置旋钮。上面八个模块里,有三个跟 NAT hole-punching 之间有非显然的相互作用。两周后我有一个大体能跑的 libp2p 原型, 后面跟着一长串我没法自信调试的边角 case。

根本性的错配是:libp2p 是一个用来构建协议的框架。我需要的是一个使用某个协议的 SDK。

候选 2:WebRTC

WebRTC 是浏览器 P2P 的显然选择,而且也有完全实打实的桌面实现 (webrtc-rs、对 libwebrtc 的绑定)。

排除它的理由:

  1. 栈太重。webrtc-rs 大概 50 个 crate 的依赖,大量是为我永远用不到的多媒体管线服务的。 libwebrtc 的绑定更糟——不管你要不要,它都会拉进来 Chromium 衍生的音视频处理。
  2. Signaling 是你自己的事。WebRTC 出名地把 DTLS+SRTP 和线协议给你,但两个 peer 怎么找到彼此是留给你的练习。结果你还是得跑一套 signaling 基础设施,这就把意义砍掉一半。
  3. NAT 穿透止步于标准层。STUN 经常能用,TURN 总是能用但带宽贵。没有像 iroh 的 home-relay 系统那样把两者透明地揉在一起的等价物。

WebRTC 是"浏览器之间视频聊天"的对的工具,是"两个想要长连接加密通道的桌面应用"的错的工具。

候选 3:syncthing 的 BEP 协议

Syncthing 实际上做了我需要的大部分事情——已知设备之间的加密 P2P 同步、通过他们的 relay 网络做 NAT 穿透、成熟的代码库。我真花了一个周末试着把 syncthing 当后端用。

它在一个具体且发人深省的方向上,形状不对。

Syncthing 优化的是文件夹状态的最终一致性。最小循环大概是这样:

  1. 文件在磁盘上变更
  2. Inotify/FSEvent 触发
  3. Syncthing 扫描、计算 block 哈希
  4. 索引更新传播给 peers
  5. Peers 请求变化的 block
  6. Block 传输

这是"让两个音乐库保持同步"的好架构,是"我刚复制了一段字符串,我希望在我切窗口之前 它已经到了另一台笔记本上"的烂架构。即便把 fsScanIntervalS 调到很低,这个 round-trip 还是好几秒。

而且,BEP 是一个文件夹同步协议——没有"对一个 peer 开一条全双工通道"的库版本,只有 "把这个文件夹和那个 peer 同步"。把它改造来传短消息,意味着把每个剪贴板条目塞进一个 临时文件再等同步。当 hackathon 凑合可以,当地基就糟糕了。

候选 4:magic-wormhole

magic-wormhole 概念上很美: 人类可发音的码(7-crossover-clockwork)、基于 PAKE 的认证密钥协商、不要账号、 用户也不需要去想任何基础设施。我很喜欢它。

但它是一个一次性协议。你生成一个码,对面输入,内容传完,通道关闭。它没有 "这两台设备有持续关系并希望无限期来回收发消息"的概念。

能不能在 magic-wormhole 之上构建那种东西?能——从一次 wormhole 交换里推导出一个长期共享密钥, 用它在……什么之上 bootstrap 一个 Noise 通道。但到那一步,你已经在重新造 iroh 已经提供的那一层, 只不过造在一个不是为这个用途设计的原语之上。

iroh 做对了什么

iroh 是 Number Zero(里面有早期 IPFS 的一些人)做的,目标明确就是一个用于 P2P 的 Rust SDK,而不是框架。光从 API 的形状你就 能看出它的哲学:

use iroh::{Endpoint, SecretKey};

const ALPN: &[u8] = b"uniclipboard/0";

// 绑定一个 P2P endpoint
let endpoint = Endpoint::builder()
    .secret_key(secret_key)
    .alpns(vec![ALPN.to_vec()])
    .discovery_n0()    // 可选:用 n0 的发现服务
    .bind()
    .await?;

// 连一个 peer(跨互联网,NAT 穿透在内部已经处理好)
let conn = endpoint.connect(peer_node_addr, ALPN).await?;
let (mut send, mut recv) = conn.open_bi().await?;

send.write_all(b"hello from device A").await?;
send.finish()?;

let response = recv.read_to_end(1024).await?;

整套搭建就这些。背后:

  • 传输层是 QUIC(经由 Quinn)——建立连接比 TCP+TLS 更快,多路复用是内建的,网络切换(Wi-Fi → 蜂窝)时连接迁移也开箱即用。
  • NAT hole-punching 内建在 iroh-net 里,组合了 STUN 类的反射地址发现和一个 DERP 风格的 relay 系统作为 fallback。
  • 直连升级是自动的。最初的包可能走 relay;一旦 hole-punching 成功,连接会透明地 升级到直连。从应用的角度,还是同一个 Connection 对象。
  • Relay 是开放的。默认的 relay 是 n0 跑的,但 relay 协议是有文档的,你也可以自己搭 ——和 Tailscale 让你自带 DERP 是一样的方式。

对 UniClipboard 而言,这意味着什么:

  • 整个 P2P 层大概是 300 行 Rust(连接生命周期、ALPN 协商、消息分帧、peer 发现的钩子)。 libp2p 版本是 1200+ 行,可靠性还更差。
  • 上线之后我没再操心过传输层。系统的每个其他部分我都遇到过 bug;iroh 没有过。
  • 一个在双层 NAT 后面(典型的移动运营商)的用户报告 hole-punching 成功率大概在 80–85%。 剩下的 15–20% 透明地降级到 relay,用户感受不到。

"iroh 不会哪天死掉/在你脚下变化吗?"

诚实的回答是:有可能。iroh 比 libp2p 年轻,团队更小,break change 更多。我在 0.x 期间已经 做过两次非平凡的迁移。

我下注的是:我所依赖的 API 表面(Endpoint、Connection、SecretKey、NodeAddr) 是小且稳定的。只要这些原语继续工作,我就能吸收下层的变动。到目前为止这个赌注成立。

万一 iroh 哪天不再被维护的应急方案:底下那些原语(QUIC + DERP 风格的 relay + STUN 风格的 hole-punching)都是标准件。在 raw Quinn 之上重新实现这套 SDK 形状会很不爽,但是有界的—— 大概 2–3 周的工作量。考虑到生产力收益,这是个可接受的风险。

关于"用 Tailscale 不就行了吗"

任何 P2P 帖子在 HN 上都会收到的常见回复是:"为什么不直接放到 Tailscale 上?"

Tailscale 很棒,我自己也在用。但是把它做成 UniClipboard 的硬依赖意味着:

  • 每个用户都要注册 Tailscale(账号、SSO 步骤、一个不属于他们的控制平面)
  • 每对设备都要在同一个 tailnet 里
  • "没有第三方能看到任何东西"的故事现在依赖于信任 Tailscale 的控制平面 (它做得非常好,但它毕竟是个第三方)

对已经在跑 Tailscale 的用户来说,确实——他们大概可以用 tailscale serve + 一个小 daemon 合成出 UniClipboard 80% 的功能。选择不强制要 Tailscale 是有意为之的:卖点是"装到两台机器上, 就能用,不用在任何地方有账号"。

总结

如果你在 2026 年挑选一个 Rust P2P 栈,我会这样想这些选项:

库 适合 不适合
iroh 桌面应用、2-N 设备的小网格、"我想 dial 一个 peer" 你需要浏览器支持、需要最大化的协议灵活性
libp2p 自定义协议、大型 peer 网络、IPFS 周边 小应用,你不需要 80% 的可配置性
WebRTC 浏览器 P2P、视频/音频 纯桌面、小载荷、不要多媒体
syncthing/BEP 文件夹同步 任何不是文件夹同步的事情
magic-wormhole 一次性传输、人类可读的码 长连接通道
Tailscale + 自研 你本来就要求 Tailscale 你想要零账号

具体到 UniClipboard——小载荷、持久的设备配对、不要基础设施、纯桌面——iroh 是干净的契合。 一年下来,我还会做同样的选择。


关于 UniClipboard

UniClipboard 是一款端到端加密、P2P 的剪贴板同步工具,支持 macOS、Windows 和 Linux。 开源协议 AGPL-3.0。没有账号,没有服务器(只有 relay fallback,而且只看得到密文)。

  • GitHub: https://github.com/uniclipboard/uniclipboard
  • Site: https://uniclipboard.app
  • 当前版本:0.6.0

如果这篇文章对你有用,整个项目会感激你给个 star,但更有用的是一份 bug report、 一份打包贡献,或者对上面任何一个技术决定的有理有据的反对意见。

本页目录

  1. 问题的形状
  2. 候选 1:libp2p
  3. 候选 2:WebRTC
  4. 候选 3:syncthing 的 BEP 协议
  5. 候选 4:magic-wormhole
  6. iroh 做对了什么
  7. "iroh 不会哪天死掉/在你脚下变化吗?"
  8. 关于"用 Tailscale 不就行了吗"
  9. 总结
  10. 关于 UniClipboard
UniClipboard

在你自己的设备之间同步剪贴板,开源开发。

链接
仓库IssuesReleases文档赞助支持
技术栈
RustTauriirohshadcn/ui
联系
X · @uniclipboardX · @mkdir700B站 · @mkdir700[email protected]
© 2026 mkdir700made with Rust 🦀 · AGPL-3.0