use if_addrs::{IfAddr, Interface};
use mdns_sd::{
DaemonEvent, DaemonStatus, HostnameResolutionEvent, IfKind, InterfaceId, IntoTxtProperties,
ScopedIp, ServiceDaemon, ServiceEvent, ServiceInfo, TxtProperty, UnregisterStatus,
};
use std::collections::{HashMap, HashSet};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::thread::sleep;
use std::time::{Duration, SystemTime};
use test_log::test;
#[test]
fn integration_success() {
let d = ServiceDaemon::new().expect("Failed to create daemon");
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();
let all_interfaces = my_ip_interfaces();
println!("all interfaces count: {}", all_interfaces.len());
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,
};
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");
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())
);
}
_ => {}
}
}
assert_eq!(addr_count, my_addrs_count);
assert_eq!(resolved_ips.len(), my_addrs_count);
assert!(!resolved_ips.is_empty());
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"
);
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);
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);
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);
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");
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);
if fullname == service2_type {
break;
}
}
e => {
println!("Received event {:?}", e);
sleep(Duration::from_millis(100));
}
},
Err(e) => {
panic!("browse error: {}", e);
}
}
}
d.shutdown().unwrap();
}
#[test]
fn service_without_properties_with_alter_net_v4() {
let d = ServiceDaemon::new().expect("Failed to create daemon");
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(); 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);
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()
);
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); 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() {
let d = ServiceDaemon::new().expect("Failed to create daemon");
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(); 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);
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()
);
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; }
assert_eq!(addrs.len(), 1); 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() {
let domain = "_serv-properties._tcp.local.";
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap();
let instance_name = now.as_micros().to_string(); 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);
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()));
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;
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"));
}
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('='));
}
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() {
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()));
let invalid_vec: Vec<u8> = vec![200, 200]; 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();
assert_eq!(property_map.get("key1"), Some(&"".to_string()));
assert_eq!(property_map.len(), 1);
}
#[test]
fn test_into_txt_properties() {
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())
);
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]
fn service_with_named_interface_only() {
let d = ServiceDaemon::new().expect("Failed to create daemon");
d.disable_interface(IfKind::All).unwrap();
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();
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);
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();
println!("Enable interface with name {}", &if_name);
d.enable_interface(&if_name).unwrap();
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() {
let d = ServiceDaemon::new().expect("Failed to create daemon");
d.disable_interface(IfKind::IPv6).unwrap();
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());
let browse_chan = d.browse(service_ipv4_only).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
);
assert!(!info.get_addresses().is_empty());
for addr in info.get_addresses().iter() {
assert!(addr.is_ipv4());
}
}
e => {
println!("Received event {:?}", e);
}
}
}
assert!(resolved);
d.shutdown().unwrap();
}
#[test]
fn service_ipv6_link_local_only() {
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());
let browse_chan = d.browse(service_ipv6_intf).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
);
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)
);
}
}
e => {
println!("Received event {:?}", e);
}
}
}
assert!(resolved);
d.shutdown().unwrap();
}
#[test]
fn test_disable_interface_cache() {
let server = ServiceDaemon::new().expect("Failed to create the server");
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");
let client = ServiceDaemon::new().expect("Failed to create the client");
sleep(Duration::from_secs(1));
println!("Disabling interface with IP: {:?}", ipv4_list);
client.disable_interface(ipv4_list).unwrap();
let handle = client.browse(ty_domain).unwrap();
let timeout = Duration::from_secs(1);
let mut resolved = false;
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;
}
}
assert!(!resolved);
server.shutdown().unwrap();
client.shutdown().unwrap();
}
#[test]
fn service_with_invalid_addr_v4() {
let d = ServiceDaemon::new().expect("Failed to create daemon");
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(); 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");
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();
assert!(!resolved);
}
#[test]
fn service_with_invalid_addr_v6() {
let d = ServiceDaemon::new().expect("Failed to create daemon");
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(); 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");
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();
assert!(!resolved);
}
#[test]
fn service_with_loopback_addr() {
let d = ServiceDaemon::new().expect("Failed to create daemon");
d.enable_interface(IfKind::LoopbackV4)
.expect("Failed to enable loopback interface");
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();
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");
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()
);
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!(
found_loopback,
"The service should include a loopback address"
);
}
#[test]
fn subtype() {
let d = ServiceDaemon::new().expect("Failed to create daemon");
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(); 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");
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();
}
#[test]
fn test_service_name_check() {
let server_daemon = ServiceDaemon::new().expect("Failed to create server daemon");
let monitor = server_daemon.monitor().unwrap();
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());
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)
}
server_daemon.set_service_name_len_max(30).unwrap();
let result = server_daemon.register(my_service);
assert!(result.is_ok());
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);
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");
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();
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> {
let test_port = fastrand::u16(8000u16..9000u16);
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) =>
{
match std::net::UdpSocket::bind((ifv4.ip, test_port)) {
Ok(_) => Some(i),
Err(e) => {
println!("failed to bind {}: {e}, skipped.", ifv4.ip);
None
}
}
}
IfAddr::V6(ifv6) =>
{
let mut sock = std::net::SocketAddrV6::new(ifv6.ip, test_port, 0, 0);
if i.is_link_local() {
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()
}
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()
}
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();
let receiver = mdns.status().unwrap();
let status = receiver.recv().unwrap();
assert!(matches!(status, DaemonStatus::Running));
let receiver = mdns.shutdown().unwrap();
let status = receiver.recv().unwrap();
println!("daemon status: {:?}", status);
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());
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();
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);
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() {
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.";
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());
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);
client.stop_browse(service).unwrap();
sleep(Duration::from_secs(2));
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!();
}
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
));
sleep(Duration::from_secs(2));
let browse_chan = client.browse(service).unwrap();
resolved = false;
while let Ok(event) = browse_chan.recv_timeout(timeout) {
match event {
ServiceEvent::ServiceResolved(info) => {
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() {
let server = ServiceDaemon::new().expect("Failed to create server");
let service = "_remove_one_addr._udp.local.";
let host_name = "remove_one_addr_host.local.";
let ip_addr1 = my_ip_interfaces()
.iter()
.find(|iface| iface.ip().is_ipv4())
.map(|iface| iface.ip())
.unwrap();
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());
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);
client.stop_browse(service).unwrap();
sleep(Duration::from_secs(2));
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");
sleep(Duration::from_secs(2));
let browse_chan = client.browse(service).unwrap();
resolved = false;
while let Ok(event) = browse_chan.recv_timeout(timeout) {
match event {
ServiceEvent::ServiceResolved(info) => {
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]
fn test_cache_flush_srv() {
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.";
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());
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));
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);
sleep(Duration::from_secs(2));
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) => {
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() {
let mdns_server = ServiceDaemon::new().expect("Failed to create mdns server");
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();
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;
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");
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);
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);
sleep(Duration::from_secs(1));
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() {
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(); let host_name = "conflict_host.local.";
let port = 5200;
let server1 = ServiceDaemon::new().expect("failed to start server1");
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");
sleep(Duration::from_secs(1));
let server2 = ServiceDaemon::new().expect("failed to start server2");
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");
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);
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 info.get_fullname().contains("(2)") {
assert_eq!(info.get_hostname(), "conflict_host-2.local.");
}
if service_names.len() == 2 {
break;
}
}
}
assert_eq!(service_names.len(), 2);
}
#[test]
fn test_name_tiebreaking() {
let ty_domain = "_tiebreaking._udp.local.";
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap();
let instance_name = now.as_micros().to_string(); let host_name = "tiebreaking_host.local.";
let port = 5200;
let server1 = ServiceDaemon::new().expect("failed to start server1");
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");
let server2 = ServiceDaemon::new().expect("failed to start server2");
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");
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);
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;
}
}
}
assert_eq!(resolved_services.len(), 2);
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() {
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(); let host_name = "conflict3_host.local.";
let port = 5200;
let server1 = ServiceDaemon::new().expect("failed to start server1");
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");
sleep(Duration::from_secs(1));
let server2 = ServiceDaemon::new().expect("failed to start server2");
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");
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);
let server3 = ServiceDaemon::new().expect("failed to start server2");
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);
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;
}
}
}
assert_eq!(service_names.len(), 3);
}
#[test]
fn test_verify_srv() {
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(); let port = 5200;
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");
let fullname = service1.get_fullname().to_string();
let server1 = ServiceDaemon::new().expect("failed to start server");
server1
.register(service1)
.expect("Failed to register service1");
sleep(Duration::from_secs(1));
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;
}
}
server1.shutdown().unwrap();
sleep(Duration::from_secs(1));
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(); let host_name = "loop_v4_host.local.";
let port = 5200;
let server = ServiceDaemon::new().expect("failed to start server");
server.set_multicast_loop_v4(false).unwrap();
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");
server
.register(service1)
.expect("Failed to register service1");
sleep(Duration::from_secs(1));
let mut resolved = false;
let client = ServiceDaemon::new().expect("failed to create mdns client");
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);
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(); let host_name = "loop_v6_host.local.";
let port = 5200;
let server = ServiceDaemon::new().expect("failed to start server");
server.set_multicast_loop_v6(false).unwrap();
let ip_addr1 = my_ip_interfaces()
.iter()
.find(|iface| iface.ip().is_ipv6())
.map(|iface| iface.ip())
.unwrap();
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");
sleep(Duration::from_secs(1));
let mut resolved = false;
let client = ServiceDaemon::new().expect("failed to create mdns client");
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);
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() {
let server = ServiceDaemon::new().expect("Failed to create server");
let service = "_ip_check._udp.local.";
let host_name = "test_ip_check_host.local.";
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());
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() {
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");
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() {
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 🌐"));
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() {
let d = ServiceDaemon::new().expect("Failed to create daemon");
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();
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);
assert!(fullname.contains("Café\\.Service\\\\"));
assert!(fullname.contains('🌐'));
d.register(my_service)
.expect("Failed to register our 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);
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!");
assert_eq!(info.get_port(), port);
assert!(!info.get_addresses().is_empty());
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() {
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()
);
}
fn timed_println(msg: String) {
let now = SystemTime::now();
let formatted_time = humantime::format_rfc3339(now);
println!("[{}] {}", formatted_time, msg);
}