From bd9b5047f3ad14a419dabaa97b2b6ef62c508820 Mon Sep 17 00:00:00 2001 From: Han Xu Date: Wed, 18 Feb 2026 21:48:51 -0800 Subject: [PATCH 01/10] fix a test --- tests/mdns_test.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/mdns_test.rs b/tests/mdns_test.rs index d618f5f..3847ce7 100644 --- a/tests/mdns_test.rs +++ b/tests/mdns_test.rs @@ -755,11 +755,11 @@ fn test_disable_interface_cache() { .duration_since(SystemTime::UNIX_EPOCH) .unwrap(); let instance_name = now.as_micros().to_string(); - let service_ip_addr = my_ip_interfaces() + let service_ip_addr: Vec<_> = my_ip_interfaces() .iter() - .find(|iface| iface.ip().is_ipv4()) .map(|iface| iface.ip()) - .unwrap(); + .filter(|ip| ip.is_ipv4() && !ip.is_loopback()) + .collect(); let host_name = "disabled_intf_host.local."; let port = 5201; @@ -767,7 +767,7 @@ fn test_disable_interface_cache() { ty_domain, &instance_name, host_name, - service_ip_addr, + &service_ip_addr[..], port, None, ) @@ -783,7 +783,7 @@ fn test_disable_interface_cache() { sleep(Duration::from_secs(1)); // Disable the interface for the client. - println!("Disabling interface with IP: {service_ip_addr}"); + println!("Disabling interface with IP: {:?}", service_ip_addr); client.disable_interface(service_ip_addr).unwrap(); // Browse for the service. From 1a083d8003f9da488aea8830c15119d48b4daef4 Mon Sep 17 00:00:00 2001 From: Han Xu Date: Wed, 18 Feb 2026 21:51:13 -0800 Subject: [PATCH 02/10] rename only --- tests/mdns_test.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/mdns_test.rs b/tests/mdns_test.rs index 3847ce7..5440f8c 100644 --- a/tests/mdns_test.rs +++ b/tests/mdns_test.rs @@ -755,7 +755,7 @@ fn test_disable_interface_cache() { .duration_since(SystemTime::UNIX_EPOCH) .unwrap(); let instance_name = now.as_micros().to_string(); - let service_ip_addr: Vec<_> = my_ip_interfaces() + let ipv4_list: Vec<_> = my_ip_interfaces() .iter() .map(|iface| iface.ip()) .filter(|ip| ip.is_ipv4() && !ip.is_loopback()) @@ -767,7 +767,7 @@ fn test_disable_interface_cache() { ty_domain, &instance_name, host_name, - &service_ip_addr[..], + &ipv4_list[..], port, None, ) @@ -783,8 +783,8 @@ fn test_disable_interface_cache() { sleep(Duration::from_secs(1)); // Disable the interface for the client. - println!("Disabling interface with IP: {:?}", service_ip_addr); - client.disable_interface(service_ip_addr).unwrap(); + println!("Disabling interface with IP: {:?}", ipv4_list); + client.disable_interface(ipv4_list).unwrap(); // Browse for the service. let handle = client.browse(ty_domain).unwrap(); From c5de63fc5128e6f6a11a51fa672c81379cc8145b Mon Sep 17 00:00:00 2001 From: Han Xu Date: Sat, 21 Feb 2026 11:09:54 -0800 Subject: [PATCH 03/10] change a log --- src/service_daemon.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/service_daemon.rs b/src/service_daemon.rs index 4ab6992..42d6159 100644 --- a/src/service_daemon.rs +++ b/src/service_daemon.rs @@ -1704,7 +1704,7 @@ impl Zeroconf { /// If no more addresses on the interface, remove the interface as well. fn del_interface_addr(&mut self, intf: &Interface) { let if_index = intf.index.unwrap_or(0); - trace!( + debug!( "del_interface_addr: {} ({if_index}) addr {}", intf.name, intf.ip() From dfb4ab806ffb91525c88439f559ad4e12a3bb737 Mon Sep 17 00:00:00 2001 From: Han Xu Date: Sat, 21 Feb 2026 13:39:09 -0800 Subject: [PATCH 04/10] use IP address to look up interfaces --- src/service_daemon.rs | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/service_daemon.rs b/src/service_daemon.rs index 42d6159..cd8f566 100644 --- a/src/service_daemon.rs +++ b/src/service_daemon.rs @@ -793,6 +793,8 @@ pub enum IfKind { Name(String), /// By an IPv4 or IPv6 address. + /// This is used to look up the interface. The intent still refers to the interface, + /// not a specific address on the interface. Addr(IpAddr), /// 127.0.0.1 (or anything in 127.0.0.0/8), enabled by default. @@ -1431,26 +1433,49 @@ impl Zeroconf { fn enable_interface(&mut self, kinds: Vec) { debug!("enable_interface: {:?}", kinds); + let interfaces = my_ip_interfaces_inner(true, self.include_apple_p2p); + for if_kind in kinds { + let if_kind = match &if_kind { + IfKind::Addr(addr) => match interfaces.iter().find(|intf| &intf.ip() == addr) { + Some(intf) => IfKind::Name(intf.name.clone()), + None => if_kind, + }, + _ => if_kind, + }; + self.if_selections.push(IfSelection { if_kind, selected: true, }); } - self.apply_intf_selections(my_ip_interfaces_inner(true, self.include_apple_p2p)); + self.apply_intf_selections(interfaces); } fn disable_interface(&mut self, kinds: Vec) { debug!("disable_interface: {:?}", kinds); + let interfaces = my_ip_interfaces_inner(true, self.include_apple_p2p); + for if_kind in kinds { + // IfKind::Addr(ip) uses the IP to identify an interface (consistent + // with join_multicast_v4 semantics). Resolve it to the interface + // name so the entire interface is disabled, not just one address. + let if_kind = match &if_kind { + IfKind::Addr(addr) => match interfaces.iter().find(|intf| &intf.ip() == addr) { + Some(intf) => IfKind::Name(intf.name.clone()), + None => if_kind, + }, + _ => if_kind, + }; + self.if_selections.push(IfSelection { if_kind, selected: false, }); } - self.apply_intf_selections(my_ip_interfaces_inner(true, self.include_apple_p2p)); + self.apply_intf_selections(interfaces); } fn set_multicast_loop_v4(&mut self, on: bool) { From 0c2756b2392bd202d3bf5035bed0975502a9360f Mon Sep 17 00:00:00 2001 From: Han Xu Date: Sat, 21 Feb 2026 17:05:42 -0800 Subject: [PATCH 05/10] limit the delete_interface to IPv4 or IPv6 --- src/service_daemon.rs | 46 +++++++++++++++++++++++-------------------- src/service_info.rs | 2 ++ 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/service_daemon.rs b/src/service_daemon.rs index cd8f566..f63e14f 100644 --- a/src/service_daemon.rs +++ b/src/service_daemon.rs @@ -804,6 +804,12 @@ pub enum IfKind { /// ::1/128, enabled by default. LoopbackV6, + + /// By interface index, IPv4 only. + IndexV4(u32), + + /// By interface index, IPv6 only. + IndexV6(u32), } impl IfKind { @@ -817,6 +823,8 @@ impl IfKind { Self::Addr(addr) => addr == &intf.ip(), Self::LoopbackV4 => intf.is_loopback() && intf.ip().is_ipv4(), Self::LoopbackV6 => intf.is_loopback() && intf.ip().is_ipv6(), + Self::IndexV4(idx) => intf.index == Some(*idx) && intf.ip().is_ipv4(), + Self::IndexV6(idx) => intf.index == Some(*idx) && intf.ip().is_ipv6(), } } } @@ -1436,16 +1444,8 @@ impl Zeroconf { let interfaces = my_ip_interfaces_inner(true, self.include_apple_p2p); for if_kind in kinds { - let if_kind = match &if_kind { - IfKind::Addr(addr) => match interfaces.iter().find(|intf| &intf.ip() == addr) { - Some(intf) => IfKind::Name(intf.name.clone()), - None => if_kind, - }, - _ => if_kind, - }; - self.if_selections.push(IfSelection { - if_kind, + if_kind: Self::resolve_addr_to_index(if_kind, &interfaces), selected: true, }); } @@ -1458,19 +1458,8 @@ impl Zeroconf { let interfaces = my_ip_interfaces_inner(true, self.include_apple_p2p); for if_kind in kinds { - // IfKind::Addr(ip) uses the IP to identify an interface (consistent - // with join_multicast_v4 semantics). Resolve it to the interface - // name so the entire interface is disabled, not just one address. - let if_kind = match &if_kind { - IfKind::Addr(addr) => match interfaces.iter().find(|intf| &intf.ip() == addr) { - Some(intf) => IfKind::Name(intf.name.clone()), - None => if_kind, - }, - _ => if_kind, - }; - self.if_selections.push(IfSelection { - if_kind, + if_kind: Self::resolve_addr_to_index(if_kind, &interfaces), selected: false, }); } @@ -1478,6 +1467,21 @@ impl Zeroconf { self.apply_intf_selections(interfaces); } + /// Resolves `IfKind::Addr(ip)` to `IndexV4(if_index)` or `IndexV6(if_index)`. + fn resolve_addr_to_index(if_kind: IfKind, interfaces: &[Interface]) -> IfKind { + if let IfKind::Addr(addr) = &if_kind { + if let Some(intf) = interfaces.iter().find(|intf| &intf.ip() == addr) { + let if_index = intf.index.unwrap_or(0); + return if addr.is_ipv4() { + IfKind::IndexV4(if_index) + } else { + IfKind::IndexV6(if_index) + }; + } + } + if_kind + } + fn set_multicast_loop_v4(&mut self, on: bool) { let Some(sock) = self.ipv4_sock.as_mut() else { return; diff --git a/src/service_info.rs b/src/service_info.rs index 22261b0..72137fe 100644 --- a/src/service_info.rs +++ b/src/service_info.rs @@ -497,6 +497,8 @@ impl ServiceInfo { IfKind::Addr(a) => *a == addr, IfKind::LoopbackV4 => matches!(addr, IpAddr::V4(ipv4) if ipv4.is_loopback()), IfKind::LoopbackV6 => matches!(addr, IpAddr::V6(ipv6) if ipv6.is_loopback()), + IfKind::IndexV4(idx) => intf.index == Some(*idx) && addr.is_ipv4(), + IfKind::IndexV6(idx) => intf.index == Some(*idx) && addr.is_ipv6(), IfKind::All => true, }); From 07879df292bcf9a5e363eff1b0f6d486af394402 Mon Sep 17 00:00:00 2001 From: Han Xu Date: Sat, 21 Feb 2026 18:34:15 -0800 Subject: [PATCH 06/10] remove cached records of disabled interface for IPv4 or IPv6 --- src/dns_cache.rs | 51 +++++++++++++++++++++++++++++++++++-------- src/service_daemon.rs | 19 ++++++++++++++-- 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/src/dns_cache.rs b/src/dns_cache.rs index c06ac34..921df96 100644 --- a/src/dns_cache.rs +++ b/src/dns_cache.rs @@ -11,9 +11,31 @@ use crate::{ }; use std::{ collections::{HashMap, HashSet}, + ops::BitOr, time::SystemTime, }; +/// Bitflags-style type for filtering by IP version. +#[derive(Clone, Copy)] +pub(crate) struct IpType(u8); + +impl IpType { + pub const V4: Self = Self(0b01); + pub const V6: Self = Self(0b10); + pub const BOTH: Self = Self(0b11); + + fn contains(self, other: Self) -> bool { + self.0 & other.0 == other.0 + } +} + +impl BitOr for IpType { + type Output = Self; + fn bitor(self, rhs: Self) -> Self { + Self(self.0 | rhs.0) + } +} + /// Associate a DnsRecord with the interface it was received on. pub(crate) struct DnsRecordIntf { pub(crate) record: DnsRecordBox, @@ -673,23 +695,34 @@ impl DnsCache { .collect() } - pub(crate) fn remove_addrs_on_disabled_intf(&mut self, disabled_if_index: u32) { + /// Removes cached address records on a disabled interface, filtered by IP version. + /// Use `IpType::V4` for A records only, `IpType::V6` for AAAA only, + /// or `IpType::V4 | IpType::V6` for both. + pub(crate) fn remove_addrs_on_disabled_intf( + &mut self, + disabled_if_index: u32, + ip_type: IpType, + ) { for (host, records) in self.addr.iter_mut() { records.retain(|record| { let Some(dns_addr) = record.record.any().downcast_ref::() else { return false; // invalid address record. }; - // Remove the record if it is on this interface. + // Remove the record if it is on this interface and matches the IP version filter. if dns_addr.interface_id.index == disabled_if_index { - debug!( - "removing ADDR on disabled intf: {:?} host {host}", - dns_addr.interface_id.name - ); - false - } else { - true + let rr_type = dns_addr.record.entry.ty; + let version_matches = (rr_type == RRType::A && ip_type.contains(IpType::V4)) + || (rr_type == RRType::AAAA && ip_type.contains(IpType::V6)); + if version_matches { + debug!( + "removing ADDR on disabled intf: {:?} host {host}", + dns_addr.interface_id.name + ); + return false; + } } + true }); } } diff --git a/src/service_daemon.rs b/src/service_daemon.rs index f63e14f..879a662 100644 --- a/src/service_daemon.rs +++ b/src/service_daemon.rs @@ -31,7 +31,7 @@ #[cfg(feature = "logging")] use crate::log::{debug, error, trace}; use crate::{ - dns_cache::{current_time_millis, DnsCache}, + dns_cache::{current_time_millis, DnsCache, IpType}, dns_parser::{ ip_address_rr_type, DnsAddress, DnsEntryExt, DnsIncoming, DnsOutgoing, DnsPointer, DnsRecordBox, DnsRecordExt, DnsSrv, DnsTxt, InterfaceId, RRType, ScopedIp, @@ -1778,7 +1778,22 @@ impl Zeroconf { debug!("del_interface_addr: removing interface {}", intf.name); self.my_intfs.remove(&if_index); self.dns_registry_map.remove(&if_index); - self.cache.remove_addrs_on_disabled_intf(if_index); + self.cache + .remove_addrs_on_disabled_intf(if_index, IpType::BOTH); + } else { + // Interface still has addresses of the other IP version. + // Remove cached address records for the disabled IP version + // only if no more addresses of that version remain. + let is_v4 = intf.addr.ip().is_ipv4(); + let version_gone = if is_v4 { + my_intf.next_ifaddr_v4().is_none() + } else { + my_intf.next_ifaddr_v6().is_none() + }; + if version_gone { + let ip_type = if is_v4 { IpType::V4 } else { IpType::V6 }; + self.cache.remove_addrs_on_disabled_intf(if_index, ip_type); + } } } From 70a8e946b7399af9cacf4ce0602fce8cb48c4a19 Mon Sep 17 00:00:00 2001 From: Han Xu Date: Sat, 21 Feb 2026 19:57:12 -0800 Subject: [PATCH 07/10] log leave multicast v4 --- src/service_daemon.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/service_daemon.rs b/src/service_daemon.rs index 879a662..d9c7d59 100644 --- a/src/service_daemon.rs +++ b/src/service_daemon.rs @@ -1755,6 +1755,8 @@ impl Zeroconf { if let Some(sock) = self.ipv4_sock.as_mut() { if let Err(e) = sock.pktinfo.leave_multicast_v4(&GROUP_ADDR_V4, &ipv4) { debug!("leave multicast group for addr {ipv4}: {e}"); + } else { + debug!("leave multicast for {ipv4}"); } } } From 6ad728112f785f39f0837d283d5f71fe2ebb8c75 Mon Sep 17 00:00:00 2001 From: Han Xu Date: Sat, 21 Feb 2026 20:26:37 -0800 Subject: [PATCH 08/10] drop IPv4 packets if IPv4 is disabled on the interface --- src/service_daemon.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/service_daemon.rs b/src/service_daemon.rs index d9c7d59..88d989e 100644 --- a/src/service_daemon.rs +++ b/src/service_daemon.rs @@ -2368,6 +2368,22 @@ impl Zeroconf { return true; // We still return true to indicate that we read something. }; + // Drop packets for an IP version that has been disabled on this interface. + // This is needed because some times the socket layer may still receive packets + // for an IP version even after we left the multicast group for that IP version. + // We want to drop such packets to avoid unnecessary processing. + let is_ipv4 = event_key == IPV4_SOCK_EVENT_KEY; + if (is_ipv4 && my_intf.next_ifaddr_v4().is_none()) + || (!is_ipv4 && my_intf.next_ifaddr_v6().is_none()) + { + debug!( + "handle_read: dropping {} packet on intf {} (disabled)", + if is_ipv4 { "IPv4" } else { "IPv6" }, + my_intf.name + ); + return true; + } + buf.truncate(sz); // reduce potential processing errors match DnsIncoming::new(buf, my_intf.into()) { From 13aaa8e3c60c1a80e2639ba9e3858a88deae6d9c Mon Sep 17 00:00:00 2001 From: Han Xu Date: Sat, 21 Feb 2026 20:43:57 -0800 Subject: [PATCH 09/10] refactoring only --- src/service_daemon.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/service_daemon.rs b/src/service_daemon.rs index 88d989e..ef252d0 100644 --- a/src/service_daemon.rs +++ b/src/service_daemon.rs @@ -1445,7 +1445,7 @@ impl Zeroconf { for if_kind in kinds { self.if_selections.push(IfSelection { - if_kind: Self::resolve_addr_to_index(if_kind, &interfaces), + if_kind: resolve_addr_to_index(if_kind, &interfaces), selected: true, }); } @@ -1459,7 +1459,7 @@ impl Zeroconf { for if_kind in kinds { self.if_selections.push(IfSelection { - if_kind: Self::resolve_addr_to_index(if_kind, &interfaces), + if_kind: resolve_addr_to_index(if_kind, &interfaces), selected: false, }); } @@ -1467,21 +1467,6 @@ impl Zeroconf { self.apply_intf_selections(interfaces); } - /// Resolves `IfKind::Addr(ip)` to `IndexV4(if_index)` or `IndexV6(if_index)`. - fn resolve_addr_to_index(if_kind: IfKind, interfaces: &[Interface]) -> IfKind { - if let IfKind::Addr(addr) = &if_kind { - if let Some(intf) = interfaces.iter().find(|intf| &intf.ip() == addr) { - let if_index = intf.index.unwrap_or(0); - return if addr.is_ipv4() { - IfKind::IndexV4(if_index) - } else { - IfKind::IndexV6(if_index) - }; - } - } - if_kind - } - fn set_multicast_loop_v4(&mut self, on: bool) { let Some(sock) = self.ipv4_sock.as_mut() else { return; @@ -4726,6 +4711,21 @@ fn handle_expired_probes( waiting_services } +/// Resolves `IfKind::Addr(ip)` to `IndexV4(if_index)` or `IndexV6(if_index)`. +fn resolve_addr_to_index(if_kind: IfKind, interfaces: &[Interface]) -> IfKind { + if let IfKind::Addr(addr) = &if_kind { + if let Some(intf) = interfaces.iter().find(|intf| &intf.ip() == addr) { + let if_index = intf.index.unwrap_or(0); + return if addr.is_ipv4() { + IfKind::IndexV4(if_index) + } else { + IfKind::IndexV6(if_index) + }; + } + } + if_kind +} + #[cfg(test)] mod tests { use super::{ From b462317022f8244d5b24d6e948ce2c121edde7b1 Mon Sep 17 00:00:00 2001 From: Han Xu Date: Sun, 22 Feb 2026 15:50:26 -0800 Subject: [PATCH 10/10] update comment --- src/service_daemon.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/service_daemon.rs b/src/service_daemon.rs index ef252d0..39e1927 100644 --- a/src/service_daemon.rs +++ b/src/service_daemon.rs @@ -793,8 +793,8 @@ pub enum IfKind { Name(String), /// By an IPv4 or IPv6 address. - /// This is used to look up the interface. The intent still refers to the interface, - /// not a specific address on the interface. + /// This is used to look up the interface. The semantics is to identify an interface of + /// IPv4 or IPv6, not a specific address on the interface. Addr(IpAddr), /// 127.0.0.1 (or anything in 127.0.0.0/8), enabled by default.