Files
lanspread/vendor/mdns-sd/tests/mdns_test.rs
T
ddidderr 4d5881d6b0 fix(mdns): retain origin and bound daemon state
Vendor the pinned mdns-sd 0.21.1 source so response records retain their observed source IP. Bound unauthenticated cache records to 1024 globally and 128 per source, deduplicate and cap timers at 4096, and poll at least once per second for expiry cleanup.

Expose packet provenance through lanspread-mdns, drop originless resolutions, and add a dedicated vendor test recipe while keeping third-party sources outside workspace formatting and Clippy.

Test Plan:
- just mdns-vendor-test (106 tests passed with socket access)
- just test
- just fmt
- just clippy
- cache/per-source, exact-refresh, timer-cap, and serde tests
- git diff --check
2026-09-12 13:08:26 +02:00

2708 lines
87 KiB
Rust

use std::{
collections::{HashMap, HashSet},
net::{IpAddr, Ipv4Addr, Ipv6Addr},
thread::sleep,
time::{Duration, SystemTime},
};
use if_addrs::{IfAddr, Interface};
use mdns_sd::{
DaemonEvent,
DaemonStatus,
HostnameResolutionEvent,
IfKind,
InterfaceId,
IntoTxtProperties,
RRType,
ScopedIp,
ServiceDaemon,
ServiceEvent,
ServiceInfo,
TxtProperty,
UnregisterStatus,
};
use test_log::test;
/// This test covers:
/// register(announce), browse(query), response, unregister, shutdown.
#[test]
fn integration_success() {
// Create a daemon
let d = ServiceDaemon::new().expect("Failed to create daemon");
// Register a service
let ty_domain = "_mdns-sd-it._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 all_interfaces = my_ip_interfaces();
println!("all interfaces count: {}", all_interfaces.len());
// as we send only once per interface and ip we need a count of unique addresses to verify number of sent unregisters later on
let mut unique_intf_idx_ip_ver_set = HashSet::new();
let mut non_idx_count = 0;
for intf in all_interfaces.iter() {
let ip_ver = match intf.addr {
IfAddr::V4(_) => 4u8,
IfAddr::V6(_) => 6u8,
};
// use the same approach as `IntfSock.multicast_send_tracker`
if let Some(idx) = intf.index {
if !unique_intf_idx_ip_ver_set.insert((idx, ip_ver)) {
println!("index {idx} IP v{ip_ver} repeated on interface {}, likely multi-addr on the same interface", intf.name);
}
} else {
non_idx_count += 1;
}
}
let unique_intf_idx_ip_ver_count = unique_intf_idx_ip_ver_set.len() + non_idx_count;
let ifaddrs_set: HashSet<_> = all_interfaces.iter().map(|intf| intf.ip()).collect();
let my_ifaddrs: Vec<_> = ifaddrs_set.into_iter().collect();
let my_addrs_count = my_ifaddrs.len();
println!("My IP {} addr(s):", my_ifaddrs.len());
for item in my_ifaddrs.iter() {
println!("{}", &item);
}
let host_name = "INTEGRATION_host.local.";
let port = 5200;
let mut properties = HashMap::new();
properties.insert("property_1".to_string(), "test".to_string());
properties.insert("property_2".to_string(), "1".to_string());
properties.insert("property_3".to_string(), "1234".to_string());
let my_service = ServiceInfo::new(
ty_domain,
&instance_name,
host_name,
&my_ifaddrs[..],
port,
Some(properties),
)
.expect("valid service info");
let fullname = my_service.get_fullname().to_string();
d.register(my_service)
.expect("Failed to register our service");
// Browse for a service
let mut resolved_ips: HashSet<IpAddr> = HashSet::new();
let mut addr_count = 0;
let browse_chan = d.browse(ty_domain).unwrap();
let timeout = Duration::from_secs(2);
while let Ok(event) = browse_chan.recv_timeout(timeout) {
match event {
ServiceEvent::SearchStarted(ty_domain) => {
println!("Search started for {}", &ty_domain);
}
ServiceEvent::ServiceFound(_ty_domain, fullname) => {
println!("Found a new service: {}", &fullname);
}
ServiceEvent::ServiceResolved(info) => {
let addrs: HashSet<_> = info
.get_addresses()
.iter()
.map(|a| a.to_ip_addr())
.collect();
addr_count = addrs.len();
println!(
"Resolved a new service: {} with {} addr(s)",
info.get_fullname(),
addrs.len()
);
for a in addrs.iter() {
println!("{}", a);
}
if info.get_fullname().contains(&instance_name) {
resolved_ips.extend(addrs);
}
let hostname = info.get_hostname();
assert_eq!(hostname, host_name);
let service_port = info.get_port();
assert_eq!(service_port, port);
let properties = info.get_properties();
assert!(properties.get("property_1").is_some());
assert!(properties.get("property_2").is_some());
assert_eq!(properties.len(), 3);
assert!(info.get_property("property_1").is_some());
assert!(info.get_property("property_2").is_some());
assert_eq!(info.get_property_val_str("property_1"), Some("test"));
assert_eq!(info.get_property_val_str("property_2"), Some("1"));
assert_eq!(
info.get_property_val("property_1").unwrap(),
Some("test".as_bytes())
);
}
_ => {}
}
}
// All addrs should have been resolved.
assert_eq!(addr_count, my_addrs_count);
// IP's can get resolved more than once if fx a cache-flush is asked from the sender of the
// MDNS records, so we look at unique IP addresses to see if they match the number of the
// network interfaces.
assert_eq!(resolved_ips.len(), my_addrs_count);
assert!(!resolved_ips.is_empty());
// Unregister the service
let receiver = d.unregister(&fullname).unwrap();
let response = receiver.recv().unwrap();
assert!(matches!(response, UnregisterStatus::OK));
let mut remove_count = 0;
let mut resolved = false;
while let Ok(event) = browse_chan.recv_timeout(timeout) {
match event {
ServiceEvent::ServiceRemoved(_ty_domain, fullname) => {
println!("Removed service: {}", &fullname);
if fullname.contains(&instance_name) {
remove_count += 1;
}
break;
}
ServiceEvent::ServiceResolved(info) => {
if info.get_fullname() == fullname {
println!("Received a resolved service event after unregister");
resolved = true;
}
}
_ => {}
}
}
assert_eq!(remove_count, 1);
assert!(
!resolved,
"Resolved event should not be received after unregister"
);
// Stop browsing the service.
d.stop_browse(ty_domain).expect("Failed to stop browsing");
let mut stopped_count = 0;
while let Ok(event) = browse_chan.recv_timeout(timeout) {
if let ServiceEvent::SearchStopped(ty) = event {
println!("Search stopped for {}", &ty);
stopped_count += 1;
break;
}
}
assert_eq!(stopped_count, 1);
// Verify metrics.
let metrics_receiver = d.get_metrics().unwrap();
let metrics = metrics_receiver.recv().unwrap();
println!("metrics: {:?}", &metrics);
assert_eq!(metrics["register"], 1);
assert_eq!(metrics["unregister"], 1);
assert!(metrics["register-resend"] >= 1);
println!("unique interface set: {:?}", unique_intf_idx_ip_ver_set);
assert_eq!(
metrics["unregister-resend"],
unique_intf_idx_ip_ver_count as i64
);
assert!(metrics["browse"] >= 2); // browse has been retransmitted.
// respond has been sent for every browse, or they are suppressed by "known answer".
let respond_count = metrics.get("respond").unwrap_or(&0);
let known_answer_count = metrics.get("known-answer-suppression").unwrap_or(&0);
assert!(*respond_count >= 2 || *known_answer_count > 0);
// Test the special meta-query of "_services._dns-sd._udp.local."
let service2_type = "_my-service2._udp.local.";
let service2_instance = "instance2";
let service2 = ServiceInfo::new(
service2_type,
service2_instance,
host_name,
&my_ifaddrs[..],
port,
None,
)
.expect("valid service info");
d.register(service2)
.expect("Failed to register the 2nd service");
// Browse using the special meta-query.
let meta_query = "_services._dns-sd._udp.local.";
let browse_chan = d.browse(meta_query).unwrap();
let timeout = Duration::from_secs(2);
loop {
match browse_chan.recv_timeout(timeout) {
Ok(event) => match event {
ServiceEvent::ServiceFound(ty_domain, fullname) => {
println!("Found a service of {}: {}", &ty_domain, &fullname);
// Among all services found, should have our 2nd service.
if fullname == service2_type {
break;
}
}
e => {
println!("Received event {:?}", e);
sleep(Duration::from_millis(100));
}
},
Err(e) => {
panic!("browse error: {}", e);
}
}
}
// Shutdown
d.shutdown().unwrap();
}
#[test]
fn service_without_properties_with_alter_net_v4() {
// Create a daemon
let d = ServiceDaemon::new().expect("Failed to create daemon");
// Register a service without properties.
let ty_domain = "_serv-no-prop._tcp.local.";
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap();
let instance_name = now.as_micros().to_string(); // Create a unique name.
let if_addrs: Vec<Interface> = my_ip_interfaces()
.into_iter()
.filter(|iface| iface.addr.ip().is_ipv4())
.collect();
let first_ip = if_addrs[0].ip();
let alter_ip = ipv4_alter_net(&if_addrs);
let host_ip = vec![first_ip, alter_ip];
let host_name = "serv-no-prop-v4.local.";
let port = 5201;
let my_service = ServiceInfo::new(
ty_domain,
&instance_name,
host_name,
&host_ip[..],
port,
None,
)
.expect("valid service info");
let fullname = my_service.get_fullname().to_string();
d.register(my_service)
.expect("Failed to register our service");
println!("Registered service with host_ip: {:?}", &host_ip);
// Browse for a service
let browse_chan = d.browse(ty_domain).unwrap();
let timeout = Duration::from_secs(2);
let timer = std::time::Instant::now() + timeout;
let mut found = false;
while std::time::Instant::now() < timer {
match browse_chan.recv_timeout(timeout) {
Ok(event) => match event {
ServiceEvent::ServiceResolved(info) => {
println!(
"Resolved a service of {} addr(s): {:?}",
&info.get_fullname(),
info.get_addresses()
);
// match only our service and not v6 one
if info.get_addresses_v4().is_empty() {
continue;
}
if fullname.as_str() == info.get_fullname() {
let addrs = info.get_addresses_v4();
assert_eq!(addrs.len(), 1); // first_ipv4 but no alter_ipv.
found = true;
break;
}
}
e => {
println!("Received event {:?}", e);
}
},
Err(e) => {
panic!("browse error: {}", e);
}
}
}
d.shutdown().unwrap();
assert!(found);
}
#[test]
fn service_without_properties_with_alter_net_v6() {
// Create a daemon
let d = ServiceDaemon::new().expect("Failed to create daemon");
// Register a service without properties.
let ty_domain = "_serv-no-prop._tcp.local.";
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap();
let instance_name = now.as_micros().to_string(); // Create a unique name.
let if_addrs: Vec<Interface> = my_ip_interfaces()
.into_iter()
.filter(|iface| iface.addr.ip().is_ipv6())
.collect();
let first_ip = if_addrs[0].ip();
let alter_ip = ipv6_alter_net(&if_addrs);
let host_ip = vec![first_ip, alter_ip];
let host_name = "serv-no-prop-v6.local.";
let port = 5201;
let my_service = ServiceInfo::new(
ty_domain,
&instance_name,
host_name,
&host_ip[..],
port,
None,
)
.expect("valid service info");
let fullname = my_service.get_fullname().to_string();
d.register(my_service)
.expect("Failed to register our service");
println!("Registered service with host_ip: {:?}", &host_ip);
// Browse for a service
let browse_chan = d.browse(ty_domain).unwrap();
let timeout = Duration::from_secs(2);
let timer = std::time::Instant::now() + timeout;
let mut found = false;
while std::time::Instant::now() < timer {
match browse_chan.recv_timeout(timeout) {
Ok(event) => match event {
ServiceEvent::ServiceResolved(info) => {
println!(
"Resolved a service of {} addr(s): {:?}",
&info.get_fullname(),
info.get_addresses()
);
// match only our service and not v4 one
if fullname.as_str() == info.get_fullname() {
let addrs: Vec<_> = info
.get_addresses()
.iter()
.filter(|a| a.is_ipv6())
.collect();
if addrs.is_empty() {
continue; // In case IPv4 addr received first.
}
assert_eq!(addrs.len(), 1); // first_ipv6 but no alter_ipv.
found = true;
break;
}
}
e => {
println!("Received event {:?}", e);
}
},
Err(e) => {
panic!("browse error: {}", e);
}
}
}
d.shutdown().unwrap();
assert!(found);
}
#[test]
fn service_txt_properties_case_insensitive() {
// Register a service with properties.
let domain = "_serv-properties._tcp.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 = "properties_host.local.";
let port = 5201;
let properties = [
("prop_CAP_CASE", "one"),
("prop_cap_case", "two"),
("prop_Cap_Lower", "three"),
];
let my_service = ServiceInfo::new(domain, &instance_name, host_name, "", port, &properties[..])
.expect("valid service info")
.enable_addr_auto();
let props = my_service.get_properties();
assert_eq!(props.len(), 2);
// Verify `get_property()` method is case insensitive and returns
// the first property with the same key.
let prop_cap_case = my_service.get_property("prop_CAP_CASE").unwrap();
assert_eq!(prop_cap_case.val_str(), "one");
assert_eq!(prop_cap_case.val(), Some("one".as_bytes()));
// Verify the original property name is kept.
let prop_mixed = my_service.get_property("prop_cap_lower").unwrap();
assert_eq!(prop_mixed.key(), "prop_Cap_Lower");
}
#[test]
fn service_txt_properties_key_ascii() {
let domain = "_mdns-ascii._tcp.local.";
let instance = "test_service_info_key_ascii";
let port = 5202;
// Verify that a key must contain ASCII only. E.g. cannot have emojis.
let properties = [("prop_ascii", "one"), ("prop_🤗", "hugging_face")];
let my_service = ServiceInfo::new(domain, instance, "myhost", "", port, &properties[..]);
assert!(my_service.is_err());
if let Err(e) = my_service {
let msg = format!("ERROR: {}", e);
assert!(msg.contains("not ASCII"));
}
// Verify that a key cannot contain '='.
let properties = [("prop_ascii", "one"), ("prop_=", "equal sign")];
let my_service = ServiceInfo::new(domain, instance, "myhost", "", port, &properties[..]);
assert!(my_service.is_err());
if let Err(e) = my_service {
let msg = format!("ERROR: {}", e);
assert!(msg.contains('='));
}
// Verify that properly formatted keys are OK.
let properties = [("prop_ascii", "one"), ("prop_2", "two")];
let my_service = ServiceInfo::new(domain, instance, "myhost", "", port, &properties[..]);
assert!(my_service.is_ok());
}
#[test]
fn test_txt_properties_into_hashmap_str() {
// Test valid UTF-8 properties
let properties = [("key1", "val1"), ("key2", "val2")].into_txt_properties();
let property_map = properties.into_property_map_str();
println!("property_map: {:?}", property_map);
assert_eq!(property_map.len(), 2);
assert_eq!(property_map.get("key1"), Some(&"val1".to_string()));
assert_eq!(property_map.get("key2"), Some(&"val2".to_string()));
// Test property with no value and property with invalid UTF-8
let invalid_vec: Vec<u8> = vec![200, 200]; // Invalid UTF-8 bytes
let prop1 = TxtProperty::from("key1");
let prop2 = TxtProperty::from(("key2", invalid_vec.as_slice()));
let properties = vec![prop1, prop2].into_txt_properties();
let property_map = properties.into_property_map_str();
// Property with no value should map to empty string
// Property with invalid UTF-8 should be skipped
assert_eq!(property_map.get("key1"), Some(&"".to_string()));
assert_eq!(property_map.len(), 1);
}
#[test]
fn test_into_txt_properties() {
// Verify (&str, String) tuple is supported.
let properties = [("key1", String::from("val1"))];
let txt_props = properties.into_txt_properties();
assert_eq!(txt_props.get_property_val_str("key1").unwrap(), "val1");
assert_eq!(
txt_props.get_property_val("key1").unwrap(),
Some("val1".as_bytes())
);
// Verify (String, String) tuple is supported.
let properties = [(String::from("key2"), String::from("val2"))];
let txt_props = properties.into_txt_properties();
assert_eq!(txt_props.get_property_val_str("key2").unwrap(), "val2");
}
#[test]
fn test_info_as_resolved_service() {
let sub_ty_domain = "_printer._sub._test._tcp.local.";
let service_info = ServiceInfo::new(
sub_ty_domain,
"my_instance",
"my_host.local.",
"192.168.0.1",
5200,
None,
)
.unwrap();
let resolved_service = service_info.as_resolved_service();
assert!(resolved_service.is_valid());
assert_eq!(resolved_service.sub_ty_domain.unwrap(), sub_ty_domain);
assert_eq!(resolved_service.ty_domain, "_test._tcp.local.");
let info_missing_addr = ServiceInfo::new(
"_test._tcp.local.",
"my_instance",
"my_host.local.",
"",
5200,
None,
)
.unwrap();
let invalid_service = info_missing_addr.as_resolved_service();
assert!(!invalid_service.is_valid());
assert!(invalid_service.sub_ty_domain.is_none());
}
/// Test enabling an interface using its name, for example "en0".
/// Also tests an instance name with Upper Case.
#[test]
fn service_with_named_interface_only() {
// Create a daemon
let d = ServiceDaemon::new().expect("Failed to create daemon");
// First, disable all interfaces.
d.disable_interface(IfKind::All).unwrap();
// Register a service with a name len > 15.
let my_ty_domain = "_named_intf_only._udp.local.";
let host_name = "named_intf_host.local.";
let host_ipv4 = "";
let port = 5202;
let my_service = ServiceInfo::new(
my_ty_domain,
"UpperCaseInstance",
host_name,
host_ipv4,
port,
None,
)
.expect("invalid service info")
.enable_addr_auto();
d.register(my_service).unwrap();
// Browse for a service and verify all addresses are IPv4.
let browse_chan = d.browse(my_ty_domain).unwrap();
let timeout = Duration::from_secs(2);
let mut resolved = false;
while let Ok(event) = browse_chan.recv_timeout(timeout) {
match event {
ServiceEvent::ServiceResolved(info) => {
let addrs = info.get_addresses();
resolved = true;
println!(
"Resolved a service of {} addr(s): {:?}",
&info.get_fullname(),
addrs
);
break;
}
e => {
println!("Received event {:?}", e);
}
}
}
assert!(!resolved);
// Second, find an interface.
let if_addrs: Vec<Interface> = my_ip_interfaces()
.into_iter()
.filter(|iface| iface.addr.ip().is_ipv4())
.collect();
let if_name = if_addrs[0].name.clone();
// Enable the named interface.
println!("Enable interface with name {}", &if_name);
d.enable_interface(&if_name).unwrap();
// Browse again.
let browse_chan = d.browse(my_ty_domain).unwrap();
let timeout = Duration::from_secs(3);
let mut resolved = false;
while let Ok(event) = browse_chan.recv_timeout(timeout) {
match event {
ServiceEvent::ServiceResolved(info) => {
let addrs = info.get_addresses();
resolved = true;
println!(
"Resolved a service of {} addr(s): {:?}",
&info.get_fullname(),
addrs
);
break;
}
e => {
println!("Received event {:?}", e);
}
}
}
assert!(resolved);
d.shutdown().unwrap();
}
#[test]
fn service_with_ipv4_only() {
// Create a daemon
let d = ServiceDaemon::new().expect("Failed to create daemon");
// Disable IPv6, so the daemon is IPv4 only now.
d.disable_interface(IfKind::IPv6).unwrap();
// Register a service with a name len > 15.
let service_ipv4_only = "_test_ipv4_only._udp.local.";
let host_name = "my_host_ipv4_only.local.";
let host_ipv4 = "";
let port = 5201;
let my_service = ServiceInfo::new(
service_ipv4_only,
"my_instance",
host_name,
host_ipv4,
port,
None,
)
.expect("invalid service info")
.enable_addr_auto();
let result = d.register(my_service);
assert!(result.is_ok());
// Browse for a service and verify all addresses are IPv4.
let browse_chan = d.browse(service_ipv4_only).unwrap();
let timeout = Duration::from_secs(2);
let mut resolved = false;
// run till the timeout and collect the resolved addresses
// from all enabled interfaces.
while let Ok(event) = browse_chan.recv_timeout(timeout) {
match event {
ServiceEvent::ServiceResolved(info) => {
let addrs = info.get_addresses();
resolved = true;
println!(
"Resolved a service of {} addr(s): {:?}",
&info.get_fullname(),
addrs
);
assert!(!info.get_addresses().is_empty());
for addr in info.get_addresses().iter() {
assert!(addr.is_ipv4());
}
// We don't break here, as there could be more addresses coming.
}
e => {
println!("Received event {:?}", e);
}
}
}
assert!(resolved);
d.shutdown().unwrap();
}
#[test]
fn service_ipv6_link_local_only() {
// Create a daemon
let d = ServiceDaemon::new().expect("Failed to create daemon");
let service_ipv6_intf = "_test_ipv6_intf._udp.local.";
let host_name = "my_host_ipv6_intf.local.";
let host_ipv4 = "";
let port = 5201;
let mut my_service = ServiceInfo::new(
service_ipv6_intf,
"my_instance",
host_name,
host_ipv4,
port,
None,
)
.expect("invalid service info")
.enable_addr_auto();
my_service.set_interfaces(vec![IfKind::IPv6]);
my_service.set_link_local_only(true);
let result = d.register(my_service);
assert!(result.is_ok());
// Browse for a services. Verify all addresses are link-local IPv6.
let browse_chan = d.browse(service_ipv6_intf).unwrap();
let timeout = Duration::from_secs(2);
let mut resolved = false;
// run till the timeout and collect the resolved addresses
// from all enabled interfaces.
while let Ok(event) = browse_chan.recv_timeout(timeout) {
match event {
ServiceEvent::ServiceResolved(info) => {
let addrs = info.get_addresses();
resolved = true;
println!(
"Resolved a service of {} addr(s): {:?}",
&info.get_fullname(),
addrs
);
assert!(!info.get_addresses().is_empty());
for addr in info.get_addresses().iter() {
assert!(addr.is_ipv6());
assert!(
matches!(addr.to_ip_addr(), IpAddr::V6(ipv6) if (ipv6.segments()[0] & 0xffc0) == 0xfe80)
);
}
// We don't break here, as there could be more addresses coming.
}
e => {
println!("Received event {:?}", e);
}
}
}
assert!(resolved);
d.shutdown().unwrap();
}
#[test]
fn test_disable_interface_cache() {
// Create a server
let server = ServiceDaemon::new().expect("Failed to create the server");
// Register a service with one IPv4.
let ty_domain = "_disable-intf._tcp.local.";
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap();
let instance_name = now.as_micros().to_string();
let ipv4_list: Vec<_> = my_ip_interfaces()
.iter()
.map(|iface| iface.ip())
.filter(|ip| ip.is_ipv4() && !ip.is_loopback())
.collect();
let host_name = "disabled_intf_host.local.";
let port = 5201;
let my_service = ServiceInfo::new(
ty_domain,
&instance_name,
host_name,
&ipv4_list[..],
port,
None,
)
.expect("Invalid service info");
server
.register(my_service)
.expect("Failed to register our service");
// Create a client
let client = ServiceDaemon::new().expect("Failed to create the client");
// Give it some time to cache mDNS records.
sleep(Duration::from_secs(1));
// Disable the interface for the client.
println!("Disabling interface with IP: {:?}", ipv4_list);
client.disable_interface(ipv4_list).unwrap();
// Browse for the service.
let handle = client.browse(ty_domain).unwrap();
let timeout = Duration::from_secs(1);
let mut resolved = false;
// run till timeout and it should not resolve.
while let Ok(event) = handle.recv_timeout(timeout) {
if let ServiceEvent::ServiceResolved(info) = event {
println!(
"Resolved a service of {} addr(s): {:?}",
&info.get_fullname(),
info.get_addresses()
);
resolved = true;
break;
}
}
// We cannot resolve the service because the interface is disabled.
assert!(!resolved);
// Clean up.
server.shutdown().unwrap();
client.shutdown().unwrap();
}
#[test]
fn service_with_invalid_addr_v4() {
// Create a daemon
let d = ServiceDaemon::new().expect("Failed to create daemon");
// Register a service without properties.
let ty_domain = "_invalid-addr._tcp.local.";
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap();
let instance_name = now.as_micros().to_string(); // Create a unique name.
let if_addrs: Vec<Interface> = my_ip_interfaces()
.into_iter()
.filter(|iface| iface.addr.ip().is_ipv4())
.collect();
let alter_ip = ipv4_alter_net(&if_addrs);
let host_name = "invalid_ipv4_host.local.";
let port = 5201;
let my_service = ServiceInfo::new(ty_domain, &instance_name, host_name, alter_ip, port, None)
.expect("valid service info");
d.register(my_service)
.expect("Failed to register our service");
// Browse for a service
let browse_chan = d.browse(ty_domain).unwrap();
let timeout = Duration::from_secs(2);
let mut resolved = false;
loop {
match browse_chan.recv_timeout(timeout) {
Ok(event) => match event {
ServiceEvent::ServiceResolved(info) => {
println!(
"Resolved a service of {} addr(s): {:?}",
&info.get_fullname(),
info.get_addresses()
);
resolved = true;
break;
}
e => {
println!("Received event {:?}", e);
}
},
Err(e) => {
println!("browse error: {}", e);
break;
}
}
}
d.shutdown().unwrap();
// We cannot resolve the service because the published address
// is not valid in the LAN.
assert!(!resolved);
}
#[test]
fn service_with_invalid_addr_v6() {
// Create a daemon
let d = ServiceDaemon::new().expect("Failed to create daemon");
// Register a service without properties.
let ty_domain = "_invalid-addr._tcp.local.";
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap();
let instance_name = now.as_micros().to_string(); // Create a unique name.
let if_addrs: Vec<Interface> = my_ip_interfaces()
.into_iter()
.filter(|iface| iface.addr.ip().is_ipv6())
.collect();
let alter_ip = ipv6_alter_net(&if_addrs);
let host_name = "my_host.local.";
let port = 5201;
let my_service = ServiceInfo::new(ty_domain, &instance_name, host_name, alter_ip, port, None)
.expect("valid service info");
d.register(my_service)
.expect("Failed to register our service");
// Browse for a service
let browse_chan = d.browse(ty_domain).unwrap();
let timeout = Duration::from_secs(2);
let mut resolved = false;
loop {
match browse_chan.recv_timeout(timeout) {
Ok(event) => match event {
ServiceEvent::ServiceResolved(info) => {
println!(
"Resolved a service of {} addr(s): {:?}",
&info.get_fullname(),
info.get_addresses()
);
resolved = true;
break;
}
e => {
println!("Received event {:?}", e);
}
},
Err(e) => {
println!("browse error: {}", e);
break;
}
}
}
d.shutdown().unwrap();
// We cannot resolve the service because the published address
// is not valid in the LAN.
assert!(!resolved);
}
#[test]
fn service_with_loopback_addr() {
// Create a daemon
let d = ServiceDaemon::new().expect("Failed to create daemon");
d.enable_interface(IfKind::LoopbackV4)
.expect("Failed to enable loopback interface");
// Define a unique service type and instance name.
let ty_domain = "_test-loopback._tcp.local.";
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap();
let instance_name = now.as_micros().to_string();
// Use a loopback address (127.0.0.1) for the service.
let loopback_ip: IpAddr = "127.0.0.1".parse().unwrap();
let host_name = "localhost.local.";
let port = 5201;
let my_service = ServiceInfo::new(
ty_domain,
&instance_name,
host_name,
loopback_ip,
port,
None,
)
.expect("valid service info");
d.register(my_service)
.expect("Failed to register our service");
// Browse for the service.
let browse_chan = d.browse(ty_domain).unwrap();
let timeout = Duration::from_secs(2);
let mut found_loopback = false;
loop {
match browse_chan.recv_timeout(timeout) {
Ok(event) => match event {
ServiceEvent::ServiceResolved(info) => {
println!(
"Resolved service {} with addresses: {:?}",
info.get_fullname(),
info.get_addresses()
);
// Check that at least one of the addresses is a loopback address.
if info.get_addresses().iter().any(|ip| ip.is_loopback()) {
found_loopback = true;
}
break;
}
e => {
println!("Received event {:?}", e);
}
},
Err(e) => {
println!("browse error: {}", e);
break;
}
}
}
d.shutdown().unwrap();
// Assert that the resolved service includes a loopback address.
assert!(
found_loopback,
"The service should include a loopback address"
);
}
#[test]
fn subtype() {
// Create a daemon
let d = ServiceDaemon::new().expect("Failed to create daemon");
// Register a service with a subdomain
let subtype_domain = "_directory._sub._test-subtype._tcp.local.";
let ty_domain = "_test-subtype._tcp.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_ipv4 = my_ip_interfaces()[0].ip().to_string();
let host_name = "subtype_host.local.";
let port = 5201;
let my_service = ServiceInfo::new(
subtype_domain,
&instance_name,
host_name,
host_ipv4,
port,
None,
)
.expect("valid service info");
let fullname = my_service.get_fullname().to_string();
d.register(my_service)
.expect("Failed to register our service");
// Browse for the service via ty_domain and subtype_domain
for domain in [ty_domain, subtype_domain].iter() {
let browse_chan = d.browse(domain).unwrap();
let timeout = Duration::from_secs(2);
loop {
match browse_chan.recv_timeout(timeout) {
Ok(event) => match event {
ServiceEvent::ServiceResolved(info) => {
println!(
"Resolved a service of {} subdomain {:?}",
&info.get_fullname(),
info.get_subtype()
);
assert_eq!(fullname.as_str(), info.get_fullname());
assert_eq!(subtype_domain, info.get_subtype().as_ref().unwrap());
break;
}
e => {
println!("Received event {:?}", e);
}
},
Err(e) => {
panic!("browse error: {}", e);
}
}
}
}
d.shutdown().unwrap();
}
/// Verify service name has to be valid.
#[test]
fn test_service_name_check() {
// Create a daemon for the server.
let server_daemon = ServiceDaemon::new().expect("Failed to create server daemon");
let monitor = server_daemon.monitor().unwrap();
// Register a service with a name len > 15.
let service_name_too_long = "_service-name-too-long._udp.local.";
let host_ipv4 = "";
let host_name = "my_host.local.";
let port = 5200;
let mut my_service = ServiceInfo::new(
service_name_too_long,
"my_instance",
host_name,
host_ipv4,
port,
None,
)
.expect("valid service info")
.enable_addr_auto();
my_service.set_requires_probe(false);
let result = server_daemon.register(my_service.clone());
assert!(result.is_ok());
// Verify that the daemon reported error.
let event = monitor.recv_timeout(Duration::from_millis(500)).unwrap();
assert!(matches!(event, DaemonEvent::Error(_)));
if let DaemonEvent::Error(e) = event {
println!("Daemon error: {}", e)
}
// Verify that we can increase the service name length max.
server_daemon.set_service_name_len_max(30).unwrap();
let result = server_daemon.register(my_service);
assert!(result.is_ok());
// Verify that the service was published successfully.
let mut published = false;
let publish_timeout = 1200;
while let Ok(event) = monitor.recv_timeout(Duration::from_millis(publish_timeout)) {
match event {
DaemonEvent::Announce(_, _) => {
published = true;
break;
}
other => {
println!("other daemon events: {:?}", other);
}
}
}
assert!(published);
// Check for the internal upper limit of service name length max.
let r = server_daemon.set_service_name_len_max(31);
assert!(r.is_err());
server_daemon.shutdown().unwrap();
}
#[test]
fn service_new_publish_after_browser() {
let service_type = "_new-pub._udp.local.";
let daemon = ServiceDaemon::new().expect("Failed to create a new daemon");
// First, starts the browser.
let receiver = daemon.browse(service_type).unwrap();
sleep(Duration::from_millis(1000));
let txt_properties = [("key1", "value1")];
let service_info = ServiceInfo::new(
"_new-pub._udp.local.",
"test1",
"my_host.local.",
"",
1234,
&txt_properties[..],
)
.expect("valid service info")
.enable_addr_auto();
// Second, publish a service.
let result = daemon.register(service_info);
assert!(result.is_ok());
let mut resolved = false;
let timeout = Duration::from_secs(2);
loop {
match receiver.recv_timeout(timeout) {
Ok(event) => match event {
ServiceEvent::ServiceResolved(info) => {
println!(
"Resolved a service of {} addr(s): {:?} props: {:?}",
&info.get_fullname(),
info.get_addresses(),
info.get_properties()
);
resolved = true;
break;
}
e => {
println!("Received event {:?}", e);
}
},
Err(e) => {
println!("browse error: {}", e);
break;
}
}
}
assert!(resolved);
daemon.shutdown().unwrap();
}
fn is_apple_p2p_by_name(name: &str) -> bool {
let p2p_prefixes = ["awdl", "llw"];
p2p_prefixes.iter().any(|prefix| name.starts_with(prefix))
}
fn my_ip_interfaces() -> Vec<Interface> {
if_addrs::get_if_addrs()
.unwrap_or_default()
.into_iter()
.filter_map(|i| {
if i.is_loopback() || i.is_p2p() || is_apple_p2p_by_name(&i.name) {
None
} else {
match &i.addr {
IfAddr::V4(ifv4) =>
// Use a 'bind' to check if this is a valid IPv4 addr.
{
match std::net::UdpSocket::bind((ifv4.ip, 0)) {
Ok(_) => Some(i),
Err(e) => {
println!("failed to bind {}: {e}, skipped.", ifv4.ip);
None
}
}
}
IfAddr::V6(ifv6) =>
// Use a 'bind' to check if this is a valid IPv6 addr.
{
let mut sock = std::net::SocketAddrV6::new(ifv6.ip, 0, 0, 0);
if i.is_link_local() {
// Only link local IPv6 address requires to specify scope_id
sock.set_scope_id(i.index.unwrap_or(0));
}
match std::net::UdpSocket::bind(sock) {
Ok(_) => Some(i),
Err(e) => {
println!("failed to bind {}: {e}, skipped.", ifv6.ip);
None
}
}
}
}
}
})
.collect()
}
/// Returns a made-up IPv4 address "net.1.1.1", where
/// `net` is one higher than any of IPv4 addresses on the host.
///
/// The idea is that this made-up address does not belong to
/// the same network as any of the host addresses.
fn ipv4_alter_net(if_addrs: &[Interface]) -> IpAddr {
let mut net_max = 0;
for if_addr in if_addrs.iter() {
match &if_addr.addr {
IfAddr::V4(iface) => {
let net = iface.ip.octets()[0];
if net > net_max {
net_max = net;
}
}
_ => panic!(),
}
}
Ipv4Addr::new(net_max + 1, 1, 1, 1).into()
}
/// Returns a made-up IPv6 address "net:1:1:1:1:1:1:1", where
/// `net` is one higher than any of IPv6 addresses on the host.
///
/// The idea is that this made-up address does not belong to
/// the same network as any of the host addresses.
fn ipv6_alter_net(if_addrs: &[Interface]) -> IpAddr {
let mut net_max = 0;
for if_addr in if_addrs.iter() {
match &if_addr.addr {
IfAddr::V6(iface) => {
let net = iface.ip.octets()[0];
if net > net_max {
net_max = net;
}
}
_ => panic!(),
}
}
Ipv6Addr::new(net_max as u16 + 1, 1, 1, 1, 1, 1, 1, 1).into()
}
#[test]
fn test_shutdown() {
let mdns = ServiceDaemon::new().unwrap();
// Check the status.
let receiver = mdns.status().unwrap();
let status = receiver.recv().unwrap();
assert!(matches!(status, DaemonStatus::Running));
// Shutdown the daemon immediately.
let receiver = mdns.shutdown().unwrap();
let status = receiver.recv().unwrap();
println!("daemon status: {:?}", status);
// Try to register and it should fail.
let service_type = "_mdns-sd-my-test._udp.local.";
let instance_name = "my_instance";
let ip = "192.168.1.12";
let host_name = "192.168.1.12.local.";
let port = 5200;
let properties = [("property_1", "test"), ("property_2", "1234")];
let my_service = ServiceInfo::new(
service_type,
instance_name,
host_name,
ip,
port,
&properties[..],
)
.unwrap();
let result = mdns.register(my_service);
assert!(result.is_err());
// Check the status again.
let receiver = mdns.status().unwrap();
let status = receiver.recv().unwrap();
assert!(matches!(status, DaemonStatus::Shutdown));
}
#[test]
fn test_hostname_resolution() {
let d = ServiceDaemon::new().expect("Failed to create daemon");
let hostname = "my_host._tcp.local.";
let service_ip_addr: ScopedIp = my_ip_interfaces()
.iter()
.find(|iface| iface.ip().is_ipv4())
.map(|iface| iface.into())
.unwrap();
let my_service = ServiceInfo::new(
"_host_res_test._tcp.local.",
"my_instance",
hostname,
&[service_ip_addr.to_ip_addr()] as &[IpAddr],
1234,
None,
)
.expect("invalid service info");
d.register(my_service).unwrap();
let event_receiver = d.resolve_hostname(hostname, Some(2000)).unwrap();
let resolved = loop {
match event_receiver.recv() {
Ok(HostnameResolutionEvent::AddressesFound(found_hostname, addresses)) => {
assert!(found_hostname == hostname);
assert!(addresses.contains(&service_ip_addr));
break true;
}
Ok(HostnameResolutionEvent::SearchStopped(_)) => break false,
Ok(event) => println!("Received event {:?}", event),
Err(_) => break false,
}
};
assert!(resolved);
d.shutdown().unwrap();
}
#[test]
fn test_hostname_resolution_case_insensitive() {
let d = ServiceDaemon::new().expect("Failed to create daemon");
let hostname = "My_casE_HOST.local.";
let service_ip_addr: ScopedIp = my_ip_interfaces()
.iter()
.find(|iface| iface.ip().is_ipv4())
.map(|iface| iface.into())
.unwrap();
let my_service = ServiceInfo::new(
"_host_case_test._tcp.local.",
"my_instance",
hostname,
&[service_ip_addr.to_ip_addr()] as &[IpAddr],
1234,
None,
)
.expect("invalid service info");
d.register(my_service).unwrap();
// Verify that lowercase hostname resolves correctly.
let hostname_lower = hostname.to_lowercase();
let event_receiver = d.resolve_hostname(&hostname_lower, Some(2000)).unwrap();
let resolved = loop {
match event_receiver.recv() {
Ok(HostnameResolutionEvent::AddressesFound(found_hostname, addresses)) => {
assert!(found_hostname == hostname);
assert!(addresses.contains(&service_ip_addr));
break true;
}
Ok(HostnameResolutionEvent::SearchStopped(_)) => break false,
Ok(_event) => {}
Err(_) => break false,
}
};
assert!(resolved);
// Verify that any-case hostname resolves correctly.
let hostname_other = "MY_CASe_hOST.local.";
let event_receiver = d.resolve_hostname(hostname_other, Some(2000)).unwrap();
let resolved = loop {
match event_receiver.recv() {
Ok(HostnameResolutionEvent::AddressesFound(found_hostname, addresses)) => {
assert!(found_hostname == hostname);
assert!(addresses.contains(&service_ip_addr));
break true;
}
Ok(HostnameResolutionEvent::SearchStopped(_)) => break false,
Ok(_event) => {}
Err(_) => break false,
}
};
assert!(resolved);
d.shutdown().unwrap();
}
#[test]
fn hostname_resolution_timeout() {
let d = ServiceDaemon::new().expect("Failed to create daemon");
let hostname = "nonexistent._tcp.local.";
let before = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("failed to get current UNIX time")
.as_millis() as u64;
let event_receiver = d.resolve_hostname(hostname, Some(2000)).unwrap();
let resolved = loop {
match event_receiver.recv() {
Ok(HostnameResolutionEvent::AddressesFound(found_hostname, _addresses)) => {
assert!(found_hostname == hostname);
break true;
}
Ok(HostnameResolutionEvent::SearchTimeout(_)) => break false,
Ok(event) => println!("Received event {:?}", event),
Err(_) => break false,
}
};
let after = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("failed to get current UNIX time")
.as_millis() as u64;
assert!(!resolved);
println!("Time spent resolving: {} ms", after - before);
assert!(after - before >= 2000 - 5);
assert!(after - before < 2000 + 1000);
d.shutdown().unwrap();
}
#[test]
fn test_cache_flush_record() {
// Create a daemon
let server = ServiceDaemon::new().expect("Failed to create server");
let service = "_test_cache_ptr._udp.local.";
let host_name = "my_host_tmp_cache_flush.local.";
// use a single IPv4 addr
let mut service_ip_addr = my_ip_interfaces()
.iter()
.find(|iface| iface.ip().is_ipv4())
.map(|iface| iface.ip())
.unwrap();
let port = 5201;
let properties = [("key", "value")];
let mut my_service = ServiceInfo::new(
service,
"my_instance",
host_name,
service_ip_addr,
port,
&properties[..],
)
.expect("invalid service info");
let result = server.register(my_service.clone());
assert!(result.is_ok());
// Browse for a service
let client = ServiceDaemon::new().expect("Failed to create client");
let browse_chan = client.browse(service).unwrap();
let timeout = Duration::from_secs(2);
let mut resolved = false;
while let Ok(event) = browse_chan.recv_timeout(timeout) {
match event {
ServiceEvent::ServiceResolved(info) => {
resolved = true;
timed_println(format!("Resolved a service of {}", &info.get_fullname()));
timed_println(format!("JLN service: {:?}", info));
break;
}
e => {
println!("Received event {:?}", e);
}
}
}
assert!(resolved);
// Stop browsing for a moment.
client.stop_browse(service).unwrap();
sleep(Duration::from_secs(2)); // Let the cache record be surely older than 1 second.
// Modify the IPv4 address for the service.
if let IpAddr::V4(ipv4) = service_ip_addr {
let bytes = ipv4.octets();
service_ip_addr = IpAddr::V4(Ipv4Addr::new(bytes[0], bytes[1], bytes[2], bytes[3] + 1));
} else {
panic!();
}
// Re-register the service to update the IPv4 addr.
my_service = ServiceInfo::new(
service,
"my_instance",
host_name,
service_ip_addr,
port,
&properties[..],
)
.unwrap();
let result = server.register(my_service);
assert!(result.is_ok());
timed_println(format!(
"Re-registered with updated IPv4 addr: {}",
&service_ip_addr
));
// Wait for the new registration sent out and cache flushed.
sleep(Duration::from_secs(2));
// Browse for the updated IPv4 address.
let browse_chan = client.browse(service).unwrap();
resolved = false;
while let Ok(event) = browse_chan.recv_timeout(timeout) {
match event {
ServiceEvent::ServiceResolved(info) => {
// Verify the address flushed and updated.
let new_addrs = info.get_addresses();
timed_println(format!("new address resolved: {:?}", new_addrs));
if new_addrs.len() == 1 {
let first_addr = new_addrs.iter().next().unwrap();
assert_eq!(&first_addr.to_ip_addr(), &service_ip_addr);
resolved = true;
break;
}
}
e => {
timed_println(format!("Received event {:?}", e));
}
}
}
assert!(resolved);
server.shutdown().unwrap();
client.shutdown().unwrap();
}
#[test]
fn test_cache_flush_remove_one_addr() {
// Create a daemon
let server = ServiceDaemon::new().expect("Failed to create server");
let service = "_remove_one_addr._udp.local.";
let host_name = "remove_one_addr_host.local.";
// Get a single IPv4 address
let ip_addr1 = my_ip_interfaces()
.iter()
.find(|iface| iface.ip().is_ipv4())
.map(|iface| iface.ip())
.unwrap();
// Make 2nd IPv4 address for the service.
let ip_addr2 = match ip_addr1 {
IpAddr::V4(ipv4) => {
let bytes = ipv4.octets();
IpAddr::V4(Ipv4Addr::new(bytes[0], bytes[1], bytes[2], bytes[3] + 1))
}
_ => {
panic!()
}
};
let port = 5201;
let mut my_service = ServiceInfo::new(
service,
"my_instance",
host_name,
&[ip_addr1, ip_addr2][..],
port,
None,
)
.expect("invalid service info");
let result = server.register(my_service.clone());
assert!(result.is_ok());
// Browse for a service
let client = ServiceDaemon::new().expect("Failed to create client");
let browse_chan = client.browse(service).unwrap();
let timeout = Duration::from_secs(2);
let mut resolved = false;
while let Ok(event) = browse_chan.recv_timeout(timeout) {
match event {
ServiceEvent::ServiceResolved(info) => {
resolved = true;
println!("Resolved a service of {}", &info.get_fullname());
break;
}
e => {
println!("Received event {:?}", e);
}
}
}
assert!(resolved);
// Stop browsing for a moment.
client.stop_browse(service).unwrap();
sleep(Duration::from_secs(2)); // Wait 1 more second for the 2nd announcement
// Re-register the service to have only 1 addr.
my_service = ServiceInfo::new(service, "my_instance", host_name, ip_addr1, port, None).unwrap();
let result = server.register(my_service.clone());
assert!(result.is_ok());
println!("Re-registered with updated IPv4 addr");
// Wait for the new registration sent out and cache flushed.
sleep(Duration::from_secs(2));
// Browse for the updated IPv4 address.
let browse_chan = client.browse(service).unwrap();
resolved = false;
while let Ok(event) = browse_chan.recv_timeout(timeout) {
match event {
ServiceEvent::ServiceResolved(info) => {
// Verify the address flushed and updated.
let new_addrs = info.get_addresses();
if new_addrs.len() == 1 {
let first_addr = new_addrs.iter().next().unwrap();
assert_eq!(&first_addr.to_ip_addr(), &ip_addr1);
resolved = true;
break;
}
}
e => {
println!("Received event {:?}", e);
}
}
}
assert!(resolved);
server.shutdown().unwrap();
client.shutdown().unwrap();
}
/// Test to verify that the cache flush of SRV records
/// do not remove the service instance.
#[test]
fn test_cache_flush_srv() {
// Create a daemon
let server = ServiceDaemon::new().expect("Failed to create server");
let service = "_test_cache_srv._udp.local.";
let old_host_name = "old_srv_host.local.";
let new_host_name = "new_srv_host.local.";
// Use a single IPv4 address
let service_ip_addr = my_ip_interfaces()
.iter()
.find(|iface| iface.ip().is_ipv4())
.map(|iface| iface.ip())
.unwrap();
let port = 5201;
let properties = [("key", "value")];
let mut my_service = ServiceInfo::new(
service,
"my_instance",
old_host_name,
service_ip_addr,
port,
&properties[..],
)
.expect("invalid service info");
let result = server.register(my_service.clone());
assert!(result.is_ok());
// Browse for the service
let client = ServiceDaemon::new().expect("Failed to create client");
let browse_chan = client.browse(service).unwrap();
let timeout = Duration::from_secs(2);
let mut resolved = false;
while let Ok(event) = browse_chan.recv_timeout(timeout) {
match event {
ServiceEvent::ServiceResolved(info) => {
resolved = true;
assert_eq!(info.get_hostname(), old_host_name);
break;
}
e => {
println!("Received event {:?}", e);
}
}
}
assert!(resolved);
sleep(Duration::from_secs(2)); // Let the cache record be older than 1 second
// Re-register the service with a new host
my_service = ServiceInfo::new(
service,
"my_instance",
new_host_name,
service_ip_addr,
port,
&properties[..],
)
.unwrap();
let result = server.register(my_service);
assert!(result.is_ok());
println!("Re-registered with updated SRV host: {}", new_host_name);
// Wait for the new registration to be sent out and cache flushed
sleep(Duration::from_secs(2));
// Browse for the updated SRV record
let timeout = Duration::from_secs(2);
let mut removed = false;
while let Ok(event) = browse_chan.recv_timeout(timeout) {
println!("Received event {:?}", event);
match event {
ServiceEvent::ServiceRemoved(ty_domain, instance_name) => {
// Verify the SRV host was flushed and updated
assert_eq!(ty_domain, service);
assert!(instance_name.starts_with("my_instance"));
removed = true;
}
ServiceEvent::ServiceResolved(info) => {
assert_eq!(info.get_hostname(), new_host_name);
resolved = true;
}
_e => {}
}
}
assert!(!removed);
assert!(resolved);
server.shutdown().unwrap();
client.shutdown().unwrap();
}
#[test]
fn test_known_answer_suppression() {
// Create a daemon
let mdns_server = ServiceDaemon::new().expect("Failed to create mdns server");
// Register a service
let ty_domain = "_known-answer._udp.local.";
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap();
let instance_name = now.as_micros().to_string(); // Create a unique name.
// Get a single IPv4 address
let ip_addr1 = my_ip_interfaces()
.iter()
.find(|iface| iface.ip().is_ipv4())
.map(|iface| iface.ip())
.unwrap();
let host_name = "known_answer_server.local.";
let port = 5200;
// Publish the service
let my_service = ServiceInfo::new(ty_domain, &instance_name, host_name, ip_addr1, port, None)
.expect("valid service info");
mdns_server
.register(my_service)
.expect("Failed to register my service");
// Browse the service
let client = ServiceDaemon::new().expect("Failed to create mdns client");
let browse_chan = client.browse(ty_domain).unwrap();
let timeout = Duration::from_secs(2);
let mut resolved = false;
while let Ok(event) = browse_chan.recv_timeout(timeout) {
match event {
ServiceEvent::ServiceResolved(info) => {
resolved = true;
println!("Resolved a service of {}", &info.get_fullname());
break;
}
other => {
println!("Received event {:?}", other);
}
}
}
assert!(resolved);
// Browse again to trigger Known Answer Suppression for sure.
let browse_chan = client.browse(ty_domain).unwrap();
resolved = false;
while let Ok(event) = browse_chan.recv_timeout(timeout) {
if let ServiceEvent::ServiceResolved(info) = event {
resolved = true;
println!("Resolved a service of {}", &info.get_fullname());
break;
}
}
assert!(resolved);
// Give the server daemon chances to handle the browse query again.
sleep(Duration::from_secs(1));
// Verify Known Answer Suppression happened.
let metrics_receiver = mdns_server.get_metrics().unwrap();
let metrics = metrics_receiver.recv().unwrap();
println!("metrics: {:?}", &metrics);
assert!(metrics["known-answer-suppression"] > 0);
}
#[test]
fn test_domain_suffix_in_browse() {
let mdns_client = ServiceDaemon::new().expect("failed to create mDNS client");
assert!(mdns_client.browse("_service-name._tcp.local").is_err());
assert!(mdns_client.browse("_service-name._tcp.local.").is_ok());
mdns_client.shutdown().unwrap();
}
#[test]
fn test_name_conflict_resolution() {
// This test registers two services using the same names, but different IP addresses.
let ty_domain = "_conflict-test._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 = "conflict_host.local.";
let port = 5200;
// Register the first service.
let server1 = ServiceDaemon::new().expect("failed to start server1");
// 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 server1
let service1 = ServiceInfo::new(ty_domain, &instance_name, host_name, ip_addr1, port, None)
.expect("valid service info");
server1
.register(service1)
.expect("Failed to register service1");
// wait for the service announced.
sleep(Duration::from_secs(1));
// Register the second service.
let server2 = ServiceDaemon::new().expect("failed to start server2");
// Modify the IPv4 address for the service.
let IpAddr::V4(ipv4) = ip_addr1 else {
panic!();
};
let bytes = ipv4.octets();
let ip_addr2 = IpAddr::V4(Ipv4Addr::new(bytes[0], bytes[1], bytes[2], bytes[3] + 1));
let service2 = ServiceInfo::new(ty_domain, &instance_name, host_name, ip_addr2, port, None)
.expect("failed to create ServiceInfo for service2");
server2
.register(service2)
.expect("failed to register service2");
// Verify name change event for the second service, due to the name conflict.
let server2_monitor = server2.monitor().unwrap();
let timeout = Duration::from_secs(2);
let mut name_changed = false;
while let Ok(event) = server2_monitor.recv_timeout(timeout) {
match event {
DaemonEvent::NameChange(change) => {
println!("server2 daemon event: {:?}", change);
name_changed = true;
break;
}
other => println!("server2 other event: {:?}", other),
}
}
assert!(name_changed);
// Verify both services are resolved.
let client = ServiceDaemon::new().expect("failed to create mdns client");
let receiver = client.browse(ty_domain).unwrap();
let timeout = Duration::from_secs(3);
let mut service_names = HashSet::new();
while let Ok(event) = receiver.recv_timeout(timeout) {
if let ServiceEvent::ServiceResolved(info) = event {
println!(
"Resolved a service: {} host {} IP {:?}",
info.get_fullname(),
info.get_hostname(),
info.get_addresses_v4()
);
service_names.insert(info.get_fullname().to_string());
// Find and verify name conflict resolution.
if info.get_fullname().contains("(2)") {
assert_eq!(info.get_hostname(), "conflict_host-2.local.");
}
// Stop the wait if both are resolved.
if service_names.len() == 2 {
break;
}
}
}
// Verify that we have resolve two services instead of one.
assert_eq!(service_names.len(), 2);
}
#[test]
fn test_name_tiebreaking() {
// This test registers two services using the same names, but different IP addresses,
// same as `test_name_conflict_resolution`, the only difference being that two servers
// do the probing at the same time. Hence tiebreaking. Server2 should win.
let ty_domain = "_tiebreaking._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 = "tiebreaking_host.local.";
let port = 5200;
// Register the first service.
let server1 = ServiceDaemon::new().expect("failed to start server1");
// 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 server1
let service1 = ServiceInfo::new(ty_domain, &instance_name, host_name, ip_addr1, port, None)
.expect("valid service info");
server1
.register(service1)
.expect("Failed to register service1");
// Register the second service immediately to trigger tiebreaking.
let server2 = ServiceDaemon::new().expect("failed to start server2");
// Modify the IPv4 address for the service.
let IpAddr::V4(ipv4_2) = ip_addr1 else {
panic!();
};
let bytes = ipv4_2.octets();
let ip_addr2 = IpAddr::V4(Ipv4Addr::new(bytes[0], bytes[1], bytes[2], bytes[3] + 1));
let service2 = ServiceInfo::new(ty_domain, &instance_name, host_name, ip_addr2, port, None)
.expect("failed to create ServiceInfo for service2");
server2
.register(service2)
.expect("failed to register service2");
// Verify name change event for the first service, per tiebreaking rules.
// Timeout is set to 3 seconds for:
// - Initial probing (750ms)
// - 1 second wait after LOST the tiebreaking
// - New probing (750ms)
// Total: 2.5s + some margin => 3s
let server1_monitor = server1.monitor().unwrap();
let timeout = Duration::from_secs(3);
let mut name_changed = false;
while let Ok(event) = server1_monitor.recv_timeout(timeout) {
match event {
DaemonEvent::NameChange(change) => {
println!("server1 daemon event: {:?}", change);
name_changed = true;
break;
}
other => println!("server1 other event: {:?}", other),
}
}
assert!(name_changed);
// Verify both services are resolved.
let client = ServiceDaemon::new().expect("failed to create mdns client");
let receiver = client.browse(ty_domain).unwrap();
let timeout = Duration::from_secs(3);
let mut resolved_services = vec![];
while let Ok(event) = receiver.recv_timeout(timeout) {
if let ServiceEvent::ServiceResolved(info) = event {
println!(
"Resolved a service: {} host {} IP {:?}",
info.get_fullname(),
info.get_hostname(),
info.get_addresses_v4()
);
resolved_services.push(info);
if resolved_services.len() == 2 {
break;
}
}
}
// Verify that we have resolve two services instead of one.
assert_eq!(resolved_services.len(), 2);
// Verify that server2 (its ip_addr2) won the tiebreaking for the hostname.
for resolved_service in resolved_services {
if resolved_service.get_hostname() == host_name {
let service_addr = resolved_service.get_addresses().iter().next().unwrap();
assert_eq!(&service_addr.to_ip_addr(), &ip_addr2);
println!("server2 won the tiebreaking");
}
}
}
#[test]
fn test_name_conflict_3() {
// Similar to `test_name_conflict_resolution` but with 3 servers.
let ty_domain = "_conflict-3._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 = "conflict3_host.local.";
let port = 5200;
// Register the first service.
let server1 = ServiceDaemon::new().expect("failed to start server1");
// 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 server1
let service1 = ServiceInfo::new(ty_domain, &instance_name, host_name, ip_addr1, port, None)
.expect("valid service info");
server1
.register(service1)
.expect("Failed to register service1");
// wait for the service announced.
sleep(Duration::from_secs(1));
// Register the second service.
let server2 = ServiceDaemon::new().expect("failed to start server2");
// Modify the IPv4 address for the service.
let IpAddr::V4(ipv4) = ip_addr1 else {
panic!();
};
let bytes = ipv4.octets();
let ip_addr2 = IpAddr::V4(Ipv4Addr::new(bytes[0], bytes[1], bytes[2], bytes[3] + 1));
let info2 = ServiceInfo::new(ty_domain, &instance_name, host_name, ip_addr2, port, None)
.expect("failed to create ServiceInfo for service2");
server2
.register(info2)
.expect("failed to register service2");
// Verify name change event for the second service, due to the name conflict.
let server2_monitor = server2.monitor().unwrap();
let timeout = Duration::from_secs(2);
let mut name_changed = false;
while let Ok(event) = server2_monitor.recv_timeout(timeout) {
match event {
DaemonEvent::NameChange(change) => {
println!("server2 daemon event: {:?}", change);
name_changed = true;
}
other => println!("server2 other event: {:?}", other),
}
}
assert!(name_changed);
// Register the third service
let server3 = ServiceDaemon::new().expect("failed to start server2");
// Modify the IPv4 address for the service.
let ip_addr3 = IpAddr::V4(Ipv4Addr::new(bytes[0], bytes[1], bytes[2], bytes[3] + 2));
let info3 = ServiceInfo::new(ty_domain, &instance_name, host_name, ip_addr3, port, None)
.expect("failed to create ServiceInfo for service2");
server3
.register(info3)
.expect("failed to register service2");
let server3_monitor = server3.monitor().unwrap();
let timeout = Duration::from_secs(3);
name_changed = false;
while let Ok(event) = server3_monitor.recv_timeout(timeout) {
match event {
DaemonEvent::NameChange(change) => {
println!("server3 daemon event: {:?}", change);
name_changed = true;
break;
}
other => println!("server3 other event: {:?}", other),
}
}
assert!(name_changed);
// Verify all services are resolved.
let client = ServiceDaemon::new().expect("failed to create mdns client");
let receiver = client.browse(ty_domain).unwrap();
let timeout = Duration::from_secs(3);
let mut service_names = HashSet::new();
while let Ok(event) = receiver.recv_timeout(timeout) {
if let ServiceEvent::ServiceResolved(info) = event {
println!(
"Resolved a service: {} host {} IP {:?}",
info.get_fullname(),
info.get_hostname(),
info.get_addresses_v4()
);
service_names.insert(info.get_fullname().to_string());
if service_names.len() >= 3 {
break;
}
}
}
// Verify that we have resolve two services instead of one.
assert_eq!(service_names.len(), 3);
}
#[test]
fn test_verify_srv() {
// start a server
let ty_domain = "_verify-srv._udp.local.";
let host_name = "verify_srv.local.";
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap();
let instance_name = now.as_micros().to_string(); // Create a unique name.
let port = 5200;
// Get a single IPv4 address
let ip_addr1 = my_ip_interfaces()
.iter()
.find(|iface| iface.ip().is_ipv4())
.map(|iface| iface.ip())
.unwrap();
// Register the service.
let service1 = ServiceInfo::new(ty_domain, &instance_name, host_name, ip_addr1, port, None)
.expect("valid service info");
let fullname = service1.get_fullname().to_string();
let server1 = ServiceDaemon::new().expect("failed to start server");
server1
.register(service1)
.expect("Failed to register service1");
// wait for the service announced.
sleep(Duration::from_secs(1));
// start a client
let client = ServiceDaemon::new().expect("failed to start client");
let receiver = client.browse(ty_domain).unwrap();
let timeout = Duration::from_secs(2);
while let Ok(event) = receiver.recv_timeout(timeout) {
if let ServiceEvent::ServiceResolved(info) = event {
println!("service resolved: {:?}", info);
break;
}
}
// kill the server without unregister (i.e. not-graceful-shutdown)
server1.shutdown().unwrap();
sleep(Duration::from_secs(1));
// check `ServiceRemoved`
client.verify(fullname, Duration::from_secs(3)).unwrap();
let timeout = Duration::from_secs(4);
let mut service_removal = false;
while let Ok(event) = receiver.recv_timeout(timeout) {
if let ServiceEvent::ServiceRemoved(service_type, fullname) = event {
service_removal = true;
println!("service removed: {service_type} : {fullname}");
break;
}
}
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");
// 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);
while let Ok(event) = receiver.recv_timeout(timeout) {
if let ServiceEvent::ServiceResolved(info) = event {
println!(
"Resolved a service: {} host {} IP {:?}",
info.get_fullname(),
info.get_hostname(),
info.get_addresses_v4()
);
resolved = true;
break;
}
}
assert!(!resolved);
// 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) {
if let ServiceEvent::ServiceResolved(info) = event {
println!(
"Resolved a service: {} host {} IP {:?}",
info.get_fullname(),
info.get_hostname(),
info.get_addresses_v4()
);
resolved = true;
break;
}
}
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 IPv6 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) {
if let ServiceEvent::ServiceResolved(info) = event {
println!(
"Resolved a service: {} host {} IP {:?}",
info.get_fullname(),
info.get_hostname(),
info.get_addresses()
);
resolved = true;
break;
}
}
assert!(!resolved);
// 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) {
if let ServiceEvent::ServiceResolved(info) = event {
println!(
"Resolved a service: {} host {} IP {:?}",
info.get_fullname(),
info.get_hostname(),
info.get_addresses()
);
resolved = true;
break;
}
}
assert!(resolved);
}
#[test]
fn test_set_ip_check_interval() {
// Create a daemon
let server = ServiceDaemon::new().expect("Failed to create server");
let service = "_ip_check._udp.local.";
let host_name = "test_ip_check_host.local.";
// use a single IPv4 addr
let service_ip_addr = my_ip_interfaces()
.iter()
.find(|iface| iface.ip().is_ipv4())
.map(|iface| iface.ip())
.unwrap();
let port = 5201;
let my_service = ServiceInfo::new(
service,
"my_instance",
host_name,
service_ip_addr,
port,
None,
)
.expect("invalid service info");
let result = server.register(my_service.clone());
assert!(result.is_ok());
// Set the IP check interval.
server.set_ip_check_interval(3).unwrap();
let interval = server.get_ip_check_interval().unwrap();
assert_eq!(interval, 3);
server.set_ip_check_interval(u32::MAX).unwrap();
let interval = server.get_ip_check_interval().unwrap();
assert_eq!(interval, u32::MAX);
server.set_ip_check_interval(0).unwrap();
let interval = server.get_ip_check_interval().unwrap();
assert_eq!(interval, 0);
server.shutdown().unwrap();
}
#[test]
fn test_rfc6763_escaping() {
// Test RFC 6763 Section 4.3: dots and backslashes must be escaped
let instance_name = "My.Path\\Service";
let service_type = "_rfc6763._tcp.local.";
let service = ServiceInfo::new(
service_type,
instance_name,
"rfc6763.local.",
"",
5555,
None,
)
.expect("Failed to create service");
// Verify escaping: dots become \. and backslashes become \\
let fullname = service.get_fullname();
println!("Escaping test: {}", fullname);
assert!(
fullname.contains("My\\.Path\\\\Service"),
"Dots and backslashes should be escaped"
);
}
#[test]
fn test_rfc6763_utf8_support() {
// Test RFC 6763 Section 4.1.1: UTF-8 support including emojis
// Also verify UTF-8 works with escaping (dots and backslashes)
let service1 = ServiceInfo::new(
"_utf8._tcp.local.",
"mdns.lib 🌐",
"test.local.",
"",
80,
None,
)
.expect("Failed to create service with emojis");
assert!(service1.get_fullname().contains("mdns\\.lib 🌐"));
// UTF-8 + escaping: "Café €.v1\2024"
let service2 = ServiceInfo::new(
"_utf8escape._tcp.local.",
"Café €.v1\\2024",
"test.local.",
"",
80,
None,
)
.expect("Failed to create service with UTF-8 and escapes");
let fullname = service2.get_fullname();
println!("UTF-8 + escaping fullname: {}", fullname);
assert!(
fullname.contains("Café €\\.v1\\\\2024"),
"UTF-8 preserved, dots and backslashes escaped"
);
}
#[test]
fn test_rfc6763_utf8_network_integration() {
// Test that UTF-8 with dots and backslashes works end-to-end over the network
// This service will be registered and browsed to verify full functionality
// Create a daemon
let d = ServiceDaemon::new().expect("Failed to create daemon");
// Register a service with UTF-8, dots, and backslashes
let ty_domain = "_utf8-network._tcp.local.";
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap();
let instance_base = now.as_micros().to_string();
// Instance name with UTF-8 characters (emoji, accents), dots, and backslashes
let instance_name = format!("Café.Service\\{} 🌐", instance_base);
let host_name = "utf8_network_host.local.";
let port = 5203;
let my_service = ServiceInfo::new(ty_domain, &instance_name, host_name, "", port, None)
.expect("valid service info")
.enable_addr_auto();
let fullname = my_service.get_fullname().to_string();
println!("Registered service fullname: {}", &fullname);
// Verify the fullname has proper escaping
assert!(fullname.contains("Café\\.Service\\\\"));
assert!(fullname.contains('🌐'));
d.register(my_service)
.expect("Failed to register our service");
// Browse for the service
let browse_chan = d.browse(ty_domain).unwrap();
let timeout = Duration::from_secs(3);
let mut resolved = false;
while let Ok(event) = browse_chan.recv_timeout(timeout) {
match event {
ServiceEvent::ServiceResolved(info) => {
let resolved_fullname = info.get_fullname();
println!("Resolved service: {}", resolved_fullname);
// Compare by checking if the resolved fullname contains our instance base
// and UTF-8 characters (network returns decoded names without escaping)
if resolved_fullname.contains(&instance_base)
&& resolved_fullname.contains("Café")
&& resolved_fullname.contains('🌐')
&& resolved_fullname.contains(ty_domain)
{
resolved = true;
println!("Successfully found UTF-8 service over network!");
// Verify the service details
assert_eq!(info.get_port(), port);
assert!(!info.get_addresses().is_empty());
// Verify UTF-8 characters are preserved
assert!(resolved_fullname.contains("Café.Service"));
assert!(resolved_fullname.contains('🌐'));
break;
}
}
ServiceEvent::SearchStarted(_) => {
println!("Search started for {}", ty_domain);
}
ServiceEvent::ServiceFound(_, found_fullname) => {
println!("Service found: {}", found_fullname);
}
_ => {}
}
}
assert!(
resolved,
"UTF-8 service with dots and backslashes should be resolved over the network"
);
d.shutdown().unwrap();
}
#[test]
fn test_interface_id_get_addrs() {
// Get actual interfaces from the OS to build a valid InterfaceId.
let if_addrs = if_addrs::get_if_addrs().expect("failed to get interfaces");
let first = if_addrs.first().expect("no interfaces found");
let intf_id = InterfaceId::from(first);
let addrs = intf_id.get_addrs();
assert!(
!addrs.is_empty(),
"interface {} should have at least one address",
intf_id.name
);
assert!(
addrs.contains(&first.ip()),
"get_addrs() should contain the address we constructed from: {}",
first.ip()
);
}
/// A helper function to include a timestamp for println.
fn timed_println(msg: String) {
let now = SystemTime::now();
let formatted_time = humantime::format_rfc3339(now);
println!("[{}] {}", formatted_time, msg);
}
#[test]
fn test_goodbye_uses_conflict_resolved_name() {
// When probing renames a service due to a conflict, unregistering it must
// send the goodbye records under the renamed (cached-by-peers) name, so
// that browsers drop the entry immediately instead of waiting out TTLs.
let ty_domain = "_conflict-bye._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 = "conflict_bye_host.local.";
let port = 5200;
// Register the first service.
let server1 = ServiceDaemon::new().expect("failed to start server1");
// Get a single IPv4 address
let ip_addr1 = my_ip_interfaces()
.iter()
.find(|iface| iface.ip().is_ipv4())
.map(|iface| iface.ip())
.unwrap();
let service1 = ServiceInfo::new(ty_domain, &instance_name, host_name, ip_addr1, port, None)
.expect("valid service info");
server1
.register(service1)
.expect("Failed to register service1");
// wait for the service announced.
sleep(Duration::from_secs(1));
// Register the second service with the same names to force a conflict.
let server2 = ServiceDaemon::new().expect("failed to start server2");
let server2_monitor = server2.monitor().unwrap();
let IpAddr::V4(ipv4) = ip_addr1 else {
panic!();
};
let bytes = ipv4.octets();
let ip_addr2 = IpAddr::V4(Ipv4Addr::new(
bytes[0],
bytes[1],
bytes[2],
bytes[3] % 254 + 1,
));
let service2 = ServiceInfo::new(ty_domain, &instance_name, host_name, ip_addr2, port, None)
.expect("failed to create ServiceInfo for service2");
let service2_fullname = service2.get_fullname().to_string();
server2
.register(service2)
.expect("failed to register service2");
// Wait for the conflict to be resolved by renaming service2.
let timeout = Duration::from_secs(2);
let mut renamed_instance = None;
while let Ok(event) = server2_monitor.recv_timeout(timeout) {
match event {
DaemonEvent::NameChange(change) => {
println!("server2 name change: {:?}", change);
if change.rr_type == RRType::SRV {
renamed_instance = Some(change.new_name);
break;
}
}
other => println!("server2 other event: {:?}", other),
}
}
let renamed_instance = renamed_instance.expect("service2 was not renamed");
// Browse until the renamed instance is resolved.
let client = ServiceDaemon::new().expect("failed to create mdns client");
let receiver = client.browse(ty_domain).unwrap();
let timeout = Duration::from_secs(3);
let mut resolved = false;
while let Ok(event) = receiver.recv_timeout(timeout) {
if let ServiceEvent::ServiceResolved(info) = event {
println!("Resolved a service: {}", info.get_fullname());
if info.get_fullname() == renamed_instance {
resolved = true;
break;
}
}
}
assert!(resolved, "the renamed instance was not resolved");
// Unregister service2 (by its original fullname) and verify the client
// sees the renamed instance removed via the goodbye packets, well before
// any record TTL could expire.
let receiver2 = server2
.unregister(&service2_fullname)
.expect("failed to unregister service2");
let status = receiver2.recv_timeout(Duration::from_secs(2)).unwrap();
println!("unregister status: {:?}", status);
let timeout = Duration::from_secs(5);
let mut removed = false;
while let Ok(event) = receiver.recv_timeout(timeout) {
if let ServiceEvent::ServiceRemoved(_ty, fullname) = event {
println!("Removed a service: {fullname}");
if fullname == renamed_instance {
removed = true;
break;
}
}
}
assert!(removed, "no goodbye seen for the renamed instance");
server1.shutdown().unwrap();
server2.shutdown().unwrap();
client.shutdown().unwrap();
}