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
+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