Skip to main content

offline_protocol/protocol/
config_accessors.rs

1//! Configuration accessors, diagnostics, and service registration.
2
3use super::mesh_relay::MeshRelayStats;
4use super::{
5    lock_shared_state, OfflineProtocol, PendingQueueMetrics, ProtocolState, KNOWN_PEER_TTL_SECS,
6    MEDIA_TRANSFER_STALE_TIMEOUT_SECS,
7};
8use crate::events::Event;
9use crate::file_transfer::FileTransferManager;
10use crate::{Error, ProtocolConfig, Result, TransportManager};
11use offline_protocol_core::{MessageId, ServiceDescriptor};
12use offline_protocol_mls::MlsManager;
13use offline_protocol_reliability::{
14    AckConfig, AckManager, Deduplicator, DeduplicatorConfig, DeduplicatorStats, RetryConfig,
15    RetryQueue,
16};
17use offline_protocol_router::DorsConfig;
18use offline_protocol_services::MeshServices;
19use std::sync::{Arc, RwLock};
20use std::time::Duration as StdDuration;
21use tracing::{error, warn};
22
23impl OfflineProtocol {
24    /// Gets the current protocol state.
25    pub fn state(&self) -> ProtocolState {
26        let Ok(state) = lock_shared_state(&self.shared_state) else {
27            error!("Failed to lock shared state in state()");
28            return ProtocolState::Stopped;
29        };
30        state.state
31    }
32
33    /// Gets the configuration.
34    pub fn config(&self) -> &ProtocolConfig {
35        &self.config
36    }
37
38    /// Gets a reference to the mesh services registry.
39    pub fn mesh_services(&self) -> &MeshServices {
40        &self.mesh_services
41    }
42
43    /// Gets a mutable reference to the transport manager.
44    ///
45    /// This allows external code (e.g., FFI) to add transports dynamically.
46    pub fn transport_manager_mut(&mut self) -> &mut TransportManager {
47        &mut self.transport_manager
48    }
49
50    /// Gets a reference to the transport manager.
51    pub fn transport_manager(&self) -> &TransportManager {
52        &self.transport_manager
53    }
54
55    /// Returns a mutable reference to the file transfer manager.
56    pub fn file_transfer_manager_mut(&mut self) -> &mut FileTransferManager {
57        &mut self.file_transfer_manager
58    }
59
60    /// Returns a reference to the file transfer manager.
61    pub fn file_transfer_manager(&self) -> &FileTransferManager {
62        &self.file_transfer_manager
63    }
64
65    /// Gets access to the MLS manager for advanced operations.
66    ///
67    /// Returns `None` if MLS is not initialized.
68    pub fn mls_manager(&self) -> Option<&Arc<RwLock<MlsManager>>> {
69        self.mls_manager.as_ref()
70    }
71
72    /// Updates the DORS configuration at runtime.
73    ///
74    /// This replaces the current DORS selector configuration with the provided config.
75    pub fn update_dors_config(&mut self, config: DorsConfig) {
76        self.transport_manager.update_selector_config(config);
77    }
78
79    /// Updates the ACK configuration at runtime.
80    ///
81    /// Note: This affects new ACK registrations; existing pending ACKs keep their original timeout.
82    /// Validated the same way [`Self::update_retry_config`] is — by building
83    /// the candidate configuration and running the real validator — rather than
84    /// by repeating its checks inline. Hand-rolled copies drift: a constraint
85    /// added to `ProtocolConfig::validate` for a new `AckConfig` field would be
86    /// enforced at construction and silently skipped on the runtime-update path.
87    pub fn update_ack_config(&mut self, config: AckConfig) -> crate::Result<()> {
88        let mut candidate = self.config.clone();
89        candidate.reliability.ack = config.clone();
90        candidate.validate()?;
91        self.ack_manager = AckManager::with_config(config.clone());
92        self.config.reliability.ack = config;
93        Ok(())
94    }
95
96    /// Updates the retry configuration at runtime.
97    ///
98    /// Note: This affects new retry entries; existing entries keep their original timing.
99    pub fn update_retry_config(&mut self, config: RetryConfig) -> crate::Result<()> {
100        let mut candidate = self.config.clone();
101        candidate.reliability.retry = config.clone();
102        candidate.validate()?;
103        self.retry_queue = RetryQueue::with_config(config.clone());
104        self.config.reliability.retry = config;
105        self.recompute_next_pending_message_expiry();
106        self.cleanup_expired_pending_messages();
107        Ok(())
108    }
109
110    /// Updates the deduplication configuration at runtime.
111    ///
112    /// Note: This clears the deduplication cache and applies the new config.
113    ///
114    /// Validated the same way its two siblings are, and for a reason that only
115    /// appeared once they were. Both of those run the *whole*
116    /// `ProtocolConfig::validate`, which checks the Bloom parameters this
117    /// method installs — so while this one accepted anything, a configuration
118    /// it had already stored could make a perfectly valid
119    /// [`Self::update_retry_config`] fail, complaining about a Bloom filter the
120    /// caller never mentioned. `Deduplicator::with_config` fails safe on those
121    /// values rather than panicking, so the cost was a confusing rejection
122    /// rather than a crash — but a rejection attributed to the wrong call is
123    /// its own kind of expensive.
124    pub fn update_dedup_config(&mut self, config: DeduplicatorConfig) -> crate::Result<()> {
125        let mut candidate = self.config.clone();
126        candidate.reliability.dedup = config.clone();
127        candidate.validate()?;
128        self.deduplicator = Deduplicator::with_config(config.clone());
129        self.config.reliability.dedup = config;
130        Ok(())
131    }
132
133    /// Gets deduplicator statistics for monitoring.
134    pub fn deduplicator_stats(&self) -> DeduplicatorStats {
135        self.deduplicator.stats()
136    }
137
138    /// Gets pending encrypted message queue counters and gauges.
139    pub fn pending_queue_metrics(&self) -> PendingQueueMetrics {
140        self.pending_queue.metrics().clone()
141    }
142
143    /// Gets the current ACK manager statistics.
144    pub fn pending_ack_count(&self) -> usize {
145        self.ack_manager.pending_count()
146    }
147
148    /// Gets the current retry queue statistics.
149    pub fn retry_queue_size(&self) -> usize {
150        self.retry_queue.len()
151    }
152
153    /// Returns whether `peer_id` is currently tracked as a discovered
154    /// neighbor. Populated by [`Self::on_neighbor_discovered`] and cleared
155    /// by [`Self::on_neighbor_lost`], by the periodic TTL sweep when the
156    /// peer has not been re-seen for `KNOWN_PEER_TTL_SECS`, or by
157    /// least-recently-seen eviction when tracking is at capacity — so this
158    /// can flip to false without any explicit loss signal. Blocked users
159    /// are never tracked.
160    pub fn is_known_peer(&self, peer_id: &str) -> bool {
161        self.known_peers.contains_key(peer_id)
162    }
163
164    /// Reports how much traffic this device is carrying for other people.
165    ///
166    /// Useful for showing a user what their device is contributing, and for
167    /// spotting a neighborhood in trouble: a rising `rate_deferred` means
168    /// forwarding is hitting its ceiling, and `peer_rate_limited` means a
169    /// single neighbor is sending more than its share. `dropped_for_capacity`
170    /// is expected to stay at zero — see [`MeshRelayStats::dropped_for_capacity`].
171    pub fn mesh_relay_stats(&self) -> MeshRelayStats {
172        let counters = self.mesh_relay.counters();
173        MeshRelayStats {
174            forwarded: counters.forwarded,
175            transmissions: counters.transmissions,
176            queued: counters.queued,
177            awaiting_transmission: self.mesh_relay.pending_len(),
178            duplicates_suppressed: counters.duplicates_suppressed,
179            covered_by_a_neighbor: counters.cancelled_by_duplicate,
180            peer_rate_limited: counters.peer_rate_limited,
181            rate_deferred: counters.rate_deferred,
182            hop_limit_reached: counters.hop_limit_reached,
183            reach_clamped: counters.ttl_clamped,
184            dropped_for_capacity: self.mesh_relay.seen_capacity_evictions(),
185        }
186    }
187
188    /// Returns a mutable reference to the retry queue (test-only).
189    #[cfg(test)]
190    pub(crate) fn retry_queue_mut(&mut self) -> &mut offline_protocol_reliability::RetryQueue {
191        &mut self.retry_queue
192    }
193
194    /// Cleans up expired entries from deduplicator, retry queue, outbox, and ack manager.
195    /// Also checks for Internet availability transitions to sync groups with relay.
196    pub(crate) fn cleanup_expired_entries(&mut self) {
197        self.deduplicator.cleanup_expired();
198        self.retry_queue.cleanup_expired();
199        self.mesh_relay.maintain(std::time::Instant::now());
200        self.prune_stale_known_peers(std::time::Instant::now());
201        self.cleanup_expired_pending_messages_if_due();
202        self.cleanup_outbox();
203        self.mesh_services.cleanup_expired();
204        self.cleanup_group_message_dedup();
205        self.check_epoch_forks();
206        self.check_leave_election_timeouts();
207        self.check_relay_group_sync();
208        let stale_transfers = self
209            .file_transfer_manager
210            .cleanup_stale_transfers(StdDuration::from_secs(MEDIA_TRANSFER_STALE_TIMEOUT_SECS));
211        for stale in stale_transfers {
212            self.pending_media_metadata.remove(&stale.file_id);
213            // Like the resource-limit drops, a stale transfer is
214            // unrecoverable (its chunks were ACKed and will not be
215            // retransmitted) — tell the app instead of going silent.
216            self.emit_event(Event::file_receive_failed(
217                stale.file_id,
218                stale.file_name,
219                stale.sender,
220                "stale_timeout".to_string(),
221            ));
222        }
223        self.cleanup_stale_media_state(StdDuration::from_secs(MEDIA_TRANSFER_STALE_TIMEOUT_SECS));
224        // Prune old timed-out ACKs that weren't cleaned up by normal retry flow
225        self.ack_manager
226            .prune_old_timeouts(std::time::Duration::from_secs(300)); // 5 minutes
227    }
228
229    /// Evicts known peers not re-seen within `KNOWN_PEER_TTL_SECS`.
230    ///
231    /// This is the only removal path for peers on carriers without a
232    /// disconnect signal (Internet, Reticulum, Nostr, WiFi Direct
233    /// message-path senders). `now` is injected for test determinism.
234    pub(crate) fn prune_stale_known_peers(&mut self, now: std::time::Instant) {
235        let ttl = StdDuration::from_secs(KNOWN_PEER_TTL_SECS);
236        let stale: Vec<String> = self
237            .known_peers
238            .iter()
239            .filter(|(_, seen)| now.saturating_duration_since(**seen) > ttl)
240            .map(|(id, _)| id.clone())
241            .collect();
242        if stale.is_empty() {
243            return;
244        }
245        tracing::debug!(
246            count = stale.len(),
247            ttl_secs = KNOWN_PEER_TTL_SECS,
248            "Evicting stale known peers"
249        );
250        for peer_id in &stale {
251            self.evict_known_peer(peer_id);
252        }
253    }
254
255    // ========================================================================
256    // SERVICE DISCOVERY & REQUEST/RESPONSE
257    // ========================================================================
258
259    /// Registers a local service that this node offers.
260    pub fn register_service(&mut self, descriptor: ServiceDescriptor) -> Result<()> {
261        self.mesh_services
262            .register_service(descriptor)
263            .map_err(Error::Service)
264    }
265
266    /// Unregisters a local service. Returns true if the service was found and removed.
267    pub fn unregister_service(&mut self, service_id: &str) -> Result<bool> {
268        self.mesh_services
269            .unregister_service(service_id)
270            .map_err(Error::Service)
271    }
272
273    /// Broadcasts a service discovery query to all known peers.
274    /// Returns a query_id. Responses arrive asynchronously as `ServiceDiscovered` events.
275    ///
276    /// **Note:** Discovery responses currently travel only one hop back (to the
277    /// immediate sender of the query). Multi-hop response relay is not yet
278    /// implemented, so services more than one hop away will generate responses
279    /// that reach intermediate forwarders but not the original querier.
280    pub fn discover_services(&mut self, service_id: Option<&str>) -> Result<String> {
281        // Service discovery messages are internal control messages (not user
282        // content), so they are exempt from require_encryption.
283
284        let peers: Vec<String> = self.known_peers.keys().cloned().collect();
285        let result = self
286            .mesh_services
287            .discover_services(&self.local_id, &peers, service_id)
288            .map_err(Error::Service)?;
289        let mut send_failures = 0usize;
290        for msg in result.messages {
291            if self
292                .send_internal_message(&msg.recipient, msg.content, msg.priority)
293                .is_err()
294            {
295                send_failures += 1;
296            }
297        }
298        if send_failures > 0 {
299            warn!(
300                failures = send_failures,
301                total = peers.len(),
302                "Some discovery broadcasts failed to send"
303            );
304        }
305        Ok(result.query_id)
306    }
307
308    /// Sends a typed service request to a specific provider peer.
309    /// Returns a request_id. The response arrives as a `ServiceResponseReceived` event.
310    pub fn send_service_request(
311        &mut self,
312        provider: &str,
313        service_id: &str,
314        method: &str,
315        body: &str,
316    ) -> Result<String> {
317        // Service requests are internal control messages (not user content),
318        // so they are exempt from require_encryption.
319        Self::validate_outbound_recipient(provider)?;
320
321        let result = self
322            .mesh_services
323            .send_service_request(provider, service_id, method, body)
324            .map_err(Error::Service)?;
325        let msg = result.message;
326        self.send_internal_message(&msg.recipient, msg.content, msg.priority)?;
327        Ok(result.request_id)
328    }
329
330    /// Responds to a service request from another peer.
331    pub fn respond_to_service_request(
332        &mut self,
333        request_id: &str,
334        requester: &str,
335        service_id: &str,
336        status: &str,
337        body: &str,
338    ) -> Result<MessageId> {
339        // Service responses are internal control messages (not user content),
340        // so they are exempt from require_encryption.
341        Self::validate_outbound_recipient(requester)?;
342
343        let result = self
344            .mesh_services
345            .respond_to_service_request(request_id, requester, service_id, status, body)
346            .map_err(Error::Service)?;
347        let msg = result.message;
348        let message_id = self.send_internal_message(&msg.recipient, msg.content, msg.priority)?;
349        Ok(message_id)
350    }
351}