From 90ea2354e059f4a23e92c50663d40f9d798e3114 Mon Sep 17 00:00:00 2001 From: Han Xu Date: Fri, 6 Dec 2024 21:18:35 -0800 Subject: [PATCH 1/5] new API to set multicast loop v4 for ServiceDaemon --- src/service_daemon.rs | 36 +++++++++++++++++--- tests/mdns_test.rs | 77 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 5 deletions(-) diff --git a/src/service_daemon.rs b/src/service_daemon.rs index 53462bc..b07f26f 100644 --- a/src/service_daemon.rs +++ b/src/service_daemon.rs @@ -401,6 +401,13 @@ impl ServiceDaemon { ))) } + /// Enable or disable the loopback for locally sent multicast packets. + /// + /// When disabled, a local querier will not receive announcements from a local responder. + pub fn set_multicast_loop_v4(&self, on: bool) -> Result<()> { + self.send_cmd(Command::SetOption(DaemonOption::MulticastLoopV4(on))) + } + /// Proactively confirms whether a service instance still valid. /// /// This call will issue queries for a service instance's SRV record and Address records. @@ -637,7 +644,7 @@ impl ServiceDaemon { } /// Creates a new UDP socket that uses `intf` to send and recv multicast. -fn new_socket_bind(intf: &Interface) -> Result { +fn new_socket_bind(intf: &Interface, should_loop: bool) -> Result { // Use the same socket for receiving and sending multicast packets. // Such socket has to bind to INADDR_ANY or IN6ADDR_ANY. let intf_ip = &intf.ip(); @@ -654,6 +661,11 @@ fn new_socket_bind(intf: &Interface) -> Result { sock.set_multicast_if_v4(ip) .map_err(|e| e_fmt!("set multicast_if on addr {}: {}", ip, e))?; + if !should_loop { + sock.set_multicast_loop_v4(false) + .map_err(|e| e_fmt!("failed to set multicast loop v4 for {ip}: {e}"))?; + } + // Test if we can send packets successfully. let multicast_addr = SocketAddrV4::new(GROUP_ADDR_V4, MDNS_PORT).into(); let test_packets = DnsOutgoing::new(0).to_data_on_wire(); @@ -904,6 +916,8 @@ struct Zeroconf { /// Service instances that are already resolved. resolved: HashSet, + + multicast_loop_v4: bool, } impl Zeroconf { @@ -918,7 +932,7 @@ impl Zeroconf { let mut dns_registry_map = HashMap::new(); for intf in my_ifaddrs { - let sock = match new_socket_bind(&intf) { + let sock = match new_socket_bind(&intf, true) { Ok(s) => s, Err(e) => { trace!("bind a socket to {}: {}. Skipped.", &intf.ip(), e); @@ -959,6 +973,7 @@ impl Zeroconf { status, pending_resolves: HashSet::new(), resolved: HashSet::new(), + multicast_loop_v4: true, } } @@ -967,6 +982,7 @@ impl Zeroconf { DaemonOption::ServiceNameLenMax(length) => self.service_name_len_max = length, DaemonOption::EnableInterface(if_kind) => self.enable_interface(if_kind), DaemonOption::DisableInterface(if_kind) => self.disable_interface(if_kind), + DaemonOption::MulticastLoopV4(on) => self.set_multicast_loop_v4(on), } } @@ -992,6 +1008,14 @@ impl Zeroconf { self.apply_intf_selections(my_ip_interfaces()); } + fn set_multicast_loop_v4(&mut self, on: bool) { + for (_, sock) in self.intf_socks.iter_mut() { + if let Err(e) = sock.set_multicast_loop_v4(on) { + debug!("failed to set multicast loop v4: {e}"); + } + } + } + fn notify_monitors(&mut self, event: DaemonEvent) { // Only retain the monitors that are still connected. self.monitors.retain(|sender| { @@ -1150,7 +1174,7 @@ impl Zeroconf { fn add_new_interface(&mut self, intf: Interface) { // Bind the new interface. let new_ip = intf.ip(); - let mut sock = match new_socket_bind(&intf) { + let mut sock = match new_socket_bind(&intf, self.multicast_loop_v4) { Ok(s) => s, Err(e) => { debug!("bind a socket to {}: {}. Skipped.", &intf.ip(), e); @@ -1599,13 +1623,14 @@ impl Zeroconf { } // Replace the closed socket with a new one. - match new_socket_bind(intf) { + match new_socket_bind(intf, self.multicast_loop_v4) { Ok(new_sock) => { trace!("reset socket for IP {}", intf.ip()); self.intf_socks.insert(intf.clone(), new_sock); } Err(e) => debug!("re-bind a socket to {:?}: {}", intf, e), } + return false; } @@ -2915,6 +2940,7 @@ enum DaemonOption { ServiceNameLenMax(u8), EnableInterface(Vec), DisableInterface(Vec), + MulticastLoopV4(bool), } /// The length of Service Domain name supported in this lib. @@ -3470,7 +3496,7 @@ mod tests { packet_buffer.add_additional_answer(invalidate_ptr_packet); for intf in intfs { - let sock = new_socket_bind(&intf).unwrap(); + let sock = new_socket_bind(&intf, true).unwrap(); send_dns_outgoing(&packet_buffer, &intf, &sock); } diff --git a/tests/mdns_test.rs b/tests/mdns_test.rs index 204c082..34da550 100644 --- a/tests/mdns_test.rs +++ b/tests/mdns_test.rs @@ -1854,6 +1854,83 @@ fn test_verify_srv() { assert!(service_removal); } +#[test] +fn test_multicast_loop_v4() { + let ty_domain = "_loop_v4._udp.local."; + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let instance_name = now.as_micros().to_string(); // Create a unique name. + let host_name = "loop_v4_host.local."; + let port = 5200; + + // Register the first service. + let server = ServiceDaemon::new().expect("failed to start server"); + server.set_multicast_loop_v4(false).unwrap(); + + // Get a single IPv4 address + let ip_addr1 = my_ip_interfaces() + .iter() + .find(|iface| iface.ip().is_ipv4()) + .map(|iface| iface.ip()) + .unwrap(); + + // Publish the service on server + let service1 = ServiceInfo::new(ty_domain, &instance_name, host_name, &ip_addr1, port, None) + .expect("valid service info"); + server + .register(service1) + .expect("Failed to register service1"); + + // wait for the service announced. + sleep(Duration::from_secs(1)); + + // start a client i.e. querier. + let mut resolved = false; + let client = ServiceDaemon::new().expect("failed to create mdns client"); + let receiver = client.browse(ty_domain).unwrap(); + + let timeout = Duration::from_secs(2); + while let Ok(event) = receiver.recv_timeout(timeout) { + match event { + ServiceEvent::ServiceResolved(info) => { + println!( + "Resolved a service: {} host {} IP {:?}", + info.get_fullname(), + info.get_hostname(), + info.get_addresses_v4() + ); + resolved = true; + break; + } + _ => {} + } + } + + assert_eq!(resolved, false); + + // enable loopback and try again. + server.set_multicast_loop_v4(true).unwrap(); + + while let Ok(event) = receiver.recv_timeout(timeout) { + match event { + ServiceEvent::ServiceResolved(info) => { + println!( + "Resolved a service: {} host {} IP {:?}", + info.get_fullname(), + info.get_hostname(), + info.get_addresses_v4() + ); + resolved = true; + break; + } + _ => {} + } + } + + assert!(resolved); +} + /// A helper function to include a timestamp for println. fn timed_println(msg: String) { let now = SystemTime::now(); From a3bd556c5c08f0818862c6c5a83e2ca564390e80 Mon Sep 17 00:00:00 2001 From: Han Xu Date: Mon, 9 Dec 2024 17:48:50 -0800 Subject: [PATCH 2/5] fix the test for Windows --- tests/mdns_test.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/mdns_test.rs b/tests/mdns_test.rs index 34da550..d217987 100644 --- a/tests/mdns_test.rs +++ b/tests/mdns_test.rs @@ -1888,6 +1888,10 @@ fn test_multicast_loop_v4() { // start a client i.e. querier. let mut resolved = false; let client = ServiceDaemon::new().expect("failed to create mdns client"); + + // For Windows, IP_MULTICAST_LOOP option works only on the receive path. + client.set_multicast_loop_v4(false).unwrap(); + let receiver = client.browse(ty_domain).unwrap(); let timeout = Duration::from_secs(2); @@ -1911,6 +1915,8 @@ fn test_multicast_loop_v4() { // enable loopback and try again. server.set_multicast_loop_v4(true).unwrap(); + client.set_multicast_loop_v4(true).unwrap(); + let receiver = client.browse(ty_domain).unwrap(); while let Ok(event) = receiver.recv_timeout(timeout) { match event { From 1760ef981e65179017c16f0179075a3f9872252f Mon Sep 17 00:00:00 2001 From: Han Xu Date: Mon, 9 Dec 2024 18:03:15 -0800 Subject: [PATCH 3/5] update doc comments --- src/service_daemon.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/service_daemon.rs b/src/service_daemon.rs index b07f26f..7f0abb9 100644 --- a/src/service_daemon.rs +++ b/src/service_daemon.rs @@ -401,9 +401,20 @@ impl ServiceDaemon { ))) } - /// Enable or disable the loopback for locally sent multicast packets. + /// Enable or disable the loopback for locally sent multicast packets in IPv4. /// - /// When disabled, a local querier will not receive announcements from a local responder. + /// When disabled, a querier will not receive announcements from a responder on the same host. + /// + /// Reference: https://learn.microsoft.com/en-us/windows/win32/winsock/ip-multicast-2 + /// + /// "The Winsock version of the IP_MULTICAST_LOOP option is semantically different than + /// the UNIX version of the IP_MULTICAST_LOOP option: + /// + /// In Winsock, the IP_MULTICAST_LOOP option applies only to the receive path. + /// In the UNIX version, the IP_MULTICAST_LOOP option applies to the send path." + /// + /// Which means, in order NOT to receive localhost announcements, you want to call + /// this API on the querier side on Windows, but on the responder side on Unix. pub fn set_multicast_loop_v4(&self, on: bool) -> Result<()> { self.send_cmd(Command::SetOption(DaemonOption::MulticastLoopV4(on))) } From 397355e8d923d23907e53b1628cbb084e7045528 Mon Sep 17 00:00:00 2001 From: Han Xu Date: Mon, 9 Dec 2024 19:42:55 -0800 Subject: [PATCH 4/5] support IPv6 --- src/service_daemon.rs | 45 +++++++++++++++++++++-- tests/mdns_test.rs | 84 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 2 deletions(-) diff --git a/src/service_daemon.rs b/src/service_daemon.rs index 7f0abb9..91dcbf6 100644 --- a/src/service_daemon.rs +++ b/src/service_daemon.rs @@ -419,6 +419,24 @@ impl ServiceDaemon { self.send_cmd(Command::SetOption(DaemonOption::MulticastLoopV4(on))) } + /// Enable or disable the loopback for locally sent multicast packets in IPv6. + /// + /// When disabled, a querier will not receive announcements from a responder on the same host. + /// + /// Reference: https://learn.microsoft.com/en-us/windows/win32/winsock/ip-multicast-2 + /// + /// "The Winsock version of the IP_MULTICAST_LOOP option is semantically different than + /// the UNIX version of the IP_MULTICAST_LOOP option: + /// + /// In Winsock, the IP_MULTICAST_LOOP option applies only to the receive path. + /// In the UNIX version, the IP_MULTICAST_LOOP option applies to the send path." + /// + /// Which means, in order NOT to receive localhost announcements, you want to call + /// this API on the querier side on Windows, but on the responder side on Unix. + pub fn set_multicast_loop_v6(&self, on: bool) -> Result<()> { + self.send_cmd(Command::SetOption(DaemonOption::MulticastLoopV6(on))) + } + /// Proactively confirms whether a service instance still valid. /// /// This call will issue queries for a service instance's SRV record and Address records. @@ -929,6 +947,8 @@ struct Zeroconf { resolved: HashSet, multicast_loop_v4: bool, + + multicast_loop_v6: bool, } impl Zeroconf { @@ -985,6 +1005,7 @@ impl Zeroconf { pending_resolves: HashSet::new(), resolved: HashSet::new(), multicast_loop_v4: true, + multicast_loop_v6: true, } } @@ -994,6 +1015,7 @@ impl Zeroconf { DaemonOption::EnableInterface(if_kind) => self.enable_interface(if_kind), DaemonOption::DisableInterface(if_kind) => self.disable_interface(if_kind), DaemonOption::MulticastLoopV4(on) => self.set_multicast_loop_v4(on), + DaemonOption::MulticastLoopV6(on) => self.set_multicast_loop_v6(on), } } @@ -1027,6 +1049,14 @@ impl Zeroconf { } } + fn set_multicast_loop_v6(&mut self, on: bool) { + for (_, sock) in self.intf_socks.iter_mut() { + if let Err(e) = sock.set_multicast_loop_v6(on) { + debug!("failed to set multicast loop v6: {e}"); + } + } + } + fn notify_monitors(&mut self, event: DaemonEvent) { // Only retain the monitors that are still connected. self.monitors.retain(|sender| { @@ -1185,7 +1215,12 @@ impl Zeroconf { fn add_new_interface(&mut self, intf: Interface) { // Bind the new interface. let new_ip = intf.ip(); - let mut sock = match new_socket_bind(&intf, self.multicast_loop_v4) { + let should_loop = if new_ip.is_ipv4() { + self.multicast_loop_v4 + } else { + self.multicast_loop_v6 + }; + let mut sock = match new_socket_bind(&intf, should_loop) { Ok(s) => s, Err(e) => { debug!("bind a socket to {}: {}. Skipped.", &intf.ip(), e); @@ -1634,7 +1669,12 @@ impl Zeroconf { } // Replace the closed socket with a new one. - match new_socket_bind(intf, self.multicast_loop_v4) { + let should_loop = if intf.ip().is_ipv4() { + self.multicast_loop_v4 + } else { + self.multicast_loop_v6 + }; + match new_socket_bind(intf, should_loop) { Ok(new_sock) => { trace!("reset socket for IP {}", intf.ip()); self.intf_socks.insert(intf.clone(), new_sock); @@ -2952,6 +2992,7 @@ enum DaemonOption { EnableInterface(Vec), DisableInterface(Vec), MulticastLoopV4(bool), + MulticastLoopV6(bool), } /// The length of Service Domain name supported in this lib. diff --git a/tests/mdns_test.rs b/tests/mdns_test.rs index d217987..f1c7eda 100644 --- a/tests/mdns_test.rs +++ b/tests/mdns_test.rs @@ -1937,6 +1937,90 @@ fn test_multicast_loop_v4() { assert!(resolved); } +#[test] +fn test_multicast_loop_v6() { + let ty_domain = "_loop_v6._udp.local."; + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let instance_name = now.as_micros().to_string(); // Create a unique name. + let host_name = "loop_v6_host.local."; + let port = 5200; + + // Register the first service. + let server = ServiceDaemon::new().expect("failed to start server"); + server.set_multicast_loop_v6(false).unwrap(); + + // Get a single IPv4 address + let ip_addr1 = my_ip_interfaces() + .iter() + .find(|iface| iface.ip().is_ipv6()) + .map(|iface| iface.ip()) + .unwrap(); + + // Publish the service on server + let service1 = ServiceInfo::new(ty_domain, &instance_name, host_name, &ip_addr1, port, None) + .expect("valid service info"); + server + .register(service1) + .expect("Failed to register service1"); + + // wait for the service announced. + sleep(Duration::from_secs(1)); + + // start a client i.e. querier. + let mut resolved = false; + let client = ServiceDaemon::new().expect("failed to create mdns client"); + + // For Windows, IP_MULTICAST_LOOP option works only on the receive path. + client.set_multicast_loop_v6(false).unwrap(); + + let receiver = client.browse(ty_domain).unwrap(); + + let timeout = Duration::from_secs(2); + while let Ok(event) = receiver.recv_timeout(timeout) { + match event { + ServiceEvent::ServiceResolved(info) => { + println!( + "Resolved a service: {} host {} IP {:?}", + info.get_fullname(), + info.get_hostname(), + info.get_addresses() + ); + resolved = true; + break; + } + _ => {} + } + } + + assert_eq!(resolved, false); + + // enable loopback and try again. + server.set_multicast_loop_v6(true).unwrap(); + client.set_multicast_loop_v6(true).unwrap(); + + let receiver = client.browse(ty_domain).unwrap(); + + while let Ok(event) = receiver.recv_timeout(timeout) { + match event { + ServiceEvent::ServiceResolved(info) => { + println!( + "Resolved a service: {} host {} IP {:?}", + info.get_fullname(), + info.get_hostname(), + info.get_addresses() + ); + resolved = true; + break; + } + _ => {} + } + } + + assert!(resolved); +} + /// A helper function to include a timestamp for println. fn timed_println(msg: String) { let now = SystemTime::now(); From 5e284e03ef511ab6069a6bceef3060cbc829bcc0 Mon Sep 17 00:00:00 2001 From: Han Xu Date: Tue, 10 Dec 2024 21:12:28 -0800 Subject: [PATCH 5/5] update doc comments --- src/service_daemon.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/service_daemon.rs b/src/service_daemon.rs index 91dcbf6..c91f7c5 100644 --- a/src/service_daemon.rs +++ b/src/service_daemon.rs @@ -403,7 +403,8 @@ impl ServiceDaemon { /// Enable or disable the loopback for locally sent multicast packets in IPv4. /// - /// When disabled, a querier will not receive announcements from a responder on the same host. + /// By default, multicast loop is enabled for IPv4. When disabled, a querier will not + /// receive announcements from a responder on the same host. /// /// Reference: https://learn.microsoft.com/en-us/windows/win32/winsock/ip-multicast-2 /// @@ -421,7 +422,8 @@ impl ServiceDaemon { /// Enable or disable the loopback for locally sent multicast packets in IPv6. /// - /// When disabled, a querier will not receive announcements from a responder on the same host. + /// By default, multicast loop is enabled for IPv6. When disabled, a querier will not + /// receive announcements from a responder on the same host. /// /// Reference: https://learn.microsoft.com/en-us/windows/win32/winsock/ip-multicast-2 ///