1#![allow(async_fn_in_trait)]
2#![cfg_attr(not(feature = "std"), no_std)]
3
4#[cfg(not(feature = "alloc"))]
135compile_error!("umsh-node currently requires the alloc feature");
136
137extern crate alloc;
138
139mod app_error;
140mod app_payload;
141mod app_util;
142#[cfg(feature = "software-crypto")]
143mod channel;
144mod dispatch;
145mod host;
146mod identity;
147pub mod identity_responder;
148pub mod location;
149mod mac;
150pub mod mac_command;
151mod node;
152mod peer;
153pub mod peer_repeaters;
154#[cfg(feature = "software-crypto")]
155mod pfs;
156mod receive;
157mod ticket;
158mod transport;
159
160pub use app_error::{AppEncodeError, AppParseError};
161pub use app_payload::{
162 expect_payload_type, parse_mac_command_payload, parse_node_identity_payload, split_payload_type,
163};
164#[cfg(feature = "software-crypto")]
165pub use channel::Channel;
166pub use host::{Host, HostError};
167pub use identity::{NodeCapabilities, NodeIdentityPayload, NodeRole};
168pub use identity_responder::{
169 IdentityRequestContext, NodeIdentityProfile, RespondDecision, default_respond_policy,
170 never_respond_policy,
171};
172pub use mac::{MacBackend, MacBackendError};
173pub use mac_command::OwnedMacCommand;
174pub use mac_command::{CommandId, MacCommand};
175#[cfg(feature = "software-crypto")]
176pub use node::BoundChannel;
177#[cfg(feature = "software-crypto")]
178pub use node::PfsStatus;
179pub use node::{LocalNode, NodeError, PfsFailure, PongMetadata, Subscription};
180pub use peer::{PING_MIC_SIZE, PeerConnection};
181pub use peer_repeaters::{
182 MAX_PEER_REPEATERS, MergedPeerRepeater, PeerRepeaterRecord, PeerRepeaterTable,
183};
184pub use receive::{ChannelInfoRef, PacketFamily, ReceivedPacketRef, RouteHops, RxMetadata, Snr};
185pub use ticket::{SendProgressTicket, SendToken};
186pub use transport::Transport;
187
188#[cfg(test)]
189mod tests {
190 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
191 use std::{
192 cell::RefCell,
193 collections::VecDeque,
194 future::Future,
195 num::NonZeroU8,
196 pin::pin,
197 rc::Rc,
198 task::{Context, Poll, RawWaker, RawWakerVTable, Waker},
199 };
200 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
201 use umsh_core::{NodeHint, PublicKey};
202 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
203 use umsh_crypto::NodeIdentity;
204 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
205 use umsh_crypto::software::SoftwareIdentity;
206 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
207 use umsh_hal::Snr;
208 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
209 use umsh_mac::MacEventRef;
210 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
211 use umsh_mac::{CapacityError, LocalIdentityId, PeerId, SendError, SendOptions, SendReceipt};
212
213 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
214 use crate::ReceivedPacketRef;
215 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
216 use crate::{MacBackend, MacBackendError, OwnedMacCommand, SendToken};
217 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
218 use umsh_core::ChannelId;
219 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
220 use umsh_text::OwnedTextMessage;
221 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
222 #[test]
223 fn peer_receive_handlers_precede_node_receive_handlers() {
224 use crate::node::{LocalNode, LocalNodeState, NodeMembership};
225
226 let mac = FakeMac::new(Vec::new());
227 let dispatcher = Rc::new(RefCell::new(crate::dispatch::EventDispatcher::new()));
228 let membership = Rc::new(RefCell::new(NodeMembership::new()));
229 let state = Rc::new(RefCell::new(LocalNodeState::new()));
230 let node = LocalNode::new(LocalIdentityId(1), mac, dispatcher, membership, state);
231 let peer = PublicKey([0x41; 32]);
232 let peer_connection = block_on_ready(node.peer(peer)).unwrap();
233
234 let call_order = Rc::new(RefCell::new(Vec::new()));
235 let peer_call_order = call_order.clone();
236 let _peer_subscription = peer_connection.on_receive(move |_| {
237 peer_call_order.borrow_mut().push("peer");
238 true
239 });
240 let node_call_order = call_order.clone();
241 let _node_subscription = node.on_receive(move |_| {
242 node_call_order.borrow_mut().push("node");
243 true
244 });
245
246 assert!(node.dispatch_received_packet(&test_unicast_packet(peer, &[0x01, 0x02])));
247 assert_eq!(call_order.borrow().as_slice(), ["peer"]);
248 }
249
250 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
251 fn test_node(mac: FakeMac) -> crate::node::LocalNode<FakeMac> {
252 use crate::node::{LocalNode, LocalNodeState, NodeMembership};
253
254 LocalNode::new(
255 LocalIdentityId(1),
256 mac,
257 Rc::new(RefCell::new(crate::dispatch::EventDispatcher::new())),
258 Rc::new(RefCell::new(NodeMembership::new())),
259 Rc::new(RefCell::new(LocalNodeState::new())),
260 )
261 }
262
263 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
264 #[test]
265 fn leaving_a_channel_unregisters_its_key_from_the_mac() {
266 let mac = FakeMac::new(Vec::new());
267 let node = test_node(mac.clone());
268 let channel = crate::Channel::private(umsh_core::ChannelKey([0x11; 32]), "trail");
269
270 block_on_ready(node.join(&channel)).unwrap();
271 assert!(mac.holds_channel(channel.key()));
272
273 block_on_ready(node.leave(&channel)).unwrap();
274 assert!(!mac.holds_channel(channel.key()));
275 assert!(node.bound_channel(&channel).is_none());
276 }
277
278 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
279 #[test]
280 fn rejoining_after_leave_re_registers_the_key_with_the_mac() {
281 let mac = FakeMac::new(Vec::new());
282 let node = test_node(mac.clone());
283 let channel = crate::Channel::private(umsh_core::ChannelKey([0x22; 32]), "camp");
284
285 let first = block_on_ready(node.join(&channel)).unwrap();
286 block_on_ready(node.leave(&channel)).unwrap();
287 let second = block_on_ready(node.join(&channel)).unwrap();
288
289 assert!(mac.holds_channel(channel.key()));
292 assert!(second.is_active());
293 assert!(!first.is_active());
294 }
295
296 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
297 #[test]
298 fn leaving_a_channel_that_was_never_joined_is_a_no_op() {
299 let mac = FakeMac::new(Vec::new());
300 let node = test_node(mac.clone());
301 let joined = crate::Channel::private(umsh_core::ChannelKey([0x33; 32]), "joined");
302 let stranger = crate::Channel::private(umsh_core::ChannelKey([0x44; 32]), "stranger");
303
304 block_on_ready(node.join(&joined)).unwrap();
305 block_on_ready(node.leave(&stranger)).unwrap();
306
307 assert!(mac.holds_channel(joined.key()));
308 assert!(node.bound_channel(&joined).is_some());
309 }
310
311 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
312 #[test]
313 fn receive_callbacks_can_observe_rx_metadata() {
314 use crate::node::{LocalNode, LocalNodeState, NodeMembership};
315
316 let mac = FakeMac::new(Vec::new());
317 let dispatcher = Rc::new(RefCell::new(crate::dispatch::EventDispatcher::new()));
318 let membership = Rc::new(RefCell::new(NodeMembership::new()));
319 let state = Rc::new(RefCell::new(LocalNodeState::new()));
320 let node = LocalNode::new(LocalIdentityId(1), mac, dispatcher, membership, state);
321 let peer = PublicKey([0x44; 32]);
322
323 let observed = Rc::new(RefCell::new(None));
324 let observed_for_callback = observed.clone();
325 let _subscription = node.on_receive(move |packet| {
326 *observed_for_callback.borrow_mut() = Some((
327 packet.rssi(),
328 packet.snr(),
329 packet.lqi(),
330 packet.received_at_ms(),
331 ));
332 true
333 });
334
335 let payload = encode_text_payload("metadata");
336 let packet = test_unicast_packet_with_rx(
337 peer,
338 &payload,
339 umsh_mac::RxMetadata::new(
340 Some(-73),
341 Some(Snr::from_centibels(123)),
342 NonZeroU8::new(200),
343 Some(123_456),
344 ),
345 );
346
347 assert!(node.dispatch_received_packet(&packet));
348 assert_eq!(
349 *observed.borrow(),
350 Some((
351 Some(-73),
352 Some(Snr::from_centibels(123)),
353 NonZeroU8::new(200),
354 Some(123_456),
355 ))
356 );
357 }
358
359 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
360 #[test]
361 fn subscription_guard_unregisters_on_drop() {
362 use crate::node::{LocalNode, LocalNodeState, NodeMembership};
363
364 let mac = FakeMac::new(Vec::new());
365 let dispatcher = Rc::new(RefCell::new(crate::dispatch::EventDispatcher::new()));
366 let membership = Rc::new(RefCell::new(NodeMembership::new()));
367 let state = Rc::new(RefCell::new(LocalNodeState::new()));
368 let node = LocalNode::new(LocalIdentityId(1), mac, dispatcher, membership, state);
369 let peer = PublicKey([0x33; 32]);
370
371 let hits = Rc::new(RefCell::new(0u32));
372 {
373 let hits = hits.clone();
374 let _subscription = node.on_receive(move |_| {
375 *hits.borrow_mut() += 1;
376 true
377 });
378 assert!(node.dispatch_received_packet(&test_unicast_packet(peer, &[0x01, 0x02])));
379 }
380
381 assert_eq!(*hits.borrow(), 1);
382 assert!(!node.dispatch_received_packet(&test_unicast_packet(peer, &[0x01, 0x02])));
383 assert_eq!(*hits.borrow(), 1);
384 }
385
386 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
387 #[test]
388 fn callbacks_observe_control_side_events_and_peer_ack_state() {
389 use crate::node::{LocalNode, LocalNodeState, NodeMembership};
390
391 let mac = FakeMac::new(Vec::new());
392 let dispatcher = Rc::new(RefCell::new(crate::dispatch::EventDispatcher::new()));
393 let membership = Rc::new(RefCell::new(NodeMembership::new()));
394 let state = Rc::new(RefCell::new(LocalNodeState::new()));
395 let node = LocalNode::new(LocalIdentityId(1), mac, dispatcher, membership, state);
396
397 let peer = PublicKey([0x42; 32]);
398 let peer_connection = block_on_ready(node.peer(peer)).unwrap();
399 let node_discovery = Rc::new(RefCell::new(Vec::new()));
400 let beacons = Rc::new(RefCell::new(Vec::new()));
401 let commands = Rc::new(RefCell::new(Vec::new()));
402 let peer_acks = Rc::new(RefCell::new(Vec::new()));
403 let peer_timeouts = Rc::new(RefCell::new(Vec::new()));
404
405 let discovery_log = node_discovery.clone();
406 let _discovered_subscription = node.on_node_discovered(move |key, name| {
407 discovery_log
408 .borrow_mut()
409 .push((key, name.map(str::to_string)));
410 });
411 let beacon_log = beacons.clone();
412 let _beacon_subscription = node.on_beacon(move |from_hint, from_key| {
413 beacon_log.borrow_mut().push((from_hint, from_key));
414 });
415 let command_log = commands.clone();
416 let _command_subscription = node.on_mac_command(move |from, command| {
417 command_log.borrow_mut().push((from, command.clone()));
418 });
419 let peer_ack_log = peer_acks.clone();
420 let _ack_subscription = peer_connection.on_ack_received(move |token| {
421 peer_ack_log.borrow_mut().push(token);
422 });
423 let peer_timeout_log = peer_timeouts.clone();
424 let _timeout_subscription = peer_connection.on_ack_timeout(move |token| {
425 peer_timeout_log.borrow_mut().push(token);
426 });
427
428 let token = SendToken::new(LocalIdentityId(1), SendReceipt(12));
429 let timeout_token = SendToken::new(LocalIdentityId(1), SendReceipt(13));
430 let hint = NodeHint([1, 2, 3]);
431 let command = OwnedMacCommand::EchoRequest {
432 data: vec![9, 8, 7],
433 };
434
435 node.dispatch_node_discovered(peer, Some("alice"));
436 node.dispatch_beacon(hint, Some(peer));
437 node.dispatch_mac_command(peer, &command);
438 node.dispatch_ack_received(peer, token);
439 node.dispatch_ack_timeout(peer, timeout_token);
440
441 assert_eq!(
442 node_discovery.borrow().as_slice(),
443 &[(peer, Some(String::from("alice")))]
444 );
445 assert_eq!(beacons.borrow().as_slice(), &[(hint, Some(peer))]);
446 assert_eq!(commands.borrow().as_slice(), &[(peer, command)]);
447 assert_eq!(peer_acks.borrow().as_slice(), &[token]);
448 assert_eq!(peer_timeouts.borrow().as_slice(), &[timeout_token]);
449 }
450
451 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
456 #[test]
457 fn ping_honours_caller_options_but_never_requests_a_mac_ack() {
458 use crate::node::{LocalNode, LocalNodeState, NodeMembership};
459 use umsh_core::MicSize;
460
461 let mac = FakeMac::new(vec![[7u8; 32], [9u8; 32]]);
462 let node = LocalNode::new(
463 LocalIdentityId(1),
464 mac.clone(),
465 Rc::new(RefCell::new(crate::dispatch::EventDispatcher::new())),
466 Rc::new(RefCell::new(NodeMembership::new())),
467 Rc::new(RefCell::new(LocalNodeState::new())),
468 );
469 let peer_connection = block_on_ready(node.peer(PublicKey([0x55; 32]))).unwrap();
470
471 let options = SendOptions::default()
472 .with_mic_size(MicSize::Mic4)
473 .with_ack_requested(true)
474 .with_trace_route()
475 .with_flood_hops(3)
476 .with_region_code([0x78, 0x53]);
477 block_on_ready(peer_connection.ping(6, &options, 1_000)).unwrap();
478
479 let sent = mac.take_unicasts().pop().expect("ping send");
480 assert_eq!(sent.options.mic_size, MicSize::Mic4);
481 assert!(sent.options.trace_route);
482 assert_eq!(sent.options.flood_hops, Some(3));
483 assert_eq!(sent.options.region_code, Some([0x78, 0x53]));
484 assert!(!sent.options.ack_requested, "the echo response is the ack");
485
486 block_on_ready(peer_connection.ping(0, &SendOptions::default().no_flood(), 1_000)).unwrap();
489 let sent = mac.take_unicasts().pop().expect("ping send");
490 assert_eq!(sent.options.flood_hops, None);
491 }
492
493 #[test]
496 fn ping_mic_size_is_eight_bytes() {
497 assert_eq!(crate::PING_MIC_SIZE, umsh_core::MicSize::Mic8);
498 assert_eq!(crate::PING_MIC_SIZE.byte_len(), 8);
499 }
500
501 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
508 #[test]
509 fn ping_data_is_not_capped_by_the_node_layer() {
510 let mac = FakeMac::new(vec![[7u8; 32]]);
511 let node = test_node(mac.clone());
512 let peer_connection = block_on_ready(node.peer(PublicKey([0x55; 32]))).unwrap();
513
514 block_on_ready(peer_connection.ping(120, &SendOptions::default(), 1_000)).unwrap();
515
516 let sent = mac.take_unicasts().pop().expect("ping send");
517 match parse_owned_mac_command(&sent.payload) {
518 OwnedMacCommand::EchoRequest { data } => {
519 assert_eq!(data.len(), 2 + 120, "2-byte nonce plus the requested fill");
520 }
521 other => panic!("unexpected ping payload: {other:?}"),
522 }
523 }
524
525 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
530 #[test]
531 fn ping_over_a_bound_channel_sends_a_blind_unicast() {
532 let mac = FakeMac::new(vec![[0x3c; 32]]);
533 let node = test_node(mac.clone());
534 let channel = crate::Channel::private(umsh_core::ChannelKey([0x66; 32]), "trail");
535 let bound = block_on_ready(node.join(&channel)).unwrap();
536
537 let peer = PublicKey([0x55; 32]);
538 let pongs = Rc::new(RefCell::new(Vec::new()));
539 let pong_log = pongs.clone();
540 let peer_connection = bound.peer(peer);
541 let _pong_subscription = peer_connection.on_pong(move |rtt_ms| {
542 pong_log.borrow_mut().push(rtt_ms);
543 });
544
545 block_on_ready(peer_connection.ping(4, &SendOptions::default(), 1_000)).unwrap();
546
547 let sent = mac.take_unicasts().pop().expect("ping send");
548 assert_eq!(
549 sent.channel,
550 Some(*channel.channel_id()),
551 "a channel-bound ping goes out blind on that channel"
552 );
553 assert!(!sent.options.ack_requested, "the echo response is the ack");
554 let nonce = match parse_owned_mac_command(&sent.payload) {
555 OwnedMacCommand::EchoRequest { data } => {
556 assert_eq!(data.len(), 2 + 4);
557 [data[0], data[1]]
558 }
559 other => panic!("unexpected ping payload: {other:?}"),
560 };
561
562 let packet = test_channel_packet(
563 peer,
564 umsh_core::PacketType::BlindUnicast,
565 channel.key(),
566 *channel.channel_id(),
567 &nonce,
568 );
569 node.match_pong(peer, &nonce, &packet, 1_250);
570
571 assert_eq!(pongs.borrow().as_slice(), &[250]);
572 }
573
574 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
575 #[test]
576 fn pfs_routed_send_tracks_ack_against_ephemeral_identity() {
577 use crate::node::{LocalNode, LocalNodeState, NodeMembership};
578
579 let mac = FakeMac::new(vec![[7u8; 32], [9u8; 32]]);
580 let dispatcher = Rc::new(RefCell::new(crate::dispatch::EventDispatcher::new()));
581 let membership = Rc::new(RefCell::new(NodeMembership::new()));
582 let state = Rc::new(RefCell::new(LocalNodeState::new()));
583 let node = LocalNode::new(
584 LocalIdentityId(1),
585 mac.clone(),
586 dispatcher.clone(),
587 membership,
588 state,
589 );
590
591 let peer = PublicKey([0x55; 32]);
592 let peer_connection = block_on_ready(node.peer(peer)).unwrap();
593 let options = SendOptions::default().with_ack_requested(true);
594
595 block_on_ready(node.request_pfs(&peer, 60, &options)).unwrap();
596 let request = mac.take_unicasts().pop().expect("request send");
597 let request_command = parse_owned_mac_command(&request.payload);
598 let request_ephemeral = match request_command {
599 OwnedMacCommand::PfsSessionRequest { ephemeral_key, .. } => ephemeral_key,
600 other => panic!("unexpected request payload: {other:?}"),
601 };
602
603 block_on_ready(node.handle_pfs_command(
604 &peer,
605 None,
606 &OwnedMacCommand::PfsSessionResponse {
607 ephemeral_key: PublicKey([0x44; 32]),
608 duration_minutes: 60,
609 },
610 &options,
611 ))
612 .unwrap();
613
614 let payload = encode_text_payload("hello over pfs");
615 let ticket = block_on_ready(peer_connection.send(&payload, &options)).unwrap();
616 let sent = mac.take_unicasts().pop().expect("pfs-routed send");
617 assert_eq!(sent.from, LocalIdentityId(10));
618 assert_eq!(sent.to, PublicKey([0x44; 32]));
619
620 let pairwise_from_pfs = PublicKey([0x44; 32]);
621 let _ = request_ephemeral; dispatcher.borrow_mut().dispatch_ticket_state(
623 sent.from,
624 &MacEventRef::AckReceived {
625 peer: pairwise_from_pfs,
626 receipt: SendReceipt(42),
627 },
628 );
629 assert!(ticket.was_acked());
630 assert!(ticket.is_finished());
631 }
632
633 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
634 #[test]
635 fn pfs_session_manager_request_and_teardown() {
636 use crate::pfs::PfsSessionManager;
637
638 let mac = FakeMac::new(vec![[3u8; 32]]);
639 let peer_long_term = PublicKey([0x55; 32]);
640 let options = SendOptions::default().with_ack_requested(true);
641
642 let mut pfs = PfsSessionManager::new();
643 block_on_ready(pfs.request_session(
644 &mac,
645 LocalIdentityId(1),
646 &peer_long_term,
647 60,
648 &options,
649 ))
650 .unwrap();
651
652 let sent = mac.take_unicasts();
653 assert_eq!(sent.len(), 1);
654 assert_eq!(sent[0].from, LocalIdentityId(1));
655 assert_eq!(sent[0].to, peer_long_term);
656 assert_eq!(
657 parse_owned_mac_command(&sent[0].payload),
658 OwnedMacCommand::PfsSessionRequest {
659 ephemeral_key: *SoftwareIdentity::from_secret_bytes(&[3u8; 32]).public_key(),
660 duration_minutes: 60,
661 }
662 );
663
664 assert!(
665 block_on_ready(pfs.end_session(
666 &mac,
667 LocalIdentityId(1),
668 &peer_long_term,
669 true,
670 &options,
671 ))
672 .unwrap()
673 );
674 let sent = mac.take_unicasts();
675 assert_eq!(sent.len(), 1);
676 assert_eq!(
677 parse_owned_mac_command(&sent[0].payload),
678 OwnedMacCommand::EndPfsSession
679 );
680 }
681
682 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
683 #[test]
684 fn pfs_end_session_errors_when_missing() {
685 use crate::pfs::PfsSessionManager;
686
687 let mac = FakeMac::new(Vec::new());
688 let options = SendOptions::default();
689 let mut pfs = PfsSessionManager::new();
690
691 let error = block_on_ready(pfs.end_session(
692 &mac,
693 LocalIdentityId(1),
694 &PublicKey([0x77; 32]),
695 true,
696 &options,
697 ))
698 .unwrap_err();
699 assert!(matches!(error, crate::NodeError::PfsSessionMissing));
700 }
701
702 #[cfg(feature = "unsafe-advanced")]
703 fn encode_text_payload(text: &str) -> Vec<u8> {
704 let message = OwnedTextMessage::basic(text);
705 let mut body = [0u8; 512];
706 let len = umsh_text::text_message::encode(&message.as_borrowed(), &mut body).unwrap();
707 let mut payload = Vec::with_capacity(len + 1);
708 payload.push(umsh_core::PayloadType::TextMessage as u8);
709 payload.extend_from_slice(&body[..len]);
710 payload
711 }
712
713 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
714 fn test_unicast_packet<'a>(from: PublicKey, payload: &'a [u8]) -> ReceivedPacketRef<'a> {
715 test_unicast_packet_with_rx(from, payload, umsh_mac::RxMetadata::default())
716 }
717
718 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
719 fn test_unicast_packet_with_rx<'a>(
720 from: PublicKey,
721 payload: &'a [u8],
722 rx: umsh_mac::RxMetadata,
723 ) -> ReceivedPacketRef<'a> {
724 let wire = Box::leak(payload.to_vec().into_boxed_slice());
725 let header = umsh_core::PacketHeader {
726 fcf: umsh_core::Fcf::new(umsh_core::PacketType::Unicast, false, false),
727 options_range: 0..0,
728 flood_hops: None,
729 dst: None,
730 channel: None,
731 source: umsh_core::SourceAddrRef::Hint(from.hint()),
732 sec_info: None,
733 body_range: 0..wire.len(),
734 mic_range: wire.len()..wire.len(),
735 total_len: wire.len(),
736 };
737 ReceivedPacketRef::new(
738 wire,
739 wire,
740 header,
741 umsh_core::ParsedOptions::default(),
742 Some(from),
743 Some(from.hint()),
744 true,
745 None,
746 rx,
747 )
748 }
749
750 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
753 fn test_channel_packet<'a>(
754 from: PublicKey,
755 packet_type: umsh_core::PacketType,
756 channel_key: &'a umsh_core::ChannelKey,
757 channel_id: umsh_core::ChannelId,
758 payload: &'a [u8],
759 ) -> ReceivedPacketRef<'a> {
760 let wire = Box::leak(payload.to_vec().into_boxed_slice());
761 let header = umsh_core::PacketHeader {
762 fcf: umsh_core::Fcf::new(packet_type, false, false),
763 options_range: 0..0,
764 flood_hops: None,
765 dst: None,
766 channel: Some(channel_id),
767 source: umsh_core::SourceAddrRef::Hint(from.hint()),
768 sec_info: None,
769 body_range: 0..wire.len(),
770 mic_range: wire.len()..wire.len(),
771 total_len: wire.len(),
772 };
773 ReceivedPacketRef::new(
774 wire,
775 wire,
776 header,
777 umsh_core::ParsedOptions::default(),
778 Some(from),
779 Some(from.hint()),
780 true,
781 Some(crate::ChannelInfoRef {
782 id: channel_id,
783 key: channel_key,
784 }),
785 umsh_mac::RxMetadata::default(),
786 )
787 }
788
789 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
793 fn test_broadcast_packet(
794 from: PublicKey,
795 flood_hops: Option<u8>,
796 route: Option<&'static [u8]>,
797 ) -> ReceivedPacketRef<'static> {
798 test_broadcast_packet_with_trace(from, flood_hops, route, None)
799 }
800
801 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
804 fn test_broadcast_packet_with_trace(
805 from: PublicKey,
806 flood_hops: Option<u8>,
807 route: Option<&'static [u8]>,
808 trace: Option<&'static [u8]>,
809 ) -> ReceivedPacketRef<'static> {
810 let route_len = route.map_or(0, <[u8]>::len);
811 let trace_len = trace.map_or(0, <[u8]>::len);
812 let mut bytes = route.map_or_else(Vec::new, <[u8]>::to_vec);
813 bytes.extend_from_slice(trace.unwrap_or(&[]));
814 let wire: &'static [u8] = Box::leak(bytes.into_boxed_slice());
815 let mut options = umsh_core::ParsedOptions::default();
816 if route.is_some() {
817 options.source_route = Some(0..route_len);
818 }
819 if trace.is_some() {
820 options.trace_route = Some(route_len..route_len + trace_len);
821 }
822 let header = umsh_core::PacketHeader {
823 fcf: umsh_core::Fcf::new(umsh_core::PacketType::Broadcast, false, false),
824 options_range: 0..route_len + trace_len,
825 flood_hops: flood_hops.map(umsh_core::FloodHops),
826 dst: None,
827 channel: None,
828 source: umsh_core::SourceAddrRef::Hint(from.hint()),
829 sec_info: None,
830 body_range: route_len + trace_len..wire.len(),
831 mic_range: wire.len()..wire.len(),
832 total_len: wire.len(),
833 };
834 ReceivedPacketRef::new(
835 wire,
836 &wire[route_len + trace_len..],
837 header,
838 options,
839 Some(from),
840 Some(from.hint()),
841 false,
842 None,
843 umsh_mac::RxMetadata::default(),
844 )
845 }
846
847 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
848 #[derive(Clone, Default)]
849 struct FakeMac {
850 state: Rc<RefCell<FakeMacState>>,
851 }
852
853 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
854 #[derive(Default)]
855 struct FakeMacState {
856 random_blocks: VecDeque<[u8; 32]>,
857 next_peer_id: u8,
858 next_ephemeral_id: u8,
859 now_ms: u64,
860 unicasts: Vec<SentUnicast>,
861 removed_ephemerals: Vec<LocalIdentityId>,
862 peers: Vec<(PublicKey, PeerId)>,
863 channels: Vec<umsh_core::ChannelKey>,
864 observations: Vec<umsh_mac::TransmitterObservation>,
865 }
866
867 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
868 #[derive(Clone, Debug, PartialEq, Eq)]
869 struct SentUnicast {
870 from: LocalIdentityId,
871 to: PublicKey,
872 payload: Vec<u8>,
873 options: SendOptions,
874 channel: Option<ChannelId>,
876 }
877
878 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
879 impl FakeMac {
880 fn new(random_blocks: Vec<[u8; 32]>) -> Self {
881 Self {
882 state: Rc::new(RefCell::new(FakeMacState {
883 random_blocks: random_blocks.into(),
884 next_peer_id: 0,
885 next_ephemeral_id: 10,
886 now_ms: 1_000,
887 ..FakeMacState::default()
888 })),
889 }
890 }
891
892 fn take_unicasts(&self) -> Vec<SentUnicast> {
893 core::mem::take(&mut self.state.borrow_mut().unicasts)
894 }
895
896 fn holds_channel(&self, key: &umsh_core::ChannelKey) -> bool {
897 self.state.borrow().channels.iter().any(|k| k.0 == key.0)
898 }
899
900 fn observe(&self, hint: umsh_core::RouterHint, rssi_dbm: i16, snr: umsh_hal::Snr) {
901 let mut state = self.state.borrow_mut();
902 let last_seen_ms = state.now_ms;
903 state.observations.push(umsh_mac::TransmitterObservation {
904 hint,
905 rssi_dbm,
906 snr,
907 last_seen_ms,
908 });
909 }
910 }
911
912 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
913 impl MacBackend for FakeMac {
914 type SendError = SendError;
915 type CapacityError = CapacityError;
916 type RunError = core::convert::Infallible;
917
918 async fn next_event(
919 &self,
920 _on_event: impl FnMut(LocalIdentityId, umsh_mac::MacEventRef<'_>),
921 ) -> Result<(), Self::RunError> {
922 Ok(())
925 }
926
927 async fn add_peer(
928 &self,
929 key: PublicKey,
930 ) -> Result<PeerId, MacBackendError<Self::SendError, Self::CapacityError>> {
931 let mut state = self.state.borrow_mut();
932 if let Some((_, existing)) = state
933 .peers
934 .iter()
935 .find(|(existing_key, _)| *existing_key == key)
936 {
937 return Ok(*existing);
938 }
939 let peer_id = PeerId(state.next_peer_id);
940 state.next_peer_id = state.next_peer_id.wrapping_add(1);
941 state.peers.push((key, peer_id));
942 Ok(peer_id)
943 }
944
945 async fn add_private_channel(
946 &self,
947 key: umsh_core::ChannelKey,
948 ) -> Result<(), MacBackendError<Self::SendError, Self::CapacityError>> {
949 let mut state = self.state.borrow_mut();
950 if !state.channels.iter().any(|k| k.0 == key.0) {
951 state.channels.push(key);
952 }
953 Ok(())
954 }
955
956 async fn add_named_channel(
957 &self,
958 _name: &str,
959 ) -> Result<(), MacBackendError<Self::SendError, Self::CapacityError>> {
960 Ok(())
961 }
962
963 async fn remove_channel(&self, key: &umsh_core::ChannelKey) -> bool {
964 let mut state = self.state.borrow_mut();
965 let before = state.channels.len();
966 state.channels.retain(|k| k.0 != key.0);
967 state.channels.len() != before
968 }
969
970 async fn send_broadcast(
971 &self,
972 _from: LocalIdentityId,
973 _payload: &[u8],
974 _options: &SendOptions,
975 ) -> Result<SendReceipt, MacBackendError<Self::SendError, Self::CapacityError>> {
976 Ok(SendReceipt(99))
977 }
978
979 async fn send_multicast(
980 &self,
981 _from: LocalIdentityId,
982 _channel: &ChannelId,
983 _payload: &[u8],
984 _options: &SendOptions,
985 ) -> Result<SendReceipt, MacBackendError<Self::SendError, Self::CapacityError>> {
986 Ok(SendReceipt(99))
987 }
988
989 async fn send_unicast(
990 &self,
991 from: LocalIdentityId,
992 dst: &PublicKey,
993 payload: &[u8],
994 options: &SendOptions,
995 ) -> Result<Option<SendReceipt>, MacBackendError<Self::SendError, Self::CapacityError>>
996 {
997 self.state.borrow_mut().unicasts.push(SentUnicast {
998 from,
999 to: *dst,
1000 payload: payload.to_vec(),
1001 options: options.clone(),
1002 channel: None,
1003 });
1004 Ok(Some(SendReceipt(42)))
1005 }
1006
1007 async fn send_blind_unicast(
1008 &self,
1009 from: LocalIdentityId,
1010 dst: &PublicKey,
1011 channel: &ChannelId,
1012 payload: &[u8],
1013 options: &SendOptions,
1014 ) -> Result<Option<SendReceipt>, MacBackendError<Self::SendError, Self::CapacityError>>
1015 {
1016 self.state.borrow_mut().unicasts.push(SentUnicast {
1017 from,
1018 to: *dst,
1019 payload: payload.to_vec(),
1020 options: options.clone(),
1021 channel: Some(*channel),
1022 });
1023 Ok(Some(SendReceipt(42)))
1024 }
1025
1026 async fn fill_random(&self, dest: &mut [u8]) {
1027 let mut state = self.state.borrow_mut();
1028 let next = state.random_blocks.pop_front().expect("test rng exhausted");
1029 dest.copy_from_slice(&next[..dest.len()]);
1030 }
1031
1032 async fn now_ms(&self) -> u64 {
1033 self.state.borrow().now_ms
1034 }
1035
1036 async fn register_ephemeral(
1037 &self,
1038 _parent: LocalIdentityId,
1039 _identity: SoftwareIdentity,
1040 ) -> Result<LocalIdentityId, MacBackendError<Self::SendError, Self::CapacityError>>
1041 {
1042 let mut state = self.state.borrow_mut();
1043 let id = LocalIdentityId(state.next_ephemeral_id);
1044 state.next_ephemeral_id = state.next_ephemeral_id.wrapping_add(1);
1045 Ok(id)
1046 }
1047
1048 async fn remove_ephemeral(&self, id: LocalIdentityId) -> bool {
1049 self.state.borrow_mut().removed_ephemerals.push(id);
1050 true
1051 }
1052
1053 async fn for_each_transmitter_observation(
1054 &self,
1055 f: &mut dyn FnMut(umsh_mac::TransmitterObservation),
1056 ) {
1057 let observations = self.state.borrow().observations.clone();
1058 for observation in observations {
1059 f(observation);
1060 }
1061 }
1062 }
1063
1064 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1065 fn parse_owned_mac_command(payload: &[u8]) -> OwnedMacCommand {
1066 OwnedMacCommand::from(
1067 crate::parse_mac_command_payload(umsh_core::PacketType::Unicast, payload).unwrap(),
1068 )
1069 }
1070
1071 #[cfg(feature = "unsafe-advanced")]
1072 fn block_on_ready<F: Future>(future: F) -> F::Output {
1073 fn raw_waker() -> RawWaker {
1074 fn clone(_: *const ()) -> RawWaker {
1075 raw_waker()
1076 }
1077 fn wake(_: *const ()) {}
1078 fn wake_by_ref(_: *const ()) {}
1079 fn drop(_: *const ()) {}
1080
1081 RawWaker::new(
1082 core::ptr::null(),
1083 &RawWakerVTable::new(clone, wake, wake_by_ref, drop),
1084 )
1085 }
1086
1087 let waker = unsafe { Waker::from_raw(raw_waker()) };
1088 let mut context = Context::from_waker(&waker);
1089 let mut future = pin!(future);
1090 match future.as_mut().poll(&mut context) {
1091 Poll::Ready(value) => value,
1092 Poll::Pending => panic!("test future unexpectedly returned Poll::Pending"),
1093 }
1094 }
1095
1096 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1099 fn responder_node(mac: &FakeMac) -> crate::LocalNode<FakeMac> {
1100 use crate::node::{LocalNode, LocalNodeState, NodeMembership};
1101 let dispatcher = Rc::new(RefCell::new(crate::dispatch::EventDispatcher::new()));
1102 let membership = Rc::new(RefCell::new(NodeMembership::new()));
1103 let state = Rc::new(RefCell::new(LocalNodeState::new()));
1104 LocalNode::new(
1105 LocalIdentityId(1),
1106 mac.clone(),
1107 dispatcher,
1108 membership,
1109 state,
1110 )
1111 }
1112
1113 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1114 fn test_profile(public_key: PublicKey) -> crate::NodeIdentityProfile {
1115 crate::NodeIdentityProfile::new(
1116 public_key,
1117 crate::NodeRole::Repeater,
1118 crate::NodeCapabilities::REPEATER | crate::NodeCapabilities::TEXT_MESSAGES,
1119 )
1120 .with_name("repeater-1")
1121 }
1122
1123 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1124 #[test]
1125 fn responder_answers_selected_request_with_unicast_identity() {
1126 let mac = FakeMac::new(Vec::new());
1127 let node = responder_node(&mac);
1128 let our_key = PublicKey([0x11; 32]);
1129 let requester = PublicKey([0x41; 32]);
1130 node.enable_identity_responder_default(test_profile(our_key));
1131
1132 let options = crate::mac_command::IdentityRequestBuilder::new()
1134 .nonce(0xCAFE_F00D)
1135 .unwrap()
1136 .filter_hint(&our_key.hint())
1137 .unwrap()
1138 .build();
1139 let packet = test_unicast_packet(requester, &[]);
1140
1141 let plan = node
1142 .evaluate_identity_request(&packet, requester, &options, 0)
1143 .expect("responder should produce a reply plan");
1144 block_on_ready(node.send_identity_response(plan));
1145
1146 let unicasts = mac.take_unicasts();
1147 assert_eq!(unicasts.len(), 1, "exactly one unicast reply");
1148 let reply = &unicasts[0];
1149 assert_eq!(reply.to, requester, "reply is addressed to the requester");
1150 assert!(!reply.options.full_source);
1153 assert_eq!(reply.options.tx_delay_ms, None);
1156
1157 assert_eq!(reply.payload[0], umsh_core::PayloadType::NodeIdentity as u8);
1158 let identity = crate::NodeIdentityPayload::from_bytes(&reply.payload[1..]).unwrap();
1159 assert_eq!(identity.role, crate::NodeRole::Repeater);
1160 assert_eq!(identity.name.as_deref(), Some("repeater-1"));
1161 assert_eq!(identity.nonce, Some(0xCAFE_F00D), "request nonce echoed");
1162 assert!(identity.signature.is_none(), "responses are unsigned");
1163 }
1164
1165 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1169 #[test]
1170 fn identity_response_follows_a_blind_request_onto_its_channel() {
1171 let mac = FakeMac::new(Vec::new());
1172 let node = responder_node(&mac);
1173 let our_key = PublicKey([0x11; 32]);
1174 let requester = PublicKey([0x41; 32]);
1175 node.enable_identity_responder_default(test_profile(our_key));
1176
1177 let channel_key = umsh_core::ChannelKey([0x5A; 32]);
1178 let channel_id = umsh_core::ChannelId([0xC1, 0xD2]);
1179 let options = crate::mac_command::IdentityRequestBuilder::new()
1180 .filter_hint(&our_key.hint())
1181 .unwrap()
1182 .build();
1183 let packet = test_channel_packet(
1184 requester,
1185 umsh_core::PacketType::BlindUnicast,
1186 &channel_key,
1187 channel_id,
1188 &[],
1189 );
1190
1191 let plan = node
1192 .evaluate_identity_request(&packet, requester, &options, 0)
1193 .expect("responder should produce a reply plan");
1194 block_on_ready(node.send_identity_response(plan));
1195
1196 let sent = mac.take_unicasts();
1197 assert_eq!(sent.len(), 1);
1198 assert_eq!(
1199 sent[0].channel,
1200 Some(channel_id),
1201 "a blind request must not be answered off its channel"
1202 );
1203 }
1204
1205 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1209 #[test]
1210 fn identity_response_to_a_multicast_solicitation_stays_unicast() {
1211 let mac = FakeMac::new(vec![[0x12; 32]]);
1212 let node = responder_node(&mac);
1213 let our_key = PublicKey([0x11; 32]);
1214 let requester = PublicKey([0x41; 32]);
1215 node.enable_identity_responder_default(test_profile(our_key));
1216
1217 let channel_key = umsh_core::ChannelKey([0x5A; 32]);
1218 let channel_id = umsh_core::ChannelId([0xC1, 0xD2]);
1219 let options = crate::mac_command::IdentityRequestBuilder::new()
1220 .filter_hint(&our_key.hint())
1221 .unwrap()
1222 .build();
1223 let packet = test_channel_packet(
1224 requester,
1225 umsh_core::PacketType::Multicast,
1226 &channel_key,
1227 channel_id,
1228 &[],
1229 );
1230
1231 let plan = node
1232 .evaluate_identity_request(&packet, requester, &options, 0)
1233 .expect("responder should produce a reply plan");
1234 block_on_ready(node.send_identity_response(plan));
1235
1236 let sent = mac.take_unicasts();
1237 assert_eq!(sent.len(), 1);
1238 assert_eq!(sent[0].channel, None);
1239 }
1240
1241 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1242 #[test]
1243 fn responder_answers_broadcast_solicitation_with_delayed_full_source_reply() {
1244 let mac = FakeMac::new(vec![[0x12; 32]]);
1245 let node = responder_node(&mac);
1246 let our_key = PublicKey([0x11; 32]);
1247 let requester = PublicKey([0x41; 32]);
1248 node.enable_identity_responder_default(test_profile(our_key));
1249
1250 let options = crate::mac_command::IdentityRequestBuilder::new()
1251 .nonce(0x0000_BEEF)
1252 .unwrap()
1253 .filter_role(crate::NodeRole::Repeater)
1254 .unwrap()
1255 .build();
1256 let packet = test_broadcast_packet(requester, None, None);
1257
1258 let plan = node
1259 .evaluate_identity_request(&packet, requester, &options, 0)
1260 .expect("selected broadcast solicitation produces a plan");
1261 block_on_ready(node.send_identity_response(plan));
1262
1263 let unicasts = mac.take_unicasts();
1264 assert_eq!(unicasts.len(), 1);
1265 let reply = &unicasts[0];
1266 assert_eq!(reply.to, requester);
1267 assert!(reply.options.full_source);
1269 let delay = reply
1272 .options
1273 .tx_delay_ms
1274 .expect("broadcast replies are jittered");
1275 assert!((500..=30_000).contains(&delay));
1276 assert_eq!(reply.options.flood_hops, None);
1279 }
1280
1281 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1284 #[test]
1285 fn responder_answers_flood_routed_hint_filtered_solicitation() {
1286 let mac = FakeMac::new(vec![[0x12; 32]]);
1287 let node = responder_node(&mac);
1288 let our_key = PublicKey([0x11; 32]);
1289 let requester = PublicKey([0x41; 32]);
1290 node.enable_identity_responder_default(test_profile(our_key));
1291
1292 let options = crate::mac_command::IdentityRequestBuilder::new()
1293 .filter_hint(&our_key.hint())
1294 .unwrap()
1295 .build();
1296 let repeated = test_broadcast_packet(requester, Some(0x21), None);
1298
1299 let plan = node
1300 .evaluate_identity_request(&repeated, requester, &options, 0)
1301 .expect("a hint-filtered solicitation may be flood routed");
1302 block_on_ready(node.send_identity_response(plan));
1303
1304 let unicasts = mac.take_unicasts();
1305 assert_eq!(unicasts.len(), 1);
1306 assert!(
1307 unicasts[0].options.flood_hops.is_some(),
1308 "the reply may be flooded back to a requester that named us"
1309 );
1310 assert_eq!(
1311 unicasts[0].options.tx_delay_ms, None,
1312 "a whole hint names one node, so the reply goes out at once"
1313 );
1314 }
1315
1316 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1320 #[test]
1321 fn responder_holds_a_reply_to_a_one_byte_hint_prefix() {
1322 let mac = FakeMac::new(vec![[0x12; 32]]);
1323 let node = responder_node(&mac);
1324 let our_key = PublicKey([0x11; 32]);
1325 let requester = PublicKey([0x41; 32]);
1326 node.enable_identity_responder_default(test_profile(our_key));
1327
1328 let options = crate::mac_command::IdentityRequestBuilder::new()
1329 .filter_hint_prefix(&our_key.hint().0[..1])
1330 .unwrap()
1331 .build();
1332 let packet = test_broadcast_packet(requester, None, None);
1333
1334 let plan = node
1335 .evaluate_identity_request(&packet, requester, &options, 0)
1336 .expect("a one-byte prefix still selects the nodes it covers");
1337 block_on_ready(node.send_identity_response(plan));
1338
1339 let unicasts = mac.take_unicasts();
1340 assert_eq!(unicasts.len(), 1);
1341 let delay = unicasts[0]
1342 .options
1343 .tx_delay_ms
1344 .expect("a prefix this short may select a crowd, which is held");
1345 assert!((500..=30_000).contains(&delay));
1346 }
1347
1348 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1353 #[test]
1354 fn responder_answers_a_router_hint_prefix_and_replies_down_the_trace() {
1355 let mac = FakeMac::new(vec![[0x12; 32]]);
1356 let node = responder_node(&mac);
1357 let our_key = PublicKey([0x11; 32]);
1358 let requester = PublicKey([0x41; 32]);
1359 node.enable_identity_responder_default(test_profile(our_key));
1360
1361 let router_hint = &our_key.hint().0[..2];
1363 let options = crate::mac_command::IdentityRequestBuilder::new()
1364 .filter_hint_prefix(router_hint)
1365 .unwrap()
1366 .build();
1367 let steered =
1370 test_broadcast_packet_with_trace(requester, Some(0x00), Some(&[]), Some(&[0x12, 0x34]));
1371
1372 let plan = node
1373 .evaluate_identity_request(&steered, requester, &options, 0)
1374 .expect("a router-hint prefix selects the node it names");
1375 block_on_ready(node.send_identity_response(plan));
1376
1377 let unicasts = mac.take_unicasts();
1378 assert_eq!(unicasts.len(), 1);
1379 assert_eq!(
1380 unicasts[0].options.source_route.as_deref(),
1381 Some([umsh_core::RouterHint([0x12, 0x34])].as_slice()),
1382 "the reply retraces the path the question came by"
1383 );
1384 assert_eq!(
1385 unicasts[0].options.tx_delay_ms, None,
1386 "two bytes name one node, so there is no crowd of replies to spread"
1387 );
1388
1389 let stranger = crate::mac_command::IdentityRequestBuilder::new()
1391 .filter_hint_prefix(&[router_hint[0], router_hint[1] ^ 0xFF])
1392 .unwrap()
1393 .build();
1394 assert!(
1395 node.evaluate_identity_request(&steered, requester, &stranger, 0)
1396 .is_none(),
1397 "a prefix naming another router selects nobody here"
1398 );
1399 }
1400
1401 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1408 #[test]
1409 fn responder_answers_one_solicitation_once() {
1410 let mac = FakeMac::new(vec![[0x12; 32], [0x13; 32], [0x14; 32]]);
1411 let node = responder_node(&mac);
1412 let our_key = PublicKey([0x11; 32]);
1413 let requester = PublicKey([0x41; 32]);
1414 node.enable_identity_responder_default(test_profile(our_key));
1415
1416 let options = crate::mac_command::IdentityRequestBuilder::new()
1417 .nonce(0x0000_BEEF)
1418 .unwrap()
1419 .filter_role(crate::NodeRole::Repeater)
1420 .unwrap()
1421 .build();
1422 let packet = test_broadcast_packet(requester, None, None);
1423
1424 let plan = node
1425 .evaluate_identity_request(&packet, requester, &options, 1_000)
1426 .expect("the first copy is answered");
1427 block_on_ready(node.send_identity_response(plan));
1428
1429 assert!(
1432 node.evaluate_identity_request(&packet, requester, &options, 3_000)
1433 .is_none(),
1434 "a copy arriving mid-hold must not queue a second reply"
1435 );
1436 assert!(
1437 node.evaluate_identity_request(&packet, requester, &options, 45_000)
1438 .is_none(),
1439 "nor one arriving after the first reply aired"
1440 );
1441 assert_eq!(mac.take_unicasts().len(), 1, "exactly one reply");
1442
1443 let asked_again = crate::mac_command::IdentityRequestBuilder::new()
1445 .nonce(0x0000_F00D)
1446 .unwrap()
1447 .filter_role(crate::NodeRole::Repeater)
1448 .unwrap()
1449 .build();
1450 let plan = node
1451 .evaluate_identity_request(&packet, requester, &asked_again, 46_000)
1452 .expect("a new solicitation is a new question");
1453 block_on_ready(node.send_identity_response(plan));
1454 assert_eq!(mac.take_unicasts().len(), 1);
1455
1456 let plan = node
1458 .evaluate_identity_request(&packet, requester, &options, 200_000)
1459 .expect("suppression is a window, not a permanent refusal");
1460 block_on_ready(node.send_identity_response(plan));
1461 assert_eq!(mac.take_unicasts().len(), 1);
1462 }
1463
1464 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1467 #[test]
1468 fn responder_answers_every_requester_that_asks() {
1469 let mac = FakeMac::new(vec![[0x12; 32], [0x13; 32]]);
1470 let node = responder_node(&mac);
1471 let our_key = PublicKey([0x11; 32]);
1472 node.enable_identity_responder_default(test_profile(our_key));
1473
1474 let options = crate::mac_command::IdentityRequestBuilder::new()
1475 .nonce(0x0000_BEEF)
1476 .unwrap()
1477 .filter_role(crate::NodeRole::Repeater)
1478 .unwrap()
1479 .build();
1480
1481 for requester in [PublicKey([0x41; 32]), PublicKey([0x42; 32])] {
1483 let packet = test_broadcast_packet(requester, None, None);
1484 let plan = node
1485 .evaluate_identity_request(&packet, requester, &options, 1_000)
1486 .expect("each requester is owed an answer");
1487 block_on_ready(node.send_identity_response(plan));
1488 }
1489 assert_eq!(mac.take_unicasts().len(), 2);
1490 }
1491
1492 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1493 #[test]
1494 fn responder_drops_repeated_or_routed_broadcast_solicitations() {
1495 let mac = FakeMac::new(Vec::new());
1496 let node = responder_node(&mac);
1497 let our_key = PublicKey([0x11; 32]);
1498 let requester = PublicKey([0x41; 32]);
1499 node.enable_identity_responder_default(test_profile(our_key));
1500 let options = crate::mac_command::IdentityRequestBuilder::new()
1501 .filter_role(crate::NodeRole::Repeater)
1502 .unwrap()
1503 .build();
1504
1505 let repeated = test_broadcast_packet(requester, Some(0x21), None);
1508 assert!(
1509 node.evaluate_identity_request(&repeated, requester, &options, 0)
1510 .is_none()
1511 );
1512
1513 let routed = test_broadcast_packet(requester, None, Some(&[0xAB, 0xCD]));
1516 assert!(
1517 node.evaluate_identity_request(&routed, requester, &options, 0)
1518 .is_none()
1519 );
1520 let hint_options = crate::mac_command::IdentityRequestBuilder::new()
1521 .filter_hint(&our_key.hint())
1522 .unwrap()
1523 .build();
1524 assert!(
1525 node.evaluate_identity_request(&routed, requester, &hint_options, 0)
1526 .is_none()
1527 );
1528
1529 let clean = test_broadcast_packet(requester, Some(0x00), Some(&[]));
1531 assert!(
1532 node.evaluate_identity_request(&clean, requester, &options, 0)
1533 .is_some()
1534 );
1535 }
1536
1537 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1543 #[test]
1544 fn responder_routes_its_reply_back_down_the_requests_trace() {
1545 let mac = FakeMac::new(vec![[0x00; 32]]);
1546 let node = responder_node(&mac);
1547 let our_key = PublicKey([0x11; 32]);
1548 let requester = PublicKey([0x41; 32]);
1549 node.enable_identity_responder_default(test_profile(our_key));
1550 let options = crate::mac_command::IdentityRequestBuilder::new()
1551 .filter_role(crate::NodeRole::Repeater)
1552 .unwrap()
1553 .build();
1554
1555 let steered = test_broadcast_packet_with_trace(
1558 requester,
1559 Some(0x00),
1560 Some(&[]),
1561 Some(&[0x12, 0x34, 0xAB, 0xCD]),
1562 );
1563 let plan = node
1564 .evaluate_identity_request(&steered, requester, &options, 0)
1565 .expect("an emptied route is an arrived request, and we answer it");
1566 block_on_ready(node.send_identity_response(plan));
1567
1568 let unicasts = mac.take_unicasts();
1569 assert_eq!(unicasts.len(), 1);
1570 let reply = &unicasts[0];
1571 let route = reply
1572 .options
1573 .source_route
1574 .as_ref()
1575 .expect("the reply is steered back");
1576 assert_eq!(
1579 route.as_slice(),
1580 &[
1581 umsh_core::RouterHint([0x12, 0x34]),
1582 umsh_core::RouterHint([0xAB, 0xCD])
1583 ]
1584 );
1585 assert_eq!(reply.options.flood_hops, None);
1588 }
1589
1590 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1593 #[test]
1594 fn responder_leaves_an_untraced_reply_unrouted() {
1595 let mac = FakeMac::new(vec![[0x00; 32]]);
1596 let node = responder_node(&mac);
1597 let our_key = PublicKey([0x11; 32]);
1598 let requester = PublicKey([0x41; 32]);
1599 node.enable_identity_responder_default(test_profile(our_key));
1600 let options = crate::mac_command::IdentityRequestBuilder::new()
1601 .filter_role(crate::NodeRole::Repeater)
1602 .unwrap()
1603 .build();
1604
1605 let direct = test_broadcast_packet(requester, None, None);
1606 let plan = node
1607 .evaluate_identity_request(&direct, requester, &options, 0)
1608 .expect("a zero-hop solicitation is answered");
1609 block_on_ready(node.send_identity_response(plan));
1610
1611 let unicasts = mac.take_unicasts();
1612 assert_eq!(unicasts.len(), 1);
1613 assert!(unicasts[0].options.source_route.is_none());
1614 assert_eq!(unicasts[0].options.flood_hops, None);
1615 }
1616
1617 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1618 #[test]
1619 fn responder_ignores_request_that_filters_exclude() {
1620 let mac = FakeMac::new(Vec::new());
1621 let node = responder_node(&mac);
1622 let our_key = PublicKey([0x11; 32]);
1623 node.enable_identity_responder_default(test_profile(our_key));
1624
1625 let other_hint = PublicKey([0x99; 32]).hint();
1627 let options = crate::mac_command::IdentityRequestBuilder::new()
1628 .filter_hint(&other_hint)
1629 .unwrap()
1630 .build();
1631 let packet = test_unicast_packet(PublicKey([0x41; 32]), &[]);
1632
1633 assert!(
1634 node.evaluate_identity_request(&packet, PublicKey([0x41; 32]), &options, 0)
1635 .is_none()
1636 );
1637 }
1638
1639 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1640 #[test]
1641 fn responder_respects_ignore_policy() {
1642 let mac = FakeMac::new(Vec::new());
1643 let node = responder_node(&mac);
1644 let our_key = PublicKey([0x11; 32]);
1645 node.enable_identity_responder(test_profile(our_key), |_ctx| {
1646 crate::RespondDecision::Ignore
1647 });
1648
1649 let options = crate::mac_command::IdentityRequestBuilder::new().build();
1651 let packet = test_unicast_packet(PublicKey([0x41; 32]), &[]);
1652
1653 assert!(
1654 node.evaluate_identity_request(&packet, PublicKey([0x41; 32]), &options, 0)
1655 .is_none()
1656 );
1657 }
1658
1659 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1664 #[test]
1665 fn never_respond_policy_keeps_the_profile_readable() {
1666 let mac = FakeMac::new(Vec::new());
1667 let node = responder_node(&mac);
1668 let our_key = PublicKey([0x11; 32]);
1669 node.enable_identity_responder(test_profile(our_key), crate::never_respond_policy);
1670
1671 let options = crate::mac_command::IdentityRequestBuilder::new().build();
1672 let packet = test_unicast_packet(PublicKey([0x41; 32]), &[]);
1673 assert!(
1674 node.evaluate_identity_request(&packet, PublicKey([0x41; 32]), &options, 0)
1675 .is_none(),
1676 "a silenced responder answers nothing"
1677 );
1678 assert_eq!(
1679 node.with_identity_profile(|profile| profile.public_key),
1680 Some(our_key),
1681 "yet the profile an advertisement is built from is still there"
1682 );
1683 }
1684
1685 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1686 #[test]
1687 fn responder_disabled_yields_no_plan() {
1688 let mac = FakeMac::new(Vec::new());
1689 let node = responder_node(&mac);
1690 let options = crate::mac_command::IdentityRequestBuilder::new().build();
1691 let packet = test_unicast_packet(PublicKey([0x41; 32]), &[]);
1692 assert!(
1693 node.evaluate_identity_request(&packet, PublicKey([0x41; 32]), &options, 0)
1694 .is_none()
1695 );
1696 }
1697
1698 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1699 #[test]
1700 fn default_policy_full_source_tracks_authentication() {
1701 use crate::identity_responder::{IdentityRequestContext, default_respond_policy};
1702 use crate::mac_command::IdentityRequestFilters;
1703
1704 let base = |source_authenticated: bool| IdentityRequestContext {
1705 from_key: PublicKey([0x41; 32]),
1706 from_hint: None,
1707 source_authenticated,
1708 has_full_source: false,
1709 channel: None,
1710 family: crate::PacketFamily::Unicast,
1711 filters: IdentityRequestFilters::new(&[]),
1712 rssi: None,
1713 snr: None,
1714 trace_route: &[],
1715 };
1716
1717 assert_eq!(
1719 default_respond_policy(&base(true)),
1720 crate::RespondDecision::Respond { full_source: false }
1721 );
1722 assert_eq!(
1724 default_respond_policy(&base(false)),
1725 crate::RespondDecision::Respond { full_source: true }
1726 );
1727 }
1728
1729 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1732 fn peer_identity(name: &str, regions: &[&str]) -> crate::NodeIdentityPayload {
1733 crate::NodeIdentityPayload {
1734 role: crate::NodeRole::Repeater,
1735 capabilities: crate::NodeCapabilities::REPEATER,
1736 name: Some(String::from(name)),
1737 location: None,
1738 altitude_m: None,
1739 timestamp: None,
1740 supported_regions: Some(regions.iter().map(|text| String::from(*text)).collect()),
1741 nonce: None,
1742 signature: None,
1743 }
1744 }
1745
1746 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1748 fn peer_repeaters_request(nonce: Option<u16>, cursor: Option<&[u8]>) -> Vec<u8> {
1749 let mut builder = crate::mac_command::PeerRepeatersRequestBuilder::new();
1750 if let Some(nonce) = nonce {
1751 builder = builder.nonce(nonce).unwrap();
1752 }
1753 if let Some(cursor) = cursor {
1754 builder = builder.cursor(cursor).unwrap();
1755 }
1756 builder.build()
1757 }
1758
1759 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1762 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1766 #[test]
1767 fn peer_repeaters_response_follows_a_blind_request_onto_its_channel() {
1768 let mac = FakeMac::new(Vec::new());
1769 let node = responder_node(&mac);
1770 node.enable_peer_repeaters_responder();
1771 let channel_id = umsh_core::ChannelId([0xC1, 0xD2]);
1772
1773 block_on_ready(node.answer_peer_repeaters_request(
1774 PublicKey([0x41; 32]),
1775 Some(channel_id),
1776 &peer_repeaters_request(Some(7), None),
1777 ));
1778
1779 let sent = mac.take_unicasts();
1780 assert_eq!(sent.len(), 1);
1781 assert_eq!(sent[0].channel, Some(channel_id));
1782 }
1783
1784 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1787 fn sent_response(mac: &FakeMac) -> (PublicKey, Vec<u8>) {
1788 let mut unicasts = mac.take_unicasts();
1789 assert_eq!(unicasts.len(), 1, "exactly one response frame");
1790 let sent = unicasts.remove(0);
1791 assert_eq!(
1792 sent.payload[0],
1793 umsh_core::PayloadType::MacCommand as u8,
1794 "responses travel as MAC commands"
1795 );
1796 let body = match crate::mac_command::parse(&sent.payload[1..]).unwrap() {
1797 crate::mac_command::MacCommand::PeerRepeatersResponse { body } => body.to_vec(),
1798 other => panic!("expected a Peer Repeaters Response, got {other:?}"),
1799 };
1800 (sent.to, body)
1801 }
1802
1803 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1804 #[test]
1805 fn a_disabled_responder_answers_nothing() {
1806 let mac = FakeMac::new(Vec::new());
1807 let node = responder_node(&mac);
1808 node.observe_peer_identity(PublicKey([0xAA; 32]), &peer_identity("Ridge", &[]), 0);
1809
1810 block_on_ready(node.answer_peer_repeaters_request(
1811 PublicKey([0x41; 32]),
1812 None,
1813 &peer_repeaters_request(None, None),
1814 ));
1815 assert!(mac.take_unicasts().is_empty());
1816 }
1817
1818 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1819 #[test]
1820 fn a_response_echoes_the_nonce_and_reports_what_both_sources_know() {
1821 let mac = FakeMac::new(Vec::new());
1822 let node = responder_node(&mac);
1823 node.enable_peer_repeaters_responder();
1824
1825 let peer = PublicKey([0xAA; 32]);
1826 node.observe_peer_identity(peer, &peer_identity("Ridge", &["SJC", "0x1234"]), 0);
1827 mac.observe(
1829 umsh_core::RouterHint([peer.0[0], peer.0[1]]),
1830 -95,
1831 umsh_hal::Snr::from_decibels(2),
1832 );
1833 mac.observe(
1835 umsh_core::RouterHint([0x11, 0x22]),
1836 -70,
1837 umsh_hal::Snr::from_decibels(-4),
1838 );
1839
1840 let requester = PublicKey([0x41; 32]);
1841 block_on_ready(node.answer_peer_repeaters_request(
1842 requester,
1843 None,
1844 &peer_repeaters_request(Some(0xBEEF), None),
1845 ));
1846
1847 let (to, body) = sent_response(&mac);
1848 assert_eq!(to, requester);
1849 let view = crate::mac_command::PeerRepeatersResponseView::new(&body);
1850 assert_eq!(view.nonce(), Some(0xBEEF), "request nonce echoed");
1851 assert_eq!(view.total(), Some(2));
1852 assert_eq!(view.cursor(), None, "one page held everything");
1853
1854 let entries: Vec<_> = view.entries().collect();
1855 assert_eq!(entries.len(), 2);
1856
1857 assert_eq!(entries[0].hint(), Some(&peer.hint().0[..]));
1858 assert_eq!(entries[0].name(), Some("Ridge"));
1859 let (rssi, snr) = entries[0].rssi_snr().unwrap();
1860 assert_eq!(rssi, -95);
1861 assert_eq!(snr, umsh_hal::Snr::from_decibels(2));
1862 assert_eq!(
1863 entries[0].regions().collect::<Vec<_>>(),
1864 vec![[0x78, 0x53], [0x12, 0x34]],
1865 "the identity's region strings arrive as derived codes"
1866 );
1867
1868 assert_eq!(
1869 entries[1].hint(),
1870 Some(&[0x11, 0x22][..]),
1871 "an unclaimed observation is named by its router hint alone"
1872 );
1873 assert_eq!(entries[1].name(), None);
1874 assert_eq!(entries[1].rssi_snr().unwrap().0, -70);
1875 assert!(entries[1].regions().next().is_none());
1876 }
1877
1878 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1879 #[test]
1880 fn a_full_table_pages_and_the_cursor_resumes_where_it_stopped() {
1881 let mac = FakeMac::new(Vec::new());
1882 let node = responder_node(&mac);
1883 node.enable_peer_repeaters_responder();
1884
1885 for seed in 0..crate::peer_repeaters::MAX_PEER_REPEATERS as u8 {
1888 node.observe_peer_identity(
1889 PublicKey([seed; 32]),
1890 &peer_identity(&format!("Repeater number {seed:08}"), &["Rogue Valley"]),
1891 0,
1892 );
1893 }
1894
1895 let requester = PublicKey([0x41; 32]);
1896 let mut seen: Vec<Vec<u8>> = Vec::new();
1897 let mut cursor: Option<Vec<u8>> = None;
1898 let mut pages = 0;
1899 loop {
1900 block_on_ready(node.answer_peer_repeaters_request(
1901 requester,
1902 None,
1903 &peer_repeaters_request(Some(1), cursor.as_deref()),
1904 ));
1905 let (_, body) = sent_response(&mac);
1906 let view = crate::mac_command::PeerRepeatersResponseView::new(&body);
1907 assert_eq!(
1908 view.total(),
1909 Some(crate::peer_repeaters::MAX_PEER_REPEATERS as u8),
1910 "every page reports the whole listing's size"
1911 );
1912 seen.extend(
1913 view.entries()
1914 .filter_map(|entry| entry.hint().map(Vec::from)),
1915 );
1916 pages += 1;
1917 assert!(pages < 10, "paging should terminate");
1918 match view.cursor() {
1919 Some(next) => cursor = Some(next.to_vec()),
1920 None => break,
1921 }
1922 }
1923
1924 assert!(pages > 1, "the table did not fit one page");
1925 assert_eq!(seen.len(), crate::peer_repeaters::MAX_PEER_REPEATERS);
1926 for seed in 0..crate::peer_repeaters::MAX_PEER_REPEATERS as u8 {
1927 assert!(
1928 seen.contains(&Vec::from(&PublicKey([seed; 32]).hint().0[..])),
1929 "every repeater was listed exactly once across the pages"
1930 );
1931 }
1932 let mut sorted = seen.clone();
1933 sorted.sort();
1934 sorted.dedup();
1935 assert_eq!(
1936 sorted.len(),
1937 seen.len(),
1938 "no entry was repeated across pages"
1939 );
1940 }
1941
1942 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1945 #[test]
1946 fn a_cursor_from_a_changed_listing_restarts_the_walk() {
1947 let mac = FakeMac::new(Vec::new());
1948 let node = responder_node(&mac);
1949 node.enable_peer_repeaters_responder();
1950 for seed in 0..3u8 {
1951 node.observe_peer_identity(PublicKey([seed; 32]), &peer_identity("Peer", &[]), 0);
1952 }
1953
1954 let generation = 3u16.to_be_bytes();
1956 block_on_ready(node.answer_peer_repeaters_request(
1957 PublicKey([0x41; 32]),
1958 None,
1959 &peer_repeaters_request(None, Some(&[generation[0], generation[1], 2])),
1960 ));
1961 let (_, body) = sent_response(&mac);
1962 let view = crate::mac_command::PeerRepeatersResponseView::new(&body);
1963 assert_eq!(view.entries().count(), 1, "resumed at the third entry");
1964
1965 node.observe_peer_identity(PublicKey([0x77; 32]), &peer_identity("Newcomer", &[]), 0);
1968 block_on_ready(node.answer_peer_repeaters_request(
1969 PublicKey([0x41; 32]),
1970 None,
1971 &peer_repeaters_request(None, Some(&[generation[0], generation[1], 2])),
1972 ));
1973 let (_, body) = sent_response(&mac);
1974 let view = crate::mac_command::PeerRepeatersResponseView::new(&body);
1975 assert_eq!(
1976 view.entries().count(),
1977 4,
1978 "the stale cursor restarted the walk"
1979 );
1980 }
1981
1982 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1985 #[test]
1986 fn an_empty_neighborhood_still_answers() {
1987 let mac = FakeMac::new(Vec::new());
1988 let node = responder_node(&mac);
1989 node.enable_peer_repeaters_responder();
1990
1991 block_on_ready(node.answer_peer_repeaters_request(
1992 PublicKey([0x41; 32]),
1993 None,
1994 &peer_repeaters_request(None, None),
1995 ));
1996 let (_, body) = sent_response(&mac);
1997 let view = crate::mac_command::PeerRepeatersResponseView::new(&body);
1998 assert_eq!(view.total(), Some(0));
1999 assert_eq!(view.entries().count(), 0);
2000 assert_eq!(view.cursor(), None);
2001 }
2002}