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

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

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

382 lines
12 KiB
Rust

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");
}