Skip to main content

rag_rat_sync/
endpoint.rs

1//! The iroh endpoint adapter (phase D, #406).
2//!
3//! Binds a QUIC endpoint that speaks [`SYNC_ALPN`] over a pinned relay, and runs one
4//! [`run_session`] per connection. iroh's stream types implement
5//! `tokio::io::AsyncRead`/`AsyncWrite`, so the bi- stream drops straight into the
6//! transport-agnostic session with no adapter shims.
7//!
8//! `Endpoint::builder(presets::Minimal)` is deliberate: Minimal disables the public n0 node
9//! directory, so discovery happens ONLY through the relay this deployment pins — a peer is
10//! reachable iff it shares the configured relay, never via a third-party lookup.
11//!
12//! # Authorization (#881)
13//!
14//! iroh authenticates the transport KEY; on top of that, both [`connect_and_sync`] and
15//! [`accept_and_sync`] run the mutual node-authorization handshake ([`run_auth_phase`]) BEFORE any
16//! inventory is exchanged. Under [`AuthPolicy::Closed`] a peer is admitted only if it presents a
17//! signed binding proving its authenticated node id belongs to a roster device of the account;
18//! under [`AuthPolicy::Open`] any peer is admitted (for a future public read-only knowledge base).
19//! The ONBOARDING case — admitting a not-yet-roster device via a one-time invite token
20//! ([`AuthPolicy::InviteToken`]) — is not implemented in this slice and fails closed; it is the
21//! pairing flow (`sync init` / `sync pair` / `sync join`) that the CLI slice wires up.
22
23use std::str::FromStr;
24
25use iroh::endpoint::presets;
26use iroh::{Endpoint, EndpointAddr, RelayMode, RelayUrl, SecretKey};
27use tokio::time::timeout;
28
29use crate::auth::{
30    AuthConfig, AuthPolicy, AuthRole, DEFAULT_PRE_AUTH_TIMEOUT, NodeAuth, run_auth_phase,
31};
32use crate::session::{DEFAULT_IDLE_TIMEOUT, SessionError, SessionReport, SyncStore, run_session};
33use crate::wire::SYNC_ALPN;
34
35/// Endpoint construction or connection setup failed, before a session could run.
36#[derive(Debug)]
37pub enum EndpointError {
38    /// The configured relay URL did not parse.
39    RelayUrl(String),
40    /// Binding the endpoint failed (socket, TLS, relay handshake).
41    Bind(String),
42    /// Dialling a peer, or accepting an inbound connection, failed.
43    Connect(String),
44}
45
46impl std::fmt::Display for EndpointError {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        match self {
49            EndpointError::RelayUrl(m) => write!(f, "invalid relay url: {m}"),
50            EndpointError::Bind(m) => write!(f, "binding the sync endpoint failed: {m}"),
51            EndpointError::Connect(m) => write!(f, "sync connection setup failed: {m}"),
52        }
53    }
54}
55
56impl std::error::Error for EndpointError {}
57
58/// Bind a sync endpoint pinned to `relay_url`, with a stable node id derived from `secret_key` (the
59/// 32-byte ed25519 seed of the transport identity — its own key, distinct from any account device
60/// key). The same seed yields the same node id across launches, so a peer's ticket stays valid.
61pub async fn build_endpoint(
62    secret_key: [u8; 32],
63    relay_url: &str,
64) -> Result<Endpoint, EndpointError> {
65    let relay_url =
66        RelayUrl::from_str(relay_url.trim()).map_err(|e| EndpointError::RelayUrl(e.to_string()))?;
67    Endpoint::builder(presets::Minimal)
68        .alpns(vec![SYNC_ALPN.to_vec()])
69        .relay_mode(RelayMode::custom([relay_url]))
70        .secret_key(SecretKey::from_bytes(&secret_key))
71        .bind()
72        .await
73        .map_err(|e| EndpointError::Bind(e.to_string()))
74}
75
76/// This endpoint's dialable address — hand it (or a ticket wrapping it) to a peer so it can
77/// [`connect_and_sync`] back.
78pub fn endpoint_addr(endpoint: &Endpoint) -> EndpointAddr {
79    endpoint.addr()
80}
81
82/// Dial `peer`, authorize each other under `policy`, then run one sync session, returning what
83/// moved. The auth handshake (#881) runs BEFORE any inventory: the dialer presents its binding,
84/// then verifies the acceptor's before revealing anything, so a poisoned address never leaks the
85/// account log to an impostor.
86pub async fn connect_and_sync<S: SyncStore + NodeAuth>(
87    endpoint: &Endpoint,
88    peer: impl Into<EndpointAddr>,
89    store: &mut S,
90    policy: AuthPolicy,
91    now_ms: i64,
92) -> Result<SessionReport, SyncFailure> {
93    let local_node = *endpoint.id().as_bytes();
94    let conn = endpoint
95        .connect(peer, SYNC_ALPN)
96        .await
97        .map_err(|e| SyncFailure::Endpoint(EndpointError::Connect(e.to_string())))?;
98    let remote_node = *conn.remote_id().as_bytes();
99    let (mut send, mut recv) = conn
100        .open_bi()
101        .await
102        .map_err(|e| SyncFailure::Endpoint(EndpointError::Connect(e.to_string())))?;
103    run_auth_phase(&mut send, &mut recv, &*store, AuthConfig {
104        role: AuthRole::Dialer,
105        account_id: store.account_id(),
106        local_node,
107        remote_node,
108        policy,
109        now_ms,
110        pre_auth_timeout: DEFAULT_PRE_AUTH_TIMEOUT,
111    })
112    .await
113    .map_err(SyncFailure::Auth)?;
114    let report = run_session(store, send, recv).await.map_err(SyncFailure::Session)?;
115    // Best-effort graceful close; the session already exchanged an explicit Done both ways.
116    conn.close(0u32.into(), b"done");
117    Ok(report)
118}
119
120/// Accept ONE inbound connection and run a session against it. D4's `sync serve` loops this;
121/// keeping it single-shot here keeps the store's `!Send` connection on one task (no spawn).
122///
123/// Every pre-session wait a peer controls — the handshake and opening the bidirectional stream — is
124/// bounded by [`DEFAULT_IDLE_TIMEOUT`]. `run_session`'s own idle timeout only starts once the
125/// stream is open, so without these a peer that connects and then stalls (never opening a stream)
126/// would hold this single-session server forever, blocking every later peer.
127pub async fn accept_and_sync<S: SyncStore + NodeAuth>(
128    endpoint: &Endpoint,
129    store: &mut S,
130    policy: AuthPolicy,
131    now_ms: i64,
132) -> Result<SessionReport, SyncFailure> {
133    let local_node = *endpoint.id().as_bytes();
134    let incoming = endpoint
135        .accept()
136        .await
137        .ok_or_else(|| SyncFailure::Endpoint(EndpointError::Connect("endpoint closed".into())))?;
138    let conn = timeout(DEFAULT_IDLE_TIMEOUT, incoming)
139        .await
140        .map_err(|_| SyncFailure::Endpoint(EndpointError::Connect("handshake timed out".into())))?
141        .map_err(|e| SyncFailure::Endpoint(EndpointError::Connect(e.to_string())))?;
142    let remote_node = *conn.remote_id().as_bytes();
143    let (mut send, mut recv) = timeout(DEFAULT_IDLE_TIMEOUT, conn.accept_bi())
144        .await
145        .map_err(|_| SyncFailure::Endpoint(EndpointError::Connect("peer opened no stream".into())))?
146        .map_err(|e| SyncFailure::Endpoint(EndpointError::Connect(e.to_string())))?;
147    // Authorize the dialer BEFORE run_session so no inventory (not even account confirmation)
148    // leaves this peer until the remote passes our policy (#881).
149    run_auth_phase(&mut send, &mut recv, &*store, AuthConfig {
150        role: AuthRole::Acceptor,
151        account_id: store.account_id(),
152        local_node,
153        remote_node,
154        policy,
155        now_ms,
156        pre_auth_timeout: DEFAULT_PRE_AUTH_TIMEOUT,
157    })
158    .await
159    .map_err(SyncFailure::Auth)?;
160    let report = run_session(store, send, recv).await.map_err(SyncFailure::Session)?;
161    conn.close(0u32.into(), b"done");
162    Ok(report)
163}
164
165/// A sync attempt that failed setting up the connection, authorizing the peer, or running the
166/// session.
167#[derive(Debug)]
168pub enum SyncFailure {
169    Endpoint(EndpointError),
170    /// The node-authorization handshake refused the peer (or we could not authorize to it).
171    Auth(crate::auth::AuthError),
172    Session(SessionError),
173}
174
175impl std::fmt::Display for SyncFailure {
176    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177        match self {
178            SyncFailure::Endpoint(e) => write!(f, "{e}"),
179            SyncFailure::Auth(e) => write!(f, "{e}"),
180            SyncFailure::Session(e) => write!(f, "{e}"),
181        }
182    }
183}
184
185impl std::error::Error for SyncFailure {}