Skip to main content

rns_core/transport/
persistence.rs

1//! Restart-safe transport state snapshots.
2//!
3//! Encoding is owned by `rns-net`; this module keeps the I/O-free transport
4//! engine responsible for selecting and reconstructing valid state.
5
6use alloc::vec::Vec;
7
8use super::tables::PathEntry;
9use super::tunnel::TunnelPath;
10use super::types::InterfaceId;
11use super::TransportEngine;
12use crate::constants;
13use crate::packet::RawPacket;
14
15const PERSIST_RANDOM_BLOBS: usize = 32;
16
17#[derive(Debug, Clone, PartialEq)]
18pub struct PersistedPath {
19    pub destination_hash: [u8; 16],
20    pub timestamp: f64,
21    pub received_from: [u8; 16],
22    pub hops: u8,
23    pub expires: f64,
24    pub random_blobs: Vec<[u8; 10]>,
25    pub interface_hash: Option<[u8; 32]>,
26    pub packet_hash: [u8; 32],
27    /// Last time this path was used; 0 means never used.
28    pub used: f64,
29}
30
31#[derive(Debug, Clone, PartialEq)]
32pub struct PersistedTunnel {
33    pub tunnel_id: [u8; 32],
34    pub interface_hash: Option<[u8; 32]>,
35    pub paths: Vec<PersistedPath>,
36    pub expires: f64,
37}
38
39#[derive(Debug, Clone, Default, PartialEq)]
40pub struct TransportStateSnapshot {
41    pub packet_hashes: Vec<[u8; 32]>,
42    pub paths: Vec<PersistedPath>,
43    pub tunnels: Vec<PersistedTunnel>,
44}
45
46#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
47pub struct RestoreStats {
48    pub packet_hashes: usize,
49    pub paths: usize,
50    pub tunnels: usize,
51    pub skipped_paths: usize,
52}
53
54impl TransportEngine {
55    /// Copy the restart-relevant portion of transport state.
56    pub fn persistence_snapshot(&self) -> TransportStateSnapshot {
57        if !self.config.transport_enabled {
58            return TransportStateSnapshot::default();
59        }
60
61        let packet_hashes = self.packet_hashlist.iter().collect();
62        let mut paths = Vec::new();
63        for (destination_hash, path_set) in &self.path_table {
64            for entry in path_set.iter() {
65                let Some(interface_hash) = self.interface_hashes.get(&entry.receiving_interface)
66                else {
67                    continue;
68                };
69                paths.push(PersistedPath::from_path_entry(
70                    *destination_hash,
71                    entry,
72                    Some(*interface_hash),
73                ));
74            }
75        }
76
77        let tunnels = self
78            .tunnel_table
79            .iter()
80            .map(|(tunnel_id, tunnel)| {
81                let interface_hash = tunnel
82                    .interface
83                    .and_then(|id| self.interface_hashes.get(&id).copied());
84                let paths = tunnel
85                    .paths
86                    .iter()
87                    .map(|(destination_hash, path)| PersistedPath {
88                        destination_hash: *destination_hash,
89                        timestamp: path.timestamp,
90                        received_from: path.received_from,
91                        hops: path.hops,
92                        expires: path.expires,
93                        random_blobs: tail_random_blobs(&path.random_blobs),
94                        interface_hash,
95                        packet_hash: path.packet_hash,
96                        used: 0.0,
97                    })
98                    .collect();
99                PersistedTunnel {
100                    tunnel_id: *tunnel_id,
101                    interface_hash,
102                    paths,
103                    expires: tunnel.expires,
104                }
105            })
106            .collect();
107
108        TransportStateSnapshot {
109            packet_hashes,
110            paths,
111            tunnels,
112        }
113    }
114
115    /// Restore state after interfaces and the announce cache are available.
116    pub fn restore_persistence_snapshot<F>(
117        &mut self,
118        snapshot: TransportStateSnapshot,
119        now: f64,
120        mut announce_lookup: F,
121    ) -> RestoreStats
122    where
123        F: FnMut(&[u8; 32]) -> Option<Vec<u8>>,
124    {
125        let mut stats = RestoreStats::default();
126        if !self.config.transport_enabled {
127            return stats;
128        }
129
130        for packet_hash in snapshot.packet_hashes {
131            self.packet_hashlist.add(packet_hash);
132        }
133        stats.packet_hashes = self.packet_hashlist.len();
134
135        let interface_ids: alloc::collections::BTreeMap<[u8; 32], InterfaceId> = self
136            .interface_hashes
137            .iter()
138            .map(|(id, hash)| (*hash, *id))
139            .collect();
140
141        for path in snapshot.paths {
142            let Some(interface_hash) = path.interface_hash else {
143                stats.skipped_paths += 1;
144                continue;
145            };
146            let Some(interface_id) = interface_ids.get(&interface_hash).copied() else {
147                stats.skipped_paths += 1;
148                continue;
149            };
150            let Some(raw) = load_cached_announce(&path, &mut announce_lookup) else {
151                stats.skipped_paths += 1;
152                continue;
153            };
154            if path.expires < now {
155                stats.skipped_paths += 1;
156                continue;
157            }
158            let destination_hash = path.destination_hash;
159            let entry = PathEntry {
160                timestamp: path.timestamp,
161                next_hop: path.received_from,
162                hops: path.hops,
163                expires: path.expires,
164                random_blobs: path.random_blobs,
165                receiving_interface: interface_id,
166                packet_hash: path.packet_hash,
167                used: path.used,
168                announce_raw: Some(raw),
169            };
170            self.upsert_path_destination(destination_hash, entry, now);
171            stats.paths += 1;
172        }
173
174        for tunnel in snapshot.tunnels {
175            if tunnel.expires < now {
176                stats.skipped_paths += tunnel.paths.len();
177                continue;
178            }
179            let mut restored_paths = alloc::collections::BTreeMap::new();
180            for path in tunnel.paths {
181                if path.expires < now || load_cached_announce(&path, &mut announce_lookup).is_none()
182                {
183                    stats.skipped_paths += 1;
184                    continue;
185                }
186                restored_paths.insert(
187                    path.destination_hash,
188                    TunnelPath {
189                        timestamp: path.timestamp,
190                        received_from: path.received_from,
191                        hops: path.hops,
192                        expires: path.expires,
193                        random_blobs: path.random_blobs,
194                        packet_hash: path.packet_hash,
195                    },
196                );
197            }
198            if restored_paths.is_empty() {
199                continue;
200            }
201            self.tunnel_table
202                .restore_detached(tunnel.tunnel_id, restored_paths, tunnel.expires);
203            stats.tunnels += 1;
204        }
205
206        stats
207    }
208}
209
210impl PersistedPath {
211    fn from_path_entry(
212        destination_hash: [u8; 16],
213        entry: &PathEntry,
214        interface_hash: Option<[u8; 32]>,
215    ) -> Self {
216        Self {
217            destination_hash,
218            timestamp: entry.timestamp,
219            received_from: entry.next_hop,
220            hops: entry.hops,
221            expires: entry.expires,
222            random_blobs: tail_random_blobs(&entry.random_blobs),
223            interface_hash,
224            packet_hash: entry.packet_hash,
225            used: entry.used,
226        }
227    }
228}
229
230fn tail_random_blobs(blobs: &[[u8; 10]]) -> Vec<[u8; 10]> {
231    blobs[blobs.len().saturating_sub(PERSIST_RANDOM_BLOBS)..].to_vec()
232}
233
234fn load_cached_announce<F>(path: &PersistedPath, lookup: &mut F) -> Option<Vec<u8>>
235where
236    F: FnMut(&[u8; 32]) -> Option<Vec<u8>>,
237{
238    let raw = lookup(&path.packet_hash)?;
239    let packet = RawPacket::unpack(&raw).ok()?;
240    if packet.packet_hash != path.packet_hash
241        || packet.destination_hash != path.destination_hash
242        || packet.flags.packet_type != constants::PACKET_TYPE_ANNOUNCE
243    {
244        return None;
245    }
246    Some(raw)
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    /// `[byte; 32]` as the packet hashlist stores and snapshots it: the
254    /// first 16 bytes, the rest zero.
255    fn stored(byte: u8) -> [u8; 32] {
256        let mut hash = [0u8; 32];
257        hash[..16].fill(byte);
258        hash
259    }
260    use crate::constants;
261    use crate::hash;
262    use crate::packet::{PacketFlags, RawPacket};
263    use crate::transport::tables::PathEntry;
264    use crate::transport::tunnel::TunnelPath;
265    use crate::transport::types::{
266        IngressControlConfig, InterfaceId, InterfaceInfo, PacketHashlistAllocation, TransportConfig,
267    };
268    use crate::transport::TransportEngine;
269    use alloc::collections::BTreeMap;
270    use alloc::string::String;
271    use alloc::vec;
272
273    fn config(hash_capacity: usize) -> TransportConfig {
274        config_with_allocation(hash_capacity, PacketHashlistAllocation::Eager)
275    }
276
277    fn config_with_allocation(
278        hash_capacity: usize,
279        allocation: PacketHashlistAllocation,
280    ) -> TransportConfig {
281        TransportConfig {
282            transport_enabled: true,
283            identity_hash: Some([0x42; 16]),
284            local_hops_delta: 0,
285            prefer_shorter_path: false,
286            max_paths_per_destination: 4,
287            packet_hashlist_max_entries: hash_capacity,
288            packet_hashlist_allocation: allocation,
289            max_discovery_pr_tags: constants::MAX_PR_TAGS,
290            max_path_destinations: usize::MAX,
291            max_tunnel_destinations_total: usize::MAX,
292            destination_timeout_secs: constants::DESTINATION_TIMEOUT,
293            announce_table_ttl_secs: constants::ANNOUNCE_TABLE_TTL,
294            announce_table_max_bytes: constants::ANNOUNCE_TABLE_MAX_BYTES,
295            announce_sig_cache_enabled: true,
296            announce_sig_cache_max_entries: constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
297            announce_sig_cache_ttl_secs: constants::ANNOUNCE_SIG_CACHE_TTL,
298            announce_queue_max_entries: 256,
299            announce_queue_max_interfaces: 1024,
300        }
301    }
302
303    #[test]
304    fn restore_packet_hashes_into_lazy_storage_preserves_order_and_truncates() {
305        let mut engine =
306            TransportEngine::new(config_with_allocation(3, PacketHashlistAllocation::Lazy));
307        let snapshot = TransportStateSnapshot {
308            packet_hashes: vec![[1; 32], [2; 32], [3; 32], [4; 32], [5; 32]],
309            paths: Vec::new(),
310            tunnels: Vec::new(),
311        };
312
313        let stats = engine.restore_persistence_snapshot(snapshot, 100.0, |_| None);
314
315        assert_eq!(stats.packet_hashes, 3);
316        assert_eq!(
317            engine.persistence_snapshot().packet_hashes,
318            vec![stored(3), stored(4), stored(5)]
319        );
320    }
321
322    fn interface(id: u64, name: &str) -> InterfaceInfo {
323        InterfaceInfo {
324            stamp_costs: Default::default(),
325            id: InterfaceId(id),
326            name: String::from(name),
327            mode: constants::MODE_FULL,
328            gravity: 0,
329            recursive_prs: false,
330            announces_from_internal: true,
331            announces_to_internal: None,
332            out_capable: true,
333            in_capable: true,
334            bitrate: None,
335            airtime_profile: None,
336            announce_rate_target: None,
337            announce_rate_grace: 0,
338            announce_rate_penalty: 0.0,
339            announce_cap: constants::ANNOUNCE_CAP,
340            is_local_client: false,
341            wants_tunnel: false,
342            tunnel_id: None,
343            mtu: constants::MTU as u32,
344            ingress_control: IngressControlConfig::disabled(),
345            ia_freq: 0.0,
346            ip_freq: 0.0,
347            op_freq: 0.0,
348            op_samples: 0,
349            started: 0.0,
350        }
351    }
352
353    fn announce(dest: [u8; 16]) -> RawPacket {
354        RawPacket::pack(
355            PacketFlags {
356                header_type: constants::HEADER_1,
357                context_flag: constants::FLAG_UNSET,
358                transport_type: constants::TRANSPORT_BROADCAST,
359                destination_type: constants::DESTINATION_SINGLE,
360                packet_type: constants::PACKET_TYPE_ANNOUNCE,
361            },
362            0,
363            &dest,
364            None,
365            constants::CONTEXT_NONE,
366            &[0x77; 32],
367        )
368        .unwrap()
369    }
370
371    fn persisted_path(dest: [u8; 16], packet: &RawPacket, iface: &str) -> PersistedPath {
372        PersistedPath {
373            destination_hash: dest,
374            timestamp: 100.0,
375            received_from: [0x22; 16],
376            hops: 3,
377            expires: 500.0,
378            random_blobs: vec![[0x33; 10]],
379            interface_hash: Some(hash::full_hash(iface.as_bytes())),
380            packet_hash: packet.packet_hash,
381            used: 0.0,
382        }
383    }
384
385    #[test]
386    fn snapshot_preserves_fifo_hash_order_and_caps_random_blob_history() {
387        let mut engine = TransportEngine::new(config(3));
388        engine.register_interface(interface(1, "alpha"));
389        engine.packet_hashlist.add([1; 32]);
390        engine.packet_hashlist.add([2; 32]);
391        engine.packet_hashlist.add([3; 32]);
392        engine.packet_hashlist.add([4; 32]);
393        let packet = announce([9; 16]);
394        engine.inject_path(
395            [9; 16],
396            PathEntry {
397                timestamp: 100.0,
398                next_hop: [8; 16],
399                hops: 2,
400                expires: 500.0,
401                random_blobs: (0..40).map(|n| [n; 10]).collect(),
402                receiving_interface: InterfaceId(1),
403                packet_hash: packet.packet_hash,
404                announce_raw: Some(packet.raw.clone()),
405                used: 0.0,
406            },
407        );
408
409        let snapshot = engine.persistence_snapshot();
410        assert_eq!(
411            snapshot.packet_hashes,
412            vec![stored(2), stored(3), stored(4)]
413        );
414        assert_eq!(snapshot.paths[0].random_blobs.len(), 32);
415        assert_eq!(snapshot.paths[0].random_blobs[0], [8; 10]);
416        assert_eq!(
417            snapshot.paths[0].interface_hash,
418            Some(hash::full_hash(b"alpha"))
419        );
420    }
421
422    #[test]
423    fn snapshot_skips_paths_whose_interface_is_not_registered() {
424        let mut engine = TransportEngine::new(config(8));
425        engine.inject_path(
426            [9; 16],
427            PathEntry {
428                timestamp: 100.0,
429                next_hop: [8; 16],
430                hops: 2,
431                expires: 500.0,
432                random_blobs: Vec::new(),
433                receiving_interface: InterfaceId(404),
434                packet_hash: [7; 32],
435                announce_raw: None,
436                used: 0.0,
437            },
438        );
439        assert!(engine.persistence_snapshot().paths.is_empty());
440    }
441
442    #[test]
443    fn restore_requires_live_interface_cached_announce_and_unexpired_path() {
444        let good_packet = announce([1; 16]);
445        let missing_packet = announce([2; 16]);
446        let mut missing_iface = persisted_path([3; 16], &good_packet, "gone");
447        missing_iface.packet_hash = good_packet.packet_hash;
448        let mut expired = persisted_path([4; 16], &good_packet, "alpha");
449        expired.expires = 99.0;
450        let snapshot = TransportStateSnapshot {
451            packet_hashes: vec![[1; 32], [2; 32], [3; 32]],
452            paths: vec![
453                persisted_path([1; 16], &good_packet, "alpha"),
454                persisted_path([2; 16], &missing_packet, "alpha"),
455                missing_iface,
456                expired,
457            ],
458            tunnels: Vec::new(),
459        };
460        let mut cache = BTreeMap::new();
461        cache.insert(good_packet.packet_hash, good_packet.raw.clone());
462        let mut engine = TransportEngine::new(config(2));
463        engine.register_interface(interface(7, "alpha"));
464        let stats =
465            engine.restore_persistence_snapshot(snapshot, 100.0, |hash| cache.get(hash).cloned());
466
467        assert_eq!(stats.packet_hashes, 2);
468        assert_eq!(stats.paths, 1);
469        assert_eq!(stats.skipped_paths, 3);
470        assert!(engine.has_path(&[1; 16]));
471        assert_eq!(engine.next_hop_interface(&[1; 16]), Some(InterfaceId(7)));
472        assert!(!engine.has_path(&[2; 16]));
473        assert_eq!(
474            engine.persistence_snapshot().packet_hashes,
475            vec![stored(2), stored(3)]
476        );
477    }
478
479    #[test]
480    fn detached_tunnel_round_trips_when_at_least_one_cached_path_survives() {
481        let packet = announce([5; 16]);
482        let mut source = TransportEngine::new(config(8));
483        source.register_interface(interface(9, "tunnel-iface"));
484        source.handle_tunnel([0x44; 32], InterfaceId(9), 100.0);
485        source.tunnel_table.store_tunnel_path(
486            &[0x44; 32],
487            [5; 16],
488            TunnelPath {
489                timestamp: 100.0,
490                received_from: [6; 16],
491                hops: 2,
492                expires: 500.0,
493                random_blobs: vec![[7; 10]],
494                packet_hash: packet.packet_hash,
495            },
496            100.0,
497            constants::DESTINATION_TIMEOUT,
498            usize::MAX,
499        );
500        let snapshot = source.persistence_snapshot();
501        assert_eq!(
502            snapshot.tunnels[0].interface_hash,
503            Some(hash::full_hash(b"tunnel-iface"))
504        );
505        assert_eq!(
506            snapshot.tunnels[0].expires,
507            100.0 + constants::TUNNEL_TIMEOUT
508        );
509        assert_eq!(snapshot.tunnels[0].paths[0].expires, 500.0);
510
511        let mut restored = TransportEngine::new(config(8));
512        let stats = restored.restore_persistence_snapshot(snapshot, 101.0, |hash| {
513            (hash == &packet.packet_hash).then(|| packet.raw.clone())
514        });
515        assert_eq!(stats.tunnels, 1);
516        let round_trip = restored.persistence_snapshot();
517        assert_eq!(round_trip.tunnels.len(), 1);
518        assert_eq!(round_trip.tunnels[0].interface_hash, None);
519        assert_eq!(
520            round_trip.tunnels[0].expires,
521            100.0 + constants::TUNNEL_TIMEOUT
522        );
523        assert_eq!(round_trip.tunnels[0].paths[0].expires, 500.0);
524        assert_eq!(round_trip.tunnels[0].paths[0].destination_hash, [5; 16]);
525    }
526}