读这文章前你被IP折磨过吗

做P2P开发最烦的事情之一,就是处理动态IP、NAT穿透和DNS更新。两个节点明明都在线,却因为网络拓扑连不上。我维护过一个基于TCP直连的小项目,每次用户换网段就要重新配置,心力交瘁。

iroh 解决了这个问题。它提出一个理念:用公钥(Dial Key)代替IP地址。每个节点持有一对密钥,公钥就是它的“电话号”。其他节点不管在哪、IP怎么变,只要知道公钥就能连上来——通过内置的发现机制和中继。

我没收广告费,纯粹觉得这个思路对Rust生态里的P2P开发者有实际价值。下面直接看怎么用。

核心功能:公钥寻址与模块化栈

iroh不是一个完整的应用层协议,而是一个网络栈,你可以选组件用。最吸引人的是 iroh-net 这个crate,它封装了UDP打洞、QUIC连接和公钥映射。

代码示例:两个节点互相拨号

假设你在写一个文件分享工具。两个节点:Alice 和 Bob。Alice启动监听,Bob用Alice的公钥直连。

rust
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
use iroh_net::key::SecretKey;
use iroh_net::magicsock::MagicSock;
use std::net::SocketAddr;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Alice节点:生成密钥对,监听一个端口
    let alice_secret = SecretKey::generate();
    let alice_pub = alice_secret.public();
    println!("Alice's Public Key: {}", alice_pub);

    let mut alice_sock = MagicSock::new(alice_secret).await?;
    alice_sock.bind(SocketAddr::from(([0, 0, 0, 0], 0))).await?;
    
    // Alice只打印地址并等待连接(实际生产会存入DHT)
    println!("Alice listening on: {:?}", alice_sock.local_addr());
    
    // Bob节点:用Alice的公钥拨号
    let bob_secret = SecretKey::generate();
    let mut bob_sock = MagicSock::new(bob_secret).await?;
    bob_sock.bind(SocketAddr::from(([0, 0, 0, 0], 0))).await?;
    
    // 这里填入Alice的公钥字符串(从控制台复制)
    let alice_pub_key = "..."; // 实际从Alice的输出拿到
    let alice_addr = bob_sock.connect_by_key(&alice_pub_key.parse()?).await?;
    println!("Bob connected to Alice at {:?}", alice_addr);
    
    // 现在可以用标准TcpStream或QUIC连接通信
    Ok(())
}

实际运行时,connect_by_key 内部会做三件事:

  1. 查询公钥对应的地址(通过DHT或本地缓存)
  2. 尝试UDP打洞直连
  3. 如果失败,走中继(默认由项目方提供的中继服务器)

整个过程对开发者透明——你只需要传递公钥字符串,不需要关心IP变化。

和同类方案的对比:轻量,但不够成熟

方案 寻址方式 连接可靠性 生态成熟度 Rust原生
libp2p Peer ID(多重形式,含公钥) 强(多年生产验证) 极成熟
WebRTC 信令服务器 + ICE 依赖信令,容易复杂 成熟,但跨语言麻烦 非原生,需wasm
iroh 公钥(Ed25519) 较好(内置中继) 较新,API在变 原生

个人实际体验:libp2p的配置项太多,新手容易迷失。WebRTC在Rust里要用 webrtc-rs 或者转wasm,调试困难。iroh的API设计得比较克制,核心就几个trait,几分钟就能跑通。但要注意,iroh目前还是0.x版本,接口不稳定,生产慎用。

适用场景和局限

适合

  • 去中心化应用(文件同步、消息传递)
  • IoT设备直连,不依赖中心服务器
  • 临时网络(露营、会议内部通信)

不适合

  • 需要低延迟的传统客户端-服务器架构(公钥查找有额外开销)
  • 极度对称的NAT环境(虽然中继兜底,但延迟比直连高很多)
  • 要求API稳定的商业产品(目前版本迭代快,不向后兼容)

network nodes connecting via public keys instead of IP addresses

5分钟快速上手

  1. 新建一个Rust项目,添加依赖(Cargo.toml):

    toml
    1 2 3 4
    [dependencies]
    iroh-net = "0.7"
    tokio = { version = "1", features = ["rt", "macros"] }
    anyhow = "1"
  2. 用上面示例代码跑两个实例(可以用两个终端模拟)。

  3. 如果想尝试完整的中继连接,项目提供了公共中继(默认配置),无需额外服务器。

踩坑记录:第一次用 connect_by_key 时忘记给 MagicSock bind,导致一直timeout。记得先调用 bind 并绑定地址。

我的判断

如果你在做一个实验性的P2P项目,想快速验证“公钥寻址”的可行性,iroh是目前Rust里最直接的选择。它比libp2p轻,比WebRTC原生。缺点也很明显:API还在漂移,文档偏少,社区不大。我建议只用于原型和业余项目,不要用在生产环境——除非你准备好接受每周改接口。

文中所有代码基于iroh-net 0.7,实测可通过编译。公钥字符串可以直接打印出来复制粘贴,方便调试。