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
This commit is contained in:
1 parent
49f8eef7b5
commit
4d5881d6b0
23 files changed
+18879
-41
No files matched your search
Vendored
+148
@@ -0,0 +1,148 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
};
|
||||
|
||||
use mdns_sd::AsIpAddrs;
|
||||
|
||||
#[test]
|
||||
fn test_addr_str() {
|
||||
assert_eq!(
|
||||
"127.0.0.1".as_ip_addrs(),
|
||||
Ok({
|
||||
let mut set = HashSet::new();
|
||||
set.insert(Ipv4Addr::new(127, 0, 0, 1).into());
|
||||
|
||||
set
|
||||
})
|
||||
);
|
||||
|
||||
let addr = "127.0.0.1".to_string();
|
||||
assert_eq!(
|
||||
addr.as_ip_addrs(),
|
||||
Ok({
|
||||
let mut set = HashSet::new();
|
||||
set.insert(Ipv4Addr::new(127, 0, 0, 1).into());
|
||||
|
||||
set
|
||||
})
|
||||
);
|
||||
|
||||
// verify that `&String` also works.
|
||||
assert_eq!(
|
||||
addr.as_ip_addrs(),
|
||||
Ok({
|
||||
let mut set = HashSet::new();
|
||||
set.insert(Ipv4Addr::new(127, 0, 0, 1).into());
|
||||
|
||||
set
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
"127.0.0.1,127.0.0.2".as_ip_addrs(),
|
||||
Ok({
|
||||
let mut set = HashSet::new();
|
||||
set.insert(Ipv4Addr::new(127, 0, 0, 1).into());
|
||||
set.insert(Ipv4Addr::new(127, 0, 0, 2).into());
|
||||
|
||||
set
|
||||
})
|
||||
);
|
||||
|
||||
let addr = "2001:db8::1".to_string();
|
||||
assert_eq!(
|
||||
addr.as_ip_addrs(),
|
||||
Ok({
|
||||
let mut set = HashSet::new();
|
||||
set.insert(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1).into());
|
||||
|
||||
set
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
"2001:db8::1,2001:db8::2".as_ip_addrs(),
|
||||
Ok({
|
||||
let mut set = HashSet::new();
|
||||
set.insert(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1).into());
|
||||
set.insert(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 2).into());
|
||||
|
||||
set
|
||||
})
|
||||
);
|
||||
|
||||
// verify that an empty string parsed into an empty set.
|
||||
assert_eq!("".as_ip_addrs(), Ok(HashSet::new()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_addr_slice() {
|
||||
assert_eq!(
|
||||
(&["127.0.0.1"][..]).as_ip_addrs(),
|
||||
Ok({
|
||||
let mut set = HashSet::new();
|
||||
set.insert(Ipv4Addr::new(127, 0, 0, 1).into());
|
||||
|
||||
set
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
(&["127.0.0.1", "127.0.0.2"][..]).as_ip_addrs(),
|
||||
Ok({
|
||||
let mut set = HashSet::new();
|
||||
set.insert(Ipv4Addr::new(127, 0, 0, 1).into());
|
||||
set.insert(Ipv4Addr::new(127, 0, 0, 2).into());
|
||||
|
||||
set
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
(&vec!["127.0.0.1", "127.0.0.2"][..]).as_ip_addrs(),
|
||||
Ok({
|
||||
let mut set = HashSet::new();
|
||||
set.insert(Ipv4Addr::new(127, 0, 0, 1).into());
|
||||
set.insert(Ipv4Addr::new(127, 0, 0, 2).into());
|
||||
|
||||
set
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
(&vec!["2001:db8::1", "2001:db8::2"][..]).as_ip_addrs(),
|
||||
Ok({
|
||||
let mut set = HashSet::new();
|
||||
set.insert(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1).into());
|
||||
set.insert(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 2).into());
|
||||
|
||||
set
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_addr_ip() {
|
||||
let ip: IpAddr = Ipv4Addr::new(127, 0, 0, 1).into();
|
||||
|
||||
assert_eq!(
|
||||
ip.as_ip_addrs(),
|
||||
Ok({
|
||||
let mut set = HashSet::new();
|
||||
set.insert(Ipv4Addr::new(127, 0, 0, 1).into());
|
||||
|
||||
set
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ip.as_ip_addrs(),
|
||||
Ok({
|
||||
let mut set = HashSet::new();
|
||||
set.insert(Ipv4Addr::new(127, 0, 0, 1).into());
|
||||
|
||||
set
|
||||
})
|
||||
);
|
||||
}
|
||||
Vendored
+2707
@@ -0,0 +1,2707 @@
|
||||
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();
|
||||
}
|
||||
+381
@@ -0,0 +1,381 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
thread::sleep,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use mdns_sd::{DaemonStatus, HostnameResolutionEvent, ServiceDaemon, ServiceEvent, ServiceInfo};
|
||||
use test_log::test;
|
||||
|
||||
/// Test that shutdown properly unregisters all services
|
||||
#[test]
|
||||
fn test_shutdown_unregisters_services() {
|
||||
let d = ServiceDaemon::new().expect("Failed to create daemon");
|
||||
|
||||
// Register a service
|
||||
let ty_domain = "_shutdown-test1._udp.local.";
|
||||
let now = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap();
|
||||
let instance_name = format!("shutdown-test-{}", now.as_micros());
|
||||
|
||||
let my_service = ServiceInfo::new(
|
||||
ty_domain,
|
||||
&instance_name,
|
||||
"shutdown-host.local.",
|
||||
"",
|
||||
5300,
|
||||
None,
|
||||
)
|
||||
.expect("valid service info")
|
||||
.enable_addr_auto();
|
||||
|
||||
let fullname = my_service.get_fullname().to_string();
|
||||
d.register(my_service).expect("Failed to register service");
|
||||
|
||||
// Give it time to announce
|
||||
sleep(Duration::from_millis(500));
|
||||
|
||||
// Browse for the service in another daemon to verify it's announced
|
||||
let d2 = ServiceDaemon::new().expect("Failed to create daemon");
|
||||
let browse_chan = d2.browse(ty_domain).unwrap();
|
||||
|
||||
let mut found = false;
|
||||
let timeout = Duration::from_secs(2);
|
||||
let timer = std::time::Instant::now() + timeout;
|
||||
while std::time::Instant::now() < timer {
|
||||
if let Ok(ServiceEvent::ServiceResolved(info)) =
|
||||
browse_chan.recv_timeout(Duration::from_millis(100))
|
||||
{
|
||||
if info.get_fullname() == fullname {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(found, "Service should be discovered before shutdown");
|
||||
|
||||
// Now shutdown the first daemon
|
||||
let shutdown_receiver = d.shutdown().unwrap();
|
||||
let status = shutdown_receiver.recv().unwrap();
|
||||
assert!(matches!(status, DaemonStatus::Shutdown));
|
||||
|
||||
// Give time for goodbye packets to be sent
|
||||
sleep(Duration::from_millis(500));
|
||||
|
||||
// Verify the service is removed
|
||||
let mut removed = false;
|
||||
let timer = std::time::Instant::now() + Duration::from_secs(2);
|
||||
while std::time::Instant::now() < timer {
|
||||
if let Ok(ServiceEvent::ServiceRemoved(_, removed_fullname)) =
|
||||
browse_chan.recv_timeout(Duration::from_millis(100))
|
||||
{
|
||||
if removed_fullname == fullname {
|
||||
removed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(removed, "Service should be removed after shutdown");
|
||||
|
||||
d2.shutdown().unwrap();
|
||||
}
|
||||
|
||||
/// Test that shutdown properly stops all browse operations
|
||||
#[test]
|
||||
fn test_shutdown_stops_browse() {
|
||||
let d = ServiceDaemon::new().expect("Failed to create daemon");
|
||||
|
||||
// Start browsing
|
||||
let ty_domain = "_shutdown-browse-test._udp.local.";
|
||||
let browse_chan = d.browse(ty_domain).unwrap();
|
||||
|
||||
// Give it time to start
|
||||
sleep(Duration::from_millis(100));
|
||||
|
||||
// Shutdown
|
||||
let shutdown_receiver = d.shutdown().unwrap();
|
||||
let status = shutdown_receiver.recv().unwrap();
|
||||
assert!(matches!(status, DaemonStatus::Shutdown));
|
||||
|
||||
// Verify we receive SearchStopped event
|
||||
let mut search_stopped = false;
|
||||
let timeout = Duration::from_secs(2);
|
||||
let timer = std::time::Instant::now() + timeout;
|
||||
|
||||
while std::time::Instant::now() < timer {
|
||||
match browse_chan.recv_timeout(Duration::from_millis(100)) {
|
||||
Ok(ServiceEvent::SearchStopped(stopped_ty)) => {
|
||||
if stopped_ty == ty_domain {
|
||||
search_stopped = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(_) => continue,
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
search_stopped,
|
||||
"Browse should be stopped with SearchStopped event"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that shutdown properly stops all hostname resolution
|
||||
#[test]
|
||||
fn test_shutdown_stops_hostname_resolution() {
|
||||
let d = ServiceDaemon::new().expect("Failed to create daemon");
|
||||
|
||||
// Start hostname resolution
|
||||
let hostname = "test-shutdown-host.local.";
|
||||
let resolve_chan = d.resolve_hostname(hostname, None).unwrap();
|
||||
|
||||
// Give it time to start
|
||||
sleep(Duration::from_millis(100));
|
||||
|
||||
// Shutdown
|
||||
let shutdown_receiver = d.shutdown().unwrap();
|
||||
let status = shutdown_receiver.recv().unwrap();
|
||||
assert!(matches!(status, DaemonStatus::Shutdown));
|
||||
|
||||
// Verify we receive SearchStopped event
|
||||
let mut search_stopped = false;
|
||||
let timeout = Duration::from_secs(2);
|
||||
let timer = std::time::Instant::now() + timeout;
|
||||
|
||||
while std::time::Instant::now() < timer {
|
||||
match resolve_chan.recv_timeout(Duration::from_millis(100)) {
|
||||
Ok(HostnameResolutionEvent::SearchStopped(stopped_hostname)) => {
|
||||
if stopped_hostname.to_lowercase() == hostname.to_lowercase() {
|
||||
search_stopped = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(_) => continue,
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
search_stopped,
|
||||
"Hostname resolution should be stopped with SearchStopped event"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that shutdown sends proper notifications to monitors
|
||||
#[test]
|
||||
fn test_shutdown_notifies_monitors() {
|
||||
let d = ServiceDaemon::new().expect("Failed to create daemon");
|
||||
|
||||
// Monitor daemon events
|
||||
let _monitor_chan = d.monitor().unwrap();
|
||||
|
||||
// Register a service
|
||||
let ty_domain = "_shutdown-monitor-test._udp.local.";
|
||||
let now = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap();
|
||||
let instance_name = format!("monitor-test-{}", now.as_micros());
|
||||
|
||||
let my_service = ServiceInfo::new(
|
||||
ty_domain,
|
||||
&instance_name,
|
||||
"monitor-host.local.",
|
||||
"",
|
||||
5301,
|
||||
None,
|
||||
)
|
||||
.expect("valid service info")
|
||||
.enable_addr_auto();
|
||||
|
||||
d.register(my_service).expect("Failed to register service");
|
||||
|
||||
// Give it time to register
|
||||
sleep(Duration::from_millis(300));
|
||||
|
||||
// Shutdown
|
||||
let shutdown_receiver = d.shutdown().unwrap();
|
||||
let status = shutdown_receiver.recv().unwrap();
|
||||
assert!(matches!(status, DaemonStatus::Shutdown));
|
||||
|
||||
// Give time for events to be processed
|
||||
sleep(Duration::from_millis(300));
|
||||
|
||||
// The monitor channel should eventually be closed or receive notification
|
||||
// For now we just verify that shutdown completes successfully
|
||||
// Future enhancement: add specific DaemonEvent for shutdown
|
||||
}
|
||||
|
||||
/// Test that shutdown handles multiple registered services
|
||||
#[test]
|
||||
fn test_shutdown_multiple_services() {
|
||||
let d = ServiceDaemon::new().expect("Failed to create daemon");
|
||||
|
||||
let ty_domain = "_shutdown-multi-test._udp.local.";
|
||||
let now = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_micros();
|
||||
|
||||
// Register multiple services
|
||||
let mut fullnames = Vec::new();
|
||||
for i in 0..3 {
|
||||
let instance_name = format!("multi-test-{}-{}", now, i);
|
||||
let my_service = ServiceInfo::new(
|
||||
ty_domain,
|
||||
&instance_name,
|
||||
&format!("multi-host-{}.local.", i),
|
||||
"",
|
||||
5302 + i,
|
||||
None,
|
||||
)
|
||||
.expect("valid service info")
|
||||
.enable_addr_auto();
|
||||
|
||||
fullnames.push(my_service.get_fullname().to_string());
|
||||
d.register(my_service).expect("Failed to register service");
|
||||
}
|
||||
|
||||
// Give time to announce
|
||||
sleep(Duration::from_millis(500));
|
||||
|
||||
// Browse for services in another daemon
|
||||
let d2 = ServiceDaemon::new().expect("Failed to create daemon");
|
||||
let browse_chan = d2.browse(ty_domain).unwrap();
|
||||
|
||||
// Verify services are discovered
|
||||
let mut found_services = HashSet::new();
|
||||
let timeout = Duration::from_secs(3);
|
||||
let timer = std::time::Instant::now() + timeout;
|
||||
while std::time::Instant::now() < timer && found_services.len() < fullnames.len() {
|
||||
if let Ok(ServiceEvent::ServiceResolved(info)) =
|
||||
browse_chan.recv_timeout(Duration::from_millis(100))
|
||||
{
|
||||
let fullname = info.get_fullname().to_string();
|
||||
if fullnames.contains(&fullname) {
|
||||
found_services.insert(fullname);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("Found {} services before shutdown", found_services.len());
|
||||
|
||||
// Shutdown the first daemon
|
||||
let shutdown_receiver = d.shutdown().unwrap();
|
||||
let status = shutdown_receiver.recv().unwrap();
|
||||
assert!(matches!(status, DaemonStatus::Shutdown));
|
||||
|
||||
// Give time for goodbye packets
|
||||
sleep(Duration::from_millis(500));
|
||||
|
||||
// Verify all services are removed
|
||||
let mut removed_services = HashSet::new();
|
||||
let timer = std::time::Instant::now() + Duration::from_secs(3);
|
||||
while std::time::Instant::now() < timer && removed_services.len() < found_services.len() {
|
||||
if let Ok(ServiceEvent::ServiceRemoved(_, removed_fullname)) =
|
||||
browse_chan.recv_timeout(Duration::from_millis(100))
|
||||
{
|
||||
if fullnames.contains(&removed_fullname) {
|
||||
removed_services.insert(removed_fullname);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("Removed {} services after shutdown", removed_services.len());
|
||||
assert_eq!(
|
||||
removed_services.len(),
|
||||
found_services.len(),
|
||||
"All discovered services should be removed after shutdown"
|
||||
);
|
||||
|
||||
d2.shutdown().unwrap();
|
||||
}
|
||||
|
||||
/// Test that operations fail gracefully after shutdown
|
||||
#[test]
|
||||
fn test_operations_fail_after_shutdown() {
|
||||
let d = ServiceDaemon::new().expect("Failed to create daemon");
|
||||
|
||||
// Shutdown
|
||||
let shutdown_receiver = d.shutdown().unwrap();
|
||||
let status = shutdown_receiver.recv().unwrap();
|
||||
assert!(matches!(status, DaemonStatus::Shutdown));
|
||||
|
||||
// Try various operations - they should all fail
|
||||
let ty_domain = "_post-shutdown-test._udp.local.";
|
||||
|
||||
// Try to register
|
||||
let my_service = ServiceInfo::new(ty_domain, "test", "test.local.", "", 5303, None).unwrap();
|
||||
let result = d.register(my_service);
|
||||
assert!(result.is_err(), "Register should fail after shutdown");
|
||||
|
||||
// Try to browse
|
||||
let result = d.browse(ty_domain);
|
||||
assert!(result.is_err(), "Browse should fail after shutdown");
|
||||
|
||||
// Try to resolve hostname
|
||||
let result = d.resolve_hostname("test.local.", None);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Resolve hostname should fail after shutdown"
|
||||
);
|
||||
|
||||
// Status should return Shutdown
|
||||
let status_receiver = d.status().unwrap();
|
||||
let status = status_receiver.recv().unwrap();
|
||||
assert!(matches!(status, DaemonStatus::Shutdown));
|
||||
}
|
||||
|
||||
/// Test that shutdown is idempotent (can be called multiple times)
|
||||
#[test]
|
||||
fn test_shutdown_idempotent() {
|
||||
let d = ServiceDaemon::new().expect("Failed to create daemon");
|
||||
|
||||
// First shutdown
|
||||
let shutdown_receiver1 = d.shutdown().unwrap();
|
||||
let status1 = shutdown_receiver1.recv().unwrap();
|
||||
assert!(matches!(status1, DaemonStatus::Shutdown));
|
||||
|
||||
// Second shutdown should also work (or fail gracefully)
|
||||
let result = d.shutdown();
|
||||
// Either succeeds or returns an error (both acceptable)
|
||||
if let Ok(shutdown_receiver2) = result {
|
||||
// If it succeeds, status should still be Shutdown
|
||||
let status2 = shutdown_receiver2.recv().unwrap();
|
||||
assert!(matches!(status2, DaemonStatus::Shutdown));
|
||||
}
|
||||
}
|
||||
|
||||
/// Test shutdown with concurrent operations
|
||||
#[test]
|
||||
fn test_shutdown_concurrent_operations() {
|
||||
let d = ServiceDaemon::new().expect("Failed to create daemon");
|
||||
let d_clone = d.clone();
|
||||
|
||||
// Start a browse operation in another thread
|
||||
let handle = std::thread::spawn(move || {
|
||||
let browse_chan = d_clone.browse("_concurrent-test._udp.local.").unwrap();
|
||||
|
||||
// Keep receiving until channel closes or SearchStopped is received
|
||||
loop {
|
||||
match browse_chan.recv_timeout(Duration::from_secs(5)) {
|
||||
Ok(ServiceEvent::SearchStopped(_)) => break,
|
||||
Ok(_) => continue,
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Give the browse time to start
|
||||
sleep(Duration::from_millis(100));
|
||||
|
||||
// Shutdown while browse is active
|
||||
let shutdown_receiver = d.shutdown().unwrap();
|
||||
let status = shutdown_receiver.recv().unwrap();
|
||||
assert!(matches!(status, DaemonStatus::Shutdown));
|
||||
|
||||
// Wait for the browse thread to complete
|
||||
handle.join().expect("Browse thread should complete");
|
||||
}
|
||||
Reference in new issue
Block a user