1use std::cell::RefCell;
17use std::rc::Rc;
18use std::time::Duration;
19
20use anyhow::{Result, anyhow, bail};
21use tokio::time::Instant;
22
23use umsh::core::{PayloadType, PublicKey, RouterHint};
24use umsh::crypto::software::SoftwareIdentity;
25use umsh::hal::Radio;
26use umsh::mac::{CachedRoute, SendOptions};
27use umsh::node::mac_command::IdentityRequestBuilder;
28use umsh::node::{
29 MacCommand, NodeCapabilities, NodeIdentityPayload, NodeRole, ReceivedPacketRef, Transport,
30 mac_command,
31};
32use umsh_sync::AsyncRefCell;
33
34use super::values::{HintPrefixArg, KeyArg};
35use crate::App;
36use crate::mesh::{self, CtlMac, NodeStack};
37use crate::output::{field, note, subfield};
38use crate::routes::{self, RouteCache};
39
40const POLL: Duration = Duration::from_millis(250);
42
43const REQUEST_FRAME: usize = 64;
46
47#[derive(Debug, clap::Args)]
48pub struct DiscoverArgs {
49 #[arg(long, short = 'W', default_value_t = 40, value_name = "SECS")]
55 pub timeout: u64,
56
57 #[arg(long)]
62 pub passive: bool,
63
64 #[arg(long, value_name = "ROLE")]
66 pub role: Option<RoleFilter>,
67
68 #[arg(long, value_name = "CAP", value_delimiter = ',')]
70 pub caps: Vec<CapFilter>,
71
72 #[arg(long, value_name = "HEX")]
75 pub hint: Option<HintPrefixArg>,
76
77 #[arg(long, value_name = "KEY")]
83 pub via: Option<KeyArg>,
84}
85
86#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
92pub enum RoleFilter {
93 Repeater,
94 Chat,
95 Tracker,
96 Sensor,
97 Bridge,
98 ChatRoom,
99}
100
101impl RoleFilter {
102 fn role(self) -> NodeRole {
103 match self {
104 Self::Repeater => NodeRole::Repeater,
105 Self::Chat => NodeRole::Chat,
106 Self::Tracker => NodeRole::Tracker,
107 Self::Sensor => NodeRole::Sensor,
108 Self::Bridge => NodeRole::Bridge,
109 Self::ChatRoom => NodeRole::ChatRoom,
110 }
111 }
112}
113
114#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
115pub enum CapFilter {
116 Repeater,
117 Mobile,
118 TextMessages,
119 Telemetry,
120 ChatRoom,
121 Coap,
122}
123
124impl CapFilter {
125 fn bit(self) -> NodeCapabilities {
126 match self {
127 Self::Repeater => NodeCapabilities::REPEATER,
128 Self::Mobile => NodeCapabilities::MOBILE,
129 Self::TextMessages => NodeCapabilities::TEXT_MESSAGES,
130 Self::Telemetry => NodeCapabilities::TELEMETRY,
131 Self::ChatRoom => NodeCapabilities::CHAT_ROOM,
132 Self::Coap => NodeCapabilities::COAP,
133 }
134 }
135}
136
137pub async fn discover(app: &mut App, args: DiscoverArgs) -> Result<()> {
139 mesh::borrowing_the_radio(app, args).await
140}
141
142impl mesh::RadioErrand for DiscoverArgs {
143 async fn run<R: Radio>(
144 self,
145 mac: &AsyncRefCell<CtlMac<R>>,
146 identity: SoftwareIdentity,
147 ) -> Result<()>
148 where
149 R::Error: core::fmt::Debug,
150 {
151 explore(mac, identity, &self).await
152 }
153}
154
155async fn explore<R: Radio>(
156 mac: &AsyncRefCell<CtlMac<R>>,
157 identity: SoftwareIdentity,
158 args: &DiscoverArgs,
159) -> Result<()>
160where
161 R::Error: core::fmt::Debug,
162{
163 let mut routes = RouteCache::load();
164 let vantage = match &args.via {
168 Some(key) => Some(steer_through(&routes, &PublicKey(key.0))?),
169 None => None,
170 };
171
172 let (mut stack, local_key) = NodeStack::build(mac, identity).await?;
173 stack.handle.set_auto_register_full_key_peers(true).await;
177
178 field("listening as", local_key.to_string());
179
180 let seen: Rc<RefCell<Vec<PublicKey>>> = Rc::new(RefCell::new(Vec::new()));
181 let started = Instant::now();
182
183 let nonce = if args.passive {
184 note("listening only; nothing is transmitted");
185 None
186 } else {
187 let nonce = ask(&mut stack, args, vantage.as_deref()).await?;
188 field("asked", describe_ask(args));
189 Some(nonce)
190 };
191
192 let sink = seen.clone();
195 let _subscription = stack.node.on_receive(move |packet| {
196 let Some(identity) = advertisement(packet) else {
197 return false;
198 };
199 let Some(key) = packet.from_key() else {
200 return false;
201 };
202 let mut seen = sink.borrow_mut();
203 if seen.contains(&key) {
204 return false;
205 }
206 seen.push(key);
207 report(&key, &identity, packet, nonce, started);
208 false
209 });
210
211 note(format!("listening for {} s", args.timeout));
212 let deadline = Instant::now() + Duration::from_secs(args.timeout);
213 loop {
214 if Instant::now() >= deadline {
215 break;
216 }
217 tokio::select! {
218 result = stack.pump_until(deadline.min(Instant::now() + POLL)) => result?,
219 _ = tokio::signal::ctrl_c() => {
222 println!();
223 break;
224 }
225 }
226 }
227
228 let count = seen.borrow().len();
229 match count {
230 0 if args.passive => note("nothing advertised itself"),
231 0 => note("nobody answered"),
232 1 => field("heard", "1 node"),
233 many => field("heard", format!("{many} nodes")),
234 }
235
236 routes.harvest(&stack.handle).await;
238 if let Err(error) = routes.store() {
239 crate::output::warn(format!("could not save learned routes: {error:#}"));
240 }
241 let _ = stack.handle.service_counter_persistence().await;
242 Ok(())
243}
244
245async fn ask<R: Radio>(
248 stack: &mut NodeStack<'_, R>,
249 args: &DiscoverArgs,
250 vantage: Option<&[RouterHint]>,
251) -> Result<u32>
252where
253 R::Error: core::fmt::Debug,
254{
255 let mut bytes = [0u8; 4];
256 stack.handle.fill_random(&mut bytes).await;
257 let nonce = u32::from_be_bytes(bytes);
258 let frame = request_frame(args, nonce)?;
259
260 let mut send = SendOptions::default().with_full_source();
262 if let Some(hops) = vantage {
263 send = send
264 .try_with_source_route(hops)
265 .map_err(|_| anyhow!("that route is longer than a request can carry"))?
266 .with_trace_route();
270 }
271 let send = send.no_flood();
275
276 stack
277 .node
278 .send_all(&frame, &send)
279 .await
280 .map_err(|error| anyhow!("asking: {error:?}"))?;
281 Ok(nonce)
282}
283
284fn request_frame(args: &DiscoverArgs, nonce: u32) -> Result<Vec<u8>> {
287 let mut builder = IdentityRequestBuilder::new()
290 .nonce(nonce)
291 .map_err(|error| anyhow!("building the request: {error:?}"))?;
292 if let Some(hint) = &args.hint {
293 builder = builder
294 .filter_hint_prefix(&hint.0)
295 .map_err(|error| anyhow!("building the hint filter: {error:?}"))?;
296 }
297 if let Some(role) = args.role {
298 builder = builder
299 .filter_role(role.role())
300 .map_err(|error| anyhow!("building the role filter: {error:?}"))?;
301 }
302 let caps = args
306 .caps
307 .iter()
308 .fold(NodeCapabilities::empty(), |bits, cap| bits | cap.bit());
309 if !args.caps.is_empty() || (args.hint.is_none() && args.role.is_none()) {
310 builder = builder
311 .filter_caps(caps)
312 .map_err(|error| anyhow!("building the capability filter: {error:?}"))?;
313 }
314
315 let options = builder.build();
316 let command = MacCommand::IdentityRequest { options: &options };
317 let mut frame = [0u8; REQUEST_FRAME];
318 frame[0] = PayloadType::MacCommand as u8;
319 let length = mac_command::encode(&command, &mut frame[1..])
320 .map_err(|error| anyhow!("encoding the request: {error:?}"))?
321 + 1;
322 Ok(frame[..length].to_vec())
323}
324
325fn steer_through(routes: &RouteCache, peer: &PublicKey) -> Result<Vec<RouterHint>> {
328 let Some(record) = routes.get(peer) else {
329 bail!("no remembered route to {peer}; reach it once — `ping {peer}` — and try again");
330 };
331 match &record.route {
332 CachedRoute::Source(hints) => Ok(hints.to_vec()),
333 CachedRoute::Direct => bail!(
337 "{peer} is a direct neighbor, so asking from its vantage is asking from here; drop --via"
338 ),
339 CachedRoute::Flood { .. } => bail!(
340 "only a flood route to {peer} is remembered ({}); a vantage needs the routers named",
341 routes::describe(&record.route)
342 ),
343 }
344}
345
346fn advertisement(packet: &ReceivedPacketRef<'_>) -> Option<NodeIdentityPayload> {
348 if packet.payload_type() != PayloadType::NodeIdentity {
349 return None;
350 }
351 NodeIdentityPayload::from_bytes(packet.payload()).ok()
354}
355
356fn report(
358 key: &PublicKey,
359 identity: &NodeIdentityPayload,
360 packet: &ReceivedPacketRef<'_>,
361 asked: Option<u32>,
362 started: Instant,
363) {
364 println!("{key}");
365 if let Some(name) = &identity.name {
366 subfield("name", name);
367 }
368 subfield("role", role_name(identity.role));
369 if !identity.capabilities.is_empty() {
370 subfield("capabilities", capability_names(identity.capabilities));
371 }
372 if let Some(location) = &identity.location
373 && !location.is_unspecified()
374 {
375 let (lat, lon) = location.center();
376 subfield(
377 "location",
378 format!("{lat:.4}, {lon:.4} (±{} bytes)", location.precision()),
379 );
380 }
381 if let Some(regions) = &identity.supported_regions
382 && !regions.is_empty()
383 {
384 subfield("regions", regions.join(", "));
385 }
386 if let Some(rssi) = packet.rssi() {
387 subfield("signal", format!("{rssi} dBm"));
388 }
389 if let Some(hops) = packet.hop_count() {
390 subfield("hops", hops);
391 }
392 subfield(
393 "heard",
394 match (asked, identity.nonce) {
395 (Some(mine), Some(theirs)) if mine == theirs => "an answer to this ask".to_string(),
396 (_, Some(_)) => "an answer to somebody else's ask".to_string(),
400 (_, None) => format!("advertised, {:.0?} in", started.elapsed()),
401 },
402 );
403 if !packet.source_authenticated() {
404 subfield("warning", "this identity is not authenticated");
405 }
406}
407
408fn role_name(role: NodeRole) -> String {
409 match role {
410 NodeRole::Unspecified => "unspecified".to_string(),
411 NodeRole::Repeater => "repeater".to_string(),
412 NodeRole::Chat => "chat".to_string(),
413 NodeRole::Tracker => "tracker".to_string(),
414 NodeRole::Sensor => "sensor".to_string(),
415 NodeRole::Bridge => "bridge".to_string(),
416 NodeRole::ChatRoom => "chat room".to_string(),
417 NodeRole::TemporarySession => "temporary session".to_string(),
418 NodeRole::Unknown(code) => format!("role {code}"),
419 }
420}
421
422fn capability_names(caps: NodeCapabilities) -> String {
423 let named = [
424 (NodeCapabilities::REPEATER, "repeater"),
425 (NodeCapabilities::MOBILE, "mobile"),
426 (NodeCapabilities::TEXT_MESSAGES, "text-messages"),
427 (NodeCapabilities::TELEMETRY, "telemetry"),
428 (NodeCapabilities::CHAT_ROOM, "chat-room"),
429 (NodeCapabilities::COAP, "coap"),
430 ];
431 let mut out: Vec<&str> = named
432 .iter()
433 .filter(|(bit, _)| caps.contains(*bit))
434 .map(|(_, name)| *name)
435 .collect();
436 let known = named
437 .iter()
438 .fold(NodeCapabilities::empty(), |bits, (bit, _)| bits | *bit);
439 let rest = caps.bits() & !known.bits();
440 let extra;
441 if rest != 0 {
442 extra = format!("0x{rest:02x}");
443 out.push(&extra);
444 }
445 out.join(", ")
446}
447
448fn describe_ask(args: &DiscoverArgs) -> String {
450 let mut parts = Vec::new();
451 if let Some(hint) = &args.hint {
452 parts.push(format!(
453 "hint {}",
454 hint.0
455 .iter()
456 .map(|byte| format!("{byte:02x}"))
457 .collect::<String>()
458 ));
459 }
460 if let Some(role) = args.role {
461 parts.push(role_name(role.role()));
462 }
463 if !args.caps.is_empty() {
464 parts.push(capability_names(
465 args.caps
466 .iter()
467 .fold(NodeCapabilities::empty(), |bits, cap| bits | cap.bit()),
468 ));
469 }
470 let who = if parts.is_empty() {
471 "every node in earshot".to_string()
472 } else {
473 parts.join(", ")
474 };
475 match &args.via {
476 Some(key) => format!("{who}, from the vantage of {key}", key = PublicKey(key.0)),
477 None => who,
478 }
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484 use umsh::core::NodeHint;
485 use umsh::node::mac_command::IdentityRequestFilters;
486
487 fn filters_of(frame: &[u8]) -> (Option<u32>, Vec<u8>) {
490 assert_eq!(frame[0], PayloadType::MacCommand as u8, "payload type");
491 let MacCommand::IdentityRequest { options } =
492 mac_command::parse(&frame[1..]).expect("parses as a MAC command")
493 else {
494 panic!("not an identity request");
495 };
496 let filters = IdentityRequestFilters::new(options);
497 (filters.nonce().expect("a readable nonce"), options.to_vec())
498 }
499
500 fn bare() -> DiscoverArgs {
501 DiscoverArgs {
502 timeout: 40,
503 passive: false,
504 role: None,
505 caps: Vec::new(),
506 hint: None,
507 via: None,
508 }
509 }
510
511 #[test]
512 fn an_unfiltered_ask_still_carries_a_filter_everyone_satisfies() {
513 let frame = request_frame(&bare(), 0x0102_0304).unwrap();
514 let (nonce, options) = filters_of(&frame);
515 assert_eq!(nonce, Some(0x0102_0304));
516
517 let filters = IdentityRequestFilters::new(&options);
521 assert!(!filters.hint_filtered());
522 for role in [NodeRole::Repeater, NodeRole::Chat, NodeRole::Sensor] {
523 assert!(
524 filters
525 .selects(
526 role,
527 NodeCapabilities::empty(),
528 &NodeHint([0x11, 0x22, 0x33])
529 )
530 .unwrap(),
531 "{role:?} should be selected by an unfiltered ask"
532 );
533 }
534 }
535
536 #[test]
537 fn a_narrowed_ask_selects_only_what_it_named() {
538 let args = DiscoverArgs {
539 role: Some(RoleFilter::Repeater),
540 caps: vec![CapFilter::Telemetry],
541 hint: Some(HintPrefixArg(vec![0xa1])),
542 ..bare()
543 };
544 let frame = request_frame(&args, 7).unwrap();
545 let (nonce, options) = filters_of(&frame);
546 assert_eq!(nonce, Some(7));
547 let filters = IdentityRequestFilters::new(&options);
548 assert!(filters.hint_filtered());
549
550 let matching = NodeHint([0xa1, 0x22, 0x33]);
551 let elsewhere = NodeHint([0xb0, 0x22, 0x33]);
552 let telemetry = NodeCapabilities::TELEMETRY | NodeCapabilities::REPEATER;
553 assert!(
554 filters
555 .selects(NodeRole::Repeater, telemetry, &matching)
556 .unwrap()
557 );
558 assert!(
561 !filters
562 .selects(NodeRole::Chat, telemetry, &matching)
563 .unwrap()
564 );
565 assert!(
566 !filters
567 .selects(NodeRole::Repeater, NodeCapabilities::REPEATER, &matching)
568 .unwrap()
569 );
570 assert!(
571 !filters
572 .selects(NodeRole::Repeater, telemetry, &elsewhere)
573 .unwrap()
574 );
575 }
576
577 #[test]
578 fn capabilities_read_as_names_and_keep_what_they_do_not_know() {
579 assert_eq!(
580 capability_names(NodeCapabilities::REPEATER | NodeCapabilities::TEXT_MESSAGES),
581 "repeater, text-messages"
582 );
583 assert_eq!(capability_names(NodeCapabilities::empty()), "");
584 assert_eq!(
586 capability_names(NodeCapabilities::from_bits_retain(0x81)),
587 "repeater, 0x80"
588 );
589 }
590
591 #[test]
592 fn an_ask_says_who_it_is_for() {
593 assert_eq!(describe_ask(&bare()), "every node in earshot");
594
595 let narrowed = DiscoverArgs {
596 role: Some(RoleFilter::Repeater),
597 caps: vec![CapFilter::Telemetry],
598 hint: Some(HintPrefixArg(vec![0xa1, 0xb2])),
599 ..bare()
600 };
601 assert_eq!(describe_ask(&narrowed), "hint a1b2, repeater, telemetry");
602 }
603
604 #[test]
605 fn a_vantage_needs_the_routers_named() {
606 let mut routes = RouteCache::default();
607 let peer = PublicKey([0x11; 32]);
608 assert!(steer_through(&routes, &peer).is_err());
610
611 routes.record(&peer, CachedRoute::Direct);
613 assert!(steer_through(&routes, &peer).is_err());
614
615 routes.record(&peer, CachedRoute::flood(5, &[]).unwrap());
617 assert!(steer_through(&routes, &peer).is_err());
618
619 let hops = [RouterHint([0xa1, 0xb2]), RouterHint([0xc3, 0xd4])];
620 routes.record(&peer, CachedRoute::source(&hops).unwrap());
621 assert_eq!(steer_through(&routes, &peer).unwrap(), hops);
622 }
623}