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};
12
13use super::beacon_lookup::{BeaconAddressLookup, DEFAULT_BEACON_PORT};
14use super::discovery::TopicTracker;
15use super::identity::IdentityManager;
16use super::membership_hook::MembershipHook;
17use super::{blob_store::BlobStore, docs_engine::DocsEngine, gossip_service::GossipService};
18
19#[non_exhaustive]
20#[derive(Clone, Debug)]
21pub enum RelayMode {
22 Default,
23 Custom { map: iroh::RelayMap, insecure: bool },
24 None,
25}
26
27#[derive(Clone, Debug)]
34#[non_exhaustive]
35pub struct DiscoveryConfig {
36 pub mdns: bool,
38 pub beacon: bool,
40 pub beacon_base_port: u16,
42 pub beacon_interval: Duration,
44 pub interface: Option<String>,
46 pub scope: Option<[u8; 16]>,
48}
49
50impl Default for DiscoveryConfig {
51 fn default() -> Self {
52 Self {
53 mdns: true,
54 beacon: true,
55 beacon_base_port: DEFAULT_BEACON_PORT,
56 beacon_interval: Duration::from_secs(1),
57 interface: None,
58 scope: None,
59 }
60 }
61}
62
63impl DiscoveryConfig {
64 #[must_use]
66 pub const fn disabled() -> Self {
67 Self {
68 mdns: false,
69 beacon: false,
70 beacon_base_port: DEFAULT_BEACON_PORT,
71 beacon_interval: Duration::from_secs(1),
72 interface: None,
73 scope: None,
74 }
75 }
76}
77
78fn mdns_service_name(scope: Option<&[u8; 16]>) -> String {
80 scope.map_or_else(
81 || "irohv1".to_owned(),
82 |bytes| format!("syncweb-{}", hex::encode(&bytes[..8])),
83 )
84}
85
86#[must_use]
90pub fn discovery_scope(network: Option<&str>) -> Option<[u8; 16]> {
91 let name = network?;
92 let id = crate::net::NetworkId::from_name(name);
93 let bytes = id.as_bytes();
94 let mut scope = [0_u8; 16];
95 scope.copy_from_slice(&bytes[..16]);
96 Some(scope)
97}
98
99fn register_discovery(endpoint: &iroh::Endpoint, discovery: &DiscoveryConfig) {
101 if discovery.mdns {
102 register_mdns(endpoint, discovery);
103 }
104 if discovery.beacon {
105 register_beacon(endpoint, discovery);
106 }
107}
108
109fn register_mdns(endpoint: &iroh::Endpoint, discovery: &DiscoveryConfig) {
110 let service_name = mdns_service_name(discovery.scope.as_ref());
111 match iroh_mdns_address_lookup::MdnsAddressLookup::builder()
112 .service_name(service_name)
113 .build(endpoint.id())
114 .map_err(|error| SyncwebError::operation("failed to build mDNS address lookup", error))
115 .and_then(|mdns| {
116 endpoint
117 .address_lookup()
118 .map_err(|error| SyncwebError::operation("no address lookup service available", error))
119 .map(|lookup| lookup.add(mdns))
120 }) {
121 Ok(()) => tracing::debug!("mDNS address lookup registered"),
122 Err(error) => tracing::warn!(
123 %error,
124 "mDNS address lookup registration failed — local peer discovery unavailable"
125 ),
126 }
127}
128
129fn register_beacon(endpoint: &iroh::Endpoint, discovery: &DiscoveryConfig) {
130 let Ok(beacon) = BeaconAddressLookup::new(
131 endpoint.id(),
132 discovery.scope,
133 discovery.beacon_base_port,
134 discovery.beacon_interval,
135 discovery.interface.as_deref(),
136 ) else {
137 return;
138 };
139 match endpoint.address_lookup() {
140 Ok(lookup) => {
141 lookup.add(beacon);
142 tracing::debug!("beacon address lookup registered");
143 }
144 Err(error) => tracing::warn!(
145 %error,
146 "beacon address lookup registration failed — no address lookup service available"
147 ),
148 }
149}
150
151pub struct IrohNode {
152 endpoint: iroh::Endpoint,
153 router: Arc<Router>,
154 blobs: iroh_blobs::BlobsProtocol,
155 docs: Docs,
156 gossip: Arc<Gossip>,
157 blob_store: BlobStore,
158 docs_engine: DocsEngine,
159 gossip_service: GossipService,
160 topic_tracker: TopicTracker,
161}
162
163impl IrohNode {
164 pub async fn new(
170 identity: IdentityManager,
171 data_dir: PathBuf,
172 relay_mode: RelayMode,
173 member_keys: Arc<RwLock<HashSet<iroh::PublicKey>>>,
174 discovery: DiscoveryConfig,
175 ) -> Result<Self> {
176 Self::new_with_address_lookup(
177 identity,
178 data_dir,
179 relay_mode,
180 MemoryLookup::new(),
181 discovery,
182 member_keys,
183 )
184 .await
185 }
186
187 pub async fn new_with_address_lookup(
191 identity: IdentityManager,
192 data_dir: PathBuf,
193 relay_mode: RelayMode,
194 address_lookup: MemoryLookup,
195 discovery: DiscoveryConfig,
196 member_keys: Arc<RwLock<HashSet<iroh::PublicKey>>>,
197 ) -> Result<Self> {
198 tokio::fs::create_dir_all(&data_dir)
199 .await
200 .map_err(|error| SyncwebError::operation("failed to create node data directory", error))?;
201 let docs_dir = data_dir.join("docs");
202 tokio::fs::create_dir_all(&docs_dir)
203 .await
204 .map_err(|error| SyncwebError::operation("failed to create docs directory", error))?;
205
206 let builder = match relay_mode {
207 RelayMode::Default => iroh::Endpoint::builder(iroh::endpoint::presets::N0),
208 RelayMode::Custom { map, insecure } => {
209 let mut b = iroh::Endpoint::builder(iroh::endpoint::presets::N0)
210 .relay_mode(iroh::endpoint::RelayMode::Custom(map));
211 if insecure {
212 b = b.ca_tls_config(iroh::tls::CaTlsConfig::insecure_skip_verify());
213 }
214 b
215 }
216 RelayMode::None => {
217 iroh::Endpoint::builder(iroh::endpoint::presets::N0).relay_mode(iroh::endpoint::RelayMode::Disabled)
218 }
219 };
220
221 let hook = MembershipHook {
222 member_keys: member_keys.clone(),
223 };
224 let endpoint = builder
225 .address_lookup(address_lookup.clone())
226 .secret_key(identity.secret_key().clone())
227 .hooks(hook)
228 .bind()
229 .await
230 .map_err(|error| SyncwebError::operation("failed to bind Iroh endpoint", error))?;
231
232 register_discovery(&endpoint, &discovery);
237
238 let fs_blob_store = iroh_blobs::store::fs::FsStore::load(data_dir.join("blobs"))
239 .await
240 .map_err(|error| SyncwebError::operation("failed to open blob store", error))?;
241 let blobs = BlobsProtocol::new(fs_blob_store.as_ref(), None);
242
243 let gossip = Arc::new(Gossip::builder().spawn(endpoint.clone()));
244
245 let docs = Docs::persistent(docs_dir)
246 .spawn(endpoint.clone(), blobs.store().clone(), gossip.as_ref().clone())
247 .await
248 .map_err(|error| SyncwebError::operation("failed to open docs store", error))?;
249
250 let router = Router::builder(endpoint.clone())
251 .accept(iroh_blobs::protocol::ALPN, blobs.clone())
252 .accept(iroh_docs::ALPN, docs.clone())
253 .accept(iroh_gossip::ALPN, gossip.clone())
254 .spawn();
255
256 let blob_store = BlobStore::new_with_address_lookup(&blobs, address_lookup);
257 let docs_engine = DocsEngine::new(&docs);
258 let gossip_service = GossipService::new(&gossip);
259 let topic_tracker = TopicTracker::new(&gossip, &endpoint);
260
261 Ok(Self {
262 endpoint,
263 router: Arc::new(router),
264 blobs,
265 docs,
266 gossip,
267 blob_store,
268 docs_engine,
269 gossip_service,
270 topic_tracker,
271 })
272 }
273
274 #[must_use]
275 pub const fn endpoint(&self) -> &iroh::Endpoint {
276 &self.endpoint
277 }
278
279 #[must_use]
280 pub const fn blobs(&self) -> &BlobsProtocol {
281 &self.blobs
282 }
283
284 #[must_use]
285 pub const fn docs(&self) -> &Docs {
286 &self.docs
287 }
288
289 #[must_use]
290 pub fn gossip(&self) -> &Gossip {
291 &self.gossip
292 }
293
294 #[must_use]
295 pub const fn blob_store(&self) -> &BlobStore {
296 &self.blob_store
297 }
298
299 #[must_use]
300 pub const fn docs_engine(&self) -> &DocsEngine {
301 &self.docs_engine
302 }
303
304 #[must_use]
305 pub const fn gossip_service(&self) -> &GossipService {
306 &self.gossip_service
307 }
308
309 #[must_use]
310 pub const fn topic_tracker(&self) -> &TopicTracker {
311 &self.topic_tracker
312 }
313
314 #[must_use]
315 pub fn is_running(&self) -> bool {
316 !self.router.is_shutdown()
317 }
318
319 pub async fn stop(&self) -> Result<()> {
323 self.router
324 .shutdown()
325 .await
326 .map_err(|error| SyncwebError::operation("failed to stop node router", error))?;
327 Ok(())
328 }
329
330 pub async fn shutdown(self) -> Result<()> {
334 self.stop().await
335 }
336}
337
338impl Drop for IrohNode {
339 fn drop(&mut self) {
340 if !self.router.is_shutdown() {
341 tracing::warn!("IrohNode dropped without calling stop() — router shutdown may be incomplete");
342 }
343 }
344}