接入现有 iroh 应用
iroh-pairing(crates/iroh-pairing/)专为嵌入已有的 iroh 应用而设计:宿主应用先创建并持有自己的 iroh::Endpoint 与业务协议 handler,再将 Pairing 附加到同一 Endpoint 上。
配对库绝不创建第二个 Endpoint、绝不启动竞争性的后台 accept loop,也绝不在配对取消或超时时调用 Router::shutdown 或 Endpoint::close。完整的独立外部消费者工程可参考仓库中的 examples/host-integration/。
1. 依赖配置
Section titled “1. 依赖配置”在宿主项目的 Cargo.toml 中引入与 iroh 1.3.0 一致的依赖:
[dependencies]iroh = { version = "=1.3.0", default-features = false, features = ["tls-ring"] }iroh-pairing = { git = "https://git.gjz010.com/gjz010/iroh-pairing" }tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time"] }iroh-pairing 默认不引入任何 CLI、GUI、相机、数据库、平台 FFI 或完整 Matrix 客户端 SDK。
2. 挂载到宿主 Endpoint 的两种方式
Section titled “2. 挂载到宿主 Endpoint 的两种方式”方式一:由 iroh::protocol::Router 统一分发(推荐)
Section titled “方式一:由 iroh::protocol::Router 统一分发(推荐)”如果宿主使用 Router 管理多条 ALPN,只需将 PAIRING_ALPN(b"/iroh-pairing/1")与业务 ALPN 注册在同一个 Router::builder 上:
use iroh::endpoint::presets;use iroh::protocol::Router;use iroh::Endpoint;use iroh_pairing::{Pairing, PairingConfig, PAIRING_ALPN};
let endpoint = Endpoint::builder(presets::N0).bind().await?;let pairing = Pairing::attach(endpoint.clone(), PairingConfig::default());
let router = Router::builder(endpoint.clone()) .accept(PAIRING_ALPN, pairing.handler()) .accept(BUSINESS_ALPN, my_business_handler) .spawn();方式二:在手写 accept 循环中按 ALPN 分发
Section titled “方式二:在手写 accept 循环中按 ALPN 分发”如果宿主维护自定义的 endpoint.accept() 循环,请在构建 Endpoint 时声明 PAIRING_ALPN,并在完成普通 QUIC/TLS 握手后按 connection.alpn() 分发给 pairing.accept(connection):
use iroh::endpoint::presets;use iroh::Endpoint;use iroh_pairing::{Pairing, PairingConfig, PAIRING_ALPN};
let endpoint = Endpoint::builder(presets::N0) .alpns(vec![PAIRING_ALPN.to_vec(), BUSINESS_ALPN.to_vec()]) .bind() .await?;
let pairing = Pairing::attach(endpoint.clone(), PairingConfig::default());
while let Some(incoming) = endpoint.accept().await { let connection = incoming.await?; // 必须先完成完整握手(禁止 0-RTT) match connection.alpn() { PAIRING_ALPN => { let pairing = pairing.clone(); tokio::spawn(async move { let _ = pairing.accept(connection).await; }); } BUSINESS_ALPN => { // 交给宿主自己的业务连接处理逻辑 } _ => connection.close(0u32.into(), b"unsupported alpn"), }}可运行的完整对比实现见 examples/host-integration/src/bin/router_host.rs 与 examples/host-integration/src/bin/manual_accept.rs。
3. 创建邀请、本地确认 SAS 与处理载荷回执
Section titled “3. 创建邀请、本地确认 SAS 与处理载荷回执”通过 pairing.create_invite(...) 创建一次配对会话(Session),并订阅结构化事件流:
use iroh_pairing::{ ApplicationContext, InviteRequest, NetworkMode, Payload, SessionEvent,};use tokio::sync::broadcast::error::RecvError;
let app_ctx = ApplicationContext { namespace: "com.example.sync".to_owned(), profile_id: "device.sync".to_owned(), profile_version: 1, role: "peer".to_owned(), accepts_roles: vec!["peer".to_owned()], purpose: "Pair two sync nodes".to_owned(), scope: vec!["sync.read".to_owned(), "sync.write".to_owned()], required_capabilities: vec![], metadata: Default::default(),};
let request = InviteRequest::new("MySyncApp", "Desktop-A", app_ctx, NetworkMode::Internet) .with_payload(Payload::new("application/json", 1, br#"{"cluster":"alpha"}"#.to_vec())?);
let session = pairing.create_invite(request).await?;
// 导出二维码字符串:// - 若由已打开的手机应用直接扫描裸邀请,可使用 pairing_wire::encode_bare(&session.invite())// - 若已部署自托管 HTTPS Web 扫码页,可生成带 fragment 的入口 URL:let invite_url = session.invite_url("https://pair.example.com/pair")?;
let mut events = session.subscribe();loop { match events.recv().await { Ok(SessionEvent::SasReady { display }) => { // 必须在宿主本机 UI 展示完整 7 个 display.emojis 与 3 个 display.decimals, // 由本地用户核对一致后调用 confirm(true),不一致调用 confirm(false): let matched = prompt_local_operator(&display); session.confirm(matched)?; } Ok(SessionEvent::Verified { peer }) => { // 此时双方已完成本地确认、双向 MAC 校验与 SAS Done,获得不可伪造的 VerifiedPeer println!("Verified peer: {}", peer.remote_id()); } Ok(SessionEvent::PayloadReceived { payload, content_digest }) => { // 宿主先校验 payload 的 content_type、schema_version 与内容合法性, // 确认接收后显式发送协议回执: validate_peer_payload(&payload)?; session.accept_payload_with_digest(Some(&content_digest))?; } Ok(SessionEvent::ExchangeFinished { outcome }) => { println!("Exchange finished: {:?}", outcome); break; } Ok(SessionEvent::Failed { code, message }) => { eprintln!("Pairing failed: {code} ({message:?})"); break; } Ok(SessionEvent::Indeterminate { reason }) => { eprintln!("Verified, but exchange outcome is indeterminate: {reason}"); break; } Err(RecvError::Lagged(_skipped)) => { // 事件通道有界;若消费者掉队,直接从权威快照恢复当前状态,无需解析日志: let snapshot = session.snapshot(); sync_ui_from_snapshot(&snapshot); } Err(RecvError::Closed) => break, _ => {} }}VerifiedPeer不可外部构造或反序列化:VerifiedPeer的字段全部私有,且不实现Deserialize。只有完整通过 Matrix SAS 流程的会话才能向宿主产出VerifiedPeer。- 身份验证(
Verified)与载荷交换(ExchangeOutcome)独立:仅向 QUIC 流写入成功不算交换完成;对端必须调用accept_payload/accept_payload_with_digest返回协议回执后,双方互发ExchangeDone才进入ExchangeOutcome::Complete。若在验证后因断连或超时无法确认对端是否收到回执,会显式返回ExchangeOutcome::Indeterminate,不会谎报成功或抹去已验证身份。 - 取消隔离:调用
session.cancel()或丢弃未完成的Session句柄只会清理该次配对事务的任务与 QUIC 流,宿主既有业务连接与Endpoint不受任何影响。
4. 可选:安装连接级 PairingGate(AuthorizationState)
Section titled “4. 可选:安装连接级 PairingGate(AuthorizationState)”如果宿主希望在 QUIC 握手完成后自动拦截未配对设备的业务 ALPN 连接,可在构建 Endpoint 时(bind() 之前)安装 AuthorizationState::gate():
use iroh::endpoint::presets;use iroh::Endpoint;use iroh_pairing::{AuthorizationState, GrantLifetime};
// 1. 声明受保护的业务 ALPN(严禁填入 PAIRING_ALPN)let auth_state = AuthorizationState::new([BUSINESS_ALPN])?;
// 2. 在 EndpointBuilder::hooks 中安装 Gate(EndpointHooks 为构建期属性,bind 后不可追加)let endpoint = Endpoint::builder(presets::N0) .hooks(auth_state.gate()) .bind() .await?;当配对会话产出 VerifiedPeer 后,注意 PairingGate 只执行 (EndpointId, ALPN) 的连接级准入,不会替宿主自动检查业务 profile 或 scope。宿主必须先按本地业务策略校验 verified_peer.application() 与 verified_peer.scope(),通过后再显式调用 grant:
let app = verified_peer.application();let profile_ok = app.namespace == "com.example.sync" && app.profile_id == "device.sync" && app.profile_version == 1 && app.role == "peer";let scope_ok = verified_peer.scope().iter().any(|s| s == "sync.write");
if profile_ok && scope_ok { // GrantLifetime::Once 仅允许消费一次新连接; // GrantLifetime::Persistent 允许建立多条新连接,直至调用 revoke。 let record = auth_state.grant(&verified_peer, BUSINESS_ALPN, GrantLifetime::Persistent)?;}PairingGate 的行为边界
Section titled “PairingGate 的行为边界”- 固定拒绝码防探测:对受管业务 ALPN 上的未授权连接(或已消费完的
Once记录),PairingGate在after_handshake阶段统一使用应用错误码403(GATE_REJECT_ERROR_CODE)与固定原因b"pairing authorization required"拒绝,不向对端泄露本地策略细节。 - 连接级 vs 资源级权限:
PairingGate只检查(remote EndpointId, ALPN)。具体文件路径、操作命令或细分资源范围仍须由业务 handler 通过auth_state.authorization(remote_id, BUSINESS_ALPN)读取已验证的TransactionContext与scope自行校验。 - 持久恢复与撤销:
auth_state.records()导出的AuthorizationRecord可由宿主写入自己的可信本地存储,重启后使用相同的宿主长期密钥创建Endpoint,并调用auth_state.restore(record)恢复(仅接受Persistent记录;严禁将网络输入或二维码内容传给restore)。auth_state.revoke(remote_id)或auth_state.revoke_alpn(remote_id, BUSINESS_ALPN)会立即拒绝后续新建立的连接;已建立的存量连接是否强制断开由宿主显式决定。
更完整的说明可参阅 docs/library-integration.md 与 docs/authorization.md。