1use 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 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 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 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 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}