1use core::{
8 cell::RefCell,
9 marker::PhantomData,
10 pin::Pin,
11 task::{Context, Poll},
12};
13use std::{
14 collections::{BTreeMap, BTreeSet, VecDeque},
15 fmt,
16 rc::Rc,
17 sync::{
18 Arc, Mutex,
19 atomic::{AtomicBool, AtomicU64, Ordering},
20 mpsc as std_mpsc,
21 },
22 time::Duration,
23};
24
25use embedded_hal_async::delay::DelayNs;
26use tokio::sync::{mpsc, oneshot};
27use umsh_core::{ChannelKey, ChannelTag, NodeHint, PayloadType, PublicKey};
28use umsh_crypto::{
29 CryptoEngine, NodeIdentity,
30 software::{SoftwareAes, SoftwareIdentity, SoftwareSha256},
31};
32use umsh_hal::{
33 Clock, CounterStore, KeyValueStore, Radio, RxInfo, RxOrigin, Snr, TxError, TxOptions,
34};
35use umsh_mac::{Mac, MacHandle, OperatingPolicy, RepeaterConfig, SendOptions};
36use umsh_node::{
37 Host, LocalNode, MacBackend, NodeCapabilities, NodeIdentityPayload, NodeIdentityProfile,
38 NodeRole, PacketFamily, SendProgressTicket, Transport,
39 location::{MAX_PRECISION, NodeLocation},
40};
41use umsh_sync::AsyncRefCell;
42use umsh_text::engine::{ArchiveResult, DeliveryState, Destination};
43use umsh_text::model::{ConversationKey, SenderScope};
44use umsh_text::validate::{DeliveryPath, Envelope};
45use umsh_ulcp::{
46 frame,
47 ids::{self, prop},
48 items,
49};
50
51use crate::mobile_chat::{
52 ChannelRegistry, MobileChatArchiveLookupRecord, MobileChatArchiveResultKind,
53 MobileChatCheckpointRecord, MobileChatComposeBatchRecord, MobileChatDeliveryRecord,
54 MobileChatDirection, MobileChatMutationKind, MobileChatMutationRecord, MobileChatOriginalRef,
55 MobileChatPresence, MobileChatRegardingRef, MobileChatRxMetadataRecord,
56 MobileChatSenderResolutionRecord, MobileChatState,
57};
58use crate::ulcp::{UlcpPropertyFrameRecord, UlcpSyncRecord};
59use crate::{MobileCounterStore, MobileError, MobileIdentity};
60
61const MAX_FRAME_SIZE: usize = 256;
62const DEFAULT_FRAME_TIME_MS: u32 = 800;
63const BEACON_FLOOD_HOPS: u8 = 5;
66
67#[derive(Clone, Copy, Debug, PartialEq, Eq, uniffi::Error)]
68pub enum MobileMeshError {
69 InvalidPeer,
70 SessionUnavailable,
71 OperationInProgress,
72 CounterPersistenceFailed,
73 SendFailed,
74 ChatComposeFailed,
75 ChatBatchMissing,
76 InvalidChannelKey,
78 ChannelCapacity,
80 UnknownConversation,
83 InvalidLocation,
87 InvalidRequest,
90}
91
92impl fmt::Display for MobileMeshError {
93 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
94 formatter.write_str(match self {
95 Self::InvalidPeer => "MESH_INVALID_PEER",
96 Self::SessionUnavailable => "MESH_SESSION_UNAVAILABLE",
97 Self::OperationInProgress => "MESH_OPERATION_IN_PROGRESS",
98 Self::CounterPersistenceFailed => "MESH_COUNTER_PERSISTENCE_FAILED",
99 Self::SendFailed => "MESH_SEND_FAILED",
100 Self::ChatComposeFailed => "MESH_CHAT_COMPOSE_FAILED",
101 Self::ChatBatchMissing => "MESH_CHAT_BATCH_MISSING",
102 Self::InvalidChannelKey => "MESH_INVALID_CHANNEL_KEY",
103 Self::ChannelCapacity => "MESH_CHANNEL_CAPACITY",
104 Self::UnknownConversation => "MESH_UNKNOWN_CONVERSATION",
105 Self::InvalidLocation => "MESH_INVALID_LOCATION",
106 Self::InvalidRequest => "MESH_INVALID_REQUEST",
107 })
108 }
109}
110
111impl std::error::Error for MobileMeshError {}
112
113#[derive(Clone, Copy, Debug, PartialEq, Eq, uniffi::Enum)]
114pub enum MobileMeshPingOutcome {
115 Reply,
116 TimedOut,
117 Failed,
118}
119
120#[derive(Clone, Debug, PartialEq, Eq, uniffi::Record)]
121pub struct MobileMeshPingEventRecord {
122 pub operation_id: u64,
123 pub outcome: MobileMeshPingOutcome,
124 pub round_trip_milliseconds: Option<u64>,
125 pub hop_count: Option<u8>,
130 pub route_hints: Vec<Vec<u8>>,
133 pub rssi_dbm: Option<i16>,
135 pub snr_centibels: Option<i16>,
136 pub lqi: Option<u8>,
137}
138
139#[derive(Clone, Copy, Debug, PartialEq, Eq, uniffi::Enum)]
141pub enum MobileMeshManagementOutcome {
142 Progress,
147 Replied,
149 Acknowledged,
152 TimedOut,
155 Failed,
160}
161
162#[derive(Clone, Debug, PartialEq, Eq, uniffi::Record)]
164pub struct MobileMeshManagementAnswerRecord {
165 pub property_id: u32,
166 pub value: Option<Vec<u8>>,
170 pub status_code: Option<u32>,
173}
174
175#[derive(Clone, Debug, PartialEq, Eq, uniffi::Record)]
180pub struct MobileMeshManagementEventRecord {
181 pub operation_id: u64,
182 pub peer_address: String,
184 pub outcome: MobileMeshManagementOutcome,
185 pub answers: Vec<MobileMeshManagementAnswerRecord>,
187 pub status_code: Option<u32>,
190 pub remaining_octets: Option<u32>,
193 pub properties_remaining: Option<u32>,
196}
197
198#[derive(Clone, Copy, Debug, PartialEq, Eq, uniffi::Enum)]
200pub enum MobileMeshResetScope {
201 Protocol,
203 Restore,
205 Reboot,
210 Factory,
214}
215
216#[derive(Clone, Debug, PartialEq, Eq, uniffi::Record)]
218pub struct MobileMeshPropertyWriteRecord {
219 pub property_id: u32,
220 pub value: Vec<u8>,
221}
222
223#[uniffi::export]
240pub fn ulcp_device_config_writes(
241 configuration: crate::ulcp::UlcpDeviceConfigRecord,
242 reported: UlcpSyncRecord,
243) -> Result<Vec<MobileMeshPropertyWriteRecord>, MobileMeshError> {
244 let values = crate::ulcp::device_config_writes(configuration, &reported)
245 .map_err(|_| MobileMeshError::InvalidRequest)?;
246 Ok(values
247 .into_iter()
248 .map(|(property_id, value)| MobileMeshPropertyWriteRecord { property_id, value })
249 .collect())
250}
251
252#[derive(Clone, Copy, Debug, PartialEq, Eq, uniffi::Enum)]
254pub enum MobileMeshRouteKind {
255 Unknown,
259 Direct,
261 Source,
263 Flood,
265}
266
267#[derive(Clone, Debug, PartialEq, Eq, uniffi::Record)]
269pub struct MobileMeshRouteRecord {
270 pub kind: MobileMeshRouteKind,
271 pub hints: Vec<Vec<u8>>,
274 pub flood_hops: Option<u8>,
276 pub flood_regions: Vec<Vec<u8>>,
278}
279
280impl MobileMeshRouteRecord {
281 fn unknown() -> Self {
282 Self {
283 kind: MobileMeshRouteKind::Unknown,
284 hints: Vec::new(),
285 flood_hops: None,
286 flood_regions: Vec::new(),
287 }
288 }
289}
290
291impl From<Option<umsh_mac::CachedRoute>> for MobileMeshRouteRecord {
292 fn from(route: Option<umsh_mac::CachedRoute>) -> Self {
293 match route {
294 None => Self::unknown(),
295 Some(umsh_mac::CachedRoute::Direct) => Self {
296 kind: MobileMeshRouteKind::Direct,
297 ..Self::unknown()
298 },
299 Some(umsh_mac::CachedRoute::Source(hops)) => Self {
300 kind: MobileMeshRouteKind::Source,
301 hints: hops.iter().map(|hop| hop.0.to_vec()).collect(),
302 ..Self::unknown()
303 },
304 Some(umsh_mac::CachedRoute::Flood { hops, regions }) => Self {
305 kind: MobileMeshRouteKind::Flood,
306 flood_hops: Some(hops),
307 flood_regions: regions.iter().map(|region| region.to_vec()).collect(),
308 ..Self::unknown()
309 },
310 }
311 }
312}
313
314#[derive(Clone, Debug, PartialEq, Eq, uniffi::Record)]
321pub struct MobileMeshAdvertisementRecord {
322 pub peer_address: String,
324 pub payload: Vec<u8>,
327 pub source_authenticated: bool,
336}
337
338#[derive(Clone, Debug, PartialEq, Eq, uniffi::Record)]
347pub struct MobileMeshPeerRepeaterRecord {
348 pub hint: Vec<u8>,
351 pub name: Option<String>,
352 pub rssi_dbm: Option<i16>,
353 pub snr_quarter_db: Option<i16>,
356 pub last_heard_minutes: Option<u16>,
358 pub location: Option<Vec<u8>>,
361 pub region_codes: Vec<Vec<u8>>,
363}
364
365#[derive(Clone, Copy, Debug, PartialEq, uniffi::Record)]
372pub struct MobileMeshSharedLocationRecord {
373 pub latitude_degrees: f64,
374 pub longitude_degrees: f64,
375 pub precision_bytes: u8,
378}
379
380#[derive(Clone, Debug, PartialEq, Eq, uniffi::Record)]
389pub struct MobileMeshPeerHeardRecord {
390 pub peer_address: Option<String>,
395 pub node_hint: Option<Vec<u8>>,
399 pub source_authenticated: bool,
404}
405
406#[uniffi::export(with_foreign)]
413pub trait MobileMeshWakeListener: Send + Sync {
414 fn on_update_pending(&self);
415}
416
417struct WakeSignal {
421 pending: AtomicBool,
422 listener: Mutex<Option<Arc<dyn MobileMeshWakeListener>>>,
423}
424
425impl WakeSignal {
426 fn new() -> Self {
427 Self {
428 pending: AtomicBool::new(false),
429 listener: Mutex::new(None),
430 }
431 }
432
433 fn notify(&self) {
434 if self.pending.swap(true, Ordering::AcqRel) {
435 return;
436 }
437 let listener = self
438 .listener
439 .lock()
440 .ok()
441 .and_then(|slot| slot.as_ref().cloned());
442 if let Some(listener) = listener {
443 listener.on_update_pending();
444 }
445 }
446
447 fn drained(&self) {
448 self.pending.store(false, Ordering::Release);
449 }
450
451 fn set_listener(&self, listener: Option<Arc<dyn MobileMeshWakeListener>>) {
452 let already_pending = {
453 let Ok(mut slot) = self.listener.lock() else {
454 return;
455 };
456 *slot = listener.clone();
457 self.pending.load(Ordering::Acquire)
458 };
459 if already_pending && let Some(listener) = listener {
462 listener.on_update_pending();
463 }
464 }
465}
466
467struct NotifyingSender<T> {
469 tx: std_mpsc::Sender<T>,
470 wake: Arc<WakeSignal>,
471}
472
473impl<T> Clone for NotifyingSender<T> {
474 fn clone(&self) -> Self {
475 Self {
476 tx: self.tx.clone(),
477 wake: self.wake.clone(),
478 }
479 }
480}
481
482impl<T> NotifyingSender<T> {
483 fn send(&self, value: T) -> Result<(), std_mpsc::SendError<T>> {
484 self.tx.send(value)?;
485 self.wake.notify();
486 Ok(())
487 }
488}
489
490#[derive(Clone, Debug, PartialEq, Eq, uniffi::Record)]
491pub struct MobileMeshSessionUpdateRecord {
492 pub outbound_frames: Vec<MobileMeshOutboundFrameRecord>,
496 pub ping_events: Vec<MobileMeshPingEventRecord>,
497 pub management_events: Vec<MobileMeshManagementEventRecord>,
500 pub advertisement_events: Vec<MobileMeshAdvertisementRecord>,
501 pub peer_heard_events: Vec<MobileMeshPeerHeardRecord>,
502 pub chat_batch_id: Option<u64>,
505 pub chat_mutations: Vec<MobileChatMutationRecord>,
506 pub chat_deliveries: Vec<MobileChatDeliveryRecord>,
507 pub chat_archive_lookups: Vec<MobileChatArchiveLookupRecord>,
508 pub chat_sender_resolutions: Vec<MobileChatSenderResolutionRecord>,
511 pub chat_diagnostics: Vec<String>,
512}
513
514#[derive(Clone, Debug, PartialEq, Eq, uniffi::Record)]
515pub struct MobileMeshOutboundFrameRecord {
516 pub id: u64,
517 pub data: Vec<u8>,
518 pub nocca: bool,
523}
524
525#[derive(Clone, Debug, PartialEq, Eq, uniffi::Record)]
526pub struct MobileMeshRxRecord {
527 pub data: Vec<u8>,
528 pub rssi_dbm: Option<i16>,
529 pub lqi: Option<u8>,
530 pub snr_cb: Option<i16>,
531}
532
533enum WorkerCommand {
534 RegisterPeers {
535 peers: Vec<PublicKey>,
536 response: oneshot::Sender<Result<(), MobileMeshError>>,
537 },
538 RemovePeers {
539 peers: Vec<PublicKey>,
540 response: oneshot::Sender<Result<(), MobileMeshError>>,
541 },
542 RegisterChannels {
543 keys: Vec<ChannelKey>,
544 response: oneshot::Sender<Result<(), MobileMeshError>>,
545 },
546 RemoveChannels {
547 keys: Vec<ChannelKey>,
548 response: oneshot::Sender<Result<(), MobileMeshError>>,
549 },
550 Ping {
551 operation_id: u64,
552 peer: PublicKey,
553 timeout_ms: u64,
554 },
555 Manage {
556 operation_id: u64,
557 peer: PublicKey,
558 request: ManagementRequest,
559 },
560 RestoreChat {
561 checkpoints: Vec<MobileChatCheckpointRecord>,
562 response: oneshot::Sender<()>,
563 },
564 ComposeChat {
565 conversation_address: String,
566 client_token: u32,
567 request: ChatComposeRequest,
568 response: oneshot::Sender<Result<MobileChatComposeBatchRecord, MobileMeshError>>,
569 },
570 CommitChatBatch {
571 batch_id: u64,
572 response: oneshot::Sender<Result<(), MobileMeshError>>,
573 },
574 RejectChatBatch {
575 batch_id: u64,
576 checkpoints: Vec<MobileChatCheckpointRecord>,
577 response: oneshot::Sender<Result<(), MobileMeshError>>,
578 },
579 ChatArchiveResult {
580 request_id: u32,
581 kind: MobileChatArchiveResultKind,
582 payload: Vec<u8>,
583 },
584 Advertise {
585 name: Option<String>,
586 timestamp: Option<u32>,
587 scheduled: bool,
591 response: oneshot::Sender<Result<(), MobileMeshError>>,
592 },
593 Beacon {
594 response: oneshot::Sender<Result<(), MobileMeshError>>,
595 },
596 SignIdentityBundle {
597 name: Option<String>,
598 timestamp: Option<u32>,
599 response: oneshot::Sender<Result<Vec<u8>, MobileMeshError>>,
600 },
601 RequestIdentity {
602 peer: PublicKey,
603 response: oneshot::Sender<Result<(), MobileMeshError>>,
604 },
605 DiscoverIdentities {
606 role_code: Option<u8>,
607 capability_bits: Option<u8>,
608 node_hint: Option<Vec<u8>>,
611 source_route: Vec<Vec<u8>>,
614 response: oneshot::Sender<Result<(), MobileMeshError>>,
615 },
616 RequestIdentityByHint {
617 conversation_address: String,
618 hint: NodeHint,
619 response: oneshot::Sender<Result<(), MobileMeshError>>,
620 },
621 PeerRepeaters {
622 peer: PublicKey,
623 response: oneshot::Sender<Result<Vec<MobileMeshPeerRepeaterRecord>, MobileMeshError>>,
624 },
625 SetDiscoverable {
626 enabled: bool,
627 name: Option<String>,
628 response: oneshot::Sender<()>,
629 },
630 SetAdvertisedLocation {
632 location: Option<NodeLocation>,
633 response: oneshot::Sender<()>,
634 },
635 SetChatDisplayName {
636 name: String,
637 response: oneshot::Sender<()>,
638 },
639 PeerRoute {
640 peer: PublicKey,
641 response: oneshot::Sender<MobileMeshRouteRecord>,
642 },
643 ClearPeerRoute {
644 peer: PublicKey,
645 response: oneshot::Sender<bool>,
646 },
647 FailOutboundTransmissions,
648 Receive(MobileMeshRxRecord),
649 Shutdown,
650}
651
652#[derive(Clone, Debug)]
655enum ReplyShape {
656 Property(u32),
658 Entries(Vec<u32>),
660 Status,
662 Acknowledgment,
665}
666
667enum ManagementRequest {
669 One { frame: Vec<u8>, shape: ReplyShape },
671 Fetch {
673 property_ids: Vec<u32>,
674 multi_hint: bool,
675 },
676}
677
678enum ChatComposeRequest {
679 Text {
680 body: String,
681 },
682 Edit {
683 original: MobileChatOriginalRef,
684 body: String,
685 },
686 Delete {
687 original: MobileChatOriginalRef,
688 },
689 Reaction {
690 target: MobileChatRegardingRef,
691 body: String,
692 },
693}
694
695struct InboundFrame {
696 record: MobileMeshRxRecord,
697}
698
699enum InboundTextSource {
701 Direct { peer: PublicKey },
703 ChannelGroup {
706 channel: ChannelTag,
707 hint: NodeHint,
708 full_key: Option<PublicKey>,
709 },
710 ChannelDirect {
712 channel: ChannelTag,
713 peer: PublicKey,
714 },
715}
716
717struct InboundText {
718 source: InboundTextSource,
719 payload: Vec<u8>,
720 received_at_ms: Option<u64>,
721 rx: MobileChatRxMetadataRecord,
722}
723
724struct InFlightChatTransmission {
725 transmission_id: u32,
726 gate_peer: Option<PublicKey>,
730 ticket: SendProgressTicket,
731 sent_reported: bool,
732 non_ack: bool,
735 queued_at_ms: u64,
740 stall_reported: bool,
741}
742
743const CHAT_TRANSMISSION_STALL_MS: u64 = 60_000;
747
748#[derive(Clone)]
749enum MobileChatWorkerEvent {
750 Mutation(MobileChatMutationRecord),
751 SenderResolution(MobileChatSenderResolutionRecord),
752 Delivery(MobileChatDeliveryRecord),
753 ArchiveLookup(MobileChatArchiveLookupRecord),
754 Diagnostic(String),
755}
756
757struct PendingChatEventBatch {
758 id: u64,
759 events: Vec<MobileChatWorkerEvent>,
760}
761
762#[derive(Debug)]
763enum BridgeRadioError {
764 Closed,
765 FrameTooLarge,
766}
767
768struct BridgeTransmitCompletions {
769 next_id: AtomicU64,
770 failure_generation: AtomicU64,
771 poisoned: AtomicBool,
779 pending: Mutex<BTreeMap<u64, oneshot::Sender<bool>>>,
780}
781
782impl BridgeTransmitCompletions {
783 fn new() -> Self {
784 Self {
785 next_id: AtomicU64::new(1),
786 failure_generation: AtomicU64::new(0),
787 poisoned: AtomicBool::new(false),
788 pending: Mutex::new(BTreeMap::new()),
789 }
790 }
791
792 fn generation(&self) -> u64 {
793 self.failure_generation.load(Ordering::SeqCst)
794 }
795
796 fn allocate(
797 &self,
798 generation: u64,
799 completion: oneshot::Sender<bool>,
800 ) -> Result<Option<u64>, BridgeRadioError> {
801 let id = self.next_id.fetch_add(1, Ordering::Relaxed).max(1);
802 let mut pending = self.pending.lock().map_err(|_| BridgeRadioError::Closed)?;
803 if self.poisoned.load(Ordering::SeqCst)
804 || generation != self.failure_generation.load(Ordering::SeqCst)
805 {
806 return Ok(None);
807 }
808 pending.insert(id, completion);
809 Ok(Some(id))
810 }
811
812 fn poison(&self) {
815 self.poisoned.store(true, Ordering::SeqCst);
816 self.failure_generation.fetch_add(1, Ordering::SeqCst);
817 }
818
819 fn clear_poison(&self) {
820 self.poisoned.store(false, Ordering::SeqCst);
821 }
822
823 fn complete(&self, id: u64, transmitted: bool) -> bool {
824 self.pending
825 .lock()
826 .ok()
827 .and_then(|mut pending| pending.remove(&id))
828 .is_some_and(|completion| completion.send(transmitted).is_ok())
829 }
830
831 fn fail_all(&self) {
832 self.failure_generation.fetch_add(1, Ordering::SeqCst);
833 let completions = self
834 .pending
835 .lock()
836 .map(|mut pending| core::mem::take(&mut *pending))
837 .unwrap_or_default();
838 for completion in completions.into_values() {
839 let _ = completion.send(false);
840 }
841 }
842}
843
844struct BridgeRadio {
845 inbound: mpsc::UnboundedReceiver<InboundFrame>,
846 outbound: NotifyingSender<MobileMeshOutboundFrameRecord>,
847 completions: Arc<BridgeTransmitCompletions>,
848}
849
850impl Radio for BridgeRadio {
851 type Error = BridgeRadioError;
852
853 async fn transmit(
854 &mut self,
855 data: &[u8],
856 options: TxOptions,
857 ) -> Result<(), TxError<Self::Error>> {
858 if data.len() > MAX_FRAME_SIZE {
859 return Err(TxError::Io(BridgeRadioError::FrameTooLarge));
860 }
861 let nocca = matches!(options.cad, umsh_hal::CadPolicy::Skip);
865 let (completion_tx, completion_rx) = oneshot::channel();
866 let generation = self.completions.generation();
867 let Some(id) = self
868 .completions
869 .allocate(generation, completion_tx)
870 .map_err(TxError::Io)?
871 else {
872 return Ok(());
875 };
876 if self
877 .outbound
878 .send(MobileMeshOutboundFrameRecord {
879 id,
880 data: data.to_vec(),
881 nocca,
882 })
883 .is_err()
884 {
885 let _ = self.completions.complete(id, false);
886 return Err(TxError::Io(BridgeRadioError::Closed));
887 }
888
889 match completion_rx.await {
897 Ok(true) => Ok(()),
898 Ok(false) => Ok(()),
904 Err(_) => Err(TxError::Io(BridgeRadioError::Closed)),
905 }
906 }
907
908 fn poll_receive(
909 &mut self,
910 cx: &mut Context<'_>,
911 buf: &mut [u8],
912 ) -> Poll<Result<RxInfo, Self::Error>> {
913 match self.inbound.poll_recv(cx) {
914 Poll::Ready(Some(frame)) => {
915 if frame.record.data.len() > buf.len() {
916 return Poll::Ready(Err(BridgeRadioError::FrameTooLarge));
917 }
918 let len = frame.record.data.len();
919 buf[..len].copy_from_slice(&frame.record.data);
920 Poll::Ready(Ok(RxInfo {
921 len,
922 rssi: frame.record.rssi_dbm.unwrap_or(0),
923 snr: Snr::from_centibels(frame.record.snr_cb.unwrap_or(0)),
924 lqi: frame.record.lqi.and_then(core::num::NonZeroU8::new),
925 origin: RxOrigin::Air,
926 }))
927 }
928 Poll::Ready(None) => Poll::Ready(Err(BridgeRadioError::Closed)),
929 Poll::Pending => Poll::Pending,
930 }
931 }
932
933 fn max_frame_size(&self) -> usize {
934 MAX_FRAME_SIZE
935 }
936 fn t_frame_ms(&self) -> u32 {
937 DEFAULT_FRAME_TIME_MS
938 }
939}
940
941#[derive(Clone)]
942struct SharedCounterStore(Arc<MobileCounterStore>);
943
944impl CounterStore for SharedCounterStore {
945 type Error = crate::CounterStoreError;
946
947 async fn load(&self, context: &[u8]) -> Result<u32, Self::Error> {
948 self.0.load_boundary(context.to_vec())
949 }
950
951 async fn store(&self, context: &[u8], value: u32) -> Result<(), Self::Error> {
952 self.0.commit_boundary(context.to_vec(), value)
953 }
954
955 async fn flush(&self) -> Result<(), Self::Error> {
956 CounterStore::flush(self.0.as_ref()).await
957 }
958}
959
960#[derive(Clone, Default)]
961struct MemoryKeyValueStore(Arc<Mutex<BTreeMap<Vec<u8>, Vec<u8>>>>);
962
963impl KeyValueStore for MemoryKeyValueStore {
964 type Error = MobileMeshError;
965
966 async fn load(&self, key: &[u8], out: &mut [u8]) -> Result<Option<usize>, Self::Error> {
967 let values = self
968 .0
969 .lock()
970 .map_err(|_| MobileMeshError::SessionUnavailable)?;
971 let Some(value) = values.get(key) else {
972 return Ok(None);
973 };
974 if value.len() > out.len() {
975 return Err(MobileMeshError::SessionUnavailable);
976 }
977 out[..value.len()].copy_from_slice(value);
978 Ok(Some(value.len()))
979 }
980
981 async fn store(&self, key: &[u8], value: &[u8]) -> Result<(), Self::Error> {
982 self.0
983 .lock()
984 .map_err(|_| MobileMeshError::SessionUnavailable)?
985 .insert(key.to_vec(), value.to_vec());
986 Ok(())
987 }
988
989 async fn delete(&self, key: &[u8]) -> Result<(), Self::Error> {
990 self.0
991 .lock()
992 .map_err(|_| MobileMeshError::SessionUnavailable)?
993 .remove(key);
994 Ok(())
995 }
996}
997
998#[derive(Clone)]
1003struct MobileClock {
1004 origin: tokio::time::Instant,
1005 sleep: Rc<RefCell<Option<Pin<Box<tokio::time::Sleep>>>>>,
1006}
1007
1008impl MobileClock {
1009 fn new() -> Self {
1010 Self {
1011 origin: tokio::time::Instant::now(),
1012 sleep: Rc::new(RefCell::new(None)),
1013 }
1014 }
1015}
1016
1017impl Clock for MobileClock {
1018 fn now_ms(&self) -> u64 {
1019 self.origin.elapsed().as_millis() as u64
1020 }
1021
1022 fn poll_delay_until(&self, cx: &mut Context<'_>, deadline_ms: u64) -> Poll<()> {
1023 let deadline = self.origin + Duration::from_millis(deadline_ms);
1024 if tokio::time::Instant::now() >= deadline {
1025 return Poll::Ready(());
1026 }
1027 let mut slot = self.sleep.borrow_mut();
1028 let sleep = slot.get_or_insert_with(|| Box::pin(tokio::time::sleep_until(deadline)));
1029 sleep.as_mut().reset(deadline);
1030 sleep.as_mut().poll(cx)
1031 }
1032}
1033
1034#[derive(Clone, Copy, Default)]
1035struct MobileDelay;
1036
1037impl DelayNs for MobileDelay {
1038 async fn delay_ns(&mut self, ns: u32) {
1039 tokio::time::sleep(Duration::from_nanos(u64::from(ns))).await;
1040 }
1041}
1042
1043struct MobilePlatform(PhantomData<()>);
1044
1045impl umsh_mac::Platform for MobilePlatform {
1046 type Identity = SoftwareIdentity;
1047 type Aes = SoftwareAes;
1048 type Sha = SoftwareSha256;
1049 type Radio = BridgeRadio;
1050 type Delay = MobileDelay;
1051 type Clock = MobileClock;
1052 type Rng = rand::rngs::ThreadRng;
1053 type CounterStore = SharedCounterStore;
1054 type KeyValueStore = MemoryKeyValueStore;
1055}
1056
1057const MOBILE_MAC_PEERS: usize = 64;
1062
1063const MOBILE_MAC_CHANNELS: usize = 32;
1068
1069type MobileMac =
1070 Mac<MobilePlatform, { umsh_mac::DEFAULT_IDENTITIES }, MOBILE_MAC_PEERS, MOBILE_MAC_CHANNELS>;
1071const MOBILE_CHAT_TRANSMIT_WINDOW: usize = 8;
1072
1073#[derive(uniffi::Object)]
1079pub struct MobileMeshSession {
1080 local_key: PublicKey,
1084 commands: mpsc::UnboundedSender<WorkerCommand>,
1085 outbound: Mutex<std_mpsc::Receiver<MobileMeshOutboundFrameRecord>>,
1086 transmit_completions: Arc<BridgeTransmitCompletions>,
1087 events: Mutex<std_mpsc::Receiver<MobileMeshPingEventRecord>>,
1088 management: Mutex<std_mpsc::Receiver<MobileMeshManagementEventRecord>>,
1089 advertisements: Mutex<std_mpsc::Receiver<MobileMeshAdvertisementRecord>>,
1090 peer_heard: Mutex<std_mpsc::Receiver<MobileMeshPeerHeardRecord>>,
1091 chat_events: Mutex<std_mpsc::Receiver<MobileChatWorkerEvent>>,
1092 pending_chat_events: Mutex<Option<PendingChatEventBatch>>,
1093 next_chat_batch_id: Mutex<u64>,
1094 next_operation_id: Mutex<u64>,
1095 wake: Arc<WakeSignal>,
1096}
1097
1098#[uniffi::export]
1099impl MobileMeshSession {
1100 #[uniffi::constructor]
1101 pub async fn new(
1102 identity: Arc<MobileIdentity>,
1103 counter_store: Arc<MobileCounterStore>,
1104 ) -> Result<Arc<Self>, MobileMeshError> {
1105 Self::build(identity, counter_store, false).await
1106 }
1107
1108 pub fn ping(&self, peer_address: String, timeout_ms: u64) -> Result<u64, MobileMeshError> {
1109 let peer = decode_peer(&peer_address).map_err(|_| MobileMeshError::InvalidPeer)?;
1110 let operation_id = self.next_operation_id()?;
1111 self.commands
1112 .send(WorkerCommand::Ping {
1113 operation_id,
1114 peer,
1115 timeout_ms,
1116 })
1117 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1118 Ok(operation_id)
1119 }
1120
1121 pub fn node_public_key(&self) -> Vec<u8> {
1130 self.local_key.0.to_vec()
1131 }
1132
1133 pub fn begin_management_get(
1141 &self,
1142 peer_address: String,
1143 property_id: u32,
1144 ) -> Result<u64, MobileMeshError> {
1145 let frame = encode_management(|buf| frame::prop_get(buf, 0, property_id))?;
1146 self.begin_management(
1147 peer_address,
1148 ManagementRequest::One {
1149 frame,
1150 shape: ReplyShape::Property(property_id),
1151 },
1152 )
1153 }
1154
1155 pub fn begin_management_set(
1161 &self,
1162 peer_address: String,
1163 property_id: u32,
1164 value: Vec<u8>,
1165 ) -> Result<u64, MobileMeshError> {
1166 let frame = encode_management(|buf| frame::prop_set(buf, 0, property_id, &value))?;
1167 self.begin_management(
1168 peer_address,
1169 ManagementRequest::One {
1170 frame,
1171 shape: ReplyShape::Property(property_id),
1172 },
1173 )
1174 }
1175
1176 pub fn begin_management_insert(
1179 &self,
1180 peer_address: String,
1181 property_id: u32,
1182 item: Vec<u8>,
1183 ) -> Result<u64, MobileMeshError> {
1184 let frame = encode_management(|buf| frame::prop_insert(buf, 0, property_id, &item))?;
1185 self.begin_management(
1186 peer_address,
1187 ManagementRequest::One {
1188 frame,
1189 shape: ReplyShape::Property(property_id),
1190 },
1191 )
1192 }
1193
1194 pub fn begin_management_remove(
1197 &self,
1198 peer_address: String,
1199 property_id: u32,
1200 selector: Vec<u8>,
1201 ) -> Result<u64, MobileMeshError> {
1202 let frame = encode_management(|buf| frame::prop_remove(buf, 0, property_id, &selector))?;
1203 self.begin_management(
1204 peer_address,
1205 ManagementRequest::One {
1206 frame,
1207 shape: ReplyShape::Property(property_id),
1208 },
1209 )
1210 }
1211
1212 pub fn begin_management_insert_admin(
1221 &self,
1222 peer_address: String,
1223 public_key: Vec<u8>,
1224 ) -> Result<u64, MobileMeshError> {
1225 if public_key.len() != items::PUBLIC_KEY_LEN {
1226 return Err(MobileMeshError::InvalidRequest);
1227 }
1228 self.begin_management_insert(peer_address, prop::DEV_ADMINS, public_key)
1229 }
1230
1231 pub fn begin_management_remove_admin(
1237 &self,
1238 peer_address: String,
1239 public_key: Vec<u8>,
1240 ) -> Result<u64, MobileMeshError> {
1241 if public_key.len() != items::PUBLIC_KEY_LEN {
1242 return Err(MobileMeshError::InvalidRequest);
1243 }
1244 self.begin_management_remove(peer_address, prop::DEV_ADMINS, public_key)
1245 }
1246
1247 pub fn begin_management_insert_peer(
1255 &self,
1256 peer_address: String,
1257 public_key: Vec<u8>,
1258 ) -> Result<u64, MobileMeshError> {
1259 if public_key.len() != items::PUBLIC_KEY_LEN {
1260 return Err(MobileMeshError::InvalidRequest);
1261 }
1262 self.begin_management_insert(peer_address, prop::DEV_PEERS, public_key)
1263 }
1264
1265 pub fn begin_management_remove_peer(
1267 &self,
1268 peer_address: String,
1269 public_key: Vec<u8>,
1270 ) -> Result<u64, MobileMeshError> {
1271 if public_key.len() != items::PUBLIC_KEY_LEN {
1272 return Err(MobileMeshError::InvalidRequest);
1273 }
1274 self.begin_management_remove(peer_address, prop::DEV_PEERS, public_key)
1275 }
1276
1277 pub fn begin_management_set_alert(
1286 &self,
1287 peer_address: String,
1288 state: crate::ulcp::UlcpAlertState,
1289 ) -> Result<u64, MobileMeshError> {
1290 let value = crate::ulcp::encode_alert_state(state).map_err(|_| {
1291 MobileMeshError::InvalidRequest
1294 })?;
1295 self.begin_management_set(peer_address, prop::ALERT, value)
1296 }
1297
1298 pub fn begin_management_get_many(
1305 &self,
1306 peer_address: String,
1307 property_ids: Vec<u32>,
1308 ) -> Result<u64, MobileMeshError> {
1309 if property_ids.is_empty() {
1310 return Err(MobileMeshError::InvalidRequest);
1311 }
1312 let frame = encode_management(|buf| frame::prop_multi_get(buf, 0, &property_ids))?;
1313 self.begin_management(
1314 peer_address,
1315 ManagementRequest::One {
1316 frame,
1317 shape: ReplyShape::Entries(property_ids),
1318 },
1319 )
1320 }
1321
1322 pub fn begin_management_set_many(
1328 &self,
1329 peer_address: String,
1330 writes: Vec<MobileMeshPropertyWriteRecord>,
1331 ) -> Result<u64, MobileMeshError> {
1332 if writes.is_empty() {
1333 return Err(MobileMeshError::InvalidRequest);
1334 }
1335 let property_ids: Vec<u32> = writes.iter().map(|write| write.property_id).collect();
1336 let entries: Vec<(u32, &[u8])> = writes
1337 .iter()
1338 .map(|write| (write.property_id, write.value.as_slice()))
1339 .collect();
1340 let frame = encode_management(|buf| frame::prop_multi_set(buf, 0, &entries))?;
1341 self.begin_management(
1342 peer_address,
1343 ManagementRequest::One {
1344 frame,
1345 shape: ReplyShape::Entries(property_ids),
1346 },
1347 )
1348 }
1349
1350 pub fn begin_management_save(&self, peer_address: String) -> Result<u64, MobileMeshError> {
1352 let frame = encode_management(|buf| frame::save(buf, 0))?;
1353 self.begin_management(
1354 peer_address,
1355 ManagementRequest::One {
1356 frame,
1357 shape: ReplyShape::Status,
1358 },
1359 )
1360 }
1361
1362 pub fn begin_management_ble_clear_bonds(
1376 &self,
1377 peer_address: String,
1378 ) -> Result<u64, MobileMeshError> {
1379 let frame = encode_management(|buf| frame::ble_clear_bonds(buf, 0))?;
1380 self.begin_management(
1381 peer_address,
1382 ManagementRequest::One {
1383 frame,
1384 shape: ReplyShape::Status,
1385 },
1386 )
1387 }
1388
1389 pub fn begin_management_reset(
1397 &self,
1398 peer_address: String,
1399 scope: MobileMeshResetScope,
1400 ) -> Result<u64, MobileMeshError> {
1401 let frame = encode_management(|buf| match scope {
1402 MobileMeshResetScope::Protocol => frame::reset(buf, 0),
1403 MobileMeshResetScope::Restore => frame::restore(buf, 0),
1404 MobileMeshResetScope::Reboot => frame::reboot(buf, 0),
1405 MobileMeshResetScope::Factory => frame::factory_reset(buf, 0),
1406 })?;
1407 self.begin_management(
1408 peer_address,
1409 ManagementRequest::One {
1410 frame,
1411 shape: ReplyShape::Acknowledgment,
1412 },
1413 )
1414 }
1415
1416 pub fn begin_management_fetch(
1431 &self,
1432 peer_address: String,
1433 property_ids: Vec<u32>,
1434 multi_hint: bool,
1435 ) -> Result<u64, MobileMeshError> {
1436 self.begin_management(
1437 peer_address,
1438 ManagementRequest::Fetch {
1439 property_ids,
1440 multi_hint,
1441 },
1442 )
1443 }
1444
1445 pub async fn advertise_identity(
1450 &self,
1451 name: Option<String>,
1452 timestamp: Option<u32>,
1453 ) -> Result<(), MobileMeshError> {
1454 self.send_advertisement(name, timestamp, false).await
1455 }
1456
1457 pub async fn advertise_identity_scheduled(
1464 &self,
1465 name: Option<String>,
1466 timestamp: Option<u32>,
1467 ) -> Result<(), MobileMeshError> {
1468 self.send_advertisement(name, timestamp, true).await
1469 }
1470
1471 async fn send_advertisement(
1472 &self,
1473 name: Option<String>,
1474 timestamp: Option<u32>,
1475 scheduled: bool,
1476 ) -> Result<(), MobileMeshError> {
1477 let (response, result) = oneshot::channel();
1478 self.commands
1479 .send(WorkerCommand::Advertise {
1480 name,
1481 timestamp,
1482 scheduled,
1483 response,
1484 })
1485 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1486 result
1487 .await
1488 .map_err(|_| MobileMeshError::SessionUnavailable)?
1489 }
1490
1491 pub async fn send_beacon(&self) -> Result<(), MobileMeshError> {
1495 let (response, result) = oneshot::channel();
1496 self.commands
1497 .send(WorkerCommand::Beacon { response })
1498 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1499 result
1500 .await
1501 .map_err(|_| MobileMeshError::SessionUnavailable)?
1502 }
1503
1504 pub async fn request_identity(&self, peer_address: String) -> Result<(), MobileMeshError> {
1510 let peer = decode_peer(&peer_address).map_err(|_| MobileMeshError::InvalidPeer)?;
1511 let (response, result) = oneshot::channel();
1512 self.commands
1513 .send(WorkerCommand::RequestIdentity { peer, response })
1514 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1515 result
1516 .await
1517 .map_err(|_| MobileMeshError::SessionUnavailable)?
1518 }
1519
1520 pub async fn discover_identities(
1548 &self,
1549 role_code: Option<u8>,
1550 capability_bits: Option<u8>,
1551 node_hint: Option<Vec<u8>>,
1552 source_route: Vec<Vec<u8>>,
1553 ) -> Result<(), MobileMeshError> {
1554 let (response, result) = oneshot::channel();
1555 self.commands
1556 .send(WorkerCommand::DiscoverIdentities {
1557 role_code,
1558 capability_bits,
1559 node_hint,
1560 source_route,
1561 response,
1562 })
1563 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1564 result
1565 .await
1566 .map_err(|_| MobileMeshError::SessionUnavailable)?
1567 }
1568
1569 pub async fn request_identity_by_hint(
1581 &self,
1582 conversation_address: String,
1583 hint: Vec<u8>,
1584 ) -> Result<(), MobileMeshError> {
1585 let hint: [u8; 3] = hint
1586 .try_into()
1587 .map_err(|_| MobileMeshError::UnknownConversation)?;
1588 let (response, result) = oneshot::channel();
1589 self.commands
1590 .send(WorkerCommand::RequestIdentityByHint {
1591 conversation_address,
1592 hint: NodeHint(hint),
1593 response,
1594 })
1595 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1596 result
1597 .await
1598 .map_err(|_| MobileMeshError::SessionUnavailable)?
1599 }
1600
1601 pub async fn request_peer_repeaters(
1610 &self,
1611 peer: Vec<u8>,
1612 ) -> Result<Vec<MobileMeshPeerRepeaterRecord>, MobileMeshError> {
1613 let peer: [u8; 32] = peer.try_into().map_err(|_| MobileMeshError::InvalidPeer)?;
1614 let (response, result) = oneshot::channel();
1615 self.commands
1616 .send(WorkerCommand::PeerRepeaters {
1617 peer: PublicKey(peer),
1618 response,
1619 })
1620 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1621 result
1622 .await
1623 .map_err(|_| MobileMeshError::SessionUnavailable)?
1624 }
1625
1626 pub async fn set_chat_display_name(&self, name: String) -> Result<(), MobileMeshError> {
1641 let (response, result) = oneshot::channel();
1642 self.commands
1643 .send(WorkerCommand::SetChatDisplayName { name, response })
1644 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1645 result
1646 .await
1647 .map_err(|_| MobileMeshError::SessionUnavailable)
1648 }
1649
1650 pub async fn set_discoverable(
1651 &self,
1652 enabled: bool,
1653 name: Option<String>,
1654 ) -> Result<(), MobileMeshError> {
1655 let (response, result) = oneshot::channel();
1656 self.commands
1657 .send(WorkerCommand::SetDiscoverable {
1658 enabled,
1659 name,
1660 response,
1661 })
1662 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1663 result
1664 .await
1665 .map_err(|_| MobileMeshError::SessionUnavailable)
1666 }
1667
1668 pub async fn set_advertised_location(
1679 &self,
1680 location: Option<MobileMeshSharedLocationRecord>,
1681 ) -> Result<(), MobileMeshError> {
1682 let location = location.map(disclosed_cell).transpose()?;
1683 let (response, result) = oneshot::channel();
1684 self.commands
1685 .send(WorkerCommand::SetAdvertisedLocation { location, response })
1686 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1687 result
1688 .await
1689 .map_err(|_| MobileMeshError::SessionUnavailable)
1690 }
1691
1692 pub async fn peer_route(
1697 &self,
1698 peer_address: String,
1699 ) -> Result<MobileMeshRouteRecord, MobileMeshError> {
1700 let peer = decode_peer(&peer_address).map_err(|_| MobileMeshError::InvalidPeer)?;
1701 let (response, result) = oneshot::channel();
1702 self.commands
1703 .send(WorkerCommand::PeerRoute { peer, response })
1704 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1705 result
1706 .await
1707 .map_err(|_| MobileMeshError::SessionUnavailable)
1708 }
1709
1710 pub async fn clear_peer_route(&self, peer_address: String) -> Result<bool, MobileMeshError> {
1715 let peer = decode_peer(&peer_address).map_err(|_| MobileMeshError::InvalidPeer)?;
1716 let (response, result) = oneshot::channel();
1717 self.commands
1718 .send(WorkerCommand::ClearPeerRoute { peer, response })
1719 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1720 result
1721 .await
1722 .map_err(|_| MobileMeshError::SessionUnavailable)
1723 }
1724
1725 pub async fn sign_identity_bundle(
1728 &self,
1729 name: Option<String>,
1730 timestamp: Option<u32>,
1731 ) -> Result<Vec<u8>, MobileMeshError> {
1732 let (response, result) = oneshot::channel();
1733 self.commands
1734 .send(WorkerCommand::SignIdentityBundle {
1735 name,
1736 timestamp,
1737 response,
1738 })
1739 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1740 result
1741 .await
1742 .map_err(|_| MobileMeshError::SessionUnavailable)?
1743 }
1744
1745 pub async fn register_peers(&self, peer_addresses: Vec<String>) -> Result<(), MobileMeshError> {
1746 let peers = peer_addresses
1747 .iter()
1748 .map(|address| decode_peer(address).map_err(|_| MobileMeshError::InvalidPeer))
1749 .collect::<Result<Vec<_>, _>>()?;
1750 let (response, result) = oneshot::channel();
1751 self.commands
1752 .send(WorkerCommand::RegisterPeers { peers, response })
1753 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1754 result
1755 .await
1756 .map_err(|_| MobileMeshError::SessionUnavailable)?
1757 }
1758
1759 pub async fn remove_peers(&self, peer_addresses: Vec<String>) -> Result<(), MobileMeshError> {
1765 let peers = peer_addresses
1766 .iter()
1767 .map(|address| decode_peer(address).map_err(|_| MobileMeshError::InvalidPeer))
1768 .collect::<Result<Vec<_>, _>>()?;
1769 let (response, result) = oneshot::channel();
1770 self.commands
1771 .send(WorkerCommand::RemovePeers { peers, response })
1772 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1773 result
1774 .await
1775 .map_err(|_| MobileMeshError::SessionUnavailable)?
1776 }
1777
1778 pub async fn register_channels(&self, keys: Vec<Vec<u8>>) -> Result<(), MobileMeshError> {
1784 let keys = decode_channel_keys(keys)?;
1785 let (response, result) = oneshot::channel();
1786 self.commands
1787 .send(WorkerCommand::RegisterChannels { keys, response })
1788 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1789 result
1790 .await
1791 .map_err(|_| MobileMeshError::SessionUnavailable)?
1792 }
1793
1794 pub async fn remove_channels(&self, keys: Vec<Vec<u8>>) -> Result<(), MobileMeshError> {
1797 let keys = decode_channel_keys(keys)?;
1798 let (response, result) = oneshot::channel();
1799 self.commands
1800 .send(WorkerCommand::RemoveChannels { keys, response })
1801 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1802 result
1803 .await
1804 .map_err(|_| MobileMeshError::SessionUnavailable)?
1805 }
1806
1807 pub fn receive(&self, frame: MobileMeshRxRecord) -> Result<(), MobileMeshError> {
1808 if frame.data.is_empty() || frame.data.len() > MAX_FRAME_SIZE {
1809 return Err(MobileMeshError::SessionUnavailable);
1810 }
1811 self.commands
1812 .send(WorkerCommand::Receive(frame))
1813 .map_err(|_| MobileMeshError::SessionUnavailable)
1814 }
1815
1816 pub fn complete_outbound_frame(
1820 &self,
1821 frame_id: u64,
1822 transmitted: bool,
1823 ) -> Result<(), MobileMeshError> {
1824 if !transmitted {
1825 self.transmit_completions.poison();
1829 self.commands
1830 .send(WorkerCommand::FailOutboundTransmissions)
1831 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1832 }
1833 self.transmit_completions
1834 .complete(frame_id, transmitted)
1835 .then_some(())
1836 .ok_or(MobileMeshError::SessionUnavailable)
1837 }
1838
1839 pub async fn restore_chat(
1840 &self,
1841 checkpoints: Vec<MobileChatCheckpointRecord>,
1842 ) -> Result<(), MobileMeshError> {
1843 let (response, result) = oneshot::channel();
1844 self.commands
1845 .send(WorkerCommand::RestoreChat {
1846 checkpoints,
1847 response,
1848 })
1849 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1850 result
1851 .await
1852 .map_err(|_| MobileMeshError::SessionUnavailable)
1853 }
1854
1855 pub async fn compose_text(
1858 &self,
1859 conversation_address: String,
1860 client_token: u32,
1861 body: String,
1862 ) -> Result<MobileChatComposeBatchRecord, MobileMeshError> {
1863 self.compose_chat(
1864 conversation_address,
1865 client_token,
1866 ChatComposeRequest::Text { body },
1867 )
1868 .await
1869 }
1870
1871 pub async fn compose_edit(
1876 &self,
1877 conversation_address: String,
1878 client_token: u32,
1879 original: MobileChatOriginalRef,
1880 body: String,
1881 ) -> Result<MobileChatComposeBatchRecord, MobileMeshError> {
1882 self.compose_chat(
1883 conversation_address,
1884 client_token,
1885 ChatComposeRequest::Edit { original, body },
1886 )
1887 .await
1888 }
1889
1890 pub async fn compose_delete(
1893 &self,
1894 conversation_address: String,
1895 client_token: u32,
1896 original: MobileChatOriginalRef,
1897 ) -> Result<MobileChatComposeBatchRecord, MobileMeshError> {
1898 self.compose_chat(
1899 conversation_address,
1900 client_token,
1901 ChatComposeRequest::Delete { original },
1902 )
1903 .await
1904 }
1905
1906 pub async fn compose_reaction(
1915 &self,
1916 conversation_address: String,
1917 client_token: u32,
1918 target: MobileChatRegardingRef,
1919 body: String,
1920 ) -> Result<MobileChatComposeBatchRecord, MobileMeshError> {
1921 self.compose_chat(
1922 conversation_address,
1923 client_token,
1924 ChatComposeRequest::Reaction { target, body },
1925 )
1926 .await
1927 }
1928
1929 pub async fn commit_chat_batch(&self, batch_id: u64) -> Result<(), MobileMeshError> {
1930 let (response, result) = oneshot::channel();
1931 self.commands
1932 .send(WorkerCommand::CommitChatBatch { batch_id, response })
1933 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1934 result
1935 .await
1936 .map_err(|_| MobileMeshError::SessionUnavailable)?
1937 }
1938
1939 pub async fn reject_chat_batch(
1940 &self,
1941 batch_id: u64,
1942 checkpoints: Vec<MobileChatCheckpointRecord>,
1943 ) -> Result<(), MobileMeshError> {
1944 let (response, result) = oneshot::channel();
1945 self.commands
1946 .send(WorkerCommand::RejectChatBatch {
1947 batch_id,
1948 checkpoints,
1949 response,
1950 })
1951 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1952 result
1953 .await
1954 .map_err(|_| MobileMeshError::SessionUnavailable)?
1955 }
1956
1957 pub fn apply_chat_archive_result(
1958 &self,
1959 request_id: u32,
1960 kind: MobileChatArchiveResultKind,
1961 payload: Vec<u8>,
1962 ) -> Result<(), MobileMeshError> {
1963 self.commands
1964 .send(WorkerCommand::ChatArchiveResult {
1965 request_id,
1966 kind,
1967 payload,
1968 })
1969 .map_err(|_| MobileMeshError::SessionUnavailable)
1970 }
1971
1972 pub fn acknowledge_chat_batch(&self, batch_id: u64) -> Result<(), MobileMeshError> {
1973 let mut pending = self
1974 .pending_chat_events
1975 .lock()
1976 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1977 if pending.as_ref().is_some_and(|batch| batch.id == batch_id) {
1978 *pending = None;
1979 self.wake.notify();
1983 }
1984 Ok(())
1985 }
1986
1987 pub fn fail_outbound_transmissions(&self) -> Result<(), MobileMeshError> {
1992 self.transmit_completions.poison();
1999 self.commands
2000 .send(WorkerCommand::FailOutboundTransmissions)
2001 .map_err(|_| MobileMeshError::SessionUnavailable)?;
2002 self.transmit_completions.fail_all();
2005 Ok(())
2006 }
2007
2008 pub fn set_wake_listener(&self, listener: Arc<dyn MobileMeshWakeListener>) {
2012 self.wake.set_listener(Some(listener));
2013 }
2014
2015 pub fn clear_wake_listener(&self) {
2016 self.wake.set_listener(None);
2017 }
2018
2019 pub fn poll_update(&self) -> MobileMeshSessionUpdateRecord {
2020 self.wake.drained();
2023 let mut outbound_frames = Vec::new();
2024 if let Ok(receiver) = self.outbound.lock() {
2025 outbound_frames.extend(receiver.try_iter());
2026 }
2027 let mut ping_events = Vec::new();
2028 if let Ok(receiver) = self.events.lock() {
2029 ping_events.extend(receiver.try_iter());
2030 }
2031 let mut management_events = Vec::new();
2032 if let Ok(receiver) = self.management.lock() {
2033 management_events.extend(receiver.try_iter());
2034 }
2035 let mut advertisement_events = Vec::new();
2036 if let Ok(receiver) = self.advertisements.lock() {
2037 advertisement_events.extend(receiver.try_iter());
2038 }
2039 let mut peer_heard_events = Vec::new();
2040 if let Ok(receiver) = self.peer_heard.lock() {
2041 peer_heard_events.extend(receiver.try_iter());
2042 }
2043 let mut chat_mutations = Vec::new();
2044 let mut chat_deliveries = Vec::new();
2045 let mut chat_archive_lookups = Vec::new();
2046 let mut chat_sender_resolutions = Vec::new();
2047 let mut chat_diagnostics = Vec::new();
2048 let mut chat_batch_id = None;
2049 if let Ok(mut pending) = self.pending_chat_events.lock() {
2050 if pending.is_none()
2051 && let Ok(receiver) = self.chat_events.lock()
2052 {
2053 let events = receiver.try_iter().collect::<Vec<_>>();
2054 if !events.is_empty()
2055 && let Ok(mut next) = self.next_chat_batch_id.lock()
2056 {
2057 let id = *next;
2058 *next = next.wrapping_add(1).max(1);
2059 *pending = Some(PendingChatEventBatch { id, events });
2060 }
2061 }
2062 if let Some(batch) = pending.as_ref() {
2063 chat_batch_id = Some(batch.id);
2064 for event in batch.events.iter().cloned() {
2065 match event {
2066 MobileChatWorkerEvent::Mutation(record) => chat_mutations.push(record),
2067 MobileChatWorkerEvent::Delivery(record) => chat_deliveries.push(record),
2068 MobileChatWorkerEvent::ArchiveLookup(record) => {
2069 chat_archive_lookups.push(record);
2070 }
2071 MobileChatWorkerEvent::SenderResolution(record) => {
2072 chat_sender_resolutions.push(record);
2073 }
2074 MobileChatWorkerEvent::Diagnostic(record) => chat_diagnostics.push(record),
2075 }
2076 }
2077 }
2078 }
2079 MobileMeshSessionUpdateRecord {
2080 outbound_frames,
2081 ping_events,
2082 management_events,
2083 advertisement_events,
2084 peer_heard_events,
2085 chat_batch_id,
2086 chat_mutations,
2087 chat_deliveries,
2088 chat_archive_lookups,
2089 chat_sender_resolutions,
2090 chat_diagnostics,
2091 }
2092 }
2093}
2094
2095impl MobileMeshSession {
2096 fn begin_management(
2099 &self,
2100 peer_address: String,
2101 request: ManagementRequest,
2102 ) -> Result<u64, MobileMeshError> {
2103 let peer = decode_peer(&peer_address).map_err(|_| MobileMeshError::InvalidPeer)?;
2104 let operation_id = self.next_operation_id()?;
2105 self.commands
2106 .send(WorkerCommand::Manage {
2107 operation_id,
2108 peer,
2109 request,
2110 })
2111 .map_err(|_| MobileMeshError::SessionUnavailable)?;
2112 Ok(operation_id)
2113 }
2114
2115 fn next_operation_id(&self) -> Result<u64, MobileMeshError> {
2116 let mut next = self
2117 .next_operation_id
2118 .lock()
2119 .map_err(|_| MobileMeshError::SessionUnavailable)?;
2120 let current = *next;
2121 *next = next.wrapping_add(1).max(1);
2122 Ok(current)
2123 }
2124
2125 async fn compose_chat(
2126 &self,
2127 conversation_address: String,
2128 client_token: u32,
2129 request: ChatComposeRequest,
2130 ) -> Result<MobileChatComposeBatchRecord, MobileMeshError> {
2131 let (response, result) = oneshot::channel();
2134 self.commands
2135 .send(WorkerCommand::ComposeChat {
2136 conversation_address,
2137 client_token,
2138 request,
2139 response,
2140 })
2141 .map_err(|_| MobileMeshError::SessionUnavailable)?;
2142 result
2143 .await
2144 .map_err(|_| MobileMeshError::SessionUnavailable)?
2145 }
2146
2147 #[cfg(test)]
2153 async fn new_with_virtual_time(
2154 identity: Arc<MobileIdentity>,
2155 counter_store: Arc<MobileCounterStore>,
2156 ) -> Result<Arc<Self>, MobileMeshError> {
2157 Self::build(identity, counter_store, true).await
2158 }
2159
2160 async fn build(
2161 identity: Arc<MobileIdentity>,
2162 counter_store: Arc<MobileCounterStore>,
2163 virtual_time: bool,
2164 ) -> Result<Arc<Self>, MobileMeshError> {
2165 let (commands, command_rx) = mpsc::unbounded_channel();
2166 let wake = Arc::new(WakeSignal::new());
2167 let (outbound_tx, outbound) = std_mpsc::channel();
2168 let (event_tx, events) = std_mpsc::channel();
2169 let (management_tx, management) = std_mpsc::channel();
2170 let (advertisement_tx, advertisements) = std_mpsc::channel();
2171 let (peer_heard_tx, peer_heard) = std_mpsc::channel();
2172 let (chat_event_tx, chat_events) = std_mpsc::channel();
2173 let outbound_tx = NotifyingSender {
2174 tx: outbound_tx,
2175 wake: wake.clone(),
2176 };
2177 let event_tx = NotifyingSender {
2178 tx: event_tx,
2179 wake: wake.clone(),
2180 };
2181 let management_tx = NotifyingSender {
2182 tx: management_tx,
2183 wake: wake.clone(),
2184 };
2185 let advertisement_tx = NotifyingSender {
2186 tx: advertisement_tx,
2187 wake: wake.clone(),
2188 };
2189 let peer_heard_tx = NotifyingSender {
2190 tx: peer_heard_tx,
2191 wake: wake.clone(),
2192 };
2193 let chat_event_tx = NotifyingSender {
2194 tx: chat_event_tx,
2195 wake: wake.clone(),
2196 };
2197 let (ready_tx, ready_rx) = oneshot::channel();
2198 let worker_identity = identity.take_for_session()?;
2199 let local_key = *worker_identity.public_key();
2200 let transmit_completions = Arc::new(BridgeTransmitCompletions::new());
2201 let worker_transmit_completions = transmit_completions.clone();
2202
2203 std::thread::Builder::new()
2204 .name("umsh-mobile-mesh".to_owned())
2205 .stack_size(16 * 1024 * 1024)
2210 .spawn(move || {
2211 let mut builder = tokio::runtime::Builder::new_current_thread();
2212 builder.enable_time();
2213 #[cfg(test)]
2214 if virtual_time {
2215 builder.start_paused(true);
2216 }
2217 #[cfg(not(test))]
2218 let _ = virtual_time;
2219 let runtime = match builder.build() {
2220 Ok(runtime) => runtime,
2221 Err(_) => {
2222 let _ = ready_tx.send(Err(MobileMeshError::SessionUnavailable));
2223 return;
2224 }
2225 };
2226 let local = tokio::task::LocalSet::new();
2227 local.block_on(
2231 &runtime,
2232 Box::pin(run_worker(
2233 worker_identity,
2234 SharedCounterStore(counter_store),
2235 command_rx,
2236 outbound_tx,
2237 worker_transmit_completions,
2238 event_tx,
2239 management_tx,
2240 advertisement_tx,
2241 peer_heard_tx,
2242 chat_event_tx,
2243 ready_tx,
2244 )),
2245 );
2246 })
2247 .map_err(|_| MobileMeshError::SessionUnavailable)?;
2248
2249 ready_rx
2250 .await
2251 .map_err(|_| MobileMeshError::SessionUnavailable)??;
2252 Ok(Arc::new(Self {
2253 local_key,
2254 commands,
2255 outbound: Mutex::new(outbound),
2256 transmit_completions,
2257 events: Mutex::new(events),
2258 management: Mutex::new(management),
2259 advertisements: Mutex::new(advertisements),
2260 peer_heard: Mutex::new(peer_heard),
2261 chat_events: Mutex::new(chat_events),
2262 pending_chat_events: Mutex::new(None),
2263 next_chat_batch_id: Mutex::new(1),
2264 next_operation_id: Mutex::new(1),
2265 wake,
2266 }))
2267 }
2268}
2269
2270impl Drop for MobileMeshSession {
2271 fn drop(&mut self) {
2272 self.transmit_completions.fail_all();
2273 let _ = self.commands.send(WorkerCommand::Shutdown);
2274 }
2275}
2276
2277fn disclosed_cell(record: MobileMeshSharedLocationRecord) -> Result<NodeLocation, MobileMeshError> {
2283 if !(1..=MAX_PRECISION).contains(&record.precision_bytes)
2284 || !record.latitude_degrees.is_finite()
2285 || record.latitude_degrees.abs() > 90.0
2286 || !record.longitude_degrees.is_finite()
2287 || record.longitude_degrees.abs() > 180.0
2288 {
2289 return Err(MobileMeshError::InvalidLocation);
2290 }
2291 Ok(NodeLocation::from_e7(
2292 (record.latitude_degrees * 1e7).round() as i32,
2293 (record.longitude_degrees * 1e7).round() as i32,
2294 record.precision_bytes,
2295 ))
2296}
2297
2298fn phone_identity_profile(
2304 public_key: PublicKey,
2305 name: Option<&str>,
2306 location: Option<NodeLocation>,
2307) -> NodeIdentityProfile {
2308 let mut profile = NodeIdentityProfile::new(
2309 public_key,
2310 NodeRole::Chat,
2311 NodeCapabilities::MOBILE | NodeCapabilities::TEXT_MESSAGES,
2312 );
2313 profile.name = name
2314 .map(|name| {
2315 let mut end = name.len().min(24);
2316 while !name.is_char_boundary(end) {
2317 end -= 1;
2318 }
2319 name[..end].to_owned()
2320 })
2321 .filter(|name| !name.is_empty());
2322 profile.location = location;
2323 profile
2324}
2325
2326async fn build_signed_identity_bundle(
2332 signer: &SoftwareIdentity,
2333 name: Option<&str>,
2334 timestamp: Option<u32>,
2335 location: Option<NodeLocation>,
2336) -> Result<Vec<u8>, MobileMeshError> {
2337 let name = name
2338 .map(|name| {
2339 let mut end = name.len().min(24);
2340 while !name.is_char_boundary(end) {
2341 end -= 1;
2342 }
2343 name[..end].to_owned()
2344 })
2345 .filter(|name| !name.is_empty());
2346 let payload = NodeIdentityPayload {
2347 role: NodeRole::Chat,
2348 capabilities: NodeCapabilities::MOBILE | NodeCapabilities::TEXT_MESSAGES,
2349 name,
2350 location,
2351 altitude_m: None,
2352 timestamp,
2353 supported_regions: None,
2354 nonce: None,
2355 signature: None,
2356 };
2357 let mut buf = [0u8; 192];
2358 let len = payload
2359 .encode_for_signing(&mut buf)
2360 .map_err(|_| MobileMeshError::SendFailed)?;
2361 let signature = signer
2362 .sign(&buf[..len])
2363 .await
2364 .map_err(|_| MobileMeshError::SendFailed)?;
2365 let mut bundle = buf[..len].to_vec();
2366 bundle.extend_from_slice(&signature);
2367 Ok(bundle)
2368}
2369
2370const MANAGEMENT_FLOOD_HOPS: u8 = 5;
2374
2375const SYNC_BATCH: usize = 8;
2382
2383fn encode_management(
2387 build: impl FnOnce(&mut [u8]) -> Result<usize, umsh_ulcp::frame::WriteError>,
2388) -> Result<Vec<u8>, MobileMeshError> {
2389 let mut buf = vec![0u8; umsh_node_mgmt::REQUEST_MAX];
2390 let len = build(&mut buf).map_err(|_| MobileMeshError::InvalidRequest)?;
2391 buf.truncate(len);
2392 Ok(buf)
2393}
2394
2395struct FetchCrawl {
2402 multi: bool,
2405 asked: Vec<u32>,
2407 pending: VecDeque<u32>,
2409 answers: Vec<MobileMeshManagementAnswerRecord>,
2413}
2414
2415impl FetchCrawl {
2416 fn new(properties: Vec<u32>, multi_hint: bool) -> Self {
2417 let mut pending: Vec<u32> = properties
2418 .into_iter()
2419 .filter(|&key| ids::admin_reachable(key))
2420 .collect();
2421 pending.dedup();
2422 Self {
2423 multi: multi_hint,
2424 asked: Vec::new(),
2425 pending: pending.into(),
2426 answers: Vec::new(),
2427 }
2428 }
2429
2430 fn next_request(&mut self) -> Result<Option<Vec<u8>>, MobileMeshError> {
2432 let batch = if self.multi { SYNC_BATCH } else { 1 };
2433 self.asked = self
2434 .pending
2435 .drain(..batch.min(self.pending.len()))
2436 .collect();
2437 match self.asked.as_slice() {
2438 [] => Ok(None),
2439 [key] => encode_management(|buf| frame::prop_get(buf, 0, *key)).map(Some),
2440 keys => encode_management(|buf| frame::prop_multi_get(buf, 0, keys)).map(Some),
2441 }
2442 }
2443
2444 fn receive(&mut self, reply: &[u8]) -> Result<(), MobileMeshError> {
2446 if let [key] = self.asked.as_slice() {
2447 let key = *key;
2448 match umsh_ulcp::reply::property(key, reply) {
2449 Ok(answer) => self.answers.push(answer_record(key, answer)),
2450 Err(_) => return Err(MobileMeshError::InvalidRequest),
2451 }
2452 return Ok(());
2453 }
2454
2455 let asked = core::mem::take(&mut self.asked);
2456 let Ok(entries) = umsh_ulcp::reply::entries(&asked, reply) else {
2457 self.multi = false;
2462 for key in asked.into_iter().rev() {
2463 self.pending.push_front(key);
2464 }
2465 return Ok(());
2466 };
2467 let mut answered = 0usize;
2468 for (key, answer) in entries.flatten() {
2469 answered += 1;
2470 self.answers.push(answer_record(key, answer));
2471 }
2472 if answered == 0 {
2477 self.multi = false;
2478 }
2479 for key in asked.into_iter().skip(answered).rev() {
2480 self.pending.push_front(key);
2481 }
2482 Ok(())
2483 }
2484}
2485
2486enum ManagementPlan {
2489 One(ReplyShape),
2490 Fetch(FetchCrawl),
2491}
2492
2493struct ManagementJob<M: MacBackend> {
2495 operation_id: u64,
2496 peer: PublicKey,
2497 manager: umsh_node_mgmt::NodeManager<M>,
2498 plan: ManagementPlan,
2499 reported_remaining: Option<u32>,
2502}
2503
2504impl<M: MacBackend> ManagementJob<M> {
2505 fn event(&self, outcome: MobileMeshManagementOutcome) -> MobileMeshManagementEventRecord {
2506 MobileMeshManagementEventRecord {
2507 operation_id: self.operation_id,
2508 peer_address: encode_peer_address(&self.peer),
2509 outcome,
2510 answers: Vec::new(),
2511 status_code: None,
2512 remaining_octets: self.manager.remaining(),
2513 properties_remaining: match &self.plan {
2514 ManagementPlan::Fetch(crawl) => Some(crawl.pending.len() as u32),
2515 ManagementPlan::One(_) => None,
2516 },
2517 }
2518 }
2519
2520 fn progress(&mut self) -> Option<MobileMeshManagementEventRecord> {
2523 let remaining = self.manager.remaining();
2524 if remaining.is_none() || remaining == self.reported_remaining {
2525 return None;
2526 }
2527 self.reported_remaining = remaining;
2528 Some(self.event(MobileMeshManagementOutcome::Progress))
2529 }
2530
2531 fn settle(
2534 &mut self,
2535 outcome: umsh_node_mgmt::Outcome,
2536 now_ms: u64,
2537 ) -> Option<MobileMeshManagementEventRecord> {
2538 match outcome {
2539 umsh_node_mgmt::Outcome::Failed(umsh_node_mgmt::Failure::TimedOut) => {
2540 return Some(self.event(MobileMeshManagementOutcome::TimedOut));
2541 }
2542 umsh_node_mgmt::Outcome::Failed(_) => {
2543 return Some(self.event(MobileMeshManagementOutcome::Failed));
2544 }
2545 umsh_node_mgmt::Outcome::NoResponse => {
2546 return Some(match self.plan {
2547 ManagementPlan::One(ReplyShape::Acknowledgment) => {
2549 self.event(MobileMeshManagementOutcome::Acknowledged)
2550 }
2551 _ => self.event(MobileMeshManagementOutcome::Failed),
2552 });
2553 }
2554 umsh_node_mgmt::Outcome::Replied { .. } => {}
2557 }
2558
2559 match &mut self.plan {
2560 ManagementPlan::One(shape) => {
2561 let shape = shape.clone();
2562 Some(self.replied(&shape))
2563 }
2564 ManagementPlan::Fetch(crawl) => {
2565 if crawl.receive(self.manager.reply()).is_err() {
2566 return Some(self.event(MobileMeshManagementOutcome::Failed));
2567 }
2568 match crawl.next_request() {
2569 Ok(Some(request)) => match self.manager.begin(&request, now_ms) {
2570 Ok(()) => {
2571 self.reported_remaining = None;
2572 None
2573 }
2574 Err(_) => Some(self.event(MobileMeshManagementOutcome::Failed)),
2575 },
2576 Ok(None) => Some(self.fetched()),
2577 Err(_) => Some(self.event(MobileMeshManagementOutcome::Failed)),
2578 }
2579 }
2580 }
2581 }
2582
2583 fn replied(&self, shape: &ReplyShape) -> MobileMeshManagementEventRecord {
2586 let reply = self.manager.reply();
2587 let mut event = self.event(MobileMeshManagementOutcome::Replied);
2588 match shape {
2589 ReplyShape::Property(key) => match umsh_ulcp::reply::property(*key, reply) {
2590 Ok(answer) => event.answers.push(answer_record(*key, answer)),
2591 Err(_) => return self.event(MobileMeshManagementOutcome::Failed),
2592 },
2593 ReplyShape::Entries(keys) => match umsh_ulcp::reply::entries(keys, reply) {
2594 Ok(entries) => {
2595 for (key, answer) in entries.flatten() {
2596 event.answers.push(answer_record(key, answer));
2597 }
2598 }
2599 Err(_) => event.status_code = umsh_ulcp::reply::status_of(reply).map(|s| s.0),
2603 },
2604 ReplyShape::Status | ReplyShape::Acknowledgment => {
2607 match umsh_ulcp::reply::status_of(reply) {
2608 Some(status) => event.status_code = Some(status.0),
2609 None => return self.event(MobileMeshManagementOutcome::Failed),
2610 }
2611 }
2612 }
2613 event
2614 }
2615
2616 fn fetched(&mut self) -> MobileMeshManagementEventRecord {
2622 let ManagementPlan::Fetch(crawl) = &mut self.plan else {
2623 return self.event(MobileMeshManagementOutcome::Failed);
2624 };
2625 let answers = core::mem::take(&mut crawl.answers);
2626 let mut event = self.event(MobileMeshManagementOutcome::Replied);
2627 event.answers = answers;
2628 event
2629 }
2630}
2631
2632#[uniffi::export]
2641pub fn ulcp_records_from_answers(
2642 answers: Vec<MobileMeshManagementAnswerRecord>,
2643) -> Vec<UlcpPropertyFrameRecord> {
2644 answers
2645 .into_iter()
2646 .filter_map(|answer| {
2647 Some(crate::ulcp::ulcp_property_record(
2648 answer.property_id,
2649 answer.value?,
2650 ))
2651 })
2652 .collect()
2653}
2654
2655fn answer_record(
2656 property_id: u32,
2657 answer: umsh_ulcp::reply::Answer<'_>,
2658) -> MobileMeshManagementAnswerRecord {
2659 MobileMeshManagementAnswerRecord {
2660 property_id,
2661 value: answer.value().map(<[u8]>::to_vec),
2662 status_code: answer.status().map(|status| status.0),
2663 }
2664}
2665
2666fn emit_management_failure(
2668 events: &NotifyingSender<MobileMeshManagementEventRecord>,
2669 operation_id: u64,
2670 peer: &PublicKey,
2671) {
2672 let _ = events.send(MobileMeshManagementEventRecord {
2673 operation_id,
2674 peer_address: encode_peer_address(peer),
2675 outcome: MobileMeshManagementOutcome::Failed,
2676 answers: Vec::new(),
2677 status_code: None,
2678 remaining_octets: None,
2679 properties_remaining: None,
2680 });
2681}
2682
2683async fn start_management<M: MacBackend>(
2685 node: &LocalNode<M>,
2686 operation_id: u64,
2687 peer: PublicKey,
2688 request: ManagementRequest,
2689 now_ms: u64,
2690 token_seed: u16,
2691) -> Option<ManagementJob<M>> {
2692 let connection = node.peer(peer).await.ok()?;
2693 let mut manager = umsh_node_mgmt::NodeManager::new(connection, token_seed);
2694 *manager.send_options_mut() = SendOptions::default()
2699 .with_ack_requested(true)
2700 .with_flood_hops(MANAGEMENT_FLOOD_HOPS)
2701 .with_trace_route();
2702 let (plan, request) = match request {
2703 ManagementRequest::One { frame, shape } => (ManagementPlan::One(shape), frame),
2704 ManagementRequest::Fetch {
2705 property_ids,
2706 multi_hint,
2707 } => {
2708 let mut crawl = FetchCrawl::new(property_ids, multi_hint);
2709 let request = crawl.next_request().ok()??;
2710 (ManagementPlan::Fetch(crawl), request)
2711 }
2712 };
2713 manager.begin(&request, now_ms).ok()?;
2714 Some(ManagementJob {
2715 operation_id,
2716 peer,
2717 manager,
2718 plan,
2719 reported_remaining: None,
2720 })
2721}
2722
2723async fn service_management<M: MacBackend>(
2726 job: &mut Option<ManagementJob<M>>,
2727 now_ms: u64,
2728 events: &NotifyingSender<MobileMeshManagementEventRecord>,
2729 token: &mut u16,
2730) {
2731 let Some(active) = job.as_mut() else {
2732 return;
2733 };
2734 loop {
2735 let progress = active.manager.service(now_ms).await;
2736 *token = active.manager.counter();
2741 match progress {
2742 Err(_) => {
2743 let _ = events.send(active.event(MobileMeshManagementOutcome::Failed));
2744 *job = None;
2745 return;
2746 }
2747 Ok(umsh_node_mgmt::Progress::Waiting { .. }) => {
2748 if let Some(progress) = active.progress() {
2749 let _ = events.send(progress);
2750 }
2751 return;
2752 }
2753 Ok(umsh_node_mgmt::Progress::Done(outcome)) => match active.settle(outcome, now_ms) {
2754 Some(event) => {
2755 let _ = events.send(event);
2756 *job = None;
2757 return;
2758 }
2759 None => {
2760 let _ = events.send(active.event(MobileMeshManagementOutcome::Progress));
2764 continue;
2765 }
2766 },
2767 }
2768 }
2769}
2770
2771async fn run_worker(
2772 identity: SoftwareIdentity,
2773 counter_store: SharedCounterStore,
2774 mut commands: mpsc::UnboundedReceiver<WorkerCommand>,
2775 outbound: NotifyingSender<MobileMeshOutboundFrameRecord>,
2776 transmit_completions: Arc<BridgeTransmitCompletions>,
2777 events: NotifyingSender<MobileMeshPingEventRecord>,
2778 management_events: NotifyingSender<MobileMeshManagementEventRecord>,
2779 advertisements: NotifyingSender<MobileMeshAdvertisementRecord>,
2780 peer_heard: NotifyingSender<MobileMeshPeerHeardRecord>,
2781 chat_events: NotifyingSender<MobileChatWorkerEvent>,
2782 ready: oneshot::Sender<Result<(), MobileMeshError>>,
2783) {
2784 let local_key = *identity.public_key();
2785 let signer = identity.clone();
2788 let (inbound_tx, inbound_rx) = mpsc::unbounded_channel();
2789 let worker_completions = transmit_completions.clone();
2790 let radio = BridgeRadio {
2791 inbound: inbound_rx,
2792 outbound,
2793 completions: transmit_completions,
2794 };
2795 let mac = MobileMac::new(
2796 radio,
2797 CryptoEngine::new(SoftwareAes, SoftwareSha256),
2798 MobileClock::new(),
2799 rand::rng(),
2800 counter_store,
2801 RepeaterConfig::default(),
2802 OperatingPolicy::default(),
2803 );
2804 let cell = AsyncRefCell::new(mac);
2805 let handle = MacHandle::new(&cell);
2806 let identity_id = match handle.add_identity(identity).await {
2807 Ok(id) => id,
2808 Err(_) => {
2809 let _ = ready.send(Err(MobileMeshError::SessionUnavailable));
2810 return;
2811 }
2812 };
2813 if handle.load_persisted_counter(identity_id).await.is_err() {
2814 let _ = ready.send(Err(MobileMeshError::CounterPersistenceFailed));
2815 return;
2816 }
2817 handle.set_auto_register_full_key_peers(true).await;
2824
2825 let mut host = Host::new(handle);
2826 let node = host.add_node(identity_id);
2827 let mut discoverable = true;
2834 let mut responder_name: Option<String> = None;
2835 let mut advertised_location: Option<NodeLocation> = None;
2836 node.enable_identity_responder_default(phone_identity_profile(
2837 local_key,
2838 responder_name.as_deref(),
2839 advertised_location,
2840 ));
2841 let channel_registry = Rc::new(RefCell::new(ChannelRegistry::default()));
2844 let mut chat = MobileChatState::new(local_key, channel_registry.clone());
2845 let peer_heard_events = peer_heard.clone();
2849 let peer_heard_subscription = node.on_receive(move |packet| {
2850 let _ = peer_heard_events.send(MobileMeshPeerHeardRecord {
2851 peer_address: packet.from_key().map(|peer| encode_peer_address(&peer)),
2852 node_hint: packet.from_hint().map(|hint| hint.0.to_vec()),
2853 source_authenticated: packet.source_authenticated(),
2854 });
2855 false
2856 });
2857 let inbound_text = Rc::new(RefCell::new(Vec::<InboundText>::new()));
2858 let inbound_text_callback = inbound_text.clone();
2859 let text_channels = channel_registry.clone();
2860 let echo_events = chat_events.clone();
2861 let text_subscription = node.on_receive(move |packet| {
2862 if packet.payload_type() != PayloadType::TextMessage {
2863 return false;
2864 }
2865 if packet.packet_family() == PacketFamily::Multicast && packet.from_key() == Some(local_key)
2882 {
2883 let distance = match packet.hop_count() {
2884 Some(hops) => format!("after {hops} hop(s)"),
2885 None => "over a source route".to_string(),
2886 };
2887 let _ = echo_events.send(MobileChatWorkerEvent::Diagnostic(format!(
2888 "own multicast relayed back {distance}"
2889 )));
2890 return true;
2891 }
2892 let channel_tag = packet.channel().map(|channel| {
2897 (
2898 crate::channel_tag(channel.key()),
2899 text_channels
2900 .borrow()
2901 .contains(&crate::channel_tag(channel.key())),
2902 )
2903 });
2904 if let Some((tag, _)) = channel_tag
2915 && tag == crate::emergency_channel_tag()
2916 && (packet.encrypted() || packet.from_key().is_none())
2917 {
2918 let reason = if packet.encrypted() {
2919 "encrypted"
2920 } else {
2921 "missing its full source key"
2922 };
2923 let _ = echo_events.send(MobileChatWorkerEvent::Diagnostic(format!(
2924 "dropped an emergency-channel text frame: {reason}"
2925 )));
2926 return false;
2927 }
2928 let source = match (packet.packet_family(), channel_tag) {
2929 (PacketFamily::Unicast, _) => match packet.from_key() {
2930 Some(peer) => InboundTextSource::Direct { peer },
2931 None => return false,
2932 },
2933 (PacketFamily::Multicast, Some((channel, true))) => match packet.from_hint() {
2936 Some(hint) => InboundTextSource::ChannelGroup {
2937 channel,
2938 hint,
2939 full_key: packet.from_key(),
2940 },
2941 None => return false,
2942 },
2943 (PacketFamily::BlindUnicast, Some((channel, true))) => match packet.from_key() {
2946 Some(peer) => InboundTextSource::ChannelDirect { channel, peer },
2947 None => return false,
2948 },
2949 _ => return false,
2950 };
2951 inbound_text_callback.borrow_mut().push(InboundText {
2952 source,
2953 payload: packet.payload().to_vec(),
2954 received_at_ms: packet.received_at_ms(),
2955 rx: MobileChatRxMetadataRecord {
2956 rssi_dbm: packet.rssi(),
2957 snr_centibels: packet.snr().map(|snr| snr.as_centibels()),
2958 lqi: packet.lqi().map(|lqi| lqi.get()),
2959 hop_count: packet.hop_count(),
2960 route_hints: packet
2961 .trace_route_hops()
2962 .map(|hop| hop.0.to_vec())
2963 .collect(),
2964 source_authenticated: packet.source_authenticated(),
2965 },
2966 });
2967 true
2968 });
2969 let advertisement_events = advertisements.clone();
2970 let advertisement_subscription = node.on_receive(move |packet| {
2971 if packet.payload_type() != PayloadType::NodeIdentity {
2972 return false;
2973 }
2974 let Some(peer) = packet.from_key() else {
2977 return false;
2978 };
2979 let _ = advertisement_events.send(MobileMeshAdvertisementRecord {
2980 peer_address: encode_peer_address(&peer),
2981 payload: packet.payload().to_vec(),
2982 source_authenticated: packet.source_authenticated(),
2983 });
2984 true
2985 });
2986 let mut management: Option<ManagementJob<_>> = None;
2989 let mut management_token: u16 = rand::random();
2997 let mut in_flight_chat = Vec::<InFlightChatTransmission>::new();
2998 let mut member_routes = BTreeMap::<(ChannelTag, [u8; 3]), MemberRoute>::new();
3001 let mut chat_pipeline_ready = BTreeSet::<[u8; 32]>::new();
3002 let mut pending_chat_transmissions = VecDeque::<umsh_text::engine::Transmission>::new();
3003 let pending = Rc::new(RefCell::new(BTreeMap::<[u8; 32], u64>::new()));
3004 let pong_pending = pending.clone();
3005 let pong_events = events.clone();
3006 let pong_subscription = node.on_pong_with_metadata(move |peer, metadata| {
3007 if let Some(operation_id) = pong_pending.borrow_mut().remove(&peer.0) {
3008 let _ = pong_events.send(MobileMeshPingEventRecord {
3009 operation_id,
3010 outcome: MobileMeshPingOutcome::Reply,
3011 round_trip_milliseconds: Some(metadata.round_trip_ms),
3012 hop_count: metadata.hop_count,
3013 route_hints: metadata
3014 .route_hints
3015 .iter()
3016 .map(|hint| hint.0.to_vec())
3017 .collect(),
3018 rssi_dbm: metadata.rssi_dbm,
3019 snr_centibels: metadata.snr_centibels,
3020 lqi: metadata.lqi,
3021 });
3022 }
3023 });
3024 let timeout_pending = pending.clone();
3025 let timeout_events = events.clone();
3026 let timeout_subscription = node.on_ping_timeout(move |peer| {
3027 if let Some(operation_id) = timeout_pending.borrow_mut().remove(&peer.0) {
3028 let _ = timeout_events.send(MobileMeshPingEventRecord {
3029 operation_id,
3030 outcome: MobileMeshPingOutcome::TimedOut,
3031 round_trip_milliseconds: None,
3032 hop_count: None,
3033 route_hints: Vec::new(),
3034 rssi_dbm: None,
3035 snr_centibels: None,
3036 lqi: None,
3037 });
3038 }
3039 });
3040 let _subscriptions = (
3041 pong_subscription,
3042 timeout_subscription,
3043 peer_heard_subscription,
3044 text_subscription,
3045 advertisement_subscription,
3046 );
3047 let _ = ready.send(Ok(()));
3048 let mut protocol_timeout_tick = tokio::time::interval(Duration::from_millis(50));
3049 protocol_timeout_tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
3050
3051 let inbound_ready = tokio::sync::Notify::new();
3061 let timeout_servicer = host.protocol_timeout_servicer();
3062
3063 let pump_loop = async {
3064 loop {
3065 if host.pump_once().await.is_err() {
3066 return;
3067 }
3068 if !inbound_text.borrow().is_empty() {
3069 inbound_ready.notify_one();
3070 }
3071 }
3072 };
3073
3074 let command_loop = async {
3075 loop {
3076 tokio::select! {
3077 biased;
3078 command = commands.recv() => {
3079 match command {
3080 Some(WorkerCommand::RegisterPeers { peers, response }) => {
3081 let mut result = Ok(());
3082 for peer in peers {
3083 if node.peer(peer).await.is_err() {
3084 result = Err(MobileMeshError::SendFailed);
3085 break;
3086 }
3087 }
3088 let _ = response.send(result);
3089 }
3090 Some(WorkerCommand::RemovePeers { peers, response }) => {
3091 for peer in peers {
3092 let _ = node.remove_peer(&peer).await;
3095 }
3096 let _ = response.send(Ok(()));
3097 }
3098 Some(WorkerCommand::RegisterChannels { keys, response }) => {
3099 let mut result = Ok(());
3100 for key in keys {
3101 if node.join(&umsh_node::Channel::private(key, "")).await.is_err() {
3105 result = Err(MobileMeshError::ChannelCapacity);
3106 break;
3107 }
3108 channel_registry
3109 .borrow_mut()
3110 .register(crate::channel_tag(&key), key);
3111 }
3112 let _ = response.send(result);
3113 }
3114 Some(WorkerCommand::RemoveChannels { keys, response }) => {
3115 for key in keys {
3116 let _ = node.leave(&umsh_node::Channel::private(key, "")).await;
3118 channel_registry
3119 .borrow_mut()
3120 .remove(&crate::channel_tag(&key));
3121 }
3122 let _ = response.send(Ok(()));
3123 }
3124 Some(WorkerCommand::Ping { operation_id, peer, timeout_ms }) => {
3125 if pending.borrow().contains_key(&peer.0) {
3126 emit_ping_failure(&events, operation_id);
3127 continue;
3128 }
3129 let result = match node.peer(peer).await {
3130 Ok(connection) => connection
3131 .ping(
3132 6,
3133 &SendOptions::default()
3139 .with_flood_hops(5)
3140 .with_trace_route()
3141 .with_trace_signal()
3142 .with_mic_size(umsh_node::PING_MIC_SIZE),
3143 timeout_ms,
3144 )
3145 .await
3146 .map(|_| ())
3147 .map_err(|_| MobileMeshError::SendFailed),
3148 Err(_) => Err(MobileMeshError::SendFailed),
3149 };
3150 if result.is_ok() {
3151 pending.borrow_mut().insert(peer.0, operation_id);
3152 if handle.service_counter_persistence().await.is_err() {
3156 emit_ping_failure(&events, operation_id);
3157 return;
3158 }
3159 }
3160 if result.is_err() {
3161 emit_ping_failure(&events, operation_id);
3162 }
3163 }
3164 Some(WorkerCommand::Manage { operation_id, peer, request }) => {
3165 if management.is_some() {
3166 emit_management_failure(&management_events, operation_id, &peer);
3167 continue;
3168 }
3169 let now_ms = handle.now_ms().await;
3170 management = start_management(
3171 &node,
3172 operation_id,
3173 peer,
3174 request,
3175 now_ms,
3176 management_token,
3177 )
3178 .await;
3179 if management.is_none() {
3180 emit_management_failure(&management_events, operation_id, &peer);
3181 continue;
3182 }
3183 service_management(
3184 &mut management,
3185 now_ms,
3186 &management_events,
3187 &mut management_token,
3188 )
3189 .await;
3190 if handle.service_counter_persistence().await.is_err() {
3191 return;
3192 }
3193 }
3194 Some(WorkerCommand::Advertise { name, timestamp, scheduled, response }) => {
3195 let result = match build_signed_identity_bundle(
3196 &signer,
3197 name.as_deref(),
3198 timestamp,
3199 advertised_location,
3200 )
3201 .await
3202 {
3203 Ok(bundle) => {
3204 let mut frame = Vec::with_capacity(bundle.len() + 1);
3205 frame.push(PayloadType::NodeIdentity as u8);
3206 frame.extend_from_slice(&bundle);
3207 let options = SendOptions::default().with_full_source();
3210 let options = if scheduled {
3211 options.no_flood()
3215 } else {
3216 options.with_trace_route().with_trace_signal()
3222 };
3223 node.send_all(&frame, &options)
3224 .await
3225 .map(|_| ())
3226 .map_err(|_| MobileMeshError::SendFailed)
3227 }
3228 Err(error) => Err(error),
3229 };
3230 if result.is_ok()
3231 && handle.service_counter_persistence().await.is_err()
3232 {
3233 let _ = response.send(Err(MobileMeshError::SendFailed));
3234 return;
3235 }
3236 let _ = response.send(result);
3237 }
3238 Some(WorkerCommand::Beacon { response }) => {
3239 let result = node
3242 .send_all(
3243 &[],
3244 &SendOptions::default()
3245 .with_flood_hops(BEACON_FLOOD_HOPS)
3246 .with_trace_route()
3247 .with_trace_signal(),
3248 )
3249 .await
3250 .map(|_| ())
3251 .map_err(|_| MobileMeshError::SendFailed);
3252 if result.is_ok()
3253 && handle.service_counter_persistence().await.is_err()
3254 {
3255 let _ = response.send(Err(MobileMeshError::SendFailed));
3256 return;
3257 }
3258 let _ = response.send(result);
3259 }
3260 Some(WorkerCommand::SignIdentityBundle { name, timestamp, response }) => {
3261 let result = build_signed_identity_bundle(
3266 &signer,
3267 name.as_deref(),
3268 timestamp,
3269 None,
3270 )
3271 .await;
3272 let _ = response.send(result);
3273 }
3274 Some(WorkerCommand::RequestIdentity { peer, response }) => {
3275 let result = match node.peer(peer).await {
3276 Ok(connection) => connection
3277 .request_identity(
3278 &SendOptions::default()
3279 .with_flood_hops(5)
3280 .with_ack_requested(false),
3281 )
3282 .await
3283 .map(|_| ())
3284 .map_err(|_| MobileMeshError::SendFailed),
3285 Err(_) => Err(MobileMeshError::SendFailed),
3286 };
3287 if result.is_ok()
3291 && handle.service_counter_persistence().await.is_err()
3292 {
3293 let _ = response.send(Err(MobileMeshError::SendFailed));
3294 return;
3295 }
3296 let _ = response.send(result);
3297 }
3298 Some(WorkerCommand::RequestIdentityByHint {
3299 conversation_address,
3300 hint,
3301 response,
3302 }) => {
3303 let channel = chat
3304 .parse_conversation_address(&conversation_address)
3305 .and_then(|conversation| match conversation {
3306 ConversationKey::ChannelGroup { channel } => Some(channel),
3307 _ => None,
3308 });
3309 let result = match channel {
3310 Some(channel) => {
3311 let route = member_routes.get(&(channel, hint.0)).cloned();
3312 let mut nonce_bytes = [0u8; 4];
3313 handle.fill_random(&mut nonce_bytes).await;
3314 request_identity_over_channel(
3315 &node,
3316 &channel_registry,
3317 channel,
3318 hint,
3319 u32::from_be_bytes(nonce_bytes),
3320 route,
3321 )
3322 .await
3323 }
3324 None => Err(MobileMeshError::UnknownConversation),
3325 };
3326 if result.is_ok()
3327 && handle.service_counter_persistence().await.is_err()
3328 {
3329 let _ = response.send(Err(MobileMeshError::SendFailed));
3330 return;
3331 }
3332 let _ = response.send(result);
3333 }
3334 Some(WorkerCommand::PeerRepeaters { peer, mut response }) => {
3335 let result = tokio::select! {
3336 result = collect_peer_repeaters(&node, &handle, peer) => result,
3337 _ = response.closed() => continue,
3342 };
3343 let _ = response.send(result);
3344 }
3345 Some(WorkerCommand::SetChatDisplayName { name, response }) => {
3346 chat.engine.set_local_handle(&name);
3347 let _ = response.send(());
3348 }
3349 Some(WorkerCommand::SetDiscoverable { enabled, name, response }) => {
3350 discoverable = enabled;
3351 responder_name = name;
3352 if discoverable {
3353 node.enable_identity_responder_default(phone_identity_profile(
3354 local_key,
3355 responder_name.as_deref(),
3356 advertised_location,
3357 ));
3358 } else {
3359 node.disable_identity_responder();
3360 }
3361 let _ = response.send(());
3362 }
3363 Some(WorkerCommand::SetAdvertisedLocation { location, response }) => {
3364 advertised_location = location;
3365 if discoverable {
3370 node.enable_identity_responder_default(phone_identity_profile(
3371 local_key,
3372 responder_name.as_deref(),
3373 advertised_location,
3374 ));
3375 }
3376 let _ = response.send(());
3377 }
3378 Some(WorkerCommand::DiscoverIdentities {
3379 role_code,
3380 capability_bits,
3381 node_hint,
3382 source_route,
3383 response,
3384 }) => {
3385 let result = async {
3386 let mut builder = umsh_node::mac_command::IdentityRequestBuilder::new();
3387 let mut nonce_bytes = [0u8; 4];
3388 handle.fill_random(&mut nonce_bytes).await;
3389 builder = builder
3390 .nonce(u32::from_be_bytes(nonce_bytes))
3391 .map_err(|_| MobileMeshError::SendFailed)?;
3392 if let Some(hint) = node_hint.as_deref() {
3396 builder = builder
3397 .filter_hint_prefix(hint)
3398 .map_err(|_| MobileMeshError::SendFailed)?;
3399 }
3400 if let Some(role) = role_code {
3401 builder = builder
3402 .filter_role(NodeRole::from_byte(role))
3403 .map_err(|_| MobileMeshError::SendFailed)?;
3404 }
3405 let unfiltered = role_code.is_none() && node_hint.is_none();
3411 let capability_bits =
3412 capability_bits.or(if unfiltered { Some(0) } else { None });
3413 if let Some(bits) = capability_bits {
3414 builder = builder
3415 .filter_caps(NodeCapabilities::from_bits_truncate(bits))
3416 .map_err(|_| MobileMeshError::SendFailed)?;
3417 }
3418 let options_block = builder.build();
3419 let cmd = umsh_node::MacCommand::IdentityRequest {
3420 options: &options_block,
3421 };
3422 let mut frame = [0u8; 128];
3423 frame[0] = PayloadType::MacCommand as u8;
3424 let length = umsh_node::mac_command::encode(&cmd, &mut frame[1..])
3425 .map_err(|_| MobileMeshError::SendFailed)?
3426 + 1;
3427 let mut options = SendOptions::default().with_full_source();
3429 if !source_route.is_empty() {
3430 let hops = source_route
3431 .iter()
3432 .map(|hint| {
3433 <[u8; 2]>::try_from(hint.as_slice())
3434 .map(umsh_core::RouterHint)
3435 .map_err(|_| MobileMeshError::SendFailed)
3436 })
3437 .collect::<Result<Vec<_>, _>>()?;
3438 options = options
3447 .try_with_source_route(&hops)
3448 .map_err(|_| MobileMeshError::SendFailed)?
3449 .with_trace_route();
3455 }
3456 let options = options.no_flood();
3463 node.send_all(&frame[..length], &options)
3464 .await
3465 .map(|_| ())
3466 .map_err(|_| MobileMeshError::SendFailed)
3467 }
3468 .await;
3469 if result.is_ok()
3470 && handle.service_counter_persistence().await.is_err()
3471 {
3472 let _ = response.send(Err(MobileMeshError::SendFailed));
3473 return;
3474 }
3475 let _ = response.send(result);
3476 }
3477 Some(WorkerCommand::PeerRoute { peer, response }) => {
3478 let _ = response.send(node.peer_route(&peer).await.into());
3479 }
3480 Some(WorkerCommand::ClearPeerRoute { peer, response }) => {
3481 let _ = response.send(node.clear_peer_route(&peer).await);
3482 }
3483 Some(WorkerCommand::RestoreChat { checkpoints, response }) => {
3484 chat.restore(&checkpoints, handle.now_ms().await);
3485 let _ = response.send(());
3486 }
3487 Some(WorkerCommand::ComposeChat {
3488 conversation_address,
3489 client_token,
3490 request,
3491 response,
3492 }) => {
3493 let conversation =
3497 chat.parse_conversation_address(&conversation_address);
3498 let result = if !chat.pending_batches.is_empty() {
3499 Err(MobileMeshError::OperationInProgress)
3500 } else if let Some(conversation) = conversation {
3501 let now_ms = handle.now_ms().await;
3502 let composed = match &request {
3503 ChatComposeRequest::Text { body } => {
3504 chat.compose_text(conversation, client_token, body, now_ms)
3505 }
3506 ChatComposeRequest::Edit { original, body } => chat.compose_edit(
3507 conversation,
3508 client_token,
3509 original,
3510 body,
3511 now_ms,
3512 ),
3513 ChatComposeRequest::Delete { original } => chat.compose_delete(
3514 conversation,
3515 client_token,
3516 original,
3517 now_ms,
3518 ),
3519 ChatComposeRequest::Reaction { target, body } => chat
3520 .compose_reaction(
3521 conversation,
3522 client_token,
3523 target,
3524 body,
3525 now_ms,
3526 ),
3527 };
3528 match composed {
3529 Ok(composed) => {
3530 for delivery in composed.deliveries {
3531 let _ = chat_events.send(
3532 MobileChatWorkerEvent::Delivery(delivery),
3533 );
3534 }
3535 for diagnostic in composed.diagnostics {
3536 let _ = chat_events.send(
3537 MobileChatWorkerEvent::Diagnostic(diagnostic),
3538 );
3539 }
3540 Ok(composed.record)
3541 }
3542 Err(()) => Err(MobileMeshError::ChatComposeFailed),
3543 }
3544 } else {
3545 Err(MobileMeshError::UnknownConversation)
3549 };
3550 let _ = response.send(result);
3551 }
3552 Some(WorkerCommand::CommitChatBatch { batch_id, response }) => {
3553 let result = match chat.pending_batches.remove(&batch_id) {
3554 Some(batch) => {
3555 let now_ms = handle.now_ms().await;
3556 let sent = queue_chat_transmissions(
3557 &node,
3558 batch.transmissions,
3559 &mut pending_chat_transmissions,
3560 &mut in_flight_chat,
3561 &chat_pipeline_ready,
3562 &channel_registry,
3563 &mut chat,
3564 now_ms,
3565 )
3566 .await;
3567 publish_chat_drain(chat.drain(), &chat_events);
3568 if sent > 0
3569 && handle.service_counter_persistence().await.is_err()
3570 {
3571 Err(MobileMeshError::CounterPersistenceFailed)
3572 } else {
3573 Ok(())
3574 }
3575 }
3576 None => Err(MobileMeshError::ChatBatchMissing),
3577 };
3578 let fatal = result == Err(MobileMeshError::CounterPersistenceFailed);
3579 let _ = response.send(result);
3580 if fatal {
3581 return;
3582 }
3583 }
3584 Some(WorkerCommand::RejectChatBatch {
3585 batch_id,
3586 checkpoints,
3587 response,
3588 }) => {
3589 let result = match chat.pending_batches.remove(&batch_id) {
3590 Some(batch) => {
3591 for transmission in batch.transmissions {
3592 chat.engine.transmit_update(
3593 transmission.transmission_id,
3594 DeliveryState::Failed,
3595 handle.now_ms().await,
3596 );
3597 }
3598 publish_chat_drain(chat.drain(), &chat_events);
3599 chat = MobileChatState::new(
3600 local_key,
3601 channel_registry.clone(),
3602 );
3603 for diagnostic in
3604 chat.restore(&checkpoints, handle.now_ms().await)
3605 {
3606 let _ = chat_events.send(
3607 MobileChatWorkerEvent::Diagnostic(diagnostic),
3608 );
3609 }
3610 Ok(())
3611 }
3612 None => Err(MobileMeshError::ChatBatchMissing),
3613 };
3614 let _ = response.send(result);
3615 }
3616 Some(WorkerCommand::ChatArchiveResult {
3617 request_id,
3618 kind,
3619 payload,
3620 }) => {
3621 let now_ms = handle.now_ms().await;
3622 match kind {
3623 MobileChatArchiveResultKind::Found => chat.engine.archive_result(
3624 request_id,
3625 ArchiveResult::Found { payload: &payload },
3626 now_ms,
3627 ),
3628 MobileChatArchiveResultKind::Deleted => chat.engine.archive_result(
3629 request_id,
3630 ArchiveResult::Deleted,
3631 now_ms,
3632 ),
3633 MobileChatArchiveResultKind::Evicted => chat.engine.archive_result(
3634 request_id,
3635 ArchiveResult::Evicted,
3636 now_ms,
3637 ),
3638 MobileChatArchiveResultKind::Unknown => chat.engine.archive_result(
3639 request_id,
3640 ArchiveResult::Unknown,
3641 now_ms,
3642 ),
3643 }
3644 let drain = chat.drain();
3645 let transmissions = drain.transmissions.clone();
3646 publish_chat_drain(drain, &chat_events);
3647 if !transmissions.is_empty() || !pending_chat_transmissions.is_empty() {
3648 let sent = queue_chat_transmissions(
3649 &node,
3650 transmissions,
3651 &mut pending_chat_transmissions,
3652 &mut in_flight_chat,
3653 &chat_pipeline_ready,
3654 &channel_registry,
3655 &mut chat,
3656 now_ms,
3657 )
3658 .await;
3659 publish_chat_drain(chat.drain(), &chat_events);
3660 if sent > 0 && handle.service_counter_persistence().await.is_err() {
3661 return;
3662 }
3663 }
3664 }
3665 Some(WorkerCommand::FailOutboundTransmissions) => {
3666 let now_ms = handle.now_ms().await;
3667 for transmission in pending_chat_transmissions.drain(..) {
3668 chat.engine.transmit_update(
3669 transmission.transmission_id,
3670 DeliveryState::Failed,
3671 now_ms,
3672 );
3673 }
3674 for transmission in in_flight_chat.drain(..) {
3675 if let Some(receipt) = transmission.ticket.receipt() {
3676 let _ = handle.cancel_pending_ack(identity_id, receipt).await;
3677 }
3678 chat.engine.transmit_update(
3679 transmission.transmission_id,
3680 DeliveryState::Failed,
3681 now_ms,
3682 );
3683 }
3684 publish_chat_drain(chat.drain(), &chat_events);
3685 worker_completions.clear_poison();
3688 }
3689 Some(WorkerCommand::Receive(record)) => {
3690 let _ = inbound_tx.send(InboundFrame { record });
3691 }
3692 Some(WorkerCommand::Shutdown) | None => return,
3693 }
3694 }
3695 _ = inbound_ready.notified() => {
3696 let received = inbound_text.borrow_mut().drain(..).collect::<Vec<_>>();
3697 for text in received {
3698 let received_at_ms = match text.received_at_ms {
3699 Some(value) => value,
3700 None => handle.now_ms().await,
3701 };
3702 let (envelope, sender_full_key) = match text.source {
3708 InboundTextSource::Direct { peer } => (
3709 Envelope {
3710 path: DeliveryPath::Unicast,
3711 conversation: ConversationKey::Direct { peer },
3712 sender: SenderScope::Peer(peer),
3713 },
3714 Some(peer),
3715 ),
3716 InboundTextSource::ChannelGroup {
3717 channel,
3718 hint,
3719 full_key,
3720 } => {
3721 if let Some(peer) = full_key {
3722 if let Some(resolution) =
3723 chat.resolve_member(channel, hint, peer)
3724 {
3725 let _ = chat_events.send(
3726 MobileChatWorkerEvent::SenderResolution(resolution),
3727 );
3728 }
3729 }
3730 remember_member_route(
3731 &mut member_routes,
3732 channel,
3733 hint,
3734 &text.rx,
3735 );
3736 (
3737 Envelope {
3738 path: DeliveryPath::Multicast,
3739 conversation: ConversationKey::ChannelGroup { channel },
3740 sender: SenderScope::ClaimedMember(hint),
3741 },
3742 full_key,
3743 )
3744 }
3745 InboundTextSource::ChannelDirect { channel, peer } => (
3746 Envelope {
3747 path: DeliveryPath::BlindUnicast,
3748 conversation: ConversationKey::ChannelDirect { channel, peer },
3749 sender: SenderScope::Peer(peer),
3750 },
3751 Some(peer),
3752 ),
3753 };
3754 let _ = chat.engine.receive(
3755 &envelope,
3756 sender_full_key,
3757 &text.payload,
3758 received_at_ms,
3759 );
3760 let mut drain = chat.drain();
3761 attach_rx_metadata(&mut drain.mutations, &text.rx);
3764 let transmissions = drain.transmissions.clone();
3765 publish_chat_drain(drain, &chat_events);
3766 if !transmissions.is_empty() || !pending_chat_transmissions.is_empty() {
3767 let sent = queue_chat_transmissions(
3768 &node,
3769 transmissions,
3770 &mut pending_chat_transmissions,
3771 &mut in_flight_chat,
3772 &chat_pipeline_ready,
3773 &channel_registry,
3774 &mut chat,
3775 received_at_ms,
3776 )
3777 .await;
3778 publish_chat_drain(chat.drain(), &chat_events);
3779 if sent > 0 && handle.service_counter_persistence().await.is_err() {
3780 return;
3781 }
3782 }
3783 }
3784 }
3785 _ = protocol_timeout_tick.tick() => {
3786 timeout_servicer.service().await;
3787 let now_ms = handle.now_ms().await;
3788 if management.is_some() {
3789 service_management(
3794 &mut management,
3795 now_ms,
3796 &management_events,
3797 &mut management_token,
3798 )
3799 .await;
3800 if handle.service_counter_persistence().await.is_err() {
3801 return;
3802 }
3803 }
3804 chat.engine.tick(now_ms);
3805 service_chat_tickets(
3806 &mut chat,
3807 &mut in_flight_chat,
3808 &mut chat_pipeline_ready,
3809 &chat_events,
3810 pending_chat_transmissions.len(),
3811 now_ms,
3812 );
3813 let drain = chat.drain();
3814 let transmissions = drain.transmissions.clone();
3815 publish_chat_drain(drain, &chat_events);
3816 if !transmissions.is_empty() || !pending_chat_transmissions.is_empty() {
3817 let sent = queue_chat_transmissions(
3818 &node,
3819 transmissions,
3820 &mut pending_chat_transmissions,
3821 &mut in_flight_chat,
3822 &chat_pipeline_ready,
3823 &channel_registry,
3824 &mut chat,
3825 now_ms,
3826 )
3827 .await;
3828 publish_chat_drain(chat.drain(), &chat_events);
3829 if sent > 0 && handle.service_counter_persistence().await.is_err() {
3830 return;
3831 }
3832 }
3833 }
3834 }
3835 }
3836 };
3837
3838 tokio::select! {
3841 _ = pump_loop => {}
3842 _ = command_loop => {}
3843 }
3844}
3845
3846async fn queue_chat_transmissions<M: MacBackend>(
3847 node: &LocalNode<M>,
3848 transmissions: Vec<umsh_text::engine::Transmission>,
3849 pending: &mut VecDeque<umsh_text::engine::Transmission>,
3850 in_flight: &mut Vec<InFlightChatTransmission>,
3851 pipeline_ready: &BTreeSet<[u8; 32]>,
3852 channels: &Rc<RefCell<ChannelRegistry>>,
3853 chat: &mut MobileChatState,
3854 now_ms: u64,
3855) -> usize {
3856 pending.extend(transmissions);
3857 if in_flight.len() >= MOBILE_CHAT_TRANSMIT_WINDOW {
3861 return 0;
3862 }
3863 let mut queued = 0;
3864 while let Some(transmission) = pending.pop_front() {
3865 let gate_peer = match transmission.destination {
3866 Destination::Peer(peer) => Some(peer),
3867 Destination::Channel(_) | Destination::ChannelPeer { .. } => None,
3870 };
3871 if let Some(peer) = gate_peer {
3872 if !pipeline_ready.contains(&peer.0)
3873 && in_flight.iter().any(|entry| entry.gate_peer == Some(peer))
3874 {
3875 pending.push_front(transmission);
3879 break;
3880 }
3881 }
3882 let mut payload = Vec::with_capacity(transmission.payload.len() + 1);
3883 payload.push(PayloadType::TextMessage as u8);
3884 payload.extend_from_slice(transmission.payload.as_slice());
3885 let sent = match transmission.destination {
3886 Destination::Peer(peer) => match node.peer(peer).await {
3887 Ok(connection) => {
3888 connection
3889 .send(&payload, &SendOptions::default().with_ack_requested(true))
3890 .await
3891 }
3892 Err(_) => {
3893 chat.engine.transmit_update(
3894 transmission.transmission_id,
3895 DeliveryState::Failed,
3896 now_ms,
3897 );
3898 continue;
3899 }
3900 },
3901 Destination::Channel(channel) => {
3902 let Some(bound) = bound_channel(node, channels, &channel) else {
3903 chat.engine.transmit_update(
3904 transmission.transmission_id,
3905 DeliveryState::Failed,
3906 now_ms,
3907 );
3908 continue;
3909 };
3910 let mut options = SendOptions::default().with_full_source();
3914 if channel == crate::emergency_channel_tag() {
3920 options = options.unencrypted();
3921 }
3922 bound.send_all(&payload, &options).await
3923 }
3924 Destination::ChannelPeer { channel, peer } => {
3925 let Some(bound) = bound_channel(node, channels, &channel) else {
3926 chat.engine.transmit_update(
3927 transmission.transmission_id,
3928 DeliveryState::Failed,
3929 now_ms,
3930 );
3931 continue;
3932 };
3933 if node.peer(peer).await.is_err() {
3940 chat.engine.transmit_update(
3941 transmission.transmission_id,
3942 DeliveryState::Failed,
3943 now_ms,
3944 );
3945 continue;
3946 }
3947 let mut options = SendOptions::default().with_full_source();
3950 if channel == crate::emergency_channel_tag() {
3958 options = options.unencrypted();
3959 }
3960 let r = bound.send(&peer, &payload, &options).await;
3961 r
3962 }
3963 };
3964 let ticket = match sent {
3965 Ok(ticket) => ticket,
3966 Err(_) => {
3967 pending.push_front(transmission);
3971 break;
3972 }
3973 };
3974 in_flight.push(InFlightChatTransmission {
3975 transmission_id: transmission.transmission_id,
3976 gate_peer,
3977 ticket,
3978 sent_reported: false,
3979 non_ack: gate_peer.is_none(),
3980 queued_at_ms: now_ms,
3981 stall_reported: false,
3982 });
3983 queued += 1;
3984 if in_flight.len() >= MOBILE_CHAT_TRANSMIT_WINDOW {
3985 break;
3986 }
3987 }
3988 queued
3989}
3990
3991async fn request_identity_over_channel<M: MacBackend>(
3998 node: &LocalNode<M>,
3999 channels: &Rc<RefCell<ChannelRegistry>>,
4000 channel: ChannelTag,
4001 hint: NodeHint,
4002 nonce: u32,
4003 route: Option<MemberRoute>,
4004) -> Result<(), MobileMeshError> {
4005 let Some(bound) = bound_channel(node, channels, &channel) else {
4006 return Err(MobileMeshError::UnknownConversation);
4007 };
4008 let options_block = umsh_node::mac_command::IdentityRequestBuilder::new()
4009 .nonce(nonce)
4010 .and_then(|builder| builder.filter_hint(&hint))
4011 .map_err(|_| MobileMeshError::SendFailed)?
4012 .build();
4013 let cmd = umsh_node::MacCommand::IdentityRequest {
4014 options: &options_block,
4015 };
4016 let mut frame = [0u8; 128];
4017 frame[0] = PayloadType::MacCommand as u8;
4018 let length = umsh_node::mac_command::encode(&cmd, &mut frame[1..])
4019 .map_err(|_| MobileMeshError::SendFailed)?
4020 + 1;
4021 let mut options = SendOptions::default().with_full_source();
4024 match route.as_ref() {
4025 Some(route) if !route.route_hints.is_empty() => {
4026 let hops = route
4027 .route_hints
4028 .iter()
4029 .filter_map(|hint| <[u8; 2]>::try_from(hint.as_slice()).ok())
4030 .map(umsh_core::RouterHint)
4031 .collect::<Vec<_>>();
4032 options = match options.try_with_source_route(&hops) {
4035 Ok(options) => options,
4036 Err(_) => SendOptions::default()
4037 .with_full_source()
4038 .with_flood_hops(flood_budget(route.hop_count)),
4039 };
4040 }
4041 Some(MemberRoute {
4042 hop_count: Some(hops),
4043 ..
4044 }) => {
4045 options = options.with_flood_hops(flood_budget(Some(*hops)));
4046 }
4047 _ => {}
4048 }
4049 bound
4050 .send_all(&frame[..length], &options)
4051 .await
4052 .map(|_| ())
4053 .map_err(|_| MobileMeshError::SendFailed)
4054}
4055
4056const PEER_REPEATERS_PAGE_TIMEOUT: Duration = Duration::from_secs(30);
4062
4063const PEER_REPEATERS_MAX_PAGES: usize = 8;
4069
4070const PEER_REPEATERS_WALK_TIMEOUT: Duration = Duration::from_secs(60);
4077
4078async fn collect_peer_repeaters<M: MacBackend>(
4084 node: &LocalNode<M>,
4085 handle: &M,
4086 peer: PublicKey,
4087) -> Result<Vec<MobileMeshPeerRepeaterRecord>, MobileMeshError> {
4088 let connection = node
4089 .peer(peer)
4090 .await
4091 .map_err(|_| MobileMeshError::InvalidPeer)?;
4092
4093 let pages: Rc<RefCell<Vec<Vec<u8>>>> = Rc::new(RefCell::new(Vec::new()));
4094 let _subscription = {
4095 let pages = pages.clone();
4096 node.on_mac_command(move |from, command| {
4097 if from != peer {
4098 return;
4099 }
4100 if let umsh_node::OwnedMacCommand::PeerRepeatersResponse { body } = command {
4101 pages.borrow_mut().push(body.clone());
4102 }
4103 })
4104 };
4105
4106 let mut listing = Vec::new();
4107 let mut cursor: Option<Vec<u8>> = None;
4108 let walk_deadline = tokio::time::Instant::now() + PEER_REPEATERS_WALK_TIMEOUT;
4109 for _ in 0..PEER_REPEATERS_MAX_PAGES {
4110 let mut nonce_bytes = [0u8; 2];
4111 handle.fill_random(&mut nonce_bytes).await;
4112 let nonce = u16::from_be_bytes(nonce_bytes);
4113 pages.borrow_mut().clear();
4114 let sent = connection
4115 .request_peer_repeaters(nonce, cursor.as_deref(), &SendOptions::default())
4116 .await;
4117 if sent.is_err() {
4118 if listing.is_empty() {
4119 return Err(MobileMeshError::SendFailed);
4120 }
4121 break;
4124 }
4125
4126 let deadline =
4127 (tokio::time::Instant::now() + PEER_REPEATERS_PAGE_TIMEOUT).min(walk_deadline);
4128 let page = loop {
4129 let matched = pages.borrow_mut().iter().position(|body| {
4130 umsh_node::mac_command::PeerRepeatersResponseView::new(body).nonce() == Some(nonce)
4131 });
4132 if let Some(index) = matched {
4133 break Some(pages.borrow_mut().remove(index));
4134 }
4135 if tokio::time::Instant::now() >= deadline {
4136 break None;
4137 }
4138 tokio::time::sleep(Duration::from_millis(20)).await;
4141 };
4142 let Some(page) = page else {
4143 break;
4146 };
4147
4148 let view = umsh_node::mac_command::PeerRepeatersResponseView::new(&page);
4149 listing.extend(view.entries().map(peer_repeater_record));
4150 match view.cursor() {
4151 Some(next) => cursor = Some(next.to_vec()),
4152 None => break,
4153 }
4154 if tokio::time::Instant::now() >= walk_deadline {
4155 break;
4158 }
4159 }
4160 Ok(listing)
4161}
4162
4163fn peer_repeater_record(
4164 entry: umsh_node::mac_command::PeerRepeaterEntryView<'_>,
4165) -> MobileMeshPeerRepeaterRecord {
4166 let signal = entry.rssi_snr();
4167 MobileMeshPeerRepeaterRecord {
4168 hint: entry.hint().map(Vec::from).unwrap_or_default(),
4169 name: entry.name().map(String::from),
4170 rssi_dbm: signal.map(|(rssi, _)| rssi),
4171 snr_quarter_db: signal.map(|(_, snr)| snr.as_quarter_db_steps()),
4172 last_heard_minutes: entry.last_heard_min(),
4173 location: entry
4174 .location()
4175 .filter(|location| !location.is_unspecified())
4176 .map(|location| location.as_bytes().to_vec()),
4177 region_codes: entry.regions().map(Vec::from).collect(),
4178 }
4179}
4180
4181fn bound_channel<M: MacBackend>(
4183 node: &LocalNode<M>,
4184 channels: &Rc<RefCell<ChannelRegistry>>,
4185 channel: &ChannelTag,
4186) -> Option<umsh_node::BoundChannel<M>> {
4187 let key = channels.borrow().key(channel)?;
4188 node.bound_channel(&umsh_node::Channel::private(key, ""))
4191}
4192
4193fn service_chat_tickets(
4194 chat: &mut MobileChatState,
4195 in_flight: &mut Vec<InFlightChatTransmission>,
4196 pipeline_ready: &mut BTreeSet<[u8; 32]>,
4197 events: &NotifyingSender<MobileChatWorkerEvent>,
4198 pending_depth: usize,
4199 now_ms: u64,
4200) {
4201 let occupied = in_flight.len();
4202 let mut index = 0;
4203 while index < in_flight.len() {
4204 let entry = &mut in_flight[index];
4205 if !entry.stall_reported
4208 && !entry.ticket.was_transmitted()
4209 && now_ms.saturating_sub(entry.queued_at_ms) >= CHAT_TRANSMISSION_STALL_MS
4210 {
4211 entry.stall_reported = true;
4212 let waited = now_ms.saturating_sub(entry.queued_at_ms) / 1000;
4213 let transmission_id = entry.transmission_id;
4214 let _ = events.send(MobileChatWorkerEvent::Diagnostic(format!(
4215 "transmission {transmission_id} has not left the radio after {waited}s \
4216 ({occupied}/{MOBILE_CHAT_TRANSMIT_WINDOW} window slots used, \
4217 {pending_depth} more waiting)"
4218 )));
4219 }
4220 let entry = &mut in_flight[index];
4221 if entry.ticket.was_transmitted() && !entry.sent_reported {
4222 chat.engine
4223 .transmit_update(entry.transmission_id, DeliveryState::Sent, now_ms);
4224 entry.sent_reported = true;
4225 }
4226 if entry.non_ack && entry.sent_reported {
4227 in_flight.swap_remove(index);
4231 } else if entry.ticket.was_acked() {
4232 if let Some(peer) = entry.gate_peer {
4233 pipeline_ready.insert(peer.0);
4234 }
4235 chat.engine
4236 .transmit_update(entry.transmission_id, DeliveryState::Acked, now_ms);
4237 in_flight.swap_remove(index);
4238 } else if entry.ticket.has_failed() {
4239 chat.engine
4240 .transmit_update(entry.transmission_id, DeliveryState::Failed, now_ms);
4241 in_flight.swap_remove(index);
4242 } else {
4243 index += 1;
4244 }
4245 }
4246}
4247
4248fn publish_chat_drain(
4249 drain: crate::mobile_chat::ChatDrain,
4250 events: &NotifyingSender<MobileChatWorkerEvent>,
4251) {
4252 for mutation in drain.mutations {
4253 let _ = events.send(MobileChatWorkerEvent::Mutation(mutation));
4254 }
4255 for delivery in drain.deliveries {
4256 let _ = events.send(MobileChatWorkerEvent::Delivery(delivery));
4257 }
4258 for lookup in drain.lookups {
4259 let _ = events.send(MobileChatWorkerEvent::ArchiveLookup(lookup));
4260 }
4261 for resolution in drain.resolutions {
4262 let _ = events.send(MobileChatWorkerEvent::SenderResolution(resolution));
4263 }
4264 for diagnostic in drain.diagnostics {
4265 let _ = events.send(MobileChatWorkerEvent::Diagnostic(diagnostic));
4266 }
4267}
4268
4269fn attach_rx_metadata(mutations: &mut [MobileChatMutationRecord], rx: &MobileChatRxMetadataRecord) {
4275 for mutation in mutations {
4276 let describes_receipt = match mutation.kind {
4277 MobileChatMutationKind::Insert => {
4278 mutation.direction == Some(MobileChatDirection::Inbound)
4279 && mutation.presence == MobileChatPresence::Present
4280 }
4281 MobileChatMutationKind::UpdateBody => true,
4282 MobileChatMutationKind::Edit | MobileChatMutationKind::Delete => false,
4283 };
4284 if describes_receipt {
4285 mutation.rx = Some(rx.clone());
4286 }
4287 }
4288}
4289
4290fn remember_member_route(
4293 routes: &mut BTreeMap<(ChannelTag, [u8; 3]), MemberRoute>,
4294 channel: ChannelTag,
4295 hint: NodeHint,
4296 rx: &MobileChatRxMetadataRecord,
4297) {
4298 routes.insert(
4299 (channel, hint.0),
4300 MemberRoute {
4301 hop_count: rx.hop_count,
4302 route_hints: rx.route_hints.clone(),
4303 },
4304 );
4305}
4306
4307fn flood_budget(hop_count: Option<u8>) -> u8 {
4312 hop_count
4313 .map(|hops| hops.saturating_sub(1))
4314 .unwrap_or(5)
4315 .max(1)
4316}
4317
4318#[derive(Clone)]
4320struct MemberRoute {
4321 hop_count: Option<u8>,
4322 route_hints: Vec<Vec<u8>>,
4323}
4324
4325fn decode_peer(address: &str) -> Result<PublicKey, MobileError> {
4326 let bytes = umsh_core::base58::decode(address.as_bytes())?;
4327 Ok(PublicKey(bytes))
4328}
4329
4330fn decode_channel_keys(keys: Vec<Vec<u8>>) -> Result<Vec<ChannelKey>, MobileMeshError> {
4331 keys.into_iter()
4332 .map(|key| {
4333 <[u8; 32]>::try_from(key.as_slice())
4334 .map(ChannelKey)
4335 .map_err(|_| MobileMeshError::InvalidChannelKey)
4336 })
4337 .collect()
4338}
4339
4340fn encode_peer_address(peer: &PublicKey) -> String {
4343 umsh_core::base58::encode(&peer.0)
4344 .into_iter()
4345 .map(char::from)
4346 .collect()
4347}
4348
4349fn emit_ping_failure(events: &NotifyingSender<MobileMeshPingEventRecord>, operation_id: u64) {
4350 let _ = events.send(MobileMeshPingEventRecord {
4351 operation_id,
4352 outcome: MobileMeshPingOutcome::Failed,
4353 round_trip_milliseconds: None,
4354 hop_count: None,
4355 route_hints: Vec::new(),
4356 rssi_dbm: None,
4357 snr_centibels: None,
4358 lqi: None,
4359 });
4360}
4361
4362#[cfg(test)]
4363mod tests {
4364 use super::*;
4365 use crate::MobileChatDeliveryState;
4366 use std::time::Instant;
4367 use umsh_crypto::NodeIdentity;
4368
4369 fn identity(seed: u8) -> Arc<MobileIdentity> {
4370 let identity = SoftwareIdentity::from_secret_bytes(&[seed; 32]);
4371 let public_identity = crate::public_identity_record(identity.public_key());
4372 Arc::new(MobileIdentity {
4373 identity: Mutex::new(Some(identity)),
4374 public_identity,
4375 })
4376 }
4377
4378 fn address(identity: &MobileIdentity) -> String {
4379 identity.public_identity.canonical_address.clone()
4380 }
4381
4382 fn is_reply(property: u32, value: &[u8]) -> Vec<u8> {
4385 let mut buf = vec![0u8; 512];
4386 let len = frame::prop_is(&mut buf, 0, property, value).unwrap();
4387 buf.truncate(len);
4388 buf
4389 }
4390
4391 fn are_reply(keys: &[u32]) -> Vec<u8> {
4394 let mut buf = vec![0u8; 512];
4395 let mut writer = frame::prop_are(&mut buf, 0).unwrap();
4396 for key in keys {
4397 writer.write_entry(*key, &[]).unwrap();
4398 }
4399 let len = writer.finish();
4400 buf.truncate(len);
4401 buf
4402 }
4403
4404 fn asked(crawl: &FetchCrawl) -> Vec<u32> {
4406 crawl.asked.clone()
4407 }
4408
4409 fn long_list() -> Vec<u32> {
4411 vec![
4412 prop::PHY_ENABLED,
4413 prop::PHY_FREQ,
4414 prop::PHY_TX_POWER,
4415 prop::PHY_LORA_BW,
4416 prop::PHY_LORA_SF,
4417 prop::PHY_LORA_CR,
4418 prop::PHY_DUTY_NOW,
4419 prop::PHY_DUTY_LIMIT,
4420 prop::DEV_NAME,
4421 prop::DEV_DISCOVERABLE,
4422 ]
4423 }
4424
4425 #[test]
4426 fn a_fetch_asks_for_what_it_was_given_and_nothing_else() {
4427 let mut crawl = FetchCrawl::new(long_list(), true);
4428 crawl.next_request().unwrap().unwrap();
4429 assert_eq!(asked(&crawl).len(), SYNC_BATCH);
4430 assert_eq!(asked(&crawl), long_list()[..SYNC_BATCH].to_vec());
4431
4432 let filtered = FetchCrawl::new(
4436 vec![prop::DEV_NAME, prop::HOST_KEY, prop::MAC_PROMISCUOUS],
4437 true,
4438 );
4439 assert_eq!(filtered.pending, vec![prop::DEV_NAME]);
4440 }
4441
4442 #[test]
4443 fn a_fetch_asks_again_for_what_a_short_answer_left_out() {
4444 let mut crawl = FetchCrawl::new(long_list(), true);
4445 crawl.next_request().unwrap().unwrap();
4446 let batch = asked(&crawl);
4447 assert!(batch.len() > 1);
4448
4449 crawl.receive(&are_reply(&batch[..2])).unwrap();
4451 assert_eq!(
4452 crawl
4453 .pending
4454 .iter()
4455 .take(batch.len() - 2)
4456 .copied()
4457 .collect::<Vec<_>>(),
4458 batch[2..].to_vec(),
4459 "the unanswered keys go back to the front of the queue"
4460 );
4461 assert_eq!(crawl.answers.len(), 2);
4462 }
4463
4464 #[test]
4465 fn a_fetch_falls_back_to_one_property_at_a_time() {
4466 let mut crawl = FetchCrawl::new(long_list(), true);
4467 crawl.next_request().unwrap().unwrap();
4468 let batch = asked(&crawl);
4469
4470 crawl
4473 .receive(&is_reply(
4474 prop::LAST_STATUS,
4475 &[umsh_ulcp::Status::UNIMPLEMENTED.0 as u8],
4476 ))
4477 .unwrap();
4478 assert!(!crawl.multi);
4479 assert_eq!(
4480 crawl
4481 .pending
4482 .iter()
4483 .take(batch.len())
4484 .copied()
4485 .collect::<Vec<_>>(),
4486 batch
4487 );
4488
4489 crawl.next_request().unwrap().unwrap();
4490 assert_eq!(asked(&crawl), vec![batch[0]]);
4491
4492 crawl
4496 .receive(&is_reply(
4497 prop::LAST_STATUS,
4498 &[umsh_ulcp::Status::PROP_NOT_FOUND.0 as u8],
4499 ))
4500 .unwrap();
4501 assert_eq!(crawl.answers.len(), 1);
4502 assert_eq!(crawl.answers[0].property_id, batch[0]);
4503 assert!(crawl.answers[0].value.is_none());
4504 assert_eq!(
4505 crawl.answers[0].status_code,
4506 Some(umsh_ulcp::Status::PROP_NOT_FOUND.0)
4507 );
4508 assert!(!crawl.pending.contains(&batch[0]));
4509 }
4510
4511 #[test]
4515 fn the_identity_card_is_one_exchange() {
4516 let card = crate::ulcp::ulcp_card_properties();
4517 let mut crawl = FetchCrawl::new(card.clone(), true);
4518 crawl.next_request().unwrap().unwrap();
4519 assert_eq!(asked(&crawl), card);
4520
4521 crawl.receive(&are_reply(&card)).unwrap();
4522 assert!(crawl.pending.is_empty(), "one exchange covered the card");
4523 assert_eq!(crawl.answers.len(), 4);
4524 assert!(crawl.next_request().unwrap().is_none());
4525 }
4526
4527 #[tokio::test]
4528 async fn the_phones_node_key_is_what_a_device_lists() {
4529 let directory = tempfile::tempdir().unwrap();
4530 let phone_identity = identity(21);
4531 let store = MobileCounterStore::new(directory.path().display().to_string()).unwrap();
4532 let phone = MobileMeshSession::new(phone_identity.clone(), store)
4533 .await
4534 .unwrap();
4535 assert_eq!(
4536 phone.node_public_key(),
4537 decode_peer(&address(&phone_identity)).unwrap().0.to_vec()
4538 );
4539 }
4540
4541 #[tokio::test]
4542 async fn one_device_is_managed_at_a_time() {
4543 let directory = tempfile::tempdir().unwrap();
4544 let phone_identity = identity(22);
4545 let device = address(&identity(23));
4546 let store = MobileCounterStore::new(directory.path().display().to_string()).unwrap();
4547 let phone = MobileMeshSession::new(phone_identity, store).await.unwrap();
4548
4549 let first = phone
4550 .begin_management_get(device.clone(), prop::DEV_NAME)
4551 .unwrap();
4552 let second = phone
4553 .begin_management_fetch(device.clone(), vec![prop::CAPS], true)
4554 .unwrap();
4555 assert_ne!(first, second);
4556
4557 let deadline = Instant::now() + Duration::from_secs(5);
4560 let refusal = loop {
4561 assert!(
4562 Instant::now() < deadline,
4563 "no report for the second operation"
4564 );
4565 if let Some(event) = phone
4566 .poll_update()
4567 .management_events
4568 .into_iter()
4569 .find(|event| event.operation_id == second)
4570 {
4571 break event;
4572 }
4573 };
4574 assert_eq!(refusal.outcome, MobileMeshManagementOutcome::Failed);
4575 assert_eq!(refusal.peer_address, device);
4576 }
4577
4578 #[tokio::test]
4579 async fn a_request_larger_than_one_payload_is_refused_before_it_is_sent() {
4580 let directory = tempfile::tempdir().unwrap();
4581 let phone_identity = identity(24);
4582 let device = address(&identity(25));
4583 let store = MobileCounterStore::new(directory.path().display().to_string()).unwrap();
4584 let phone = MobileMeshSession::new(phone_identity, store).await.unwrap();
4585
4586 assert_eq!(
4587 phone.begin_management_set(device.clone(), prop::DEV_NAME, vec![0x41; 400]),
4588 Err(MobileMeshError::InvalidRequest)
4589 );
4590 assert_eq!(
4591 phone.begin_management_get_many(device, Vec::new()),
4592 Err(MobileMeshError::InvalidRequest)
4593 );
4594 }
4595
4596 fn hint_of(identity: &MobileIdentity) -> Vec<u8> {
4599 decode_peer(&address(identity)).unwrap().0[..3].to_vec()
4600 }
4601
4602 async fn channel_session(name: &str) -> Arc<MobileMeshSession> {
4603 let directory = tempfile::tempdir().unwrap();
4604 let store =
4605 MobileCounterStore::new(directory.path().join(name).display().to_string()).unwrap();
4606 std::mem::forget(directory);
4608 MobileMeshSession::new(identity(31), store).await.unwrap()
4609 }
4610
4611 #[tokio::test]
4612 async fn channel_registration_is_idempotent_and_reversible() {
4613 let session = channel_session("channels").await;
4614 let key = vec![0x5au8; 32];
4615
4616 session.register_channels(vec![key.clone()]).await.unwrap();
4617 session.register_channels(vec![key.clone()]).await.unwrap();
4620 session.remove_channels(vec![key.clone()]).await.unwrap();
4621 session.remove_channels(vec![key.clone()]).await.unwrap();
4623 session.register_channels(vec![key]).await.unwrap();
4625 }
4626
4627 #[tokio::test]
4628 async fn channel_keys_must_be_full_length() {
4629 let session = channel_session("shortkey").await;
4630 assert_eq!(
4631 session.register_channels(vec![vec![0x01; 31]]).await,
4632 Err(MobileMeshError::InvalidChannelKey)
4633 );
4634 assert_eq!(
4635 session.remove_channels(vec![Vec::new()]).await,
4636 Err(MobileMeshError::InvalidChannelKey)
4637 );
4638 }
4639
4640 #[tokio::test]
4641 async fn the_phone_mac_holds_more_channels_than_the_embedded_default() {
4642 let session = channel_session("capacity").await;
4643 let keys: Vec<Vec<u8>> = (0..MOBILE_MAC_CHANNELS)
4645 .map(|index| {
4646 let mut key = vec![0u8; 32];
4647 key[0] = index as u8;
4648 key[1] = 0xA5;
4649 key
4650 })
4651 .collect();
4652 assert!(keys.len() > umsh_mac::DEFAULT_CHANNELS);
4653 session.register_channels(keys).await.unwrap();
4654
4655 let overflow = vec![vec![0xFFu8; 32]];
4656 assert_eq!(
4657 session.register_channels(overflow).await,
4658 Err(MobileMeshError::ChannelCapacity)
4659 );
4660 }
4661
4662 #[tokio::test]
4663 async fn two_rust_sessions_complete_an_authenticated_ping() {
4664 let directory = tempfile::tempdir().unwrap();
4665 let alice_identity = identity(7);
4666 let bob_identity = identity(9);
4667 let alice_root = directory.path().join("alice");
4668 let bob_root = directory.path().join("bob");
4669 let alice_store = MobileCounterStore::new(alice_root.display().to_string()).unwrap();
4670 let bob_store = MobileCounterStore::new(bob_root.display().to_string()).unwrap();
4671 let alice = MobileMeshSession::new(alice_identity.clone(), alice_store)
4672 .await
4673 .unwrap();
4674 let bob = MobileMeshSession::new(bob_identity.clone(), bob_store)
4675 .await
4676 .unwrap();
4677 assert!(!alice_root.exists());
4681 assert!(!bob_root.exists());
4682
4683 let operation = alice.ping(address(&bob_identity), 2_000).unwrap();
4687 let _ = bob.ping(address(&alice_identity), 2_000).unwrap();
4688 let deadline = Instant::now() + Duration::from_secs(10);
4689 loop {
4690 let alice_update = alice.poll_update();
4691 for frame in alice_update.outbound_frames {
4692 assert!(
4693 alice_root.exists(),
4694 "Alice released a frame before persisting its reservation"
4695 );
4696 alice.complete_outbound_frame(frame.id, true).unwrap();
4697 bob.receive(MobileMeshRxRecord {
4698 data: frame.data,
4699 rssi_dbm: Some(-40),
4700 lqi: None,
4701 snr_cb: Some(100),
4702 })
4703 .unwrap();
4704 }
4705 if let Some(event) = alice_update.ping_events.into_iter().next() {
4706 assert_eq!(event.operation_id, operation);
4707 assert_eq!(event.outcome, MobileMeshPingOutcome::Reply);
4708 assert!(event.round_trip_milliseconds.is_some());
4709 assert_eq!(event.hop_count, Some(1));
4710 assert!(event.route_hints.is_empty());
4711 assert_eq!(event.rssi_dbm, Some(-42));
4712 assert_eq!(event.snr_centibels, Some(90));
4713 assert_eq!(event.lqi, None);
4714 break;
4715 }
4716
4717 let bob_update = bob.poll_update();
4718 for frame in bob_update.outbound_frames {
4719 assert!(
4720 bob_root.exists(),
4721 "Bob released a frame before persisting its reservation"
4722 );
4723 bob.complete_outbound_frame(frame.id, true).unwrap();
4724 alice
4725 .receive(MobileMeshRxRecord {
4726 data: frame.data,
4727 rssi_dbm: Some(-42),
4728 lqi: None,
4729 snr_cb: Some(90),
4730 })
4731 .unwrap();
4732 }
4733 assert!(Instant::now() < deadline, "ping did not complete");
4734 std::thread::sleep(Duration::from_millis(5));
4735 }
4736 }
4737
4738 async fn complete_ping(alice: &MobileMeshSession, bob: &MobileMeshSession, target: String) {
4741 let operation = alice.ping(target, 2_000).unwrap();
4742 let deadline = Instant::now() + Duration::from_secs(10);
4743 loop {
4744 let alice_update = alice.poll_update();
4745 for frame in alice_update.outbound_frames {
4746 alice.complete_outbound_frame(frame.id, true).unwrap();
4747 bob.receive(MobileMeshRxRecord {
4748 data: frame.data,
4749 rssi_dbm: Some(-40),
4750 lqi: None,
4751 snr_cb: Some(100),
4752 })
4753 .unwrap();
4754 }
4755 if let Some(event) = alice_update.ping_events.into_iter().next() {
4756 assert_eq!(event.operation_id, operation);
4757 assert_eq!(event.outcome, MobileMeshPingOutcome::Reply);
4758 break;
4759 }
4760 let bob_update = bob.poll_update();
4761 for frame in bob_update.outbound_frames {
4762 bob.complete_outbound_frame(frame.id, true).unwrap();
4763 alice
4764 .receive(MobileMeshRxRecord {
4765 data: frame.data,
4766 rssi_dbm: Some(-42),
4767 lqi: None,
4768 snr_cb: Some(90),
4769 })
4770 .unwrap();
4771 }
4772 assert!(Instant::now() < deadline, "ping did not complete");
4773 std::thread::sleep(Duration::from_millis(5));
4774 }
4775 }
4776
4777 #[tokio::test]
4778 async fn removed_peer_re_registers_cleanly_and_traffic_still_flows() {
4779 let directory = tempfile::tempdir().unwrap();
4780 let alice_identity = identity(21);
4781 let bob_identity = identity(23);
4782 let alice_store =
4783 MobileCounterStore::new(directory.path().join("alice").display().to_string()).unwrap();
4784 let bob_store =
4785 MobileCounterStore::new(directory.path().join("bob").display().to_string()).unwrap();
4786 let alice = MobileMeshSession::new(alice_identity.clone(), alice_store)
4787 .await
4788 .unwrap();
4789 let bob = MobileMeshSession::new(bob_identity.clone(), bob_store)
4790 .await
4791 .unwrap();
4792
4793 alice
4794 .register_peers(vec![address(&bob_identity)])
4795 .await
4796 .unwrap();
4797 bob.register_peers(vec![address(&alice_identity)])
4798 .await
4799 .unwrap();
4800 complete_ping(&alice, &bob, address(&bob_identity)).await;
4801
4802 alice
4805 .remove_peers(vec![address(&bob_identity)])
4806 .await
4807 .unwrap();
4808 alice
4809 .remove_peers(vec![address(&bob_identity)])
4810 .await
4811 .unwrap();
4812 alice
4813 .remove_peers(vec![address(&alice_identity)])
4814 .await
4815 .unwrap();
4816
4817 alice
4821 .register_peers(vec![address(&bob_identity)])
4822 .await
4823 .unwrap();
4824 complete_ping(&alice, &bob, address(&bob_identity)).await;
4825 }
4826
4827 #[tokio::test]
4830 async fn request_peer_repeaters_emits_a_unicast_addressed_to_the_peer() {
4831 let directory = tempfile::tempdir().unwrap();
4832 let alice_identity = identity(51);
4833 let bob_key = *SoftwareIdentity::from_secret_bytes(&[53; 32]).public_key();
4834 let alice_store =
4835 MobileCounterStore::new(directory.path().join("alice").display().to_string()).unwrap();
4836 let alice = MobileMeshSession::new(alice_identity, alice_store)
4837 .await
4838 .unwrap();
4839
4840 let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
4844 let frame = tokio::select! {
4845 _ = alice.request_peer_repeaters(bob_key.0.to_vec()) => {
4846 panic!("the listing cannot complete with nobody to answer")
4847 }
4848 frame = async {
4849 loop {
4850 let update = alice.poll_update();
4851 if let Some(frame) = update.outbound_frames.into_iter().next() {
4852 break frame;
4853 }
4854 assert!(tokio::time::Instant::now() < deadline, "no request went out");
4855 tokio::time::sleep(Duration::from_millis(5)).await;
4856 }
4857 } => frame,
4858 };
4859
4860 let header = umsh_core::PacketHeader::parse(&frame.data).unwrap();
4864 assert_eq!(header.packet_type(), umsh_core::PacketType::Unicast);
4865 assert_eq!(
4866 header.dst,
4867 Some(umsh_core::NodeHint::from_public_key(&bob_key))
4868 );
4869 alice.complete_outbound_frame(frame.id, true).unwrap();
4870
4871 assert_eq!(
4873 alice.request_peer_repeaters(vec![0x01, 0x02]).await,
4874 Err(MobileMeshError::InvalidPeer)
4875 );
4876 }
4877
4878 #[tokio::test]
4879 async fn discover_identities_emits_one_acceptable_zero_hop_broadcast() {
4880 let directory = tempfile::tempdir().unwrap();
4881 let alice_identity = identity(31);
4882 let bob_identity = identity(33);
4883 let alice_store =
4884 MobileCounterStore::new(directory.path().join("alice").display().to_string()).unwrap();
4885 let bob_store =
4886 MobileCounterStore::new(directory.path().join("bob").display().to_string()).unwrap();
4887 let alice = MobileMeshSession::new(alice_identity.clone(), alice_store)
4888 .await
4889 .unwrap();
4890 let bob = MobileMeshSession::new(bob_identity.clone(), bob_store)
4891 .await
4892 .unwrap();
4893
4894 alice
4895 .discover_identities(None, Some(0x02), None, Vec::new())
4896 .await
4897 .unwrap();
4898
4899 let deadline = Instant::now() + Duration::from_secs(10);
4900 let frames = loop {
4901 let update = alice.poll_update();
4902 if !update.outbound_frames.is_empty() {
4903 break update.outbound_frames;
4904 }
4905 assert!(Instant::now() < deadline, "solicitation never went out");
4906 std::thread::sleep(Duration::from_millis(5));
4907 };
4908 assert_eq!(frames.len(), 1);
4910 let frame = frames.into_iter().next().unwrap();
4911 alice.complete_outbound_frame(frame.id, true).unwrap();
4912 let header = umsh_core::PacketHeader::parse(&frame.data).unwrap();
4913 assert_eq!(header.packet_type(), umsh_core::PacketType::Broadcast);
4914 assert!(header.flood_hops.is_none());
4916 assert!(header.fcf.full_source());
4918
4919 bob.receive(MobileMeshRxRecord {
4923 data: frame.data,
4924 rssi_dbm: Some(-40),
4925 lqi: None,
4926 snr_cb: Some(100),
4927 })
4928 .unwrap();
4929
4930 alice
4934 .discover_identities(None, None, None, Vec::new())
4935 .await
4936 .unwrap();
4937 let deadline = Instant::now() + Duration::from_secs(10);
4938 let frames = loop {
4939 let update = alice.poll_update();
4940 if !update.outbound_frames.is_empty() {
4941 break update.outbound_frames;
4942 }
4943 assert!(Instant::now() < deadline, "solicitation never went out");
4944 std::thread::sleep(Duration::from_millis(5));
4945 };
4946 assert_eq!(frames.len(), 1);
4947 let frame = frames.into_iter().next().unwrap();
4948 alice.complete_outbound_frame(frame.id, true).unwrap();
4949 let header = umsh_core::PacketHeader::parse(&frame.data).unwrap();
4950 let body = &frame.data[header.body_range.clone()];
4951 assert_eq!(body[0], umsh_core::PayloadType::MacCommand as u8);
4952 let umsh_node::MacCommand::IdentityRequest { options } =
4953 umsh_node::mac_command::parse(&body[1..]).unwrap()
4954 else {
4955 panic!("expected an identity request");
4956 };
4957 let has_vacuous_caps_filter = umsh_core::options::OptionDecoder::new(options)
4958 .filter_map(Result::ok)
4959 .any(|(number, value)| {
4960 number == umsh_node::mac_command::identity_filter::FILTER_NODE_CAPS && value == [0]
4961 });
4962 assert!(has_vacuous_caps_filter);
4963
4964 alice
4969 .discover_identities(
4970 None,
4971 None,
4972 Some(vec![0x5A, 0x5B]),
4973 vec![vec![0xAB, 0xCD], vec![0x12, 0x34]],
4974 )
4975 .await
4976 .unwrap();
4977 let deadline = Instant::now() + Duration::from_secs(10);
4978 let frames = loop {
4979 let update = alice.poll_update();
4980 if !update.outbound_frames.is_empty() {
4981 break update.outbound_frames;
4982 }
4983 assert!(
4984 Instant::now() < deadline,
4985 "routed solicitation never went out"
4986 );
4987 std::thread::sleep(Duration::from_millis(5));
4988 };
4989 assert_eq!(frames.len(), 1);
4990 let frame = frames.into_iter().next().unwrap();
4991 alice.complete_outbound_frame(frame.id, true).unwrap();
4992 let header = umsh_core::PacketHeader::parse(&frame.data).unwrap();
4993 assert_eq!(header.packet_type(), umsh_core::PacketType::Broadcast);
4994 assert!(header.flood_hops.is_none());
4998 assert!(header.fcf.full_source());
4999 let options =
5000 umsh_core::ParsedOptions::extract(&frame.data, header.options_range.clone()).unwrap();
5001 let route = options
5003 .source_route
5004 .clone()
5005 .map(|range| frame.data[range].to_vec())
5006 .expect("a steered request carries a Route option");
5007 assert_eq!(route, vec![0xAB, 0xCD, 0x12, 0x34]);
5008 let trace = options
5011 .trace_route
5012 .clone()
5013 .map(|range| frame.data[range].to_vec())
5014 .expect("a steered request carries a trace route");
5015 assert!(trace.is_empty());
5016
5017 let body = &frame.data[header.body_range.clone()];
5020 let umsh_node::MacCommand::IdentityRequest { options } =
5021 umsh_node::mac_command::parse(&body[1..]).unwrap()
5022 else {
5023 panic!("expected an identity request");
5024 };
5025 let filters: Vec<_> = umsh_core::options::OptionDecoder::new(options)
5026 .filter_map(Result::ok)
5027 .map(|(number, value)| (number, value.to_vec()))
5028 .collect();
5029 assert!(filters.contains(&(
5030 umsh_node::mac_command::identity_filter::FILTER_NODE_HINT,
5031 vec![0x5A, 0x5B]
5032 )));
5033 assert!(
5034 !filters
5035 .iter()
5036 .any(|(number, _)| *number
5037 == umsh_node::mac_command::identity_filter::FILTER_NODE_CAPS),
5038 "a hint-filtered ask needs no vacuous capability filter"
5039 );
5040 }
5041
5042 #[tokio::test]
5050 async fn discover_solicitation_earns_a_stranger_reply_end_to_end() {
5051 let directory = tempfile::tempdir().unwrap();
5052 let alice_identity = identity(21);
5053 let bob_identity = identity(23);
5054 let alice_store =
5055 MobileCounterStore::new(directory.path().join("alice").display().to_string()).unwrap();
5056 let bob_store =
5057 MobileCounterStore::new(directory.path().join("bob").display().to_string()).unwrap();
5058 let alice = MobileMeshSession::new_with_virtual_time(alice_identity.clone(), alice_store)
5063 .await
5064 .unwrap();
5065 let bob = MobileMeshSession::new_with_virtual_time(bob_identity.clone(), bob_store)
5066 .await
5067 .unwrap();
5068 bob.set_discoverable(true, Some("Bob's phone".into()))
5070 .await
5071 .unwrap();
5072 bob.set_advertised_location(Some(MobileMeshSharedLocationRecord {
5075 latitude_degrees: 48.1173,
5076 longitude_degrees: 11.5167,
5077 precision_bytes: 5,
5078 }))
5079 .await
5080 .unwrap();
5081
5082 alice
5083 .discover_identities(None, None, None, Vec::new())
5084 .await
5085 .unwrap();
5086
5087 let bob_address = address(&bob_identity);
5089 let deadline = Instant::now() + Duration::from_secs(15);
5090 let event = 'outer: loop {
5091 let alice_update = alice.poll_update();
5092 for frame in alice_update.outbound_frames {
5093 alice.complete_outbound_frame(frame.id, true).unwrap();
5094 bob.receive(MobileMeshRxRecord {
5095 data: frame.data,
5096 rssi_dbm: Some(-40),
5097 lqi: None,
5098 snr_cb: Some(100),
5099 })
5100 .unwrap();
5101 }
5102 for event in alice_update.advertisement_events {
5103 if event.peer_address == bob_address {
5104 break 'outer event;
5105 }
5106 }
5107 let bob_update = bob.poll_update();
5108 for frame in bob_update.outbound_frames {
5109 bob.complete_outbound_frame(frame.id, true).unwrap();
5110 alice
5111 .receive(MobileMeshRxRecord {
5112 data: frame.data,
5113 rssi_dbm: Some(-42),
5114 lqi: None,
5115 snr_cb: Some(90),
5116 })
5117 .unwrap();
5118 }
5119 assert!(Instant::now() < deadline, "no identity reply reached Alice");
5120 std::thread::sleep(Duration::from_millis(5));
5121 };
5122 assert!(event.source_authenticated);
5125 let payload = umsh_node::NodeIdentityPayload::from_bytes(&event.payload).unwrap();
5126 assert_eq!(payload.name.as_deref(), Some("Bob's phone"));
5127 let cell = payload
5128 .location
5129 .expect("the reply serves the shared location");
5130 assert_eq!(cell.precision(), 5);
5131 let (lat, lon) = cell.center();
5132 assert!((f64::from(lat) - 48.1173).abs() < 0.01);
5133 assert!((f64::from(lon) - 11.5167).abs() < 0.01);
5134
5135 bob.set_discoverable(false, None).await.unwrap();
5137 alice
5138 .discover_identities(None, None, None, Vec::new())
5139 .await
5140 .unwrap();
5141 let quiet_until = Instant::now() + Duration::from_secs(6);
5142 while Instant::now() < quiet_until {
5143 let alice_update = alice.poll_update();
5144 for frame in alice_update.outbound_frames {
5145 alice.complete_outbound_frame(frame.id, true).unwrap();
5146 bob.receive(MobileMeshRxRecord {
5147 data: frame.data,
5148 rssi_dbm: Some(-40),
5149 lqi: None,
5150 snr_cb: Some(100),
5151 })
5152 .unwrap();
5153 }
5154 let bob_update = bob.poll_update();
5155 assert!(
5156 bob_update.outbound_frames.is_empty(),
5157 "Bob answered while not discoverable"
5158 );
5159 std::thread::sleep(Duration::from_millis(20));
5160 }
5161 }
5162
5163 #[tokio::test]
5164 async fn peer_route_is_visible_and_resettable() {
5165 let directory = tempfile::tempdir().unwrap();
5166 let alice_identity = identity(11);
5167 let bob_identity = identity(13);
5168 let alice_store =
5169 MobileCounterStore::new(directory.path().join("alice").display().to_string()).unwrap();
5170 let bob_store =
5171 MobileCounterStore::new(directory.path().join("bob").display().to_string()).unwrap();
5172 let alice = MobileMeshSession::new(alice_identity.clone(), alice_store)
5173 .await
5174 .unwrap();
5175 let bob = MobileMeshSession::new(bob_identity.clone(), bob_store)
5176 .await
5177 .unwrap();
5178
5179 assert_eq!(
5182 alice.peer_route(address(&bob_identity)).await.unwrap(),
5183 MobileMeshRouteRecord::unknown()
5184 );
5185 assert!(
5186 !alice
5187 .clear_peer_route(address(&bob_identity))
5188 .await
5189 .unwrap()
5190 );
5191
5192 let operation = alice.ping(address(&bob_identity), 2_000).unwrap();
5193 let _ = bob.ping(address(&alice_identity), 2_000).unwrap();
5194 let deadline = Instant::now() + Duration::from_secs(10);
5195 loop {
5196 let alice_update = alice.poll_update();
5197 for frame in alice_update.outbound_frames {
5198 alice.complete_outbound_frame(frame.id, true).unwrap();
5199 bob.receive(MobileMeshRxRecord {
5200 data: frame.data,
5201 rssi_dbm: Some(-40),
5202 lqi: None,
5203 snr_cb: Some(100),
5204 })
5205 .unwrap();
5206 }
5207 if let Some(event) = alice_update.ping_events.into_iter().next() {
5208 assert_eq!(event.operation_id, operation);
5209 assert_eq!(event.outcome, MobileMeshPingOutcome::Reply);
5210 break;
5211 }
5212
5213 let bob_update = bob.poll_update();
5214 for frame in bob_update.outbound_frames {
5215 bob.complete_outbound_frame(frame.id, true).unwrap();
5216 alice
5217 .receive(MobileMeshRxRecord {
5218 data: frame.data,
5219 rssi_dbm: Some(-42),
5220 lqi: None,
5221 snr_cb: Some(90),
5222 })
5223 .unwrap();
5224 }
5225 assert!(Instant::now() < deadline, "ping did not complete");
5226 std::thread::sleep(Duration::from_millis(5));
5227 }
5228
5229 let route = alice.peer_route(address(&bob_identity)).await.unwrap();
5234 assert_eq!(route.kind, MobileMeshRouteKind::Direct);
5235 assert!(route.hints.is_empty());
5236 assert_eq!(route.flood_hops, None);
5237
5238 assert!(
5241 alice
5242 .clear_peer_route(address(&bob_identity))
5243 .await
5244 .unwrap()
5245 );
5246 assert_eq!(
5247 alice.peer_route(address(&bob_identity)).await.unwrap(),
5248 MobileMeshRouteRecord::unknown()
5249 );
5250 assert!(
5251 !alice
5252 .clear_peer_route(address(&bob_identity))
5253 .await
5254 .unwrap()
5255 );
5256
5257 let operation = alice.ping(address(&bob_identity), 2_000).unwrap();
5260 let deadline = Instant::now() + Duration::from_secs(10);
5261 loop {
5262 let alice_update = alice.poll_update();
5263 for frame in alice_update.outbound_frames {
5264 alice.complete_outbound_frame(frame.id, true).unwrap();
5265 bob.receive(MobileMeshRxRecord {
5266 data: frame.data,
5267 rssi_dbm: Some(-40),
5268 lqi: None,
5269 snr_cb: Some(100),
5270 })
5271 .unwrap();
5272 }
5273 if let Some(event) = alice_update.ping_events.into_iter().next() {
5274 assert_eq!(event.operation_id, operation);
5275 assert_eq!(event.outcome, MobileMeshPingOutcome::Reply);
5276 break;
5277 }
5278
5279 let bob_update = bob.poll_update();
5280 for frame in bob_update.outbound_frames {
5281 bob.complete_outbound_frame(frame.id, true).unwrap();
5282 alice
5283 .receive(MobileMeshRxRecord {
5284 data: frame.data,
5285 rssi_dbm: Some(-42),
5286 lqi: None,
5287 snr_cb: Some(90),
5288 })
5289 .unwrap();
5290 }
5291 assert!(
5292 Instant::now() < deadline,
5293 "ping after reset did not complete"
5294 );
5295 std::thread::sleep(Duration::from_millis(5));
5296 }
5297 assert_eq!(
5298 alice.peer_route(address(&bob_identity)).await.unwrap().kind,
5299 MobileMeshRouteKind::Direct
5300 );
5301 }
5302
5303 #[tokio::test]
5304 async fn broadcast_advertisement_reaches_peer_with_valid_signature() {
5305 let directory = tempfile::tempdir().unwrap();
5306 let alice_identity = identity(21);
5307 let bob_identity = identity(23);
5308 let alice_store =
5309 MobileCounterStore::new(directory.path().join("alice").display().to_string()).unwrap();
5310 let bob_store =
5311 MobileCounterStore::new(directory.path().join("bob").display().to_string()).unwrap();
5312 let alice = MobileMeshSession::new(alice_identity.clone(), alice_store)
5313 .await
5314 .unwrap();
5315 let bob = MobileMeshSession::new(bob_identity, bob_store)
5316 .await
5317 .unwrap();
5318
5319 let bundle = alice
5321 .sign_identity_bundle(Some("Alice's Phone".to_owned()), Some(1_760_000_000))
5322 .await
5323 .unwrap();
5324 let record = crate::decode_node_identity(address(&alice_identity), bundle.clone()).unwrap();
5325 assert_eq!(record.signature, crate::IdentitySignatureState::Valid);
5326 assert_eq!(record.name.as_deref(), Some("Alice's Phone"));
5327 assert_eq!(record.role_label, "Chat");
5328 let uri = crate::node_uri_with_identity(address(&alice_identity), bundle).unwrap();
5329 assert!(
5330 crate::inspect_node_uri(uri)
5331 .unwrap()
5332 .identity_payload
5333 .is_some()
5334 );
5335
5336 alice
5337 .advertise_identity(Some("Alice's Phone".to_owned()), None)
5338 .await
5339 .unwrap();
5340
5341 let deadline = Instant::now() + Duration::from_secs(10);
5342 loop {
5343 for frame in alice.poll_update().outbound_frames {
5344 alice.complete_outbound_frame(frame.id, true).unwrap();
5345 bob.receive(MobileMeshRxRecord {
5346 data: frame.data,
5347 rssi_dbm: Some(-50),
5348 lqi: None,
5349 snr_cb: None,
5350 })
5351 .unwrap();
5352 }
5353 let bob_update = bob.poll_update();
5354 let heard = bob_update.peer_heard_events;
5358 if let Some(event) = bob_update.advertisement_events.into_iter().next() {
5359 assert_eq!(
5360 heard.iter().find_map(|record| record.peer_address.clone()),
5361 Some(address(&alice_identity)),
5362 "the frame that carried the advertisement also reported presence"
5363 );
5364 assert_eq!(event.peer_address, address(&alice_identity));
5365 assert!(!event.source_authenticated);
5369 let received =
5370 crate::decode_node_identity(event.peer_address, event.payload).unwrap();
5371 assert_eq!(received.signature, crate::IdentitySignatureState::Valid);
5372 assert_eq!(received.name.as_deref(), Some("Alice's Phone"));
5373 break;
5374 }
5375 assert!(Instant::now() < deadline, "advertisement not received");
5376 std::thread::sleep(Duration::from_millis(5));
5377 }
5378 }
5379
5380 #[tokio::test]
5384 async fn a_shared_location_rides_adverts_but_never_the_durable_bundle() {
5385 let directory = tempfile::tempdir().unwrap();
5386 let alice_identity = identity(51);
5387 let bob_identity = identity(53);
5388 let alice_store =
5389 MobileCounterStore::new(directory.path().join("alice").display().to_string()).unwrap();
5390 let bob_store =
5391 MobileCounterStore::new(directory.path().join("bob").display().to_string()).unwrap();
5392 let alice = MobileMeshSession::new(alice_identity.clone(), alice_store)
5393 .await
5394 .unwrap();
5395 let bob = MobileMeshSession::new(bob_identity, bob_store)
5396 .await
5397 .unwrap();
5398
5399 alice
5400 .set_advertised_location(Some(MobileMeshSharedLocationRecord {
5401 latitude_degrees: 37.774_929,
5402 longitude_degrees: -122.419_416,
5403 precision_bytes: 5,
5404 }))
5405 .await
5406 .unwrap();
5407
5408 let bundle = alice
5411 .sign_identity_bundle(Some("Alice's Phone".to_owned()), None)
5412 .await
5413 .unwrap();
5414 let record = crate::decode_node_identity(address(&alice_identity), bundle).unwrap();
5415 assert_eq!(record.signature, crate::IdentitySignatureState::Valid);
5416 assert!(
5417 record.latitude.is_none(),
5418 "a QR bundle never places its owner"
5419 );
5420
5421 let mut received = Vec::new();
5423 for share in [true, false] {
5424 if !share {
5425 alice.set_advertised_location(None).await.unwrap();
5426 }
5427 alice.advertise_identity(None, None).await.unwrap();
5428 let deadline = Instant::now() + Duration::from_secs(10);
5429 'advert: loop {
5430 for frame in alice.poll_update().outbound_frames {
5431 alice.complete_outbound_frame(frame.id, true).unwrap();
5432 bob.receive(MobileMeshRxRecord {
5433 data: frame.data,
5434 rssi_dbm: Some(-50),
5435 lqi: None,
5436 snr_cb: None,
5437 })
5438 .unwrap();
5439 }
5440 for event in bob.poll_update().advertisement_events {
5441 received.push(
5442 crate::decode_node_identity(event.peer_address, event.payload).unwrap(),
5443 );
5444 break 'advert;
5445 }
5446 assert!(Instant::now() < deadline, "advertisement not received");
5447 std::thread::sleep(Duration::from_millis(5));
5448 }
5449 }
5450
5451 let shared = &received[0];
5452 assert_eq!(shared.location_precision, Some(5));
5453 assert!((shared.latitude.unwrap() - 37.774_929).abs() < 0.01);
5454 assert!((shared.longitude.unwrap() + 122.419_416).abs() < 0.01);
5455 assert!(received[1].latitude.is_none());
5457 }
5458
5459 #[test]
5462 fn a_location_that_names_no_place_is_refused() {
5463 let valid = MobileMeshSharedLocationRecord {
5464 latitude_degrees: 37.774_929,
5465 longitude_degrees: -122.419_416,
5466 precision_bytes: 5,
5467 };
5468 for broken in [
5469 MobileMeshSharedLocationRecord {
5472 precision_bytes: 0,
5473 ..valid
5474 },
5475 MobileMeshSharedLocationRecord {
5476 precision_bytes: MAX_PRECISION + 1,
5477 ..valid
5478 },
5479 MobileMeshSharedLocationRecord {
5480 latitude_degrees: 90.1,
5481 ..valid
5482 },
5483 MobileMeshSharedLocationRecord {
5484 longitude_degrees: -180.1,
5485 ..valid
5486 },
5487 MobileMeshSharedLocationRecord {
5488 latitude_degrees: f64::NAN,
5489 ..valid
5490 },
5491 ] {
5492 assert!(matches!(
5493 disclosed_cell(broken),
5494 Err(MobileMeshError::InvalidLocation)
5495 ));
5496 }
5497 let cell = disclosed_cell(valid).unwrap();
5498 assert_eq!(cell.precision(), 5);
5499 }
5500
5501 #[tokio::test]
5504 async fn a_beacon_reports_presence_and_carries_nothing() {
5505 let directory = tempfile::tempdir().unwrap();
5506 let alice_identity = identity(31);
5507 let bob_identity = identity(33);
5508 let alice_store =
5509 MobileCounterStore::new(directory.path().join("alice").display().to_string()).unwrap();
5510 let bob_store =
5511 MobileCounterStore::new(directory.path().join("bob").display().to_string()).unwrap();
5512 let alice = MobileMeshSession::new(alice_identity.clone(), alice_store)
5513 .await
5514 .unwrap();
5515 let bob = MobileMeshSession::new(bob_identity, bob_store)
5516 .await
5517 .unwrap();
5518
5519 alice.send_beacon().await.unwrap();
5520
5521 let deadline = Instant::now() + Duration::from_secs(10);
5522 loop {
5523 for frame in alice.poll_update().outbound_frames {
5524 let header = umsh_core::PacketHeader::parse(&frame.data).unwrap();
5525 assert!(header.is_beacon(), "a beacon carries no body");
5526 let options =
5527 umsh_core::ParsedOptions::extract(&frame.data, header.options_range.clone())
5528 .unwrap();
5529 assert!(options.trace_route.is_some());
5530 assert!(
5531 options.trace_signal.is_some(),
5532 "the pair is what makes the trace worth collecting"
5533 );
5534 alice.complete_outbound_frame(frame.id, true).unwrap();
5535 bob.receive(MobileMeshRxRecord {
5536 data: frame.data,
5537 rssi_dbm: Some(-50),
5538 lqi: None,
5539 snr_cb: None,
5540 })
5541 .unwrap();
5542 }
5543 let bob_update = bob.poll_update();
5544 assert!(
5545 bob_update.advertisement_events.is_empty(),
5546 "an empty beacon identifies nobody"
5547 );
5548 if let Some(heard) = bob_update.peer_heard_events.into_iter().next() {
5549 assert_eq!(heard.peer_address, None);
5553 assert_eq!(heard.node_hint, Some(hint_of(&alice_identity)));
5554 assert!(!heard.source_authenticated);
5555 break;
5556 }
5557 assert!(Instant::now() < deadline, "beacon not received");
5558 std::thread::sleep(Duration::from_millis(5));
5559 }
5560 }
5561
5562 #[tokio::test]
5566 async fn a_scheduled_advertisement_stays_with_the_neighbours() {
5567 let directory = tempfile::tempdir().unwrap();
5568 let local_identity = identity(41);
5569 let store =
5570 MobileCounterStore::new(directory.path().join("local").display().to_string()).unwrap();
5571 let session = MobileMeshSession::new(local_identity, store).await.unwrap();
5572
5573 session
5574 .advertise_identity_scheduled(Some("Phone".to_owned()), None)
5575 .await
5576 .unwrap();
5577
5578 let deadline = Instant::now() + Duration::from_secs(10);
5579 loop {
5580 let frames = session.poll_update().outbound_frames;
5581 if let Some(frame) = frames.into_iter().next() {
5582 let header = umsh_core::PacketHeader::parse(&frame.data).unwrap();
5583 assert!(
5584 header.flood_hops.is_none(),
5585 "a scheduled advertisement is not flooded"
5586 );
5587 let options =
5588 umsh_core::ParsedOptions::extract(&frame.data, header.options_range.clone())
5589 .unwrap();
5590 assert!(options.trace_route.is_none());
5591 assert!(
5592 header.fcf.full_source(),
5593 "the detached signature is only checkable against the key"
5594 );
5595 session.complete_outbound_frame(frame.id, true).unwrap();
5596 break;
5597 }
5598 assert!(Instant::now() < deadline, "advertisement never queued");
5599 std::thread::sleep(Duration::from_millis(5));
5600 }
5601 }
5602
5603 #[tokio::test]
5608 async fn virtual_time_fast_forwards_protocol_timeouts() {
5609 let directory = tempfile::tempdir().unwrap();
5610 let local_identity = identity(61);
5611 let silent_peer = identity(62);
5612 let store = MobileCounterStore::new(directory.path().join("virtual").display().to_string())
5613 .unwrap();
5614 let session = MobileMeshSession::new_with_virtual_time(local_identity, store)
5615 .await
5616 .unwrap();
5617 let started = Instant::now();
5618 let operation = session.ping(address(&silent_peer), 30_000).unwrap();
5619
5620 let deadline = started + Duration::from_secs(5);
5621 loop {
5622 let update = session.poll_update();
5623 for frame in update.outbound_frames {
5624 session.complete_outbound_frame(frame.id, true).unwrap();
5625 }
5626 if let Some(event) = update.ping_events.into_iter().next() {
5627 assert_eq!(event.operation_id, operation);
5628 assert_eq!(event.outcome, MobileMeshPingOutcome::TimedOut);
5629 break;
5630 }
5631 assert!(
5632 Instant::now() < deadline,
5633 "virtual-time ping timeout never fired"
5634 );
5635 std::thread::sleep(Duration::from_millis(2));
5636 }
5637 assert!(
5638 started.elapsed() < Duration::from_secs(5),
5639 "a 30s virtual timeout must not take real-time seconds"
5640 );
5641 }
5642
5643 #[tokio::test]
5644 async fn silent_peer_completes_with_timeout_event() {
5645 let directory = tempfile::tempdir().unwrap();
5646 let local_identity = identity(11);
5647 let silent_peer = identity(13);
5648 let store =
5649 MobileCounterStore::new(directory.path().join("local").display().to_string()).unwrap();
5650 let session = MobileMeshSession::new(local_identity, store).await.unwrap();
5651 let operation = session.ping(address(&silent_peer), 100).unwrap();
5652 let deadline = Instant::now() + Duration::from_secs(2);
5653
5654 loop {
5655 let update = session.poll_update();
5656 for frame in update.outbound_frames {
5657 session.complete_outbound_frame(frame.id, true).unwrap();
5658 }
5659 if let Some(event) = update.ping_events.into_iter().next() {
5660 assert_eq!(event.operation_id, operation);
5661 assert_eq!(event.outcome, MobileMeshPingOutcome::TimedOut);
5662 assert_eq!(event.round_trip_milliseconds, None);
5663 assert_eq!(event.hop_count, None);
5664 assert!(event.route_hints.is_empty());
5665 assert_eq!(event.rssi_dbm, None);
5666 break;
5667 }
5668 assert!(Instant::now() < deadline, "silent ping never timed out");
5669 std::thread::sleep(Duration::from_millis(10));
5670 }
5671 }
5672
5673 struct TestWakeListener {
5674 signal: std_mpsc::Sender<()>,
5675 }
5676
5677 impl MobileMeshWakeListener for TestWakeListener {
5678 fn on_update_pending(&self) {
5679 let _ = self.signal.send(());
5680 }
5681 }
5682
5683 #[tokio::test]
5687 async fn wake_listener_fires_on_pending_data_and_rearms_after_drain() {
5688 let directory = tempfile::tempdir().unwrap();
5689 let local_identity = identity(63);
5690 let silent_peer = identity(64);
5691 let store =
5692 MobileCounterStore::new(directory.path().join("wake").display().to_string()).unwrap();
5693 let session = MobileMeshSession::new(local_identity, store).await.unwrap();
5694 let (signal, wakes) = std_mpsc::channel();
5695 session.set_wake_listener(Arc::new(TestWakeListener { signal }));
5696
5697 let operation = session.ping(address(&silent_peer), 100).unwrap();
5699 wakes
5700 .recv_timeout(Duration::from_secs(5))
5701 .expect("no wake for the outbound ping frame");
5702
5703 let update = session.poll_update();
5704 assert!(
5705 !update.outbound_frames.is_empty(),
5706 "wake fired but nothing was pending"
5707 );
5708 for frame in update.outbound_frames {
5709 session.complete_outbound_frame(frame.id, true).unwrap();
5710 }
5711
5712 wakes
5715 .recv_timeout(Duration::from_secs(5))
5716 .expect("no wake for the ping timeout event");
5717 let deadline = Instant::now() + Duration::from_secs(2);
5718 loop {
5719 let update = session.poll_update();
5720 if let Some(event) = update.ping_events.into_iter().next() {
5721 assert_eq!(event.operation_id, operation);
5722 assert_eq!(event.outcome, MobileMeshPingOutcome::TimedOut);
5723 break;
5724 }
5725 assert!(Instant::now() < deadline, "timeout event never surfaced");
5726 std::thread::sleep(Duration::from_millis(5));
5727 }
5728 }
5729
5730 #[tokio::test]
5733 async fn wake_listener_registered_late_fires_for_already_pending_data() {
5734 let directory = tempfile::tempdir().unwrap();
5735 let local_identity = identity(65);
5736 let silent_peer = identity(66);
5737 let store =
5738 MobileCounterStore::new(directory.path().join("wake-late").display().to_string())
5739 .unwrap();
5740 let session = MobileMeshSession::new(local_identity, store).await.unwrap();
5741
5742 session.ping(address(&silent_peer), 5_000).unwrap();
5743 std::thread::sleep(Duration::from_millis(200));
5747
5748 let (signal, wakes) = std_mpsc::channel();
5749 session.set_wake_listener(Arc::new(TestWakeListener { signal }));
5750 wakes
5751 .recv_timeout(Duration::from_secs(5))
5752 .expect("late-registered listener never fired");
5753 assert!(!session.poll_update().outbound_frames.is_empty());
5754 }
5755
5756 #[tokio::test]
5757 async fn chat_checkpoint_batch_gates_transmission_and_delivers_owned_mutation() {
5758 let directory = tempfile::tempdir().unwrap();
5759 let alice_identity = identity(21);
5760 let bob_identity = identity(22);
5761 let alice_root = directory.path().join("chat-alice");
5762 let alice_store = MobileCounterStore::new(alice_root.display().to_string()).unwrap();
5763 let bob_store =
5764 MobileCounterStore::new(directory.path().join("chat-bob").display().to_string())
5765 .unwrap();
5766 let alice = MobileMeshSession::new(alice_identity.clone(), alice_store)
5767 .await
5768 .unwrap();
5769 let bob = MobileMeshSession::new(bob_identity.clone(), bob_store)
5770 .await
5771 .unwrap();
5772 let alice_address = address(&alice_identity);
5773 alice
5774 .register_peers(vec![address(&bob_identity)])
5775 .await
5776 .unwrap();
5777 bob.register_peers(vec![alice_address.clone()])
5778 .await
5779 .unwrap();
5780
5781 let batch = alice
5782 .compose_text(address(&bob_identity), 77, "hello from Rust".to_owned())
5783 .await
5784 .unwrap();
5785 assert_eq!(
5786 batch.checkpoint.conversation_address,
5787 address(&bob_identity)
5788 );
5789 assert!(!batch.archives.is_empty());
5790 assert_eq!(batch.mutations.len(), 1);
5791 assert_eq!(batch.mutations[0].body.as_deref(), Some("hello from Rust"));
5792 assert_eq!(batch.mutations[0].fragment_count, Some(1));
5793 assert_eq!(
5794 alice
5795 .compose_text(address(&bob_identity), 78, "second".to_owned())
5796 .await,
5797 Err(MobileMeshError::OperationInProgress)
5798 );
5799 assert!(alice.poll_update().outbound_frames.is_empty());
5800 assert!(
5801 !alice_root.exists(),
5802 "compose alone must not touch counters"
5803 );
5804
5805 alice.commit_chat_batch(batch.batch_id).await.unwrap();
5806 assert!(alice_root.exists());
5807
5808 let deadline = Instant::now() + Duration::from_secs(10);
5811 loop {
5812 let alice_update = alice.poll_update();
5813 for frame in alice_update.outbound_frames {
5814 alice.complete_outbound_frame(frame.id, true).unwrap();
5815 bob.receive(MobileMeshRxRecord {
5816 data: frame.data,
5817 rssi_dbm: Some(-55),
5818 lqi: Some(200),
5819 snr_cb: Some(70),
5820 })
5821 .unwrap();
5822 }
5823 let bob_update = bob.poll_update();
5824 for frame in bob_update.outbound_frames.iter().cloned() {
5825 bob.complete_outbound_frame(frame.id, true).unwrap();
5826 alice
5827 .receive(MobileMeshRxRecord {
5828 data: frame.data,
5829 rssi_dbm: Some(-55),
5830 lqi: Some(200),
5831 snr_cb: Some(70),
5832 })
5833 .unwrap();
5834 }
5835 if let Some(mutation) = bob_update.chat_mutations.first() {
5836 assert_eq!(mutation.body.as_deref(), Some("hello from Rust"));
5837 assert_eq!(
5838 mutation.sender_address.as_deref(),
5839 Some(alice_address.as_str())
5840 );
5841 assert_eq!(
5842 mutation.direction,
5843 Some(crate::MobileChatDirection::Inbound)
5844 );
5845 let batch_id = bob_update.chat_batch_id.expect("owned chat batch");
5846 assert_eq!(
5847 bob.poll_update().chat_batch_id,
5848 Some(batch_id),
5849 "unacknowledged chat effects must be replayed"
5850 );
5851 bob.acknowledge_chat_batch(batch_id).unwrap();
5852 assert!(bob.poll_update().chat_mutations.is_empty());
5853 break;
5854 }
5855 assert!(Instant::now() < deadline, "chat frame did not arrive");
5856 std::thread::sleep(Duration::from_millis(5));
5857 }
5858 }
5859
5860 #[tokio::test]
5861 async fn fragmented_chat_message_crosses_mobile_radio_bridge() {
5862 let directory = tempfile::tempdir().unwrap();
5863 let alice_identity = identity(31);
5864 let bob_identity = identity(32);
5865 let alice_store =
5866 MobileCounterStore::new(directory.path().join("long-alice").display().to_string())
5867 .unwrap();
5868 let bob_store =
5869 MobileCounterStore::new(directory.path().join("long-bob").display().to_string())
5870 .unwrap();
5871 let alice = MobileMeshSession::new(alice_identity.clone(), alice_store)
5872 .await
5873 .unwrap();
5874 let bob = MobileMeshSession::new(bob_identity.clone(), bob_store)
5875 .await
5876 .unwrap();
5877 let alice_address = address(&alice_identity);
5878 let bob_address = address(&bob_identity);
5879 alice
5880 .register_peers(vec![bob_address.clone()])
5881 .await
5882 .unwrap();
5883 bob.register_peers(vec![alice_address.clone()])
5884 .await
5885 .unwrap();
5886
5887 let body = "fragmented mobile message ".repeat(16);
5888 let batch = alice
5889 .compose_text(bob_address, 91, body.clone())
5890 .await
5891 .unwrap();
5892 let fragment_count = usize::from(batch.mutations[0].fragment_count.unwrap_or(1));
5893 assert!(fragment_count > 1);
5894 alice.commit_chat_batch(batch.batch_id).await.unwrap();
5895
5896 let deadline = Instant::now() + Duration::from_secs(10);
5899 let mut outbound_lengths = Vec::new();
5900 let mut return_lengths = Vec::new();
5901 let mut receiver_complete = false;
5902 let mut sender_delivered = false;
5903 loop {
5904 let alice_update = alice.poll_update();
5905 let alice_frames = alice_update.outbound_frames;
5906 assert!(
5907 alice_frames.len() <= 1,
5908 "the mobile bridge must wait for physical TX completion"
5909 );
5910 for frame in alice_frames {
5911 outbound_lengths.push(frame.data.len());
5912 alice.complete_outbound_frame(frame.id, true).unwrap();
5913 bob.receive(MobileMeshRxRecord {
5914 data: frame.data,
5915 rssi_dbm: Some(-55),
5916 lqi: Some(200),
5917 snr_cb: Some(70),
5918 })
5919 .unwrap();
5920 }
5921 sender_delivered |= alice_update
5922 .chat_deliveries
5923 .iter()
5924 .any(|delivery| delivery.state == MobileChatDeliveryState::Acknowledged);
5925 if let Some(batch_id) = alice_update.chat_batch_id {
5926 alice.acknowledge_chat_batch(batch_id).unwrap();
5927 }
5928 let bob_update = bob.poll_update();
5929 for frame in bob_update.outbound_frames.iter().cloned() {
5930 return_lengths.push(frame.data.len());
5931 bob.complete_outbound_frame(frame.id, true).unwrap();
5932 alice
5933 .receive(MobileMeshRxRecord {
5934 data: frame.data,
5935 rssi_dbm: Some(-55),
5936 lqi: Some(200),
5937 snr_cb: Some(70),
5938 })
5939 .unwrap();
5940 }
5941 if let Some(mutation) = bob_update
5942 .chat_mutations
5943 .iter()
5944 .find(|mutation| mutation.complete == Some(true))
5945 {
5946 assert_eq!(mutation.body.as_deref(), Some(body.as_str()));
5947 receiver_complete = true;
5948 }
5949 if let Some(batch_id) = bob_update.chat_batch_id {
5950 bob.acknowledge_chat_batch(batch_id).unwrap();
5951 }
5952 if receiver_complete && sender_delivered {
5953 assert!(
5954 outbound_lengths.len() <= fragment_count * 2 + 4,
5955 "fragment delivery was unexpectedly amplified: {outbound_lengths:?}"
5956 );
5957 break;
5958 }
5959 assert!(
5960 Instant::now() < deadline,
5961 "fragmented chat did not complete at both endpoints; receiver_complete={receiver_complete}, sender_delivered={sender_delivered}, outbound lengths: {outbound_lengths:?}; return lengths: {return_lengths:?}"
5962 );
5963 std::thread::sleep(Duration::from_millis(5));
5964 }
5965 }
5966
5967 #[tokio::test]
5968 async fn ulcp_link_failure_terminates_pending_chat_delivery() {
5969 let directory = tempfile::tempdir().unwrap();
5970 let local_identity = identity(41);
5971 let peer_identity = identity(42);
5972 let store =
5973 MobileCounterStore::new(directory.path().join("failed-send").display().to_string())
5974 .unwrap();
5975 let session = MobileMeshSession::new(local_identity, store).await.unwrap();
5976 session
5977 .register_peers(vec![address(&peer_identity)])
5978 .await
5979 .unwrap();
5980 let batch = session
5981 .compose_text(address(&peer_identity), 17, "will fail".into())
5982 .await
5983 .unwrap();
5984 session.commit_chat_batch(batch.batch_id).await.unwrap();
5985 session.fail_outbound_transmissions().unwrap();
5986
5987 let deadline = Instant::now() + Duration::from_secs(2);
5988 loop {
5989 let update = session.poll_update();
5990 if update
5991 .chat_deliveries
5992 .iter()
5993 .any(|delivery| delivery.state == MobileChatDeliveryState::Failed)
5994 {
5995 break;
5996 }
5997 if let Some(batch_id) = update.chat_batch_id {
5998 session.acknowledge_chat_batch(batch_id).unwrap();
5999 }
6000 assert!(
6001 Instant::now() < deadline,
6002 "link failure did not terminate chat delivery"
6003 );
6004 std::thread::sleep(Duration::from_millis(5));
6005 }
6006 }
6007
6008 #[tokio::test]
6014 async fn mid_batch_failure_suppresses_fragments_queued_behind_the_blocked_one() {
6015 let directory = tempfile::tempdir().unwrap();
6016 let alice_identity = identity(51);
6017 let bob_identity = identity(52);
6018 let alice_store =
6019 MobileCounterStore::new(directory.path().join("mid-alice").display().to_string())
6020 .unwrap();
6021 let bob_store =
6022 MobileCounterStore::new(directory.path().join("mid-bob").display().to_string())
6023 .unwrap();
6024 let alice = MobileMeshSession::new(alice_identity.clone(), alice_store)
6025 .await
6026 .unwrap();
6027 let bob = MobileMeshSession::new(bob_identity.clone(), bob_store)
6028 .await
6029 .unwrap();
6030 let bob_address = address(&bob_identity);
6031 alice
6032 .register_peers(vec![bob_address.clone()])
6033 .await
6034 .unwrap();
6035 bob.register_peers(vec![address(&alice_identity)])
6036 .await
6037 .unwrap();
6038
6039 let warmup = alice
6042 .compose_text(bob_address.clone(), 1, "warmup".to_owned())
6043 .await
6044 .unwrap();
6045 alice.commit_chat_batch(warmup.batch_id).await.unwrap();
6046 let deadline = Instant::now() + Duration::from_secs(10);
6047 loop {
6048 let alice_update = alice.poll_update();
6049 for frame in alice_update.outbound_frames {
6050 alice.complete_outbound_frame(frame.id, true).unwrap();
6051 bob.receive(MobileMeshRxRecord {
6052 data: frame.data,
6053 rssi_dbm: Some(-50),
6054 lqi: None,
6055 snr_cb: Some(80),
6056 })
6057 .unwrap();
6058 }
6059 let acked = alice_update
6060 .chat_deliveries
6061 .iter()
6062 .any(|delivery| delivery.state == MobileChatDeliveryState::Acknowledged);
6063 if let Some(batch_id) = alice_update.chat_batch_id {
6064 alice.acknowledge_chat_batch(batch_id).unwrap();
6065 }
6066 let bob_update = bob.poll_update();
6067 for frame in bob_update.outbound_frames.iter().cloned() {
6068 bob.complete_outbound_frame(frame.id, true).unwrap();
6069 alice
6070 .receive(MobileMeshRxRecord {
6071 data: frame.data,
6072 rssi_dbm: Some(-50),
6073 lqi: None,
6074 snr_cb: Some(80),
6075 })
6076 .unwrap();
6077 }
6078 if let Some(batch_id) = bob_update.chat_batch_id {
6079 bob.acknowledge_chat_batch(batch_id).unwrap();
6080 }
6081 if acked {
6082 break;
6083 }
6084 assert!(Instant::now() < deadline, "warmup exchange never acked");
6085 std::thread::sleep(Duration::from_millis(5));
6086 }
6087
6088 let body = "storm test payload ".repeat(24);
6091 let batch = alice
6092 .compose_text(bob_address, 2, body.clone())
6093 .await
6094 .unwrap();
6095 assert!(batch.mutations[0].fragment_count.unwrap_or(1) > 1);
6096 alice.commit_chat_batch(batch.batch_id).await.unwrap();
6097
6098 let deadline = Instant::now() + Duration::from_secs(10);
6102 loop {
6103 let update = alice.poll_update();
6104 if let Some(batch_id) = update.chat_batch_id {
6105 alice.acknowledge_chat_batch(batch_id).unwrap();
6106 }
6107 if !update.outbound_frames.is_empty() {
6108 break;
6109 }
6110 assert!(
6111 Instant::now() < deadline,
6112 "first fragment never reached the platform"
6113 );
6114 std::thread::sleep(Duration::from_millis(5));
6115 }
6116 let fail_at = Instant::now();
6117 alice.fail_outbound_transmissions().unwrap();
6118
6119 let mut saw_failed = false;
6123 let quiet_deadline = fail_at + Duration::from_millis(1_000);
6124 while Instant::now() < quiet_deadline {
6125 let update = alice.poll_update();
6126 assert!(
6127 update.outbound_frames.is_empty(),
6128 "fragments queued behind a failed batch were still dispatched"
6129 );
6130 saw_failed |= update
6131 .chat_deliveries
6132 .iter()
6133 .any(|delivery| delivery.state == MobileChatDeliveryState::Failed);
6134 if let Some(batch_id) = update.chat_batch_id {
6135 alice.acknowledge_chat_batch(batch_id).unwrap();
6136 }
6137 std::thread::sleep(Duration::from_millis(10));
6138 }
6139 assert!(saw_failed, "batch failure never reported to the transcript");
6140
6141 let retry = alice
6143 .compose_text(address(&bob_identity), 3, "after failure".to_owned())
6144 .await
6145 .unwrap();
6146 alice.commit_chat_batch(retry.batch_id).await.unwrap();
6147 let deadline = Instant::now() + Duration::from_secs(10);
6148 loop {
6149 let update = alice.poll_update();
6150 if let Some(batch_id) = update.chat_batch_id {
6151 alice.acknowledge_chat_batch(batch_id).unwrap();
6152 }
6153 if !update.outbound_frames.is_empty() {
6154 break;
6155 }
6156 assert!(
6157 Instant::now() < deadline,
6158 "transmissions never resumed after failure recovery"
6159 );
6160 std::thread::sleep(Duration::from_millis(5));
6161 }
6162 }
6163
6164 #[tokio::test]
6171 async fn a_channel_group_message_crosses_two_sessions() {
6172 let directory = tempfile::tempdir().unwrap();
6173 let alice_identity = identity(61);
6174 let bob_identity = identity(62);
6175 let alice = MobileMeshSession::new(
6176 alice_identity.clone(),
6177 MobileCounterStore::new(directory.path().join("ch-alice").display().to_string())
6178 .unwrap(),
6179 )
6180 .await
6181 .unwrap();
6182 let bob = MobileMeshSession::new(
6183 bob_identity.clone(),
6184 MobileCounterStore::new(directory.path().join("ch-bob").display().to_string()).unwrap(),
6185 )
6186 .await
6187 .unwrap();
6188
6189 let key = vec![0x5Cu8; 32];
6190 let conversation = crate::channel_conversation_address(key.clone()).unwrap();
6191 assert!(conversation.starts_with("ch:"));
6192 alice.register_channels(vec![key.clone()]).await.unwrap();
6193 bob.register_channels(vec![key]).await.unwrap();
6194
6195 let batch = alice
6196 .compose_text(conversation.clone(), 1, "regroup at the ridge".to_owned())
6197 .await
6198 .unwrap();
6199 assert_eq!(batch.checkpoint.conversation_address, conversation);
6200 alice.commit_chat_batch(batch.batch_id).await.unwrap();
6201
6202 let mut alice_states = Vec::new();
6203 let mut received: Option<MobileChatMutationRecord> = None;
6204 let mut resolution: Option<MobileChatSenderResolutionRecord> = None;
6205 let deadline = Instant::now() + Duration::from_secs(10);
6208 while received.is_none()
6209 || resolution.is_none()
6210 || !alice_states.contains(&MobileChatDeliveryState::Sent)
6211 {
6212 let alice_update = alice.poll_update();
6213 for frame in alice_update.outbound_frames {
6214 alice.complete_outbound_frame(frame.id, true).unwrap();
6215 bob.receive(MobileMeshRxRecord {
6216 data: frame.data,
6217 rssi_dbm: Some(-70),
6218 lqi: None,
6219 snr_cb: Some(60),
6220 })
6221 .unwrap();
6222 }
6223 alice_states.extend(
6224 alice_update
6225 .chat_deliveries
6226 .iter()
6227 .map(|delivery| delivery.state),
6228 );
6229 if let Some(batch_id) = alice_update.chat_batch_id {
6230 alice.acknowledge_chat_batch(batch_id).unwrap();
6231 }
6232
6233 let bob_update = bob.poll_update();
6234 for frame in bob_update.outbound_frames {
6235 bob.complete_outbound_frame(frame.id, true).unwrap();
6236 }
6237 if let Some(record) = bob_update
6238 .chat_mutations
6239 .iter()
6240 .find(|mutation| mutation.body.as_deref() == Some("regroup at the ridge"))
6241 {
6242 received = Some(record.clone());
6243 }
6244 if let Some(record) = bob_update.chat_sender_resolutions.first() {
6245 resolution = Some(record.clone());
6246 }
6247 if let Some(batch_id) = bob_update.chat_batch_id {
6248 bob.acknowledge_chat_batch(batch_id).unwrap();
6249 }
6250 assert!(
6251 Instant::now() < deadline,
6252 "group message incomplete (mutation: {}, resolution: {}, states: {alice_states:?})",
6253 received.is_some(),
6254 resolution.is_some()
6255 );
6256 std::thread::sleep(Duration::from_millis(5));
6257 }
6258
6259 let received = received.unwrap();
6260 assert_eq!(
6261 received.conversation_address.as_deref(),
6262 Some(&conversation[..])
6263 );
6264 assert_eq!(received.direction, Some(MobileChatDirection::Inbound));
6265 assert_eq!(
6268 received.sender_hint.as_deref(),
6269 Some(&hint_of(&alice_identity)[..])
6270 );
6271 assert_eq!(
6272 received.sender_address.as_deref(),
6273 Some(&address(&alice_identity)[..])
6274 );
6275 let rx = received
6276 .rx
6277 .expect("a received frame carries radio metadata");
6278 assert_eq!(rx.rssi_dbm, Some(-70));
6279 assert_eq!(rx.snr_centibels, Some(60));
6280 assert_eq!(rx.hop_count, Some(1));
6283
6284 let resolution = resolution.unwrap();
6285 assert_eq!(resolution.conversation_address, conversation);
6286 assert_eq!(resolution.sender_hint, hint_of(&alice_identity));
6287 assert_eq!(resolution.sender_address, address(&alice_identity));
6288
6289 assert!(alice_states.contains(&MobileChatDeliveryState::Sent));
6291 assert!(!alice_states.contains(&MobileChatDeliveryState::Acknowledged));
6292 }
6293
6294 #[tokio::test]
6304 async fn emergency_chat_is_sent_readable_and_encrypted_copies_are_refused() {
6305 use umsh_core::{MicSize, PacketBuilder, PacketHeader};
6306 use umsh_crypto::{
6307 CryptoEngine, PairwiseKeys,
6308 software::{SoftwareAes, SoftwareSha256},
6309 };
6310
6311 let directory = tempfile::tempdir().unwrap();
6312 let alice_identity = identity(71);
6313 let alice = MobileMeshSession::new(
6314 alice_identity.clone(),
6315 MobileCounterStore::new(directory.path().join("sos-alice").display().to_string())
6316 .unwrap(),
6317 )
6318 .await
6319 .unwrap();
6320 let bob = MobileMeshSession::new(
6321 identity(72),
6322 MobileCounterStore::new(directory.path().join("sos-bob").display().to_string())
6323 .unwrap(),
6324 )
6325 .await
6326 .unwrap();
6327
6328 let key = crate::inspect_channel_name(crate::EMERGENCY_CHANNEL_NAME.to_owned())
6329 .unwrap()
6330 .key;
6331 let conversation = crate::channel_conversation_address(key.clone()).unwrap();
6332 alice.register_channels(vec![key.clone()]).await.unwrap();
6333 bob.register_channels(vec![key.clone()]).await.unwrap();
6334
6335 let body = "tower down at mile 14";
6336 let batch = alice
6337 .compose_text(conversation.clone(), 1, body.to_owned())
6338 .await
6339 .unwrap();
6340 alice.commit_chat_batch(batch.batch_id).await.unwrap();
6341
6342 let engine = CryptoEngine::new(SoftwareAes, SoftwareSha256);
6346 let channel_keys =
6347 engine.derive_channel_keys(&crate::channel_key_from_bytes(&key).unwrap());
6348 let deadline = Instant::now() + Duration::from_secs(10);
6349 let mut frame = None;
6350 while frame.is_none() {
6351 for outbound in alice.poll_update().outbound_frames {
6352 alice.complete_outbound_frame(outbound.id, true).unwrap();
6353 let header = match PacketHeader::parse(&outbound.data) {
6354 Ok(header) => header,
6355 Err(_) => continue,
6356 };
6357 if header.channel == Some(channel_keys.channel_id)
6358 && header.packet_type() == umsh_core::PacketType::Multicast
6359 {
6360 frame = Some((outbound.data, header));
6361 }
6362 }
6363 assert!(Instant::now() < deadline, "no emergency frame was sent");
6364 std::thread::sleep(Duration::from_millis(5));
6365 }
6366 let (frame, header) = frame.unwrap();
6367
6368 let sec_info = header.sec_info.expect("a multicast frame carries SECINFO");
6370 assert!(
6371 !sec_info.scf.encrypted(),
6372 "emergency chat must be readable by any node in range"
6373 );
6374 assert!(
6375 matches!(header.source, umsh_core::SourceAddrRef::FullKeyAt { .. }),
6376 "emergency chat must name its sender outright"
6377 );
6378
6379 let payload = {
6385 let mut opened = frame.clone();
6386 let range = engine
6387 .open_packet(
6388 &mut opened,
6389 &header,
6390 &PairwiseKeys {
6391 k_enc: channel_keys.k_enc,
6392 k_mic: channel_keys.k_mic,
6393 },
6394 )
6395 .unwrap();
6396 opened[range].to_vec()
6397 };
6398 assert!(
6399 sec_info.frame_counter > 0,
6400 "the forged copy needs a lower counter than the genuine one"
6401 );
6402 let alice_key = decode_peer(&address(&alice_identity)).unwrap();
6403 let mut buf = [0u8; 256];
6404 let mut forged = PacketBuilder::new(&mut buf)
6405 .multicast(channel_keys.channel_id)
6406 .source_full(&alice_key)
6407 .frame_counter(sec_info.frame_counter - 1)
6408 .encrypted()
6409 .mic_size(MicSize::Mic16)
6410 .payload(&payload)
6411 .build()
6412 .unwrap();
6413 engine
6414 .seal_packet(
6415 &mut forged,
6416 &PairwiseKeys {
6417 k_enc: channel_keys.k_enc,
6418 k_mic: channel_keys.k_mic,
6419 },
6420 )
6421 .unwrap();
6422 bob.receive(MobileMeshRxRecord {
6423 data: forged.as_bytes().to_vec(),
6424 rssi_dbm: Some(-70),
6425 lqi: None,
6426 snr_cb: Some(60),
6427 })
6428 .unwrap();
6429
6430 let mut refusal = None;
6431 let deadline = Instant::now() + Duration::from_secs(10);
6432 while refusal.is_none() {
6433 let update = bob.poll_update();
6434 assert!(
6435 !update
6436 .chat_mutations
6437 .iter()
6438 .any(|mutation| mutation.body.as_deref() == Some(body)),
6439 "an encrypted emergency frame reached the transcript"
6440 );
6441 refusal = update
6442 .chat_diagnostics
6443 .iter()
6444 .find(|line| line.contains("emergency-channel"))
6445 .cloned();
6446 if let Some(batch_id) = update.chat_batch_id {
6447 bob.acknowledge_chat_batch(batch_id).unwrap();
6448 }
6449 assert!(
6450 Instant::now() < deadline,
6451 "the encrypted copy was not refused"
6452 );
6453 std::thread::sleep(Duration::from_millis(5));
6454 }
6455 assert!(refusal.unwrap().contains("encrypted"));
6456
6457 bob.receive(MobileMeshRxRecord {
6459 data: frame,
6460 rssi_dbm: Some(-70),
6461 lqi: None,
6462 snr_cb: Some(60),
6463 })
6464 .unwrap();
6465 let deadline = Instant::now() + Duration::from_secs(10);
6466 let mut received = false;
6467 while !received {
6468 let update = bob.poll_update();
6469 received = update
6470 .chat_mutations
6471 .iter()
6472 .any(|mutation| mutation.body.as_deref() == Some(body));
6473 if let Some(batch_id) = update.chat_batch_id {
6474 bob.acknowledge_chat_batch(batch_id).unwrap();
6475 }
6476 assert!(Instant::now() < deadline, "the readable copy never arrived");
6477 std::thread::sleep(Duration::from_millis(5));
6478 }
6479 }
6480
6481 #[tokio::test]
6502 async fn a_dropped_emergency_fragment_is_repaired() {
6503 let directory = tempfile::tempdir().unwrap();
6504 let alice = MobileMeshSession::new(
6505 identity(73),
6506 MobileCounterStore::new(directory.path().join("sos-frag-a").display().to_string())
6507 .unwrap(),
6508 )
6509 .await
6510 .unwrap();
6511 let bob = MobileMeshSession::new(
6512 identity(74),
6513 MobileCounterStore::new(directory.path().join("sos-frag-b").display().to_string())
6514 .unwrap(),
6515 )
6516 .await
6517 .unwrap();
6518
6519 let key = crate::inspect_channel_name(crate::EMERGENCY_CHANNEL_NAME.to_owned())
6520 .unwrap()
6521 .key;
6522 let conversation = crate::channel_conversation_address(key.clone()).unwrap();
6523 alice.register_channels(vec![key.clone()]).await.unwrap();
6524 bob.register_channels(vec![key]).await.unwrap();
6525
6526 let body: String = (0..600)
6527 .map(|index| char::from(b'a' + (index % 26) as u8))
6528 .collect();
6529 let batch = alice
6530 .compose_text(conversation.clone(), 1, body.clone())
6531 .await
6532 .unwrap();
6533 assert!(batch.mutations[0].fragment_count.unwrap() > 1);
6534 let archives: std::collections::HashMap<(u8, Option<u8>), Vec<u8>> = batch
6537 .archives
6538 .iter()
6539 .map(|archive| {
6540 (
6541 (archive.message_id, archive.fragment_index),
6542 archive.payload.clone(),
6543 )
6544 })
6545 .collect();
6546 alice.commit_chat_batch(batch.batch_id).await.unwrap();
6547
6548 let mut sent = 0;
6549 let mut repairs = 0;
6550 let mut assembled: Option<String> = None;
6551 let deadline = Instant::now() + Duration::from_secs(40);
6552 while assembled.as_deref() != Some(body.as_str()) {
6553 let alice_update = alice.poll_update();
6554 for frame in alice_update.outbound_frames {
6555 alice.complete_outbound_frame(frame.id, true).unwrap();
6556 sent += 1;
6557 if sent == 2 {
6560 continue;
6561 }
6562 bob.receive(MobileMeshRxRecord {
6563 data: frame.data,
6564 rssi_dbm: Some(-70),
6565 lqi: None,
6566 snr_cb: Some(60),
6567 })
6568 .unwrap();
6569 }
6570 for lookup in &alice_update.chat_archive_lookups {
6571 match archives.get(&(lookup.message_id, lookup.fragment_index)) {
6572 Some(payload) => alice
6573 .apply_chat_archive_result(
6574 lookup.request_id,
6575 MobileChatArchiveResultKind::Found,
6576 payload.clone(),
6577 )
6578 .unwrap(),
6579 None => alice
6580 .apply_chat_archive_result(
6581 lookup.request_id,
6582 MobileChatArchiveResultKind::Unknown,
6583 Vec::new(),
6584 )
6585 .unwrap(),
6586 }
6587 }
6588 if let Some(batch_id) = alice_update.chat_batch_id {
6589 alice.acknowledge_chat_batch(batch_id).unwrap();
6590 }
6591
6592 let bob_update = bob.poll_update();
6593 for frame in bob_update.outbound_frames {
6594 bob.complete_outbound_frame(frame.id, true).unwrap();
6595 repairs += 1;
6596 alice
6597 .receive(MobileMeshRxRecord {
6598 data: frame.data,
6599 rssi_dbm: Some(-70),
6600 lqi: None,
6601 snr_cb: Some(60),
6602 })
6603 .unwrap();
6604 }
6605 for mutation in &bob_update.chat_mutations {
6606 if let Some(text) = mutation.body.as_deref() {
6607 assembled = Some(text.to_owned());
6608 }
6609 }
6610 if let Some(batch_id) = bob_update.chat_batch_id {
6611 bob.acknowledge_chat_batch(batch_id).unwrap();
6612 }
6613 assert!(
6614 Instant::now() < deadline,
6615 "a dropped emergency fragment was never repaired \
6616 ({repairs} repair frame(s), assembled {:?})",
6617 assembled.as_ref().map(|text| text.len())
6618 );
6619 std::thread::sleep(Duration::from_millis(5));
6620 }
6621 assert!(repairs > 0, "the message assembled without any repair");
6622 }
6623
6624 #[tokio::test]
6632 async fn a_relayed_copy_of_our_own_group_message_is_not_transcribed() {
6633 let directory = tempfile::tempdir().unwrap();
6634 let identity = identity(67);
6635 let session = MobileMeshSession::new(
6636 identity.clone(),
6637 MobileCounterStore::new(directory.path().join("echo").display().to_string()).unwrap(),
6638 )
6639 .await
6640 .unwrap();
6641
6642 let key = vec![0x3Eu8; 32];
6643 let conversation = crate::channel_conversation_address(key.clone()).unwrap();
6644 session.register_channels(vec![key]).await.unwrap();
6645
6646 let batch = session
6647 .compose_text(conversation.clone(), 1, "anyone out there".to_owned())
6648 .await
6649 .unwrap();
6650 session.commit_chat_batch(batch.batch_id).await.unwrap();
6651
6652 let deadline = Instant::now() + Duration::from_secs(10);
6655 let mut echoed = 0;
6656 let mut inbound = Vec::new();
6657 while echoed == 0
6658 || Instant::now() < deadline.min(Instant::now() + Duration::from_millis(1))
6659 {
6660 let update = session.poll_update();
6661 for frame in update.outbound_frames {
6662 session.complete_outbound_frame(frame.id, true).unwrap();
6663 session
6664 .receive(MobileMeshRxRecord {
6665 data: frame.data,
6666 rssi_dbm: Some(-60),
6667 lqi: None,
6668 snr_cb: Some(70),
6669 })
6670 .unwrap();
6671 echoed += 1;
6672 }
6673 inbound.extend(
6674 update
6675 .chat_mutations
6676 .iter()
6677 .filter(|mutation| mutation.direction == Some(MobileChatDirection::Inbound))
6678 .cloned(),
6679 );
6680 if let Some(batch_id) = update.chat_batch_id {
6681 session.acknowledge_chat_batch(batch_id).unwrap();
6682 }
6683 if echoed > 0 && Instant::now() > deadline {
6684 break;
6685 }
6686 std::thread::sleep(Duration::from_millis(5));
6687 if echoed > 0 {
6688 for _ in 0..20 {
6690 let update = session.poll_update();
6691 inbound.extend(
6692 update
6693 .chat_mutations
6694 .iter()
6695 .filter(|mutation| {
6696 mutation.direction == Some(MobileChatDirection::Inbound)
6697 })
6698 .cloned(),
6699 );
6700 if let Some(batch_id) = update.chat_batch_id {
6701 session.acknowledge_chat_batch(batch_id).unwrap();
6702 }
6703 std::thread::sleep(Duration::from_millis(5));
6704 }
6705 break;
6706 }
6707 }
6708
6709 assert!(echoed > 0, "the session never transmitted the message");
6710 assert!(
6711 inbound.is_empty(),
6712 "our own relayed message was transcribed as inbound: {inbound:?}"
6713 );
6714 }
6715
6716 #[tokio::test]
6724 async fn a_message_addressed_to_ourselves_is_transcribed() {
6725 let directory = tempfile::tempdir().unwrap();
6726 let identity = identity(71);
6727 let session = MobileMeshSession::new(
6728 identity.clone(),
6729 MobileCounterStore::new(directory.path().join("self").display().to_string()).unwrap(),
6730 )
6731 .await
6732 .unwrap();
6733
6734 let own_address = address(&identity);
6735 session
6736 .register_peers(vec![own_address.clone()])
6737 .await
6738 .unwrap();
6739
6740 let body = "note to self".to_owned();
6741 let batch = session
6742 .compose_text(own_address, 7, body.clone())
6743 .await
6744 .unwrap();
6745 session.commit_chat_batch(batch.batch_id).await.unwrap();
6746
6747 let deadline = Instant::now() + Duration::from_secs(10);
6750 let mut inbound = Vec::new();
6751 while Instant::now() < deadline {
6752 let update = session.poll_update();
6753 for frame in update.outbound_frames {
6754 session.complete_outbound_frame(frame.id, true).unwrap();
6755 session
6756 .receive(MobileMeshRxRecord {
6757 data: frame.data,
6758 rssi_dbm: Some(-60),
6759 lqi: None,
6760 snr_cb: Some(70),
6761 })
6762 .unwrap();
6763 }
6764 inbound.extend(
6765 update
6766 .chat_mutations
6767 .iter()
6768 .filter(|mutation| mutation.direction == Some(MobileChatDirection::Inbound))
6769 .cloned(),
6770 );
6771 if let Some(batch_id) = update.chat_batch_id {
6772 session.acknowledge_chat_batch(batch_id).unwrap();
6773 }
6774 if !inbound.is_empty() {
6775 break;
6776 }
6777 std::thread::sleep(Duration::from_millis(5));
6778 }
6779
6780 assert!(
6781 inbound
6782 .iter()
6783 .any(|mutation| mutation.body.as_deref() == Some(body.as_str())),
6784 "the message we sent ourselves never arrived: {inbound:?}"
6785 );
6786 }
6787
6788 #[tokio::test]
6792 async fn composing_to_an_unheld_channel_is_refused() {
6793 let directory = tempfile::tempdir().unwrap();
6794 let session = MobileMeshSession::new(
6795 identity(63),
6796 MobileCounterStore::new(directory.path().join("unheld").display().to_string()).unwrap(),
6797 )
6798 .await
6799 .unwrap();
6800
6801 let key = vec![0x77u8; 32];
6802 let conversation = crate::channel_conversation_address(key.clone()).unwrap();
6803 assert_eq!(
6804 session
6805 .compose_text(conversation.clone(), 1, "hello".to_owned())
6806 .await,
6807 Err(MobileMeshError::UnknownConversation)
6808 );
6809
6810 session.register_channels(vec![key.clone()]).await.unwrap();
6811 let batch = session
6812 .compose_text(conversation.clone(), 2, "hello".to_owned())
6813 .await
6814 .expect("a joined channel composes");
6815 session
6818 .reject_chat_batch(batch.batch_id, Vec::new())
6819 .await
6820 .unwrap();
6821
6822 session.remove_channels(vec![key]).await.unwrap();
6823 assert_eq!(
6824 session
6825 .compose_text(conversation, 3, "hello".to_owned())
6826 .await,
6827 Err(MobileMeshError::UnknownConversation)
6828 );
6829 }
6830
6831 #[tokio::test]
6834 async fn a_malformed_conversation_address_is_refused() {
6835 let directory = tempfile::tempdir().unwrap();
6836 let session = MobileMeshSession::new(
6837 identity(64),
6838 MobileCounterStore::new(directory.path().join("malformed").display().to_string())
6839 .unwrap(),
6840 )
6841 .await
6842 .unwrap();
6843 for address in ["ch:not-hex", "ch:0011", "definitely not base58 !!"] {
6844 assert_eq!(
6845 session
6846 .compose_text(address.to_owned(), 1, "hello".to_owned())
6847 .await,
6848 Err(MobileMeshError::UnknownConversation),
6849 "{address} should not resolve to a conversation"
6850 );
6851 }
6852 }
6853
6854 #[tokio::test]
6857 async fn direct_chat_still_delivers_and_now_carries_radio_metadata() {
6858 let directory = tempfile::tempdir().unwrap();
6859 let alice_identity = identity(65);
6860 let bob_identity = identity(66);
6861 let alice = MobileMeshSession::new(
6862 alice_identity.clone(),
6863 MobileCounterStore::new(directory.path().join("dm-alice").display().to_string())
6864 .unwrap(),
6865 )
6866 .await
6867 .unwrap();
6868 let bob = MobileMeshSession::new(
6869 bob_identity.clone(),
6870 MobileCounterStore::new(directory.path().join("dm-bob").display().to_string()).unwrap(),
6871 )
6872 .await
6873 .unwrap();
6874 let bob_address = address(&bob_identity);
6875 alice
6876 .register_peers(vec![bob_address.clone()])
6877 .await
6878 .unwrap();
6879 bob.register_peers(vec![address(&alice_identity)])
6880 .await
6881 .unwrap();
6882
6883 let batch = alice
6884 .compose_text(bob_address.clone(), 1, "still here".to_owned())
6885 .await
6886 .unwrap();
6887 assert_eq!(batch.checkpoint.conversation_address, bob_address);
6888 alice.commit_chat_batch(batch.batch_id).await.unwrap();
6889
6890 let deadline = Instant::now() + Duration::from_secs(10);
6891 let received = loop {
6892 let alice_update = alice.poll_update();
6893 for frame in alice_update.outbound_frames {
6894 alice.complete_outbound_frame(frame.id, true).unwrap();
6895 bob.receive(MobileMeshRxRecord {
6896 data: frame.data,
6897 rssi_dbm: Some(-55),
6898 lqi: None,
6899 snr_cb: Some(75),
6900 })
6901 .unwrap();
6902 }
6903 if let Some(batch_id) = alice_update.chat_batch_id {
6904 alice.acknowledge_chat_batch(batch_id).unwrap();
6905 }
6906 let bob_update = bob.poll_update();
6907 for frame in bob_update.outbound_frames {
6908 bob.complete_outbound_frame(frame.id, true).unwrap();
6909 alice
6910 .receive(MobileMeshRxRecord {
6911 data: frame.data,
6912 rssi_dbm: Some(-55),
6913 lqi: None,
6914 snr_cb: Some(75),
6915 })
6916 .unwrap();
6917 }
6918 let found = bob_update
6919 .chat_mutations
6920 .iter()
6921 .find(|mutation| mutation.body.as_deref() == Some("still here"))
6922 .cloned();
6923 if let Some(batch_id) = bob_update.chat_batch_id {
6924 bob.acknowledge_chat_batch(batch_id).unwrap();
6925 }
6926 if let Some(found) = found {
6927 break found;
6928 }
6929 assert!(
6930 Instant::now() < deadline,
6931 "the direct message never arrived"
6932 );
6933 std::thread::sleep(Duration::from_millis(5));
6934 };
6935
6936 assert_eq!(
6937 received.conversation_address.as_deref(),
6938 Some(&address(&alice_identity)[..])
6939 );
6940 assert_eq!(received.sender_hint, None);
6943 assert_eq!(
6944 received.sender_address.as_deref(),
6945 Some(&address(&alice_identity)[..])
6946 );
6947 let rx = received
6948 .rx
6949 .expect("a received frame carries radio metadata");
6950 assert_eq!(rx.rssi_dbm, Some(-55));
6951 assert_eq!(rx.snr_centibels, Some(75));
6952 assert!(rx.source_authenticated);
6953 }
6954
6955 fn assert_batch_id_matches_events(update: &MobileMeshSessionUpdateRecord) {
6963 let has_events = !update.chat_mutations.is_empty()
6964 || !update.chat_deliveries.is_empty()
6965 || !update.chat_archive_lookups.is_empty()
6966 || !update.chat_sender_resolutions.is_empty()
6967 || !update.chat_diagnostics.is_empty();
6968 assert_eq!(
6969 update.chat_batch_id.is_some(),
6970 has_events,
6971 "batch id {:?} disagrees with the batch's contents",
6972 update.chat_batch_id
6973 );
6974 }
6975
6976 #[tokio::test]
6982 async fn a_fragmented_channel_group_message_arrives_whole() {
6983 let directory = tempfile::tempdir().unwrap();
6984 let alice_identity = identity(71);
6985 let bob_identity = identity(72);
6986 let alice = MobileMeshSession::new(
6987 alice_identity.clone(),
6988 MobileCounterStore::new(directory.path().join("frag-alice").display().to_string())
6989 .unwrap(),
6990 )
6991 .await
6992 .unwrap();
6993 let bob = MobileMeshSession::new(
6994 bob_identity.clone(),
6995 MobileCounterStore::new(directory.path().join("frag-bob").display().to_string())
6996 .unwrap(),
6997 )
6998 .await
6999 .unwrap();
7000
7001 let key = vec![0x9Au8; 32];
7002 let conversation = crate::channel_conversation_address(key.clone()).unwrap();
7003 alice.register_channels(vec![key.clone()]).await.unwrap();
7004 bob.register_channels(vec![key]).await.unwrap();
7005
7006 let body: String = (0..600)
7008 .map(|index| char::from(b'a' + (index % 26) as u8))
7009 .collect();
7010 let batch = alice
7011 .compose_text(conversation.clone(), 1, body.clone())
7012 .await
7013 .unwrap();
7014 let fragments = batch.mutations[0].fragment_count.unwrap();
7015 assert!(
7016 fragments > 1,
7017 "the test body must fragment, got {fragments}"
7018 );
7019 alice.commit_chat_batch(batch.batch_id).await.unwrap();
7020
7021 let mut transmitted = 0;
7022 let mut repairs = 0;
7023 let mut assembled: Option<String> = None;
7024 let deadline = Instant::now() + Duration::from_secs(15);
7025 while assembled.as_deref() != Some(body.as_str()) {
7026 let alice_update = alice.poll_update();
7027 assert_batch_id_matches_events(&alice_update);
7028 for frame in alice_update.outbound_frames {
7029 alice.complete_outbound_frame(frame.id, true).unwrap();
7030 transmitted += 1;
7031 bob.receive(MobileMeshRxRecord {
7032 data: frame.data,
7033 rssi_dbm: Some(-70),
7034 lqi: None,
7035 snr_cb: Some(60),
7036 })
7037 .unwrap();
7038 }
7039 if let Some(batch_id) = alice_update.chat_batch_id {
7040 alice.acknowledge_chat_batch(batch_id).unwrap();
7041 }
7042
7043 let bob_update = bob.poll_update();
7044 assert_batch_id_matches_events(&bob_update);
7045 for frame in bob_update.outbound_frames {
7046 bob.complete_outbound_frame(frame.id, true).unwrap();
7047 repairs += 1;
7050 alice
7051 .receive(MobileMeshRxRecord {
7052 data: frame.data,
7053 rssi_dbm: Some(-70),
7054 lqi: None,
7055 snr_cb: Some(60),
7056 })
7057 .unwrap();
7058 }
7059 for mutation in &bob_update.chat_mutations {
7060 if let Some(text) = mutation.body.as_deref() {
7061 assembled = Some(text.to_owned());
7062 }
7063 }
7064 if let Some(batch_id) = bob_update.chat_batch_id {
7065 bob.acknowledge_chat_batch(batch_id).unwrap();
7066 }
7067 assert!(
7068 Instant::now() < deadline,
7069 "fragmented group message never completed \
7070 ({transmitted} frame(s) transmitted of {fragments}, \
7071 assembled {:?})",
7072 assembled.as_ref().map(|text| text.len())
7073 );
7074 std::thread::sleep(Duration::from_millis(5));
7075 }
7076
7077 assert!(
7083 transmitted >= usize::from(fragments),
7084 "only {transmitted} of {fragments} fragment(s) were transmitted"
7085 );
7086 assert_eq!(repairs, 0, "the message needed {repairs} repair request(s)");
7087 }
7088}