1use iroh::address_lookup::memory::MemoryLookup;
2use iroh::protocol::Router;
3use iroh_blobs::BlobsProtocol;
4use iroh_docs::protocol::Docs;
5use iroh_gossip::net::Gossip;
6use std::collections::HashSet;
7use std::path::PathBuf;
8use std::sync::{Arc, RwLock};
9use std::time::Duration;
10
11use crate::error::{Result, SyncwebError};
12use crate::net::RelayManager;
13
14use super::beacon_lookup::{BeaconAddressLookup, DEFAULT_BEACON_PORT};
15use super::blob_store::BlobStore;
16use super::discovery::TopicTracker;
17use super::docs_engine::DocsEngine;
18use super::identity::IdentityManager;
19use super::membership_hook::MembershipHook;
20
21#[non_exhaustive]
22#[derive(Clone, Debug)]
23pub enum RelayMode {
24 Default,
25 Custom { map: iroh::RelayMap, insecure: bool },
26 None,
27}
28
29#[derive(Clone, Debug)]
36#[non_exhaustive]
37pub struct DiscoveryConfig {
38 pub mdns: bool,
40 pub beacon: bool,
42 pub beacon_base_port: u16,
44 pub beacon_interval: Duration,
46 pub interface: Option<String>,
48 pub scope: Option<[u8; 16]>,
50}
51
52impl Default for DiscoveryConfig {
53 fn default() -> Self {
54 Self {
55 mdns: true,
56 beacon: true,
57 beacon_base_port: DEFAULT_BEACON_PORT,
58 beacon_interval: Duration::from_secs(1),
59 interface: None,
60 scope: None,
61 }
62 }
63}
64
65impl DiscoveryConfig {
66 #[must_use]
68 pub const fn disabled() -> Self {
69 Self {
70 mdns: false,
71 beacon: false,
72 beacon_base_port: DEFAULT_BEACON_PORT,
73 beacon_interval: Duration::from_secs(1),
74 interface: None,
75 scope: None,
76 }
77 }
78}
79
80fn mdns_service_name(scope: Option<&[u8; 16]>) -> String {
82 scope.map_or_else(
83 || "irohv1".to_owned(),
84 |bytes| format!("syncweb-{}", hex::encode(&bytes[..8])),
85 )
86}
87
88#[must_use]
92pub fn discovery_scope(network: Option<&str>) -> Option<[u8; 16]> {
93 let name = network?;
94 let id = crate::net::NetworkId::from_name(name);
95 let bytes = id.as_bytes();
96 let mut scope = [0_u8; 16];
97 scope.copy_from_slice(&bytes[..16]);
98 Some(scope)
99}
100
101fn register_discovery(endpoint: &iroh::Endpoint, discovery: &DiscoveryConfig) {
103 if discovery.mdns {
104 register_mdns(endpoint, discovery);
105 }
106 if discovery.beacon {
107 register_beacon(endpoint, discovery);
108 }
109}
110
111fn register_mdns(endpoint: &iroh::Endpoint, discovery: &DiscoveryConfig) {
112 let service_name = mdns_service_name(discovery.scope.as_ref());
113 match iroh_mdns_address_lookup::MdnsAddressLookup::builder()
114 .service_name(service_name)
115 .build(endpoint.id())
116 .map_err(|error| SyncwebError::operation("failed to build mDNS address lookup", error))
117 .and_then(|mdns| {
118 endpoint
119 .address_lookup()
120 .map_err(|error| SyncwebError::operation("no address lookup service available", error))
121 .map(|lookup| lookup.add(mdns))
122 }) {
123 Ok(()) => tracing::debug!("mDNS address lookup registered"),
124 Err(error) => tracing::warn!(
125 %error,
126 "mDNS address lookup registration failed — local peer discovery unavailable"
127 ),
128 }
129}
130
131fn register_beacon(endpoint: &iroh::Endpoint, discovery: &DiscoveryConfig) {
132 let Ok(beacon) = BeaconAddressLookup::new(
133 endpoint.id(),
134 discovery.scope,
135 discovery.beacon_base_port,
136 discovery.beacon_interval,
137 discovery.interface.as_deref(),
138 ) else {
139 return;
140 };
141 match endpoint.address_lookup() {
142 Ok(lookup) => {
143 lookup.add(beacon);
144 tracing::debug!("beacon address lookup registered");
145 }
146 Err(error) => tracing::warn!(
147 %error,
148 "beacon address lookup registration failed — no address lookup service available"
149 ),
150 }
151}
152
153pub struct IrohNode {
154 endpoint: iroh::Endpoint,
155 router: Arc<Router>,
156 blobs: iroh_blobs::BlobsProtocol,
157 docs: Docs,
158 gossip: Arc<Gossip>,
159 blob_store: BlobStore,
160 docs_engine: DocsEngine,
161 topic_tracker: TopicTracker,
162 relay: Option<RelayManager>,
165}
166
167impl IrohNode {
168 pub async fn new(
174 identity: IdentityManager,
175 data_dir: PathBuf,
176 relay_mode: RelayMode,
177 member_keys: Arc<RwLock<HashSet<iroh::PublicKey>>>,
178 discovery: DiscoveryConfig,
179 ) -> Result<Self> {
180 Self::new_with_address_lookup(
181 identity,
182 data_dir,
183 relay_mode,
184 MemoryLookup::new(),
185 discovery,
186 member_keys,
187 )
188 .await
189 }
190
191 pub async fn new_with_relay(
202 identity: IdentityManager,
203 data_dir: PathBuf,
204 relay_mode: RelayMode,
205 member_keys: Arc<RwLock<HashSet<iroh::PublicKey>>>,
206 discovery: DiscoveryConfig,
207 relay: Option<RelayManager>,
208 ) -> Result<Self> {
209 Self::new_with_address_lookup_and_relay(
210 identity,
211 data_dir,
212 relay_mode,
213 MemoryLookup::new(),
214 discovery,
215 member_keys,
216 relay,
217 )
218 .await
219 }
220
221 pub async fn new_with_address_lookup(
225 identity: IdentityManager,
226 data_dir: PathBuf,
227 relay_mode: RelayMode,
228 address_lookup: MemoryLookup,
229 discovery: DiscoveryConfig,
230 member_keys: Arc<RwLock<HashSet<iroh::PublicKey>>>,
231 ) -> Result<Self> {
232 Self::new_with_address_lookup_and_relay(
233 identity,
234 data_dir,
235 relay_mode,
236 address_lookup,
237 discovery,
238 member_keys,
239 None,
240 )
241 .await
242 }
243
244 pub async fn new_with_address_lookup_and_relay(
248 identity: IdentityManager,
249 data_dir: PathBuf,
250 relay_mode: RelayMode,
251 address_lookup: MemoryLookup,
252 discovery: DiscoveryConfig,
253 member_keys: Arc<RwLock<HashSet<iroh::PublicKey>>>,
254 relay: Option<RelayManager>,
255 ) -> Result<Self> {
256 tokio::fs::create_dir_all(&data_dir)
257 .await
258 .map_err(|error| SyncwebError::operation("failed to create node data directory", error))?;
259 let docs_dir = data_dir.join("docs");
260 tokio::fs::create_dir_all(&docs_dir)
261 .await
262 .map_err(|error| SyncwebError::operation("failed to create docs directory", error))?;
263
264 let base = match relay_mode {
265 RelayMode::Default => iroh::Endpoint::builder(iroh::endpoint::presets::N0),
266 RelayMode::Custom { map, insecure } => {
267 let mut b = iroh::Endpoint::builder(iroh::endpoint::presets::N0)
268 .relay_mode(iroh::endpoint::RelayMode::Custom(map));
269 if insecure {
270 b = b.ca_tls_config(iroh::tls::CaTlsConfig::insecure_skip_verify());
271 }
272 b
273 }
274 RelayMode::None => {
275 iroh::Endpoint::builder(iroh::endpoint::presets::N0).relay_mode(iroh::endpoint::RelayMode::Disabled)
276 }
277 };
278
279 let hook = MembershipHook {
280 member_keys: member_keys.clone(),
281 };
282 let mut builder = base
283 .address_lookup(address_lookup.clone())
284 .secret_key(identity.secret_key().clone())
285 .hooks(hook);
286 if let Some(manager) = &relay {
287 builder = builder.add_custom_transport(Arc::new(manager.transport()));
288 }
289 let endpoint = builder
290 .bind()
291 .await
292 .map_err(|error| SyncwebError::operation("failed to bind Iroh endpoint", error))?;
293
294 register_discovery(&endpoint, &discovery);
299
300 let fs_blob_store = iroh_blobs::store::fs::FsStore::load(data_dir.join("blobs"))
301 .await
302 .map_err(|error| SyncwebError::operation("failed to open blob store", error))?;
303 let blobs = BlobsProtocol::new(fs_blob_store.as_ref(), None);
304
305 let gossip = Arc::new(Gossip::builder().spawn(endpoint.clone()));
306
307 let docs = Docs::persistent(docs_dir)
308 .spawn(endpoint.clone(), blobs.store().clone(), gossip.as_ref().clone())
309 .await
310 .map_err(|error| SyncwebError::operation("failed to open docs store", error))?;
311
312 let router = Router::builder(endpoint.clone())
313 .accept(iroh_blobs::protocol::ALPN, blobs.clone())
314 .accept(iroh_docs::ALPN, docs.clone())
315 .accept(iroh_gossip::ALPN, gossip.clone())
316 .spawn();
317
318 let blob_store = BlobStore::new_with_address_lookup(&blobs, address_lookup);
319 let relay_addr = relay.as_ref().map(RelayManager::local_addr);
320 let docs_engine = DocsEngine::new(&docs, blobs.store(), endpoint.id(), relay_addr.clone());
321 let topic_tracker = TopicTracker::new(&gossip, &endpoint, relay_addr);
322
323 Ok(Self {
324 endpoint,
325 router: Arc::new(router),
326 blobs,
327 docs,
328 gossip,
329 blob_store,
330 docs_engine,
331 topic_tracker,
332 relay,
333 })
334 }
335
336 #[must_use]
339 pub fn relay_addr(&self) -> Option<iroh_base::CustomAddr> {
340 self.relay.as_ref().map(RelayManager::local_addr)
341 }
342
343 #[must_use]
344 pub const fn endpoint(&self) -> &iroh::Endpoint {
345 &self.endpoint
346 }
347
348 #[must_use]
349 pub const fn blobs(&self) -> &BlobsProtocol {
350 &self.blobs
351 }
352
353 #[must_use]
354 pub const fn docs(&self) -> &Docs {
355 &self.docs
356 }
357
358 #[must_use]
359 pub fn gossip(&self) -> &Gossip {
360 &self.gossip
361 }
362
363 #[must_use]
364 pub const fn blob_store(&self) -> &BlobStore {
365 &self.blob_store
366 }
367
368 #[must_use]
369 pub const fn docs_engine(&self) -> &DocsEngine {
370 &self.docs_engine
371 }
372
373 #[must_use]
374 pub const fn topic_tracker(&self) -> &TopicTracker {
375 &self.topic_tracker
376 }
377
378 #[must_use]
379 pub fn is_running(&self) -> bool {
380 !self.router.is_shutdown()
381 }
382
383 pub async fn stop(&self) -> Result<()> {
387 self.blob_store
388 .inner()
389 .sync_db()
390 .await
391 .map_err(|error| SyncwebError::operation("failed to flush blob store", error))?;
392 self.router
393 .shutdown()
394 .await
395 .map_err(|error| SyncwebError::operation("failed to stop node router", error))?;
396 if let Some(relay) = &self.relay {
397 relay.shutdown().await;
398 }
399 Ok(())
400 }
401
402 pub async fn shutdown(self) -> Result<()> {
406 self.stop().await
407 }
408}
409
410impl Drop for IrohNode {
411 fn drop(&mut self) {
412 if !self.router.is_shutdown() {
413 tracing::warn!("IrohNode dropped without calling stop() — router shutdown may be incomplete");
414 }
415 }
416}