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;
153#[cfg(feature = "software-crypto")]
154mod pfs;
155mod receive;
156mod ticket;
157mod transport;
158
159pub use app_error::{AppEncodeError, AppParseError};
160pub use app_payload::{
161 expect_payload_type, parse_mac_command_payload, parse_node_identity_payload, split_payload_type,
162};
163#[cfg(feature = "software-crypto")]
164pub use channel::Channel;
165pub use host::{Host, HostError};
166pub use identity::{NodeCapabilities, NodeIdentityPayload, NodeRole};
167pub use identity_responder::{
168 IdentityRequestContext, NodeIdentityProfile, RespondDecision, default_respond_policy,
169 never_respond_policy,
170};
171pub use mac::{MacBackend, MacBackendError};
172pub use mac_command::OwnedMacCommand;
173pub use mac_command::{CommandId, MacCommand};
174#[cfg(feature = "software-crypto")]
175pub use node::BoundChannel;
176#[cfg(feature = "software-crypto")]
177pub use node::PfsStatus;
178pub use node::{LocalNode, NodeError, PfsFailure, PongMetadata, Subscription};
179pub use peer::{PING_MIC_SIZE, PeerConnection};
180pub use receive::{ChannelInfoRef, PacketFamily, ReceivedPacketRef, RouteHops, RxMetadata, Snr};
181pub use ticket::{SendProgressTicket, SendToken};
182pub use transport::Transport;
183
184#[cfg(test)]
185mod tests {
186 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
187 use std::{
188 cell::RefCell,
189 collections::VecDeque,
190 future::Future,
191 num::NonZeroU8,
192 pin::pin,
193 rc::Rc,
194 task::{Context, Poll, RawWaker, RawWakerVTable, Waker},
195 };
196 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
197 use umsh_core::{NodeHint, PublicKey};
198 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
199 use umsh_crypto::NodeIdentity;
200 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
201 use umsh_crypto::software::SoftwareIdentity;
202 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
203 use umsh_hal::Snr;
204 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
205 use umsh_mac::MacEventRef;
206 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
207 use umsh_mac::{CapacityError, LocalIdentityId, PeerId, SendError, SendOptions, SendReceipt};
208
209 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
210 use crate::ReceivedPacketRef;
211 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
212 use crate::{MacBackend, MacBackendError, OwnedMacCommand, SendToken};
213 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
214 use umsh_core::ChannelId;
215 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
216 use umsh_text::OwnedTextMessage;
217 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
218 #[test]
219 fn peer_receive_handlers_precede_node_receive_handlers() {
220 use crate::node::{LocalNode, LocalNodeState, NodeMembership};
221
222 let mac = FakeMac::new(Vec::new());
223 let dispatcher = Rc::new(RefCell::new(crate::dispatch::EventDispatcher::new()));
224 let membership = Rc::new(RefCell::new(NodeMembership::new()));
225 let state = Rc::new(RefCell::new(LocalNodeState::new()));
226 let node = LocalNode::new(LocalIdentityId(1), mac, dispatcher, membership, state);
227 let peer = PublicKey([0x41; 32]);
228 let peer_connection = block_on_ready(node.peer(peer)).unwrap();
229
230 let call_order = Rc::new(RefCell::new(Vec::new()));
231 let peer_call_order = call_order.clone();
232 let _peer_subscription = peer_connection.on_receive(move |_| {
233 peer_call_order.borrow_mut().push("peer");
234 true
235 });
236 let node_call_order = call_order.clone();
237 let _node_subscription = node.on_receive(move |_| {
238 node_call_order.borrow_mut().push("node");
239 true
240 });
241
242 assert!(node.dispatch_received_packet(&test_unicast_packet(peer, &[0x01, 0x02])));
243 assert_eq!(call_order.borrow().as_slice(), ["peer"]);
244 }
245
246 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
247 fn test_node(mac: FakeMac) -> crate::node::LocalNode<FakeMac> {
248 use crate::node::{LocalNode, LocalNodeState, NodeMembership};
249
250 LocalNode::new(
251 LocalIdentityId(1),
252 mac,
253 Rc::new(RefCell::new(crate::dispatch::EventDispatcher::new())),
254 Rc::new(RefCell::new(NodeMembership::new())),
255 Rc::new(RefCell::new(LocalNodeState::new())),
256 )
257 }
258
259 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
260 #[test]
261 fn leaving_a_channel_unregisters_its_key_from_the_mac() {
262 let mac = FakeMac::new(Vec::new());
263 let node = test_node(mac.clone());
264 let channel = crate::Channel::private(umsh_core::ChannelKey([0x11; 32]), "trail");
265
266 block_on_ready(node.join(&channel)).unwrap();
267 assert!(mac.holds_channel(channel.key()));
268
269 block_on_ready(node.leave(&channel)).unwrap();
270 assert!(!mac.holds_channel(channel.key()));
271 assert!(node.bound_channel(&channel).is_none());
272 }
273
274 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
275 #[test]
276 fn rejoining_after_leave_re_registers_the_key_with_the_mac() {
277 let mac = FakeMac::new(Vec::new());
278 let node = test_node(mac.clone());
279 let channel = crate::Channel::private(umsh_core::ChannelKey([0x22; 32]), "camp");
280
281 let first = block_on_ready(node.join(&channel)).unwrap();
282 block_on_ready(node.leave(&channel)).unwrap();
283 let second = block_on_ready(node.join(&channel)).unwrap();
284
285 assert!(mac.holds_channel(channel.key()));
288 assert!(second.is_active());
289 assert!(!first.is_active());
290 }
291
292 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
293 #[test]
294 fn leaving_a_channel_that_was_never_joined_is_a_no_op() {
295 let mac = FakeMac::new(Vec::new());
296 let node = test_node(mac.clone());
297 let joined = crate::Channel::private(umsh_core::ChannelKey([0x33; 32]), "joined");
298 let stranger = crate::Channel::private(umsh_core::ChannelKey([0x44; 32]), "stranger");
299
300 block_on_ready(node.join(&joined)).unwrap();
301 block_on_ready(node.leave(&stranger)).unwrap();
302
303 assert!(mac.holds_channel(joined.key()));
304 assert!(node.bound_channel(&joined).is_some());
305 }
306
307 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
308 #[test]
309 fn receive_callbacks_can_observe_rx_metadata() {
310 use crate::node::{LocalNode, LocalNodeState, NodeMembership};
311
312 let mac = FakeMac::new(Vec::new());
313 let dispatcher = Rc::new(RefCell::new(crate::dispatch::EventDispatcher::new()));
314 let membership = Rc::new(RefCell::new(NodeMembership::new()));
315 let state = Rc::new(RefCell::new(LocalNodeState::new()));
316 let node = LocalNode::new(LocalIdentityId(1), mac, dispatcher, membership, state);
317 let peer = PublicKey([0x44; 32]);
318
319 let observed = Rc::new(RefCell::new(None));
320 let observed_for_callback = observed.clone();
321 let _subscription = node.on_receive(move |packet| {
322 *observed_for_callback.borrow_mut() = Some((
323 packet.rssi(),
324 packet.snr(),
325 packet.lqi(),
326 packet.received_at_ms(),
327 ));
328 true
329 });
330
331 let payload = encode_text_payload("metadata");
332 let packet = test_unicast_packet_with_rx(
333 peer,
334 &payload,
335 umsh_mac::RxMetadata::new(
336 Some(-73),
337 Some(Snr::from_centibels(123)),
338 NonZeroU8::new(200),
339 Some(123_456),
340 ),
341 );
342
343 assert!(node.dispatch_received_packet(&packet));
344 assert_eq!(
345 *observed.borrow(),
346 Some((
347 Some(-73),
348 Some(Snr::from_centibels(123)),
349 NonZeroU8::new(200),
350 Some(123_456),
351 ))
352 );
353 }
354
355 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
356 #[test]
357 fn subscription_guard_unregisters_on_drop() {
358 use crate::node::{LocalNode, LocalNodeState, NodeMembership};
359
360 let mac = FakeMac::new(Vec::new());
361 let dispatcher = Rc::new(RefCell::new(crate::dispatch::EventDispatcher::new()));
362 let membership = Rc::new(RefCell::new(NodeMembership::new()));
363 let state = Rc::new(RefCell::new(LocalNodeState::new()));
364 let node = LocalNode::new(LocalIdentityId(1), mac, dispatcher, membership, state);
365 let peer = PublicKey([0x33; 32]);
366
367 let hits = Rc::new(RefCell::new(0u32));
368 {
369 let hits = hits.clone();
370 let _subscription = node.on_receive(move |_| {
371 *hits.borrow_mut() += 1;
372 true
373 });
374 assert!(node.dispatch_received_packet(&test_unicast_packet(peer, &[0x01, 0x02])));
375 }
376
377 assert_eq!(*hits.borrow(), 1);
378 assert!(!node.dispatch_received_packet(&test_unicast_packet(peer, &[0x01, 0x02])));
379 assert_eq!(*hits.borrow(), 1);
380 }
381
382 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
383 #[test]
384 fn callbacks_observe_control_side_events_and_peer_ack_state() {
385 use crate::node::{LocalNode, LocalNodeState, NodeMembership};
386
387 let mac = FakeMac::new(Vec::new());
388 let dispatcher = Rc::new(RefCell::new(crate::dispatch::EventDispatcher::new()));
389 let membership = Rc::new(RefCell::new(NodeMembership::new()));
390 let state = Rc::new(RefCell::new(LocalNodeState::new()));
391 let node = LocalNode::new(LocalIdentityId(1), mac, dispatcher, membership, state);
392
393 let peer = PublicKey([0x42; 32]);
394 let peer_connection = block_on_ready(node.peer(peer)).unwrap();
395 let node_discovery = Rc::new(RefCell::new(Vec::new()));
396 let beacons = Rc::new(RefCell::new(Vec::new()));
397 let commands = Rc::new(RefCell::new(Vec::new()));
398 let peer_acks = Rc::new(RefCell::new(Vec::new()));
399 let peer_timeouts = Rc::new(RefCell::new(Vec::new()));
400
401 let discovery_log = node_discovery.clone();
402 let _discovered_subscription = node.on_node_discovered(move |key, name| {
403 discovery_log
404 .borrow_mut()
405 .push((key, name.map(str::to_string)));
406 });
407 let beacon_log = beacons.clone();
408 let _beacon_subscription = node.on_beacon(move |from_hint, from_key| {
409 beacon_log.borrow_mut().push((from_hint, from_key));
410 });
411 let command_log = commands.clone();
412 let _command_subscription = node.on_mac_command(move |from, command| {
413 command_log.borrow_mut().push((from, command.clone()));
414 });
415 let peer_ack_log = peer_acks.clone();
416 let _ack_subscription = peer_connection.on_ack_received(move |token| {
417 peer_ack_log.borrow_mut().push(token);
418 });
419 let peer_timeout_log = peer_timeouts.clone();
420 let _timeout_subscription = peer_connection.on_ack_timeout(move |token| {
421 peer_timeout_log.borrow_mut().push(token);
422 });
423
424 let token = SendToken::new(LocalIdentityId(1), SendReceipt(12));
425 let timeout_token = SendToken::new(LocalIdentityId(1), SendReceipt(13));
426 let hint = NodeHint([1, 2, 3]);
427 let command = OwnedMacCommand::EchoRequest {
428 data: vec![9, 8, 7],
429 };
430
431 node.dispatch_node_discovered(peer, Some("alice"));
432 node.dispatch_beacon(hint, Some(peer));
433 node.dispatch_mac_command(peer, &command);
434 node.dispatch_ack_received(peer, token);
435 node.dispatch_ack_timeout(peer, timeout_token);
436
437 assert_eq!(
438 node_discovery.borrow().as_slice(),
439 &[(peer, Some(String::from("alice")))]
440 );
441 assert_eq!(beacons.borrow().as_slice(), &[(hint, Some(peer))]);
442 assert_eq!(commands.borrow().as_slice(), &[(peer, command)]);
443 assert_eq!(peer_acks.borrow().as_slice(), &[token]);
444 assert_eq!(peer_timeouts.borrow().as_slice(), &[timeout_token]);
445 }
446
447 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
452 #[test]
453 fn ping_honours_caller_options_but_never_requests_a_mac_ack() {
454 use crate::node::{LocalNode, LocalNodeState, NodeMembership};
455 use umsh_core::MicSize;
456
457 let mac = FakeMac::new(vec![[7u8; 32], [9u8; 32]]);
458 let node = LocalNode::new(
459 LocalIdentityId(1),
460 mac.clone(),
461 Rc::new(RefCell::new(crate::dispatch::EventDispatcher::new())),
462 Rc::new(RefCell::new(NodeMembership::new())),
463 Rc::new(RefCell::new(LocalNodeState::new())),
464 );
465 let peer_connection = block_on_ready(node.peer(PublicKey([0x55; 32]))).unwrap();
466
467 let options = SendOptions::default()
468 .with_mic_size(MicSize::Mic4)
469 .with_ack_requested(true)
470 .with_trace_route()
471 .with_flood_hops(3)
472 .with_region_code([0x78, 0x53]);
473 block_on_ready(peer_connection.ping(6, &options, 1_000)).unwrap();
474
475 let sent = mac.take_unicasts().pop().expect("ping send");
476 assert_eq!(sent.options.mic_size, MicSize::Mic4);
477 assert!(sent.options.trace_route);
478 assert_eq!(sent.options.flood_hops, Some(3));
479 assert_eq!(sent.options.region_code, Some([0x78, 0x53]));
480 assert!(!sent.options.ack_requested, "the echo response is the ack");
481
482 block_on_ready(peer_connection.ping(0, &SendOptions::default().no_flood(), 1_000)).unwrap();
485 let sent = mac.take_unicasts().pop().expect("ping send");
486 assert_eq!(sent.options.flood_hops, None);
487 }
488
489 #[test]
492 fn ping_mic_size_is_eight_bytes() {
493 assert_eq!(crate::PING_MIC_SIZE, umsh_core::MicSize::Mic8);
494 assert_eq!(crate::PING_MIC_SIZE.byte_len(), 8);
495 }
496
497 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
498 #[test]
499 fn pfs_routed_send_tracks_ack_against_ephemeral_identity() {
500 use crate::node::{LocalNode, LocalNodeState, NodeMembership};
501
502 let mac = FakeMac::new(vec![[7u8; 32], [9u8; 32]]);
503 let dispatcher = Rc::new(RefCell::new(crate::dispatch::EventDispatcher::new()));
504 let membership = Rc::new(RefCell::new(NodeMembership::new()));
505 let state = Rc::new(RefCell::new(LocalNodeState::new()));
506 let node = LocalNode::new(
507 LocalIdentityId(1),
508 mac.clone(),
509 dispatcher.clone(),
510 membership,
511 state,
512 );
513
514 let peer = PublicKey([0x55; 32]);
515 let peer_connection = block_on_ready(node.peer(peer)).unwrap();
516 let options = SendOptions::default().with_ack_requested(true);
517
518 block_on_ready(node.request_pfs(&peer, 60, &options)).unwrap();
519 let request = mac.take_unicasts().pop().expect("request send");
520 let request_command = parse_owned_mac_command(&request.payload);
521 let request_ephemeral = match request_command {
522 OwnedMacCommand::PfsSessionRequest { ephemeral_key, .. } => ephemeral_key,
523 other => panic!("unexpected request payload: {other:?}"),
524 };
525
526 block_on_ready(node.handle_pfs_command(
527 &peer,
528 &OwnedMacCommand::PfsSessionResponse {
529 ephemeral_key: PublicKey([0x44; 32]),
530 duration_minutes: 60,
531 },
532 &options,
533 ))
534 .unwrap();
535
536 let payload = encode_text_payload("hello over pfs");
537 let ticket = block_on_ready(peer_connection.send(&payload, &options)).unwrap();
538 let sent = mac.take_unicasts().pop().expect("pfs-routed send");
539 assert_eq!(sent.from, LocalIdentityId(10));
540 assert_eq!(sent.to, PublicKey([0x44; 32]));
541
542 let pairwise_from_pfs = PublicKey([0x44; 32]);
543 let _ = request_ephemeral; dispatcher.borrow_mut().dispatch_ticket_state(
545 sent.from,
546 &MacEventRef::AckReceived {
547 peer: pairwise_from_pfs,
548 receipt: SendReceipt(42),
549 },
550 );
551 assert!(ticket.was_acked());
552 assert!(ticket.is_finished());
553 }
554
555 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
556 #[test]
557 fn pfs_session_manager_request_and_teardown() {
558 use crate::pfs::PfsSessionManager;
559
560 let mac = FakeMac::new(vec![[3u8; 32]]);
561 let peer_long_term = PublicKey([0x55; 32]);
562 let options = SendOptions::default().with_ack_requested(true);
563
564 let mut pfs = PfsSessionManager::new();
565 block_on_ready(pfs.request_session(
566 &mac,
567 LocalIdentityId(1),
568 &peer_long_term,
569 60,
570 &options,
571 ))
572 .unwrap();
573
574 let sent = mac.take_unicasts();
575 assert_eq!(sent.len(), 1);
576 assert_eq!(sent[0].from, LocalIdentityId(1));
577 assert_eq!(sent[0].to, peer_long_term);
578 assert_eq!(
579 parse_owned_mac_command(&sent[0].payload),
580 OwnedMacCommand::PfsSessionRequest {
581 ephemeral_key: *SoftwareIdentity::from_secret_bytes(&[3u8; 32]).public_key(),
582 duration_minutes: 60,
583 }
584 );
585
586 assert!(
587 block_on_ready(pfs.end_session(
588 &mac,
589 LocalIdentityId(1),
590 &peer_long_term,
591 true,
592 &options,
593 ))
594 .unwrap()
595 );
596 let sent = mac.take_unicasts();
597 assert_eq!(sent.len(), 1);
598 assert_eq!(
599 parse_owned_mac_command(&sent[0].payload),
600 OwnedMacCommand::EndPfsSession
601 );
602 }
603
604 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
605 #[test]
606 fn pfs_end_session_errors_when_missing() {
607 use crate::pfs::PfsSessionManager;
608
609 let mac = FakeMac::new(Vec::new());
610 let options = SendOptions::default();
611 let mut pfs = PfsSessionManager::new();
612
613 let error = block_on_ready(pfs.end_session(
614 &mac,
615 LocalIdentityId(1),
616 &PublicKey([0x77; 32]),
617 true,
618 &options,
619 ))
620 .unwrap_err();
621 assert!(matches!(error, crate::NodeError::PfsSessionMissing));
622 }
623
624 #[cfg(feature = "unsafe-advanced")]
625 fn encode_text_payload(text: &str) -> Vec<u8> {
626 let message = OwnedTextMessage::basic(text);
627 let mut body = [0u8; 512];
628 let len = umsh_text::text_message::encode(&message.as_borrowed(), &mut body).unwrap();
629 let mut payload = Vec::with_capacity(len + 1);
630 payload.push(umsh_core::PayloadType::TextMessage as u8);
631 payload.extend_from_slice(&body[..len]);
632 payload
633 }
634
635 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
636 fn test_unicast_packet<'a>(from: PublicKey, payload: &'a [u8]) -> ReceivedPacketRef<'a> {
637 test_unicast_packet_with_rx(from, payload, umsh_mac::RxMetadata::default())
638 }
639
640 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
641 fn test_unicast_packet_with_rx<'a>(
642 from: PublicKey,
643 payload: &'a [u8],
644 rx: umsh_mac::RxMetadata,
645 ) -> ReceivedPacketRef<'a> {
646 let wire = Box::leak(payload.to_vec().into_boxed_slice());
647 let header = umsh_core::PacketHeader {
648 fcf: umsh_core::Fcf::new(umsh_core::PacketType::Unicast, false, false),
649 options_range: 0..0,
650 flood_hops: None,
651 dst: None,
652 channel: None,
653 source: umsh_core::SourceAddrRef::Hint(from.hint()),
654 sec_info: None,
655 body_range: 0..wire.len(),
656 mic_range: wire.len()..wire.len(),
657 total_len: wire.len(),
658 };
659 ReceivedPacketRef::new(
660 wire,
661 wire,
662 header,
663 umsh_core::ParsedOptions::default(),
664 Some(from),
665 Some(from.hint()),
666 true,
667 None,
668 rx,
669 )
670 }
671
672 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
676 fn test_broadcast_packet(
677 from: PublicKey,
678 flood_hops: Option<u8>,
679 route: Option<&'static [u8]>,
680 ) -> ReceivedPacketRef<'static> {
681 let route_len = route.map_or(0, <[u8]>::len);
682 let wire: &'static [u8] = Box::leak(
683 route
684 .map_or_else(Vec::new, <[u8]>::to_vec)
685 .into_boxed_slice(),
686 );
687 let mut options = umsh_core::ParsedOptions::default();
688 if route.is_some() {
689 options.source_route = Some(0..route_len);
690 }
691 let header = umsh_core::PacketHeader {
692 fcf: umsh_core::Fcf::new(umsh_core::PacketType::Broadcast, false, false),
693 options_range: 0..route_len,
694 flood_hops: flood_hops.map(umsh_core::FloodHops),
695 dst: None,
696 channel: None,
697 source: umsh_core::SourceAddrRef::Hint(from.hint()),
698 sec_info: None,
699 body_range: route_len..wire.len(),
700 mic_range: wire.len()..wire.len(),
701 total_len: wire.len(),
702 };
703 ReceivedPacketRef::new(
704 wire,
705 &wire[route_len..],
706 header,
707 options,
708 Some(from),
709 Some(from.hint()),
710 false,
711 None,
712 umsh_mac::RxMetadata::default(),
713 )
714 }
715
716 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
717 #[derive(Clone, Default)]
718 struct FakeMac {
719 state: Rc<RefCell<FakeMacState>>,
720 }
721
722 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
723 #[derive(Default)]
724 struct FakeMacState {
725 random_blocks: VecDeque<[u8; 32]>,
726 next_peer_id: u8,
727 next_ephemeral_id: u8,
728 now_ms: u64,
729 unicasts: Vec<SentUnicast>,
730 removed_ephemerals: Vec<LocalIdentityId>,
731 peers: Vec<(PublicKey, PeerId)>,
732 channels: Vec<umsh_core::ChannelKey>,
733 }
734
735 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
736 #[derive(Clone, Debug, PartialEq, Eq)]
737 struct SentUnicast {
738 from: LocalIdentityId,
739 to: PublicKey,
740 payload: Vec<u8>,
741 options: SendOptions,
742 }
743
744 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
745 impl FakeMac {
746 fn new(random_blocks: Vec<[u8; 32]>) -> Self {
747 Self {
748 state: Rc::new(RefCell::new(FakeMacState {
749 random_blocks: random_blocks.into(),
750 next_peer_id: 0,
751 next_ephemeral_id: 10,
752 now_ms: 1_000,
753 ..FakeMacState::default()
754 })),
755 }
756 }
757
758 fn take_unicasts(&self) -> Vec<SentUnicast> {
759 core::mem::take(&mut self.state.borrow_mut().unicasts)
760 }
761
762 fn holds_channel(&self, key: &umsh_core::ChannelKey) -> bool {
763 self.state.borrow().channels.iter().any(|k| k.0 == key.0)
764 }
765 }
766
767 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
768 impl MacBackend for FakeMac {
769 type SendError = SendError;
770 type CapacityError = CapacityError;
771 type RunError = core::convert::Infallible;
772
773 async fn next_event(
774 &self,
775 _on_event: impl FnMut(LocalIdentityId, umsh_mac::MacEventRef<'_>),
776 ) -> Result<(), Self::RunError> {
777 Ok(())
780 }
781
782 async fn add_peer(
783 &self,
784 key: PublicKey,
785 ) -> Result<PeerId, MacBackendError<Self::SendError, Self::CapacityError>> {
786 let mut state = self.state.borrow_mut();
787 if let Some((_, existing)) = state
788 .peers
789 .iter()
790 .find(|(existing_key, _)| *existing_key == key)
791 {
792 return Ok(*existing);
793 }
794 let peer_id = PeerId(state.next_peer_id);
795 state.next_peer_id = state.next_peer_id.wrapping_add(1);
796 state.peers.push((key, peer_id));
797 Ok(peer_id)
798 }
799
800 async fn add_private_channel(
801 &self,
802 key: umsh_core::ChannelKey,
803 ) -> Result<(), MacBackendError<Self::SendError, Self::CapacityError>> {
804 let mut state = self.state.borrow_mut();
805 if !state.channels.iter().any(|k| k.0 == key.0) {
806 state.channels.push(key);
807 }
808 Ok(())
809 }
810
811 async fn add_named_channel(
812 &self,
813 _name: &str,
814 ) -> Result<(), MacBackendError<Self::SendError, Self::CapacityError>> {
815 Ok(())
816 }
817
818 async fn remove_channel(&self, key: &umsh_core::ChannelKey) -> bool {
819 let mut state = self.state.borrow_mut();
820 let before = state.channels.len();
821 state.channels.retain(|k| k.0 != key.0);
822 state.channels.len() != before
823 }
824
825 async fn send_broadcast(
826 &self,
827 _from: LocalIdentityId,
828 _payload: &[u8],
829 _options: &SendOptions,
830 ) -> Result<SendReceipt, MacBackendError<Self::SendError, Self::CapacityError>> {
831 Ok(SendReceipt(99))
832 }
833
834 async fn send_multicast(
835 &self,
836 _from: LocalIdentityId,
837 _channel: &ChannelId,
838 _payload: &[u8],
839 _options: &SendOptions,
840 ) -> Result<SendReceipt, MacBackendError<Self::SendError, Self::CapacityError>> {
841 Ok(SendReceipt(99))
842 }
843
844 async fn send_unicast(
845 &self,
846 from: LocalIdentityId,
847 dst: &PublicKey,
848 payload: &[u8],
849 options: &SendOptions,
850 ) -> Result<Option<SendReceipt>, MacBackendError<Self::SendError, Self::CapacityError>>
851 {
852 self.state.borrow_mut().unicasts.push(SentUnicast {
853 from,
854 to: *dst,
855 payload: payload.to_vec(),
856 options: options.clone(),
857 });
858 Ok(Some(SendReceipt(42)))
859 }
860
861 async fn send_blind_unicast(
862 &self,
863 from: LocalIdentityId,
864 dst: &PublicKey,
865 _channel: &ChannelId,
866 payload: &[u8],
867 options: &SendOptions,
868 ) -> Result<Option<SendReceipt>, MacBackendError<Self::SendError, Self::CapacityError>>
869 {
870 self.send_unicast(from, dst, payload, options).await
871 }
872
873 async fn fill_random(&self, dest: &mut [u8]) {
874 let mut state = self.state.borrow_mut();
875 let next = state.random_blocks.pop_front().expect("test rng exhausted");
876 dest.copy_from_slice(&next[..dest.len()]);
877 }
878
879 async fn now_ms(&self) -> u64 {
880 self.state.borrow().now_ms
881 }
882
883 async fn register_ephemeral(
884 &self,
885 _parent: LocalIdentityId,
886 _identity: SoftwareIdentity,
887 ) -> Result<LocalIdentityId, MacBackendError<Self::SendError, Self::CapacityError>>
888 {
889 let mut state = self.state.borrow_mut();
890 let id = LocalIdentityId(state.next_ephemeral_id);
891 state.next_ephemeral_id = state.next_ephemeral_id.wrapping_add(1);
892 Ok(id)
893 }
894
895 async fn remove_ephemeral(&self, id: LocalIdentityId) -> bool {
896 self.state.borrow_mut().removed_ephemerals.push(id);
897 true
898 }
899 }
900
901 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
902 fn parse_owned_mac_command(payload: &[u8]) -> OwnedMacCommand {
903 OwnedMacCommand::from(
904 crate::parse_mac_command_payload(umsh_core::PacketType::Unicast, payload).unwrap(),
905 )
906 }
907
908 #[cfg(feature = "unsafe-advanced")]
909 fn block_on_ready<F: Future>(future: F) -> F::Output {
910 fn raw_waker() -> RawWaker {
911 fn clone(_: *const ()) -> RawWaker {
912 raw_waker()
913 }
914 fn wake(_: *const ()) {}
915 fn wake_by_ref(_: *const ()) {}
916 fn drop(_: *const ()) {}
917
918 RawWaker::new(
919 core::ptr::null(),
920 &RawWakerVTable::new(clone, wake, wake_by_ref, drop),
921 )
922 }
923
924 let waker = unsafe { Waker::from_raw(raw_waker()) };
925 let mut context = Context::from_waker(&waker);
926 let mut future = pin!(future);
927 match future.as_mut().poll(&mut context) {
928 Poll::Ready(value) => value,
929 Poll::Pending => panic!("test future unexpectedly returned Poll::Pending"),
930 }
931 }
932
933 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
936 fn responder_node(mac: &FakeMac) -> crate::LocalNode<FakeMac> {
937 use crate::node::{LocalNode, LocalNodeState, NodeMembership};
938 let dispatcher = Rc::new(RefCell::new(crate::dispatch::EventDispatcher::new()));
939 let membership = Rc::new(RefCell::new(NodeMembership::new()));
940 let state = Rc::new(RefCell::new(LocalNodeState::new()));
941 LocalNode::new(
942 LocalIdentityId(1),
943 mac.clone(),
944 dispatcher,
945 membership,
946 state,
947 )
948 }
949
950 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
951 fn test_profile(public_key: PublicKey) -> crate::NodeIdentityProfile {
952 crate::NodeIdentityProfile::new(
953 public_key,
954 crate::NodeRole::Repeater,
955 crate::NodeCapabilities::REPEATER | crate::NodeCapabilities::TEXT_MESSAGES,
956 )
957 .with_name("repeater-1")
958 }
959
960 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
961 #[test]
962 fn responder_answers_selected_request_with_unicast_identity() {
963 let mac = FakeMac::new(Vec::new());
964 let node = responder_node(&mac);
965 let our_key = PublicKey([0x11; 32]);
966 let requester = PublicKey([0x41; 32]);
967 node.enable_identity_responder_default(test_profile(our_key));
968
969 let options = crate::mac_command::IdentityRequestBuilder::new()
971 .nonce(0xCAFE_F00D)
972 .unwrap()
973 .filter_hint(&our_key.hint())
974 .unwrap()
975 .build();
976 let packet = test_unicast_packet(requester, &[]);
977
978 let plan = node
979 .evaluate_identity_request(&packet, requester, &options)
980 .expect("responder should produce a reply plan");
981 block_on_ready(node.send_identity_response(plan));
982
983 let unicasts = mac.take_unicasts();
984 assert_eq!(unicasts.len(), 1, "exactly one unicast reply");
985 let reply = &unicasts[0];
986 assert_eq!(reply.to, requester, "reply is addressed to the requester");
987 assert!(!reply.options.full_source);
990 assert_eq!(reply.options.tx_delay_ms, None);
993
994 assert_eq!(reply.payload[0], umsh_core::PayloadType::NodeIdentity as u8);
995 let identity = crate::NodeIdentityPayload::from_bytes(&reply.payload[1..]).unwrap();
996 assert_eq!(identity.role, crate::NodeRole::Repeater);
997 assert_eq!(identity.name.as_deref(), Some("repeater-1"));
998 assert_eq!(identity.nonce, Some(0xCAFE_F00D), "request nonce echoed");
999 assert!(identity.signature.is_none(), "responses are unsigned");
1000 }
1001
1002 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1003 #[test]
1004 fn responder_answers_broadcast_solicitation_with_delayed_full_source_reply() {
1005 let mac = FakeMac::new(vec![[0x12; 32]]);
1006 let node = responder_node(&mac);
1007 let our_key = PublicKey([0x11; 32]);
1008 let requester = PublicKey([0x41; 32]);
1009 node.enable_identity_responder_default(test_profile(our_key));
1010
1011 let options = crate::mac_command::IdentityRequestBuilder::new()
1012 .nonce(0x0000_BEEF)
1013 .unwrap()
1014 .filter_role(crate::NodeRole::Repeater)
1015 .unwrap()
1016 .build();
1017 let packet = test_broadcast_packet(requester, None, None);
1018
1019 let plan = node
1020 .evaluate_identity_request(&packet, requester, &options)
1021 .expect("selected broadcast solicitation produces a plan");
1022 block_on_ready(node.send_identity_response(plan));
1023
1024 let unicasts = mac.take_unicasts();
1025 assert_eq!(unicasts.len(), 1);
1026 let reply = &unicasts[0];
1027 assert_eq!(reply.to, requester);
1028 assert!(reply.options.full_source);
1030 let delay = reply
1033 .options
1034 .tx_delay_ms
1035 .expect("broadcast replies are jittered");
1036 assert!((500..=30_000).contains(&delay));
1037 assert_eq!(reply.options.flood_hops, None);
1040 }
1041
1042 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1045 #[test]
1046 fn responder_answers_flood_routed_hint_filtered_solicitation() {
1047 let mac = FakeMac::new(vec![[0x12; 32]]);
1048 let node = responder_node(&mac);
1049 let our_key = PublicKey([0x11; 32]);
1050 let requester = PublicKey([0x41; 32]);
1051 node.enable_identity_responder_default(test_profile(our_key));
1052
1053 let options = crate::mac_command::IdentityRequestBuilder::new()
1054 .filter_hint(&our_key.hint())
1055 .unwrap()
1056 .build();
1057 let repeated = test_broadcast_packet(requester, Some(0x21), None);
1059
1060 let plan = node
1061 .evaluate_identity_request(&repeated, requester, &options)
1062 .expect("a hint-filtered solicitation may be flood routed");
1063 block_on_ready(node.send_identity_response(plan));
1064
1065 let unicasts = mac.take_unicasts();
1066 assert_eq!(unicasts.len(), 1);
1067 assert!(
1068 unicasts[0].options.flood_hops.is_some(),
1069 "the reply may be flooded back to a requester that named us"
1070 );
1071 }
1072
1073 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1074 #[test]
1075 fn responder_drops_repeated_or_routed_broadcast_solicitations() {
1076 let mac = FakeMac::new(Vec::new());
1077 let node = responder_node(&mac);
1078 let our_key = PublicKey([0x11; 32]);
1079 let requester = PublicKey([0x41; 32]);
1080 node.enable_identity_responder_default(test_profile(our_key));
1081 let options = crate::mac_command::IdentityRequestBuilder::new()
1082 .filter_role(crate::NodeRole::Repeater)
1083 .unwrap()
1084 .build();
1085
1086 let repeated = test_broadcast_packet(requester, Some(0x21), None);
1089 assert!(
1090 node.evaluate_identity_request(&repeated, requester, &options)
1091 .is_none()
1092 );
1093
1094 let routed = test_broadcast_packet(requester, None, Some(&[0xAB, 0xCD]));
1097 assert!(
1098 node.evaluate_identity_request(&routed, requester, &options)
1099 .is_none()
1100 );
1101 let hint_options = crate::mac_command::IdentityRequestBuilder::new()
1102 .filter_hint(&our_key.hint())
1103 .unwrap()
1104 .build();
1105 assert!(
1106 node.evaluate_identity_request(&routed, requester, &hint_options)
1107 .is_none()
1108 );
1109
1110 let clean = test_broadcast_packet(requester, Some(0x00), Some(&[]));
1112 assert!(
1113 node.evaluate_identity_request(&clean, requester, &options)
1114 .is_some()
1115 );
1116 }
1117
1118 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1119 #[test]
1120 fn responder_ignores_request_that_filters_exclude() {
1121 let mac = FakeMac::new(Vec::new());
1122 let node = responder_node(&mac);
1123 let our_key = PublicKey([0x11; 32]);
1124 node.enable_identity_responder_default(test_profile(our_key));
1125
1126 let other_hint = PublicKey([0x99; 32]).hint();
1128 let options = crate::mac_command::IdentityRequestBuilder::new()
1129 .filter_hint(&other_hint)
1130 .unwrap()
1131 .build();
1132 let packet = test_unicast_packet(PublicKey([0x41; 32]), &[]);
1133
1134 assert!(
1135 node.evaluate_identity_request(&packet, PublicKey([0x41; 32]), &options)
1136 .is_none()
1137 );
1138 }
1139
1140 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1141 #[test]
1142 fn responder_respects_ignore_policy() {
1143 let mac = FakeMac::new(Vec::new());
1144 let node = responder_node(&mac);
1145 let our_key = PublicKey([0x11; 32]);
1146 node.enable_identity_responder(test_profile(our_key), |_ctx| {
1147 crate::RespondDecision::Ignore
1148 });
1149
1150 let options = crate::mac_command::IdentityRequestBuilder::new().build();
1152 let packet = test_unicast_packet(PublicKey([0x41; 32]), &[]);
1153
1154 assert!(
1155 node.evaluate_identity_request(&packet, PublicKey([0x41; 32]), &options)
1156 .is_none()
1157 );
1158 }
1159
1160 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1165 #[test]
1166 fn never_respond_policy_keeps_the_profile_readable() {
1167 let mac = FakeMac::new(Vec::new());
1168 let node = responder_node(&mac);
1169 let our_key = PublicKey([0x11; 32]);
1170 node.enable_identity_responder(test_profile(our_key), crate::never_respond_policy);
1171
1172 let options = crate::mac_command::IdentityRequestBuilder::new().build();
1173 let packet = test_unicast_packet(PublicKey([0x41; 32]), &[]);
1174 assert!(
1175 node.evaluate_identity_request(&packet, PublicKey([0x41; 32]), &options)
1176 .is_none(),
1177 "a silenced responder answers nothing"
1178 );
1179 assert_eq!(
1180 node.with_identity_profile(|profile| profile.public_key),
1181 Some(our_key),
1182 "yet the profile an advertisement is built from is still there"
1183 );
1184 }
1185
1186 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1187 #[test]
1188 fn responder_disabled_yields_no_plan() {
1189 let mac = FakeMac::new(Vec::new());
1190 let node = responder_node(&mac);
1191 let options = crate::mac_command::IdentityRequestBuilder::new().build();
1192 let packet = test_unicast_packet(PublicKey([0x41; 32]), &[]);
1193 assert!(
1194 node.evaluate_identity_request(&packet, PublicKey([0x41; 32]), &options)
1195 .is_none()
1196 );
1197 }
1198
1199 #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
1200 #[test]
1201 fn default_policy_full_source_tracks_authentication() {
1202 use crate::identity_responder::{IdentityRequestContext, default_respond_policy};
1203 use crate::mac_command::IdentityRequestFilters;
1204
1205 let base = |source_authenticated: bool| IdentityRequestContext {
1206 from_key: PublicKey([0x41; 32]),
1207 from_hint: None,
1208 source_authenticated,
1209 has_full_source: false,
1210 channel: None,
1211 family: crate::PacketFamily::Unicast,
1212 filters: IdentityRequestFilters::new(&[]),
1213 rssi: None,
1214 snr: None,
1215 };
1216
1217 assert_eq!(
1219 default_respond_policy(&base(true)),
1220 crate::RespondDecision::Respond { full_source: false }
1221 );
1222 assert_eq!(
1224 default_respond_policy(&base(false)),
1225 crate::RespondDecision::Respond { full_source: true }
1226 );
1227 }
1228}