1use super::*;
39
40pub(crate) const DESKTOP_DISCOVERY_PORT: u16 = 29700;
45
46pub(super) const PROBE_INTERVAL: std::time::Duration = std::time::Duration::from_secs(30);
48const UDP_PEER_TTL_SECS: u32 = LAN_PEER_STALE_TIMEOUT_FLOOR_SECS as u32;
55
56const UDP_KEY_PREFIX: &str = "udp:";
59
60const MAX_UDP_PEERS: usize = 64;
63
64const MAX_ID_LEN: usize = 128;
65const MAX_HOSTNAME_LEN: usize = 253;
66const MAX_VERSION_LEN: usize = 64;
67const MAX_CHANNEL_LEN: usize = 64;
68const MAX_AUTH_KEY_LEN: usize = 256;
69const MIN_PEER_PORT: u16 = 1024;
71
72#[derive(Debug, Clone, PartialEq, Eq)]
74pub(super) struct UdpPeer {
75 pub instance_id: String,
76 pub hostname: String,
77 pub version: String,
78 pub channel: String,
80 pub os: String,
83 pub address: IpAddr,
84 pub port: u16,
85 pub auth_key: String,
86}
87
88pub(super) fn parse_peer_reply(bytes: &[u8], src: &std::net::SocketAddr) -> Option<UdpPeer> {
92 let value: serde_json::Value = serde_json::from_slice(bytes).ok()?;
93 if value.get("type").and_then(|t| t.as_str()) != Some(UDP_RESPONSE_TYPE)
94 || !is_probe_version_match(&value)
95 {
96 return None;
97 }
98 if !is_lan_source(src) {
99 return None;
100 }
101 let text = |key: &str, max: usize| -> Option<String> {
102 let s = value.get(key)?.as_str()?;
103 (s.len() <= max).then(|| s.to_string())
104 };
105 let instance_id = text("instance_id", MAX_ID_LEN).filter(|s| !s.is_empty())?;
106 let auth_key = text("auth_key", MAX_AUTH_KEY_LEN).filter(|s| !s.is_empty())?;
109 let hostname = text("hostname", MAX_HOSTNAME_LEN)?;
110 let version = text("version", MAX_VERSION_LEN)?;
111 let channel = text("channel", MAX_CHANNEL_LEN).unwrap_or_default();
114 let os = value
115 .get("os")
116 .and_then(|v| v.as_str())
117 .and_then(crate::backend::host_os::sanitize_os)
118 .unwrap_or_default();
119 let port = u16::try_from(value.get("port")?.as_u64()?)
120 .ok()
121 .filter(|p| *p >= MIN_PEER_PORT)?;
122 Some(UdpPeer {
123 instance_id,
124 hostname,
125 version,
126 channel,
127 os,
128 address: src.ip(),
129 port,
130 auth_key,
131 })
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub(super) enum Merge {
137 Inserted,
139 Refreshed,
141 Dropped,
143}
144
145fn udp_key(address: &str, port: u16) -> String {
151 format!("{UDP_KEY_PREFIX}{address}:{port}")
152}
153
154fn find_existing<'a>(
157 instances: &'a mut HashMap<String, LanInstance>,
158 peer: &UdpPeer,
159) -> Option<(&'a String, &'a mut LanInstance)> {
160 let address = peer.address.to_string();
161 instances
162 .iter_mut()
163 .find(|(_, i)| i.address == address && i.port == peer.port)
164}
165
166pub(super) fn merge_udp_peer(
170 instances: &mut HashMap<String, LanInstance>,
171 peer: &UdpPeer,
172 now: u64,
173) -> Merge {
174 if let Some((key, existing)) = find_existing(instances, peer) {
175 existing.last_seen = now;
176 if key.starts_with(UDP_KEY_PREFIX) {
177 existing.instance_id = peer.instance_id.clone();
181 existing.hostname = peer.hostname.clone();
182 existing.version = peer.version.clone();
183 if !peer.channel.is_empty() {
187 existing.channel = peer.channel.clone();
188 }
189 if !peer.os.is_empty() {
190 existing.os = peer.os.clone();
191 }
192 existing.auth_key = peer.auth_key.clone();
193 } else {
194 if existing.auth_key.is_empty() {
195 existing.auth_key = peer.auth_key.clone();
196 }
197 if existing.channel.is_empty() {
198 existing.channel = peer.channel.clone();
199 }
200 if existing.os.is_empty() {
201 existing.os = peer.os.clone();
202 }
203 }
204 return Merge::Refreshed;
205 }
206 if instances
207 .keys()
208 .filter(|k| k.starts_with(UDP_KEY_PREFIX))
209 .count()
210 >= MAX_UDP_PEERS
211 {
212 return Merge::Dropped;
213 }
214 let address = peer.address.to_string();
215 instances.insert(
216 udp_key(&address, peer.port),
217 LanInstance {
218 instance_id: peer.instance_id.clone(),
219 hostname: peer.hostname.clone(),
220 version: peer.version.clone(),
221 channel: peer.channel.clone(),
222 os: peer.os.clone(),
223 address,
224 port: peer.port,
225 auth_key: peer.auth_key.clone(),
226 agents: Vec::new(),
227 agent_status: Default::default(),
228 agent_lines: Default::default(),
229 first_seen: now,
230 last_seen: now,
231 last_polled_ok: 0,
232 other_ttl_secs: UDP_PEER_TTL_SECS,
233 },
234 );
235 Merge::Inserted
236}
237
238pub(super) fn drop_udp_duplicates(instances: &mut HashMap<String, LanInstance>) {
243 let mdns: Vec<(String, u16)> = instances
244 .iter()
245 .filter(|(k, _)| !k.starts_with(UDP_KEY_PREFIX))
246 .map(|(_, i)| (i.address.clone(), i.port))
247 .collect();
248 instances.retain(|key, i| {
249 !key.starts_with(UDP_KEY_PREFIX)
250 || !mdns
251 .iter()
252 .any(|(addr, port)| *addr == i.address && *port == i.port)
253 });
254}
255
256pub(super) fn probe_targets(
261 broadcasts: impl IntoIterator<Item = Ipv4Addr>,
262 port: u16,
263) -> Vec<std::net::SocketAddr> {
264 let mut all: BTreeSet<Ipv4Addr> = broadcasts
265 .into_iter()
266 .filter(|b| !b.is_loopback())
267 .collect();
268 all.insert(Ipv4Addr::BROADCAST);
269 all.into_iter()
270 .map(|b| std::net::SocketAddr::from((b, port)))
271 .collect()
272}
273
274pub(super) fn local_broadcasts() -> Vec<Ipv4Addr> {
276 if_addrs::get_if_addrs()
277 .unwrap_or_default()
278 .into_iter()
279 .filter(|i| !i.is_loopback())
280 .filter_map(|i| match i.addr {
281 if_addrs::IfAddr::V4(v4) => v4.broadcast,
282 _ => None,
283 })
284 .collect()
285}
286
287pub(super) fn probe_datagram() -> Vec<u8> {
290 serde_json::to_vec(&json!({ "type": UDP_PROBE_TYPE, "v": UDP_PROTOCOL_VERSION }))
291 .unwrap_or_default()
292}
293
294impl LanDiscovery {
295 pub(super) async fn udp_peer_loop(self: Arc<Self>, cancel: oneshot::Receiver<()>) {
299 let socket = match UdpSocket::bind(("0.0.0.0", DESKTOP_DISCOVERY_PORT)).await {
300 Ok(s) => s,
301 Err(e) => {
302 tracing::debug!(
303 port = DESKTOP_DISCOVERY_PORT,
304 "UDP peer discovery not started (bind failed, likely another local instance \
305 holds this port): {e}"
306 );
307 return;
308 }
309 };
310 if let Err(e) = socket.set_broadcast(true) {
311 tracing::warn!(error = %e, "UDP peer discovery: cannot enable broadcast, not starting");
312 return;
313 }
314 tracing::debug!(
315 port = DESKTOP_DISCOVERY_PORT,
316 "UDP peer discovery listening"
317 );
318 let targets = || probe_targets(local_broadcasts(), DESKTOP_DISCOVERY_PORT);
319 self.udp_peer_loop_on(socket, targets, PROBE_INTERVAL, cancel)
320 .await;
321 }
322
323 pub(super) async fn udp_peer_loop_on(
326 self: Arc<Self>,
327 socket: UdpSocket,
328 targets: impl Fn() -> Vec<std::net::SocketAddr>,
329 every: std::time::Duration,
330 mut cancel: oneshot::Receiver<()>,
331 ) {
332 let own_port = socket.local_addr().map(|a| a.port()).unwrap_or(0);
333 let probe = probe_datagram();
334 let mut ticker = tokio::time::interval(every);
335 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
336 let mut buf = [0u8; 1024];
337 loop {
338 tokio::select! {
339 _ = &mut cancel => {
340 tracing::debug!("UDP peer discovery stopping");
341 return;
342 }
343 _ = ticker.tick() => {
344 for target in targets() {
345 if let Err(e) = socket.send_to(&probe, target).await {
348 tracing::debug!(%target, error = %e, "UDP peer probe send failed");
349 }
350 }
351 }
352 result = socket.recv_from(&mut buf) => match result {
353 Ok((len, src)) => self.handle_datagram(&socket, own_port, &buf[..len], src).await,
354 Err(e) => {
355 tracing::debug!("UDP peer discovery recv_from error: {e}");
359 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
360 }
361 },
362 }
363 }
364 }
365
366 async fn handle_datagram(
367 &self,
368 socket: &UdpSocket,
369 own_port: u16,
370 bytes: &[u8],
371 src: std::net::SocketAddr,
372 ) {
373 let own_addrs = crate::backend::lan_listeners::cached_local_addresses();
375 if src.port() == own_port && own_addrs.contains(&src.ip()) {
376 return;
377 }
378 if is_valid_probe(bytes) {
379 if !is_lan_source(&src) {
382 return;
383 }
384 if let Ok(payload) = serde_json::to_vec(&self.build_identity_response()) {
388 if let Err(e) = socket.send_to(&payload, src).await {
389 tracing::debug!(%src, error = %e, "UDP peer reply send failed");
390 }
391 }
392 return;
393 }
394 let Some(peer) = parse_peer_reply(bytes, &src) else {
395 return;
396 };
397 let own_addr_list = [peer.address];
401 if is_self_resolution(peer.port, &own_addr_list, self.port, &own_addrs) {
402 return;
403 }
404 let now = agentmux_common::time::now_secs_u64();
405 let outcome = merge_udp_peer(&mut self.instances.write(), &peer, now);
406 match outcome {
407 Merge::Inserted => {
408 tracing::info!(
409 peer_id = %peer.instance_id,
410 address = %peer.address,
411 port = peer.port,
412 "LAN peer discovered (UDP broadcast)"
413 );
414 self.broadcast_instances();
415 }
416 Merge::Refreshed => {}
417 Merge::Dropped => {
418 tracing::debug!(%src, "UDP peer table full, reply ignored");
419 }
420 }
421 }
422}
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427
428 fn src(ip: &str) -> std::net::SocketAddr {
429 format!("{ip}:{DESKTOP_DISCOVERY_PORT}").parse().unwrap()
430 }
431
432 fn reply(id: &str, port: u64) -> Vec<u8> {
433 serde_json::to_vec(&json!({
434 "type": UDP_RESPONSE_TYPE, "v": 1, "instance_id": id, "hostname": "area54",
435 "version": "0.59.4", "port": port, "auth_key": "k-secret",
436 }))
437 .unwrap()
438 }
439
440 fn peer(id: &str, addr: &str, port: u16) -> UdpPeer {
441 UdpPeer {
442 instance_id: id.into(),
443 hostname: "h".into(),
444 version: "0.59.4".into(),
445 channel: "stable".into(),
446 os: String::new(),
447 address: addr.parse().unwrap(),
448 port,
449 auth_key: "k".into(),
450 }
451 }
452
453 fn mdns_entry(id: &str, addr: &str, port: u16) -> LanInstance {
454 LanInstance {
455 instance_id: id.into(),
456 hostname: "mdns-name".into(),
457 version: "0.59.4".into(),
458 channel: String::new(),
459 os: String::new(),
460 address: addr.into(),
461 port,
462 auth_key: "mdns-key".into(),
463 agents: vec!["korp".into()],
464 agent_status: Default::default(),
465 agent_lines: Default::default(),
466 first_seen: 1,
467 last_seen: 1,
468 last_polled_ok: 0,
469 other_ttl_secs: 4500,
470 }
471 }
472
473 #[test]
474 fn a_well_formed_reply_from_a_lan_address_is_a_peer() {
475 let p = parse_peer_reply(&reply("abc", 29700), &src("10.0.0.26")).unwrap();
476 assert_eq!(p.instance_id, "abc");
477 assert_eq!(p.address, "10.0.0.26".parse::<IpAddr>().unwrap());
478 assert_eq!(p.port, 29700);
479 assert_eq!(p.auth_key, "k-secret");
480 }
481
482 #[test]
483 fn the_channel_is_read_when_present_and_optional_when_not() {
484 let mut v: serde_json::Value = serde_json::from_slice(&reply("abc", 29700)).unwrap();
485 assert_eq!(
486 parse_peer_reply(&serde_json::to_vec(&v).unwrap(), &src("10.0.0.26"))
487 .unwrap()
488 .channel,
489 ""
490 );
491 v["channel"] = json!("dev-fix-lan");
492 assert_eq!(
493 parse_peer_reply(&serde_json::to_vec(&v).unwrap(), &src("10.0.0.26"))
494 .unwrap()
495 .channel,
496 "dev-fix-lan"
497 );
498 v["channel"] = json!("c".repeat(MAX_CHANNEL_LEN + 1));
500 assert_eq!(
501 parse_peer_reply(&serde_json::to_vec(&v).unwrap(), &src("10.0.0.26"))
502 .unwrap()
503 .channel,
504 ""
505 );
506 }
507
508 #[test]
510 fn the_os_is_read_when_it_is_a_plain_token_and_dropped_otherwise() {
511 let mut v: serde_json::Value = serde_json::from_slice(&reply("abc", 29700)).unwrap();
512 let os_of = |v: &serde_json::Value| {
513 parse_peer_reply(&serde_json::to_vec(v).unwrap(), &src("10.0.0.26"))
514 .unwrap()
515 .os
516 };
517 assert_eq!(os_of(&v), "");
518 for ok in ["windows", "macos", "linux"] {
519 v["os"] = json!(ok);
520 assert_eq!(os_of(&v), ok);
521 }
522 for bad in ["Windows", "<b>x</b>", "../etc", "", "a-very-long-platform-name"] {
524 v["os"] = json!(bad);
525 assert_eq!(os_of(&v), "", "{bad:?}");
526 }
527 v["os"] = json!(42);
528 assert_eq!(os_of(&v), "");
529 }
530
531 #[test]
532 fn a_reply_without_an_os_keeps_the_known_one() {
533 let mut t = HashMap::new();
534 let mut with = peer("abc", "10.0.0.26", 29700);
535 with.os = "macos".into();
536 merge_udp_peer(&mut t, &with, 100);
537 assert_eq!(t["udp:10.0.0.26:29700"].os, "macos");
538 merge_udp_peer(&mut t, &peer("abc", "10.0.0.26", 29700), 200);
539 assert_eq!(t["udp:10.0.0.26:29700"].os, "macos");
540 }
541
542 #[test]
543 fn an_mdns_entry_with_no_os_learns_it_from_a_udp_reply() {
544 let mut t = HashMap::new();
545 t.insert("mdns-full".to_string(), mdns_entry("abc", "10.0.0.26", 29700));
546 let mut udp = peer("abc", "10.0.0.26", 29700);
547 udp.os = "linux".into();
548 merge_udp_peer(&mut t, &udp, 100);
549 assert_eq!(t["mdns-full"].os, "linux");
550 }
551
552 #[test]
553 fn the_address_is_the_datagrams_source_never_a_claimed_one() {
554 let mut v: serde_json::Value = serde_json::from_slice(&reply("abc", 29700)).unwrap();
555 v["address"] = json!("10.9.9.9");
556 let p = parse_peer_reply(&serde_json::to_vec(&v).unwrap(), &src("10.0.0.26")).unwrap();
557 assert_eq!(p.address.to_string(), "10.0.0.26");
558 }
559
560 #[test]
561 fn replies_that_are_not_trustworthy_enough_to_record_are_refused() {
562 let ok = src("10.0.0.26");
563 assert!(parse_peer_reply(&reply("abc", 29700), &src("8.8.8.8")).is_none());
565 assert!(parse_peer_reply(&probe_datagram(), &ok).is_none());
567 assert!(parse_peer_reply(b"not json", &ok).is_none());
568 let mut v: serde_json::Value = serde_json::from_slice(&reply("abc", 29700)).unwrap();
569 v["v"] = json!(2);
570 assert!(parse_peer_reply(&serde_json::to_vec(&v).unwrap(), &ok).is_none());
571 assert!(parse_peer_reply(&reply("abc", 80), &ok).is_none());
573 assert!(parse_peer_reply(&reply("abc", 70000), &ok).is_none());
574 assert!(parse_peer_reply(&reply("", 29700), &ok).is_none());
576 for field in ["instance_id", "auth_key", "hostname", "version", "port"] {
577 let mut v: serde_json::Value = serde_json::from_slice(&reply("abc", 29700)).unwrap();
578 v.as_object_mut().unwrap().remove(field);
579 assert!(
580 parse_peer_reply(&serde_json::to_vec(&v).unwrap(), &ok).is_none(),
581 "{field}"
582 );
583 }
584 let mut v: serde_json::Value = serde_json::from_slice(&reply("abc", 29700)).unwrap();
585 v["auth_key"] = json!("k".repeat(MAX_AUTH_KEY_LEN + 1));
586 assert!(parse_peer_reply(&serde_json::to_vec(&v).unwrap(), &ok).is_none());
587 v["auth_key"] = json!("");
588 assert!(parse_peer_reply(&serde_json::to_vec(&v).unwrap(), &ok).is_none());
589 }
590
591 #[test]
592 fn a_new_peer_is_recorded_under_a_udp_key_with_a_short_ttl() {
593 let mut t = HashMap::new();
594 assert_eq!(
595 merge_udp_peer(&mut t, &peer("abc", "10.0.0.26", 29700), 100),
596 Merge::Inserted
597 );
598 let e = &t["udp:10.0.0.26:29700"];
599 assert_eq!(
600 (e.last_seen, e.first_seen, e.other_ttl_secs),
601 (100, 100, UDP_PEER_TTL_SECS)
602 );
603 assert!(e.agents.is_empty());
604 }
605
606 #[test]
607 fn a_peer_mdns_already_knows_is_not_listed_twice() {
608 for known in [
610 mdns_entry("abc", "10.0.0.26", 29700),
611 mdns_entry("", "10.0.0.26", 29700),
612 ] {
613 let mut t = HashMap::new();
614 t.insert("agentmux-x._agentmux._tcp.local.".to_string(), known);
615 assert_eq!(
616 merge_udp_peer(&mut t, &peer("abc", "10.0.0.26", 29700), 500),
617 Merge::Refreshed
618 );
619 assert_eq!(t.len(), 1, "no second entry");
620 let e = &t["agentmux-x._agentmux._tcp.local."];
621 assert_eq!(e.last_seen, 500, "liveness moves");
622 assert_eq!(e.hostname, "mdns-name", "mDNS data is authoritative");
623 assert_eq!(e.auth_key, "mdns-key");
624 }
625 }
626
627 #[test]
628 fn a_reply_without_a_channel_keeps_the_known_one() {
629 let mut t = HashMap::new();
631 merge_udp_peer(&mut t, &peer("abc", "10.0.0.26", 29700), 100);
632 assert_eq!(t["udp:10.0.0.26:29700"].channel, "stable");
633 let mut no_channel = peer("abc", "10.0.0.26", 29700);
634 no_channel.channel = String::new();
635 merge_udp_peer(&mut t, &no_channel, 200);
636 assert_eq!(t["udp:10.0.0.26:29700"].channel, "stable");
637 }
638
639 #[test]
640 fn a_later_reply_updates_a_udp_entry_in_place() {
641 let mut t = HashMap::new();
642 merge_udp_peer(&mut t, &peer("abc", "10.0.0.26", 29700), 100);
643 let mut renamed = peer("abc", "10.0.0.26", 29700);
644 renamed.hostname = "area54-renamed".into();
645 assert_eq!(merge_udp_peer(&mut t, &renamed, 200), Merge::Refreshed);
646 assert_eq!(t.len(), 1);
647 let e = &t["udp:10.0.0.26:29700"];
648 assert_eq!((e.hostname.as_str(), e.last_seen), ("area54-renamed", 200));
649 }
650
651 #[test]
654 fn two_machines_on_the_same_release_are_two_peers() {
655 let mut t = HashMap::new();
656 assert_eq!(
657 merge_udp_peer(&mut t, &peer("v0.59.5", "10.0.0.26", 29700), 1),
658 Merge::Inserted
659 );
660 assert_eq!(
661 merge_udp_peer(&mut t, &peer("v0.59.5", "10.0.0.195", 29700), 1),
662 Merge::Inserted
663 );
664 assert_eq!(t.len(), 2);
665 t.insert(
667 "mdns-a".into(),
668 mdns_entry("v0.59.5", "10.0.0.26", 29700),
669 );
670 drop_udp_duplicates(&mut t);
671 assert!(t.contains_key("udp:10.0.0.195:29700"));
672 assert!(!t.contains_key("udp:10.0.0.26:29700"));
673 }
674
675 #[test]
676 fn the_udp_peer_table_is_bounded() {
677 let mut t = HashMap::new();
678 for n in 0..MAX_UDP_PEERS {
679 assert_eq!(
680 merge_udp_peer(
681 &mut t,
682 &peer(
683 &format!("id{n}"),
684 &format!("10.0.{}.{}", n / 200, n % 200 + 1),
685 29700
686 ),
687 1
688 ),
689 Merge::Inserted
690 );
691 }
692 assert_eq!(
693 merge_udp_peer(&mut t, &peer("one-too-many", "10.1.0.1", 29700), 1),
694 Merge::Dropped
695 );
696 assert_eq!(
698 merge_udp_peer(&mut t, &peer("id0", "10.0.0.1", 29700), 2),
699 Merge::Refreshed
700 );
701 t.insert("mdns".into(), mdns_entry("m", "10.2.0.1", 29700));
703 assert_eq!(
704 t.keys().filter(|k| k.starts_with("udp:")).count(),
705 MAX_UDP_PEERS
706 );
707 }
708
709 #[test]
710 fn an_mdns_resolution_after_the_udp_one_replaces_it() {
711 let mut t = HashMap::new();
712 merge_udp_peer(&mut t, &peer("abc", "10.0.0.26", 29700), 100);
713 merge_udp_peer(&mut t, &peer("other", "10.0.0.40", 29700), 100);
714 t.insert(
715 "agentmux-x._agentmux._tcp.local.".to_string(),
716 mdns_entry("abc", "10.0.0.26", 29700),
717 );
718 drop_udp_duplicates(&mut t);
719 assert!(!t.contains_key("udp:10.0.0.26:29700"));
720 assert!(
721 t.contains_key("udp:10.0.0.40:29700"),
722 "an unrelated UDP peer stays"
723 );
724 assert!(t.contains_key("agentmux-x._agentmux._tcp.local."));
725 }
726
727 #[test]
728 fn probes_go_to_the_limited_and_every_directed_broadcast_once() {
729 let t = probe_targets(
730 [
731 "192.168.1.255".parse().unwrap(),
732 "172.17.31.255".parse().unwrap(),
733 "192.168.1.255".parse().unwrap(),
734 "127.255.255.255".parse().unwrap(),
735 ],
736 DESKTOP_DISCOVERY_PORT,
737 );
738 let t: Vec<String> = t.iter().map(|a| a.to_string()).collect();
739 assert_eq!(
740 t,
741 [
742 "172.17.31.255:29700",
743 "192.168.1.255:29700",
744 "255.255.255.255:29700"
745 ]
746 );
747 assert_eq!(probe_targets([], 29700).len(), 1);
749 }
750
751 #[test]
752 fn the_probe_is_one_every_responder_already_accepts() {
753 assert!(is_valid_probe(&probe_datagram()));
754 }
755
756 #[test]
757 fn the_desktop_port_is_in_the_fixed_block() {
758 assert!(crate::backend::lan_ports::LAN_PORT_RANGE.contains(&DESKTOP_DISCOVERY_PORT));
759 assert!(
760 DESKTOP_DISCOVERY_PORT < 32768,
761 "below every OS's ephemeral range"
762 );
763 }
764
765 fn instance(id: &str, host: &str, port: u16, key: &str) -> Arc<LanDiscovery> {
768 Arc::new(LanDiscovery {
769 daemon: ServiceDaemon::new().expect("daemon construction (no register/browse)"),
770 instances: Arc::new(RwLock::new(HashMap::new())),
771 instance_id: id.to_string(),
772 event_bus: Arc::new(crate::backend::eventbus::EventBus::new()),
773 service_fullname: String::new(),
774 auth_key: key.to_string(),
775 hostname: host.to_string(),
776 version: "0.59.4".to_string(),
777 port,
778 udp_cancel: Mutex::new(None),
779 udp_peer_cancel: Mutex::new(None),
780 agent_names_cancel: Mutex::new(None),
781 instance_record_cancel: Mutex::new(None),
782 instances_dir: None,
783 instance_record_live: Mutex::new(false),
784 announced_v4: Arc::new(Mutex::new(BTreeSet::new())),
785 monitored: true,
786 started_at: std::time::Instant::now(),
787 })
788 }
789
790 #[tokio::test]
794 async fn two_instances_find_each_other_by_probe_alone() {
795 let a = instance("id-a", "host-a", 55001, "key-a");
796 let b = instance("id-b", "host-b", 55002, "key-b");
797 let sa = UdpSocket::bind("127.0.0.1:0").await.unwrap();
798 let sb = UdpSocket::bind("127.0.0.1:0").await.unwrap();
799 let (addr_a, addr_b) = (sa.local_addr().unwrap(), sb.local_addr().unwrap());
800 let (cancel_a, rx_a) = oneshot::channel();
801 let (cancel_b, rx_b) = oneshot::channel();
802 let every = std::time::Duration::from_millis(50);
803 tokio::spawn(
806 a.clone()
807 .udp_peer_loop_on(sa, move || vec![addr_b, addr_a], every, rx_a),
808 );
809 tokio::spawn(
810 b.clone()
811 .udp_peer_loop_on(sb, move || vec![addr_a, addr_b], every, rx_b),
812 );
813
814 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
815 let (seen_by_a, seen_by_b) = loop {
816 let (x, y) = (a.get_instances(), b.get_instances());
817 if (!x.is_empty() && !y.is_empty()) || std::time::Instant::now() > deadline {
818 break (x, y);
819 }
820 tokio::time::sleep(std::time::Duration::from_millis(25)).await;
821 };
822 let _ = (cancel_a.send(()), cancel_b.send(()));
823
824 assert_eq!(seen_by_a.len(), 1, "a sees b once: {seen_by_a:?}");
825 assert_eq!(seen_by_b.len(), 1, "b sees a once: {seen_by_b:?}");
826 let b_as_seen = &seen_by_a[0];
827 assert_eq!(
828 (
829 b_as_seen.instance_id.as_str(),
830 b_as_seen.hostname.as_str(),
831 b_as_seen.port,
832 b_as_seen.auth_key.as_str()
833 ),
834 ("id-b", "host-b", 55002, "key-b")
835 );
836 assert_eq!(
837 b_as_seen.address, "127.0.0.1",
838 "the address is the datagram's source"
839 );
840 assert_eq!(seen_by_b[0].instance_id, "id-a");
841 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
843 assert!(
844 a.get_instances().iter().all(|i| i.instance_id != "id-a"),
845 "a listed itself"
846 );
847 assert!(
848 b.get_instances().iter().all(|i| i.instance_id != "id-b"),
849 "b listed itself"
850 );
851 }
852
853 #[tokio::test]
856 async fn a_machine_on_our_release_is_a_peer_and_our_own_address_is_us() {
857 let a = instance("v0.59.5", "host-a", 55001, "key-a");
858 let socket = UdpSocket::bind("127.0.0.1:0").await.unwrap();
859 let own_port = socket.local_addr().unwrap().port();
860 let from: std::net::SocketAddr = "10.0.0.77:29700".parse().unwrap();
862 a.handle_datagram(&socket, own_port, &reply("v0.59.5", 29799), from)
863 .await;
864 assert_eq!(
865 a.get_instances().len(),
866 1,
867 "another machine on our release is not us"
868 );
869 if let Some(own) = crate::backend::lan_listeners::cached_local_addresses()
871 .iter()
872 .find(|ip| ip.is_ipv4() && !ip.is_loopback())
873 {
874 let from_self = std::net::SocketAddr::new(*own, 29700);
875 a.handle_datagram(&socket, own_port, &reply("v0.59.5", 55001), from_self)
876 .await;
877 assert_eq!(a.get_instances().len(), 1, "our own address and port is us");
878 }
879 }
880
881 #[tokio::test]
885 async fn a_udp_peer_is_listed_for_exactly_the_staleness_floor() {
886 let a = instance("id-a", "host-a", 55001, "key-a");
887 let now = agentmux_common::time::now_secs_u64();
888 let floor = LAN_PEER_STALE_TIMEOUT_FLOOR_SECS;
889 assert_eq!(
890 u64::from(UDP_PEER_TTL_SECS),
891 floor,
892 "the constant states the real lifetime"
893 );
894 for (age, listed) in [(0, true), (floor - 5, true), (floor + 5, false)] {
895 merge_udp_peer(
896 &mut a.instances.write(),
897 &peer("abc", "10.0.0.26", 29700),
898 now - age,
899 );
900 assert_eq!(a.get_instances().len(), usize::from(listed), "age {age}s");
901 a.instances.write().clear();
902 }
903 }
904
905 #[test]
909 fn a_restarted_peer_keeps_one_entry_under_its_new_id() {
910 let mut t = HashMap::new();
911 merge_udp_peer(&mut t, &peer("v0.59.4", "10.0.0.26", 29700), 100);
912 assert_eq!(
913 merge_udp_peer(&mut t, &peer("v0.59.5", "10.0.0.26", 29700), 200),
914 Merge::Refreshed
915 );
916 assert_eq!(t.len(), 1, "{:?}", t.keys().collect::<Vec<_>>());
917 let e = &t["udp:10.0.0.26:29700"];
918 assert_eq!(
919 (e.instance_id.as_str(), e.first_seen, e.last_seen),
920 ("v0.59.5", 100, 200)
921 );
922 }
923}