From 7944f58c2a0ff6e858ffbf255867606a5aae0fd3 Mon Sep 17 00:00:00 2001 From: Le-Maz Date: Wed, 23 Jul 2025 14:26:21 +0200 Subject: [PATCH 1/6] feat(iroh): add passive mode for mdns discovery (#3401) Adds an `advertise` option to control whether a node broadcasts its presence via mDNS. When false, the node will only listen for other peers without advertising itself. This is useful for ephemeral nodes that need to discover services without spamming the local network with their own short-lived IDs. --- iroh/src/discovery/mdns.rs | 95 +++++++++++++++++++++++++++++++------- 1 file changed, 78 insertions(+), 17 deletions(-) diff --git a/iroh/src/discovery/mdns.rs b/iroh/src/discovery/mdns.rs index 96c7607856..f3e03a772d 100644 --- a/iroh/src/discovery/mdns.rs +++ b/iroh/src/discovery/mdns.rs @@ -67,12 +67,16 @@ const USER_DATA_ATTRIBUTE: &str = "user-data"; /// How long we will wait before we stop sending discovery items const DISCOVERY_DURATION: Duration = Duration::from_secs(10); +/// Whether this node should advertise itself +const MDNS_ADVERTISE: bool = true; + /// Discovery using `swarm-discovery`, a variation on mdns #[derive(Debug)] pub struct MdnsDiscovery { #[allow(dead_code)] handle: AbortOnDropHandle<()>, sender: mpsc::Sender, + advertise: bool, /// When `local_addrs` changes, we re-publish our info. local_addrs: Watchable>, } @@ -127,39 +131,67 @@ impl Subscribers { /// Builder for [`MdnsDiscovery`]. #[derive(Debug)] -pub struct MdnsDiscoveryBuilder; +pub struct MdnsDiscoveryBuilder { + advertise: bool, +} + +impl MdnsDiscoveryBuilder { + /// See [`MdnsDiscovery::builder`]. + pub fn new() -> Self { + Self { advertise: true } + } + + /// Sets whether this node should advertise its presence. + /// + /// Default is [`DEFAULT_ADVERTISE`]. + pub fn advertise(mut self, advertise: bool) -> Self { + self.advertise = advertise; + self + } + + /// See [`MdnsDiscovery::new`]. + pub fn build(self, node_id: NodeId) -> Result { + MdnsDiscovery::new(node_id, self.advertise) + } +} impl IntoDiscovery for MdnsDiscoveryBuilder { fn into_discovery( self, context: &DiscoveryContext, ) -> Result { - MdnsDiscovery::new(context.node_id()) + self.build(context.node_id()) } } impl MdnsDiscovery { /// Returns a [`MdnsDiscoveryBuilder`] that implements [`IntoDiscovery`]. pub fn builder() -> MdnsDiscoveryBuilder { - MdnsDiscoveryBuilder + MdnsDiscoveryBuilder::new() } /// Create a new [`MdnsDiscovery`] Service. /// - /// This starts a [`Discoverer`] that broadcasts your addresses and receives addresses from other nodes in your local network. + /// This starts a [`Discoverer`] that broadcasts your addresses (if advertise is set to true) + /// and receives addresses from other nodes in your local network. /// /// # Errors /// Returns an error if the network does not allow ipv4 OR ipv6. /// /// # Panics /// This relies on [`tokio::runtime::Handle::current`] and will panic if called outside of the context of a tokio runtime. - pub fn new(node_id: NodeId) -> Result { + pub fn new(node_id: NodeId, advertise: bool) -> Result { debug!("Creating new MdnsDiscovery service"); let (send, mut recv) = mpsc::channel(64); let task_sender = send.clone(); let rt = tokio::runtime::Handle::current(); - let discovery = - MdnsDiscovery::spawn_discoverer(node_id, task_sender.clone(), BTreeSet::new(), &rt)?; + let discovery = MdnsDiscovery::spawn_discoverer( + node_id, + advertise, + task_sender.clone(), + BTreeSet::new(), + &rt, + )?; let local_addrs: Watchable> = Watchable::default(); let mut addrs_change = local_addrs.watch(); @@ -311,6 +343,7 @@ impl MdnsDiscovery { let handle = task::spawn(discovery_fut.instrument(info_span!("swarm-discovery.actor"))); Ok(Self { handle: AbortOnDropHandle::new(handle), + advertise: advertise, sender: send, local_addrs, }) @@ -318,6 +351,7 @@ impl MdnsDiscovery { fn spawn_discoverer( node_id: PublicKey, + advertise: bool, sender: mpsc::Sender, socketaddrs: BTreeSet, rt: &tokio::runtime::Handle, @@ -337,15 +371,17 @@ impl MdnsDiscovery { sender.send(Message::Discovery(node_id, peer)).await.ok(); }); }; - let addrs = MdnsDiscovery::socketaddrs_to_addrs(&socketaddrs); let node_id_str = data_encoding::BASE32_NOPAD .encode(node_id.as_bytes()) .to_ascii_lowercase(); let mut discoverer = Discoverer::new_interactive(N0_LOCAL_SWARM.to_string(), node_id_str) .with_callback(callback) .with_ip_class(IpClass::Auto); - for addr in addrs { - discoverer = discoverer.with_addrs(addr.0, addr.1); + if advertise { + let addrs = MdnsDiscovery::socketaddrs_to_addrs(&socketaddrs); + for addr in addrs { + discoverer = discoverer.with_addrs(addr.0, addr.1); + } } discoverer .spawn(rt) @@ -406,7 +442,9 @@ impl Discovery for MdnsDiscovery { } fn publish(&self, data: &NodeData) { - self.local_addrs.set(Some(data.clone())).ok(); + if self.advertise { + self.local_addrs.set(Some(data.clone())).ok(); + } } fn subscribe(&self) -> Option> { @@ -440,8 +478,10 @@ mod tests { #[tokio::test] #[traced_test] async fn mdns_publish_resolve() -> Result { - let (_, discovery_a) = make_discoverer()?; - let (node_id_b, discovery_b) = make_discoverer()?; + // Create discoverer A with advertise=false (only listens) + let (_, discovery_a) = make_discoverer(false)?; + // Create discoverer B with advertise=true (will broadcast) + let (node_id_b, discovery_b) = make_discoverer(true)?; // make addr info for discoverer b let user_data: UserData = "foobar".parse()?; @@ -477,11 +517,11 @@ mod tests { let mut node_ids = BTreeSet::new(); let mut discoverers = vec![]; - let (_, discovery) = make_discoverer()?; + let (_, discovery) = make_discoverer(false)?; let node_data = NodeData::new(None, BTreeSet::from(["0.0.0.0:11111".parse().unwrap()])); for i in 0..num_nodes { - let (node_id, discovery) = make_discoverer()?; + let (node_id, discovery) = make_discoverer(true)?; let user_data: UserData = format!("node{i}").parse()?; let node_data = node_data.clone().with_user_data(Some(user_data.clone())); node_ids.insert((node_id, Some(user_data))); @@ -513,9 +553,30 @@ mod tests { .context("timeout")? } - fn make_discoverer() -> Result<(PublicKey, MdnsDiscovery)> { + #[tokio::test] + #[traced_test] + async fn non_advertising_node_not_discovered() -> Result { + let (_, discovery_a) = make_discoverer(false)?; + let (node_id_b, discovery_b) = make_discoverer(false)?; + + let node_data = NodeData::new(None, BTreeSet::from(["0.0.0.0:11111".parse().unwrap()])); + + discovery_b.publish(&node_data); + + let mut stream = discovery_a.resolve(node_id_b).unwrap(); + + let result = tokio::time::timeout(Duration::from_secs(2), stream.next()).await; + assert!( + result.is_err(), + "Expected timeout since node isn't advertising" + ); + + Ok(()) + } + + fn make_discoverer(advertise: bool) -> Result<(PublicKey, MdnsDiscovery)> { let node_id = SecretKey::generate(rand::thread_rng()).public(); - Ok((node_id, MdnsDiscovery::new(node_id)?)) + Ok((node_id, MdnsDiscovery::new(node_id, advertise)?)) } } } From 87a71cfc68686468c8662fe0e1dbdd99c8d39aba Mon Sep 17 00:00:00 2001 From: Le-Maz Date: Wed, 23 Jul 2025 16:06:35 +0200 Subject: [PATCH 2/6] fix: fix references to MDNS_ADVERTISE constant --- iroh/src/discovery/mdns.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/iroh/src/discovery/mdns.rs b/iroh/src/discovery/mdns.rs index f3e03a772d..b2b05f2e01 100644 --- a/iroh/src/discovery/mdns.rs +++ b/iroh/src/discovery/mdns.rs @@ -138,12 +138,14 @@ pub struct MdnsDiscoveryBuilder { impl MdnsDiscoveryBuilder { /// See [`MdnsDiscovery::builder`]. pub fn new() -> Self { - Self { advertise: true } + Self { + advertise: MDNS_ADVERTISE, + } } /// Sets whether this node should advertise its presence. /// - /// Default is [`DEFAULT_ADVERTISE`]. + /// Default is [`MDNS_ADVERTISE`]. pub fn advertise(mut self, advertise: bool) -> Self { self.advertise = advertise; self From 0ac35db7379175675540691cc5d0f3fad98de4f2 Mon Sep 17 00:00:00 2001 From: Le-Maz Date: Wed, 23 Jul 2025 17:05:03 +0200 Subject: [PATCH 3/6] fix: fix clippy check errors --- iroh/src/discovery/mdns.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/iroh/src/discovery/mdns.rs b/iroh/src/discovery/mdns.rs index b2b05f2e01..d81717c647 100644 --- a/iroh/src/discovery/mdns.rs +++ b/iroh/src/discovery/mdns.rs @@ -157,6 +157,12 @@ impl MdnsDiscoveryBuilder { } } +impl Default for MdnsDiscoveryBuilder { + fn default() -> Self { + Self::new() + } +} + impl IntoDiscovery for MdnsDiscoveryBuilder { fn into_discovery( self, @@ -345,7 +351,7 @@ impl MdnsDiscovery { let handle = task::spawn(discovery_fut.instrument(info_span!("swarm-discovery.actor"))); Ok(Self { handle: AbortOnDropHandle::new(handle), - advertise: advertise, + advertise, sender: send, local_addrs, }) From 4195c8faa5b574a6c2bd90bf0d27b5e6c45f11b5 Mon Sep 17 00:00:00 2001 From: Le-Maz Date: Thu, 24 Jul 2025 22:09:03 +0200 Subject: [PATCH 4/6] remove MDNS_ADVERTISE constant because it was pointed out as unneccessary --- iroh/src/discovery/mdns.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/iroh/src/discovery/mdns.rs b/iroh/src/discovery/mdns.rs index d81717c647..71578f3458 100644 --- a/iroh/src/discovery/mdns.rs +++ b/iroh/src/discovery/mdns.rs @@ -67,9 +67,6 @@ const USER_DATA_ATTRIBUTE: &str = "user-data"; /// How long we will wait before we stop sending discovery items const DISCOVERY_DURATION: Duration = Duration::from_secs(10); -/// Whether this node should advertise itself -const MDNS_ADVERTISE: bool = true; - /// Discovery using `swarm-discovery`, a variation on mdns #[derive(Debug)] pub struct MdnsDiscovery { @@ -138,14 +135,12 @@ pub struct MdnsDiscoveryBuilder { impl MdnsDiscoveryBuilder { /// See [`MdnsDiscovery::builder`]. pub fn new() -> Self { - Self { - advertise: MDNS_ADVERTISE, - } + Self { advertise: true } } /// Sets whether this node should advertise its presence. /// - /// Default is [`MDNS_ADVERTISE`]. + /// Default is true. pub fn advertise(mut self, advertise: bool) -> Self { self.advertise = advertise; self From 56c0e0e26055953337aca077653ace1b5567e90f Mon Sep 17 00:00:00 2001 From: Le-Maz Date: Thu, 24 Jul 2025 22:15:46 +0200 Subject: [PATCH 5/6] improve comments in MdnsDiscoveryBuilder --- iroh/src/discovery/mdns.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/iroh/src/discovery/mdns.rs b/iroh/src/discovery/mdns.rs index 71578f3458..0fcae95bc2 100644 --- a/iroh/src/discovery/mdns.rs +++ b/iroh/src/discovery/mdns.rs @@ -133,7 +133,7 @@ pub struct MdnsDiscoveryBuilder { } impl MdnsDiscoveryBuilder { - /// See [`MdnsDiscovery::builder`]. + /// Creates a new [`MdnsDiscoveryBuilder`] with default settings. pub fn new() -> Self { Self { advertise: true } } @@ -146,7 +146,7 @@ impl MdnsDiscoveryBuilder { self } - /// See [`MdnsDiscovery::new`]. + /// Builds an [`MdnsDiscovery`] instance with the configured settings. pub fn build(self, node_id: NodeId) -> Result { MdnsDiscovery::new(node_id, self.advertise) } From 3398d4cfa33fae77e72c371bd53aacca9d614643 Mon Sep 17 00:00:00 2001 From: Le-Maz Date: Thu, 24 Jul 2025 22:18:08 +0200 Subject: [PATCH 6/6] add a third node to non advertising node test --- iroh/src/discovery/mdns.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/iroh/src/discovery/mdns.rs b/iroh/src/discovery/mdns.rs index 0fcae95bc2..25c892db0e 100644 --- a/iroh/src/discovery/mdns.rs +++ b/iroh/src/discovery/mdns.rs @@ -562,16 +562,24 @@ mod tests { let (_, discovery_a) = make_discoverer(false)?; let (node_id_b, discovery_b) = make_discoverer(false)?; - let node_data = NodeData::new(None, BTreeSet::from(["0.0.0.0:11111".parse().unwrap()])); + let (node_id_c, discovery_c) = make_discoverer(true)?; + let node_data_c = + NodeData::new(None, BTreeSet::from(["0.0.0.0:22222".parse().unwrap()])); + discovery_c.publish(&node_data_c); - discovery_b.publish(&node_data); + let node_data_b = + NodeData::new(None, BTreeSet::from(["0.0.0.0:11111".parse().unwrap()])); + discovery_b.publish(&node_data_b); - let mut stream = discovery_a.resolve(node_id_b).unwrap(); + let mut stream_c = discovery_a.resolve(node_id_c).unwrap(); + let result_c = tokio::time::timeout(Duration::from_secs(2), stream_c.next()).await; + assert!(result_c.is_ok(), "Advertising node should be discoverable"); - let result = tokio::time::timeout(Duration::from_secs(2), stream.next()).await; + let mut stream_b = discovery_a.resolve(node_id_b).unwrap(); + let result_b = tokio::time::timeout(Duration::from_secs(2), stream_b.next()).await; assert!( - result.is_err(), - "Expected timeout since node isn't advertising" + result_b.is_err(), + "Expected timeout since node b isn't advertising" ); Ok(())