From 45ce98694a305711976f44504aff8d2f82355257 Mon Sep 17 00:00:00 2001 From: Han Xu Date: Wed, 25 Mar 2026 21:20:23 -0700 Subject: [PATCH 1/2] fix: ScopedIp considered Eq when interface_ids change --- src/dns_parser.rs | 21 ++------------------- src/service_daemon.rs | 11 +++++++++-- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/src/dns_parser.rs b/src/dns_parser.rs index cf603f2..025a4c5 100644 --- a/src/dns_parser.rs +++ b/src/dns_parser.rs @@ -21,7 +21,7 @@ use std::{ collections::HashMap, convert::TryInto, fmt, - hash::{Hash, Hasher}, + hash::Hash, net::{IpAddr, Ipv4Addr, Ipv6Addr}, str, time::SystemTime, @@ -66,10 +66,7 @@ impl From<&Interface> for InterfaceId { } /// An IPv4 address with interface identifiers indicating which interfaces discovered it. -/// -/// Two `ScopedIpV4` values are considered equal if their addresses are equal, -/// regardless of which interfaces discovered them. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Eq, PartialEq, Hash)] #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] pub struct ScopedIpV4 { addr: Ipv4Addr, @@ -77,20 +74,6 @@ pub struct ScopedIpV4 { interface_ids: Vec, } -impl PartialEq for ScopedIpV4 { - fn eq(&self, other: &Self) -> bool { - self.addr == other.addr - } -} - -impl Eq for ScopedIpV4 {} - -impl Hash for ScopedIpV4 { - fn hash(&self, state: &mut H) { - self.addr.hash(state); - } -} - impl ScopedIpV4 { /// Creates a new `ScopedIpV4` with a single interface identifier. pub fn new(addr: Ipv4Addr, interface_id: InterfaceId) -> Self { diff --git a/src/service_daemon.rs b/src/service_daemon.rs index 872b36e..b4e02fa 100644 --- a/src/service_daemon.rs +++ b/src/service_daemon.rs @@ -2561,8 +2561,15 @@ impl Zeroconf { } else { let scoped = dns_a.address(); if let ScopedIp::V4(v4) = &scoped { - // Merge interface_ids if this V4 addr already exists - if let Some(mut existing) = resolved_service.addresses.take(&scoped) { + // Merge interface_ids if this V4 addr already exists. + // Linear scan by IP since Eq/Hash include interface_ids. + let existing = resolved_service + .addresses + .iter() + .find(|a| a.to_ip_addr() == IpAddr::V4(*v4.addr())) + .cloned(); + if let Some(mut existing) = existing { + resolved_service.addresses.remove(&existing); if let ScopedIp::V4(existing_v4) = &mut existing { for id in v4.interface_ids() { existing_v4.add_interface_id(id.clone()); From c4f2c98b1f9cf8f13c63f0f89b7c44631d003838 Mon Sep 17 00:00:00 2001 From: Han Xu Date: Wed, 25 Mar 2026 21:32:10 -0700 Subject: [PATCH 2/2] add a test --- src/service_info.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/service_info.rs b/src/service_info.rs index c2ec995..dfb6379 100644 --- a/src/service_info.rs +++ b/src/service_info.rs @@ -1706,6 +1706,33 @@ mod tests { assert!(service_info.is_address_supported(&intf_loopback_v6)); } + #[test] + fn test_scoped_ip_set_detects_interface_id_change() { + use crate::{InterfaceId, ScopedIp, ScopedIpV4}; + use std::collections::HashSet; + + let intf1 = InterfaceId { + name: "en0".to_string(), + index: 1, + }; + let intf2 = InterfaceId { + name: "en1".to_string(), + index: 2, + }; + let addr = Ipv4Addr::new(192, 168, 1, 100); + + let scoped_v4_one_intf = ScopedIpV4::new(addr, intf1); + let mut scoped_v4_two_intfs = scoped_v4_one_intf.clone(); + scoped_v4_two_intfs.add_interface_id(intf2); + + assert_ne!(scoped_v4_one_intf, scoped_v4_two_intfs); + + let set_old: HashSet = HashSet::from([ScopedIp::V4(scoped_v4_one_intf)]); + let set_new: HashSet = HashSet::from([ScopedIp::V4(scoped_v4_two_intfs)]); + + assert_ne!(set_old, set_new); + } + #[cfg(test)] #[cfg(feature = "serde")] mod serde {