1use super::tables::{PathEntry, PathSet};
2use crate::constants;
3
4pub fn timebase_from_random_blob(blob: &[u8; 10]) -> u64 {
6 let mut bytes = [0u8; 8];
7 bytes[3..8].copy_from_slice(&blob[5..10]);
8 u64::from_be_bytes(bytes)
9}
10
11pub fn timebase_from_random_blobs(blobs: &[[u8; 10]]) -> u64 {
13 let mut timebase: u64 = 0;
14 for blob in blobs {
15 let emitted = timebase_from_random_blob(blob);
16 if emitted > timebase {
17 timebase = emitted;
18 }
19 }
20 timebase
21}
22
23pub fn extract_random_blob(packet_data: &[u8]) -> Option<[u8; 10]> {
28 let offset = constants::KEYSIZE / 8 + constants::NAME_HASH_LENGTH / 8;
29 if packet_data.len() < offset + 10 {
30 return None;
31 }
32 let mut blob = [0u8; 10];
33 blob.copy_from_slice(&packet_data[offset..offset + 10]);
34 Some(blob)
35}
36
37#[derive(Debug, PartialEq, Eq)]
38pub enum PathDecision {
39 Add,
40 Reject,
41}
42
43pub fn should_update_path(
47 existing: Option<&PathEntry>,
48 announce_hops: u8,
49 announce_emitted_ts: u64,
50 random_blob: &[u8; 10],
51 path_is_unresponsive: bool,
52 now: f64,
53 prefer_shorter_path: bool,
54) -> PathDecision {
55 if announce_hops > constants::PATHFINDER_M {
57 return PathDecision::Reject;
58 }
59
60 let existing = match existing {
61 None => return PathDecision::Add,
62 Some(e) => e,
63 };
64
65 let path_timebase = timebase_from_random_blobs(&existing.random_blobs);
66 let blob_is_new = !existing.random_blobs.contains(random_blob);
67
68 if announce_hops <= existing.hops {
69 if prefer_shorter_path && announce_hops < existing.hops {
71 return PathDecision::Add;
72 }
73 if blob_is_new && announce_emitted_ts > path_timebase {
75 return PathDecision::Add;
76 }
77 if announce_emitted_ts == path_timebase && path_is_unresponsive {
79 return PathDecision::Add;
80 }
81 PathDecision::Reject
82 } else {
83 let path_expired = now >= existing.expires;
85
86 if path_expired && blob_is_new {
87 return PathDecision::Add;
88 }
89
90 if announce_emitted_ts > path_timebase && blob_is_new {
91 return PathDecision::Add;
92 }
93
94 if announce_emitted_ts == path_timebase && path_is_unresponsive {
95 return PathDecision::Add;
96 }
97
98 PathDecision::Reject
99 }
100}
101
102#[derive(Debug, PartialEq, Eq)]
104pub enum MultiPathDecision {
105 ReplacePrimary,
107 AddAlternative,
109 Reject,
111}
112
113#[allow(clippy::too_many_arguments)]
124pub fn decide_announce_multipath(
125 existing_set: Option<&PathSet>,
126 announce_hops: u8,
127 announce_emitted_ts: u64,
128 random_blob: &[u8; 10],
129 next_hop: &[u8; 16],
130 path_is_unresponsive: bool,
131 now: f64,
132 prefer_shorter_path: bool,
133) -> MultiPathDecision {
134 decide_announce_multipath_with_gravity(
135 existing_set,
136 announce_hops,
137 announce_emitted_ts,
138 random_blob,
139 next_hop,
140 path_is_unresponsive,
141 now,
142 prefer_shorter_path,
143 None,
144 None,
145 )
146}
147
148#[allow(clippy::too_many_arguments)]
150pub fn decide_announce_multipath_with_gravity(
151 existing_set: Option<&PathSet>,
152 announce_hops: u8,
153 announce_emitted_ts: u64,
154 random_blob: &[u8; 10],
155 next_hop: &[u8; 16],
156 path_is_unresponsive: bool,
157 now: f64,
158 prefer_shorter_path: bool,
159 current_gravity: Option<i64>,
160 announce_gravity: Option<i64>,
161) -> MultiPathDecision {
162 if announce_hops > constants::PATHFINDER_M {
164 return MultiPathDecision::Reject;
165 }
166
167 let path_set = match existing_set {
168 None => return MultiPathDecision::ReplacePrimary,
169 Some(ps) if ps.is_empty() => return MultiPathDecision::ReplacePrimary,
170 Some(ps) => ps,
171 };
172
173 if is_higher_gravity_replacement(
174 path_set,
175 announce_hops,
176 announce_emitted_ts,
177 current_gravity,
178 announce_gravity,
179 ) {
180 return MultiPathDecision::ReplacePrimary;
181 }
182
183 if let Some(existing_path) = path_set.find_by_next_hop(next_hop) {
185 let decision = should_update_path(
186 Some(existing_path),
187 announce_hops,
188 announce_emitted_ts,
189 random_blob,
190 path_is_unresponsive,
191 now,
192 prefer_shorter_path,
193 );
194 match decision {
195 PathDecision::Add => MultiPathDecision::ReplacePrimary,
196 PathDecision::Reject => MultiPathDecision::Reject,
197 }
198 } else {
199 let all_blobs = path_set.all_random_blobs();
201 let blob_is_new = !all_blobs.contains(random_blob);
202
203 if !blob_is_new {
204 return MultiPathDecision::Reject;
205 }
206
207 let max_timebase = timebase_from_random_blobs(&all_blobs);
208 if announce_emitted_ts >= max_timebase {
209 MultiPathDecision::AddAlternative
210 } else {
211 MultiPathDecision::Reject
212 }
213 }
214}
215
216pub fn is_higher_gravity_replacement(
219 path_set: &PathSet,
220 announce_hops: u8,
221 announce_emitted_ts: u64,
222 current_gravity: Option<i64>,
223 announce_gravity: Option<i64>,
224) -> bool {
225 let (Some(primary), Some(current_gravity), Some(announce_gravity)) =
226 (path_set.primary(), current_gravity, announce_gravity)
227 else {
228 return false;
229 };
230 announce_hops <= primary.hops
231 && announce_emitted_ts == timebase_from_random_blobs(&primary.random_blobs)
232 && announce_gravity > current_gravity
233}
234
235#[cfg(test)]
236mod tests {
237 use super::super::types::InterfaceId;
238 use super::*;
239
240 fn make_blob(timebase: u64) -> [u8; 10] {
241 let mut blob = [0u8; 10];
242 let bytes = timebase.to_be_bytes();
243 blob[5..10].copy_from_slice(&bytes[3..8]);
245 blob
246 }
247
248 fn make_path_entry(hops: u8, blobs: &[[u8; 10]], expires: f64) -> PathEntry {
249 PathEntry {
250 timestamp: 1000.0,
251 next_hop: [0xAA; 16],
252 hops,
253 expires,
254 random_blobs: blobs.to_vec(),
255 receiving_interface: InterfaceId(1),
256 packet_hash: [0xBB; 32],
257 announce_raw: None,
258 }
259 }
260
261 #[test]
262 fn test_timebase_extraction() {
263 let blob = make_blob(12345);
264 assert_eq!(timebase_from_random_blob(&blob), 12345);
265 }
266
267 #[test]
268 fn test_timebase_from_multiple_blobs() {
269 let b1 = make_blob(100);
270 let b2 = make_blob(200);
271 let b3 = make_blob(50);
272 assert_eq!(timebase_from_random_blobs(&[b1, b2, b3]), 200);
273 }
274
275 #[test]
276 fn test_timebase_empty_blobs() {
277 assert_eq!(timebase_from_random_blobs(&[]), 0);
278 }
279
280 #[test]
281 fn test_extract_random_blob() {
282 let mut data = [0u8; 100];
284 data[74..84].copy_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
286 let blob = extract_random_blob(&data).unwrap();
287 assert_eq!(blob, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
288 }
289
290 #[test]
291 fn test_extract_random_blob_too_short() {
292 let data = [0u8; 80]; assert!(extract_random_blob(&data).is_none());
294 }
295
296 #[test]
299 fn test_no_existing_path_always_add() {
300 let blob = make_blob(100);
301 assert_eq!(
302 should_update_path(None, 3, 100, &blob, false, 1000.0, false),
303 PathDecision::Add
304 );
305 }
306
307 #[test]
308 fn test_hop_limit_reject() {
309 let blob = make_blob(100);
310 assert_eq!(
311 should_update_path(None, 129, 100, &blob, false, 1000.0, false),
312 PathDecision::Reject
313 );
314 }
315
316 #[test]
317 fn test_fewer_hops_new_blob_newer_emission_add() {
318 let old_blob = make_blob(100);
319 let new_blob = make_blob(200);
320 let entry = make_path_entry(5, &[old_blob], 9999.0);
321 assert_eq!(
322 should_update_path(Some(&entry), 3, 200, &new_blob, false, 1000.0, false),
323 PathDecision::Add
324 );
325 }
326
327 #[test]
328 fn test_fewer_hops_duplicate_blob_reject() {
329 let blob = make_blob(100);
330 let entry = make_path_entry(5, &[blob], 9999.0);
331 assert_eq!(
332 should_update_path(Some(&entry), 3, 200, &blob, false, 1000.0, false),
333 PathDecision::Reject
334 );
335 }
336
337 #[test]
338 fn test_fewer_hops_same_emission_unresponsive_add() {
339 let old_blob = make_blob(100);
340 let mut different_blob = [0u8; 10];
341 different_blob[0] = 0xFF;
342 different_blob[5..10].copy_from_slice(&100u64.to_be_bytes()[3..8]);
343
344 let entry = make_path_entry(5, &[old_blob], 9999.0);
345 assert_eq!(
346 should_update_path(Some(&entry), 3, 100, &different_blob, true, 1000.0, false),
347 PathDecision::Add
348 );
349 }
350
351 #[test]
352 fn test_fewer_hops_same_emission_responsive_reject() {
353 let old_blob = make_blob(100);
354 let mut different_blob = [0u8; 10];
355 different_blob[0] = 0xFF;
356 different_blob[5..10].copy_from_slice(&100u64.to_be_bytes()[3..8]);
357
358 let entry = make_path_entry(5, &[old_blob], 9999.0);
359 assert_eq!(
360 should_update_path(Some(&entry), 3, 100, &different_blob, false, 1000.0, false),
361 PathDecision::Reject
362 );
363 }
364
365 #[test]
366 fn test_more_hops_expired_path_new_blob_add() {
367 let old_blob = make_blob(100);
368 let new_blob = make_blob(50); let entry = make_path_entry(2, &[old_blob], 500.0); assert_eq!(
372 should_update_path(Some(&entry), 5, 50, &new_blob, false, 600.0, false), PathDecision::Add
374 );
375 }
376
377 #[test]
378 fn test_more_hops_not_expired_older_emission_reject() {
379 let old_blob = make_blob(200);
380 let new_blob = make_blob(100);
381 let entry = make_path_entry(2, &[old_blob], 9999.0);
382
383 assert_eq!(
384 should_update_path(Some(&entry), 5, 100, &new_blob, false, 1000.0, false),
385 PathDecision::Reject
386 );
387 }
388
389 #[test]
390 fn test_more_hops_newer_emission_new_blob_add() {
391 let old_blob = make_blob(100);
392 let new_blob = make_blob(200);
393 let entry = make_path_entry(2, &[old_blob], 9999.0);
394
395 assert_eq!(
396 should_update_path(Some(&entry), 5, 200, &new_blob, false, 1000.0, false),
397 PathDecision::Add
398 );
399 }
400
401 #[test]
402 fn test_more_hops_same_emission_unresponsive_add() {
403 let old_blob = make_blob(100);
404 let mut different_blob = [0u8; 10];
405 different_blob[0] = 0xFF;
406 different_blob[5..10].copy_from_slice(&100u64.to_be_bytes()[3..8]);
407
408 let entry = make_path_entry(2, &[old_blob], 9999.0);
409
410 assert_eq!(
411 should_update_path(Some(&entry), 5, 100, &different_blob, true, 1000.0, false),
412 PathDecision::Add
413 );
414 }
415
416 #[test]
417 fn test_more_hops_same_emission_responsive_reject() {
418 let old_blob = make_blob(100);
419 let mut different_blob = [0u8; 10];
420 different_blob[0] = 0xFF;
421 different_blob[5..10].copy_from_slice(&100u64.to_be_bytes()[3..8]);
422
423 let entry = make_path_entry(2, &[old_blob], 9999.0);
424
425 assert_eq!(
426 should_update_path(Some(&entry), 5, 100, &different_blob, false, 1000.0, false),
427 PathDecision::Reject
428 );
429 }
430
431 #[test]
432 fn test_more_hops_duplicate_blob_reject() {
433 let blob = make_blob(200);
434 let entry = make_path_entry(2, &[blob], 9999.0);
435
436 assert_eq!(
437 should_update_path(Some(&entry), 5, 200, &blob, false, 1000.0, false),
438 PathDecision::Reject
439 );
440 }
441
442 #[test]
443 fn test_equal_hops_new_blob_newer_emission_add() {
444 let old_blob = make_blob(100);
445 let new_blob = make_blob(200);
446 let entry = make_path_entry(3, &[old_blob], 9999.0);
447
448 assert_eq!(
449 should_update_path(Some(&entry), 3, 200, &new_blob, false, 1000.0, false),
450 PathDecision::Add
451 );
452 }
453
454 #[test]
457 fn test_prefer_shorter_path_strictly_fewer_hops_duplicate_blob_add() {
458 let blob = make_blob(100);
459 let entry = make_path_entry(5, &[blob], 9999.0);
460 assert_eq!(
461 should_update_path(Some(&entry), 3, 100, &blob, false, 1000.0, true),
462 PathDecision::Add
463 );
464 }
465
466 #[test]
467 fn test_prefer_shorter_path_equal_hops_duplicate_blob_reject() {
468 let blob = make_blob(100);
470 let entry = make_path_entry(3, &[blob], 9999.0);
471 assert_eq!(
472 should_update_path(Some(&entry), 3, 100, &blob, false, 1000.0, true),
473 PathDecision::Reject
474 );
475 }
476
477 #[test]
478 fn test_prefer_shorter_path_more_hops_duplicate_blob_reject() {
479 let blob = make_blob(100);
481 let entry = make_path_entry(2, &[blob], 9999.0);
482 assert_eq!(
483 should_update_path(Some(&entry), 5, 100, &blob, false, 1000.0, true),
484 PathDecision::Reject
485 );
486 }
487
488 #[test]
491 fn test_multipath_no_existing_set_replace_primary() {
492 let blob = make_blob(100);
493 assert_eq!(
494 decide_announce_multipath(None, 3, 100, &blob, &[0xBB; 16], false, 1000.0, false),
495 MultiPathDecision::ReplacePrimary
496 );
497 }
498
499 #[test]
500 fn test_multipath_same_nexthop_update() {
501 let blob_old = make_blob(100);
502 let blob_new = make_blob(200);
503 let entry = make_path_entry(3, &[blob_old], 9999.0);
504 let ps = PathSet::from_single(entry, 3);
505
506 assert_eq!(
508 decide_announce_multipath(
509 Some(&ps),
510 2,
511 200,
512 &blob_new,
513 &[0xAA; 16],
514 false,
515 1000.0,
516 false
517 ),
518 MultiPathDecision::ReplacePrimary
519 );
520 }
521
522 #[test]
523 fn test_multipath_same_nexthop_reject() {
524 let blob = make_blob(100);
525 let entry = make_path_entry(3, &[blob], 9999.0);
526 let ps = PathSet::from_single(entry, 3);
527
528 assert_eq!(
530 decide_announce_multipath(Some(&ps), 3, 100, &blob, &[0xAA; 16], false, 1000.0, false),
531 MultiPathDecision::Reject
532 );
533 }
534
535 #[test]
536 fn test_multipath_new_nexthop_novel_blob_add_alternative() {
537 let blob_existing = make_blob(100);
538 let blob_new = make_blob(200);
539 let entry = make_path_entry(3, &[blob_existing], 9999.0);
540 let ps = PathSet::from_single(entry, 3);
541
542 assert_eq!(
544 decide_announce_multipath(
545 Some(&ps),
546 4,
547 200,
548 &blob_new,
549 &[0xCC; 16],
550 false,
551 1000.0,
552 false
553 ),
554 MultiPathDecision::AddAlternative
555 );
556 }
557
558 #[test]
559 fn test_multipath_new_nexthop_known_blob_reject() {
560 let blob = make_blob(100);
561 let entry = make_path_entry(3, &[blob], 9999.0);
562 let ps = PathSet::from_single(entry, 3);
563
564 assert_eq!(
566 decide_announce_multipath(Some(&ps), 4, 100, &blob, &[0xCC; 16], false, 1000.0, false),
567 MultiPathDecision::Reject
568 );
569 }
570
571 #[test]
572 fn test_multipath_new_nexthop_older_emission_reject() {
573 let blob_existing = make_blob(200);
574 let blob_new = make_blob(100); let entry = make_path_entry(3, &[blob_existing], 9999.0);
576 let ps = PathSet::from_single(entry, 3);
577
578 assert_eq!(
580 decide_announce_multipath(
581 Some(&ps),
582 4,
583 100,
584 &blob_new,
585 &[0xCC; 16],
586 false,
587 1000.0,
588 false
589 ),
590 MultiPathDecision::Reject
591 );
592 }
593
594 #[test]
595 fn test_multipath_hop_limit_reject() {
596 let blob = make_blob(100);
597 assert_eq!(
598 decide_announce_multipath(None, 129, 100, &blob, &[0xBB; 16], false, 1000.0, false),
599 MultiPathDecision::Reject
600 );
601 }
602
603 #[test]
604 fn same_emission_prefers_strictly_higher_gravity() {
605 let blob = make_blob(100);
606 let entry = make_path_entry(3, &[blob], 9999.0);
607 let ps = PathSet::from_single(entry, 3);
608
609 let decide = |current, incoming, hops| {
610 decide_announce_multipath_with_gravity(
611 Some(&ps),
612 hops,
613 100,
614 &blob,
615 &[0xCC; 16],
616 false,
617 1000.0,
618 false,
619 Some(current),
620 Some(incoming),
621 )
622 };
623
624 assert_eq!(decide(0, 1, 3), MultiPathDecision::ReplacePrimary);
625 assert_eq!(decide(-5, -2, 3), MultiPathDecision::ReplacePrimary);
626 assert_eq!(decide(1, 1, 3), MultiPathDecision::Reject);
627 assert_eq!(decide(2, 1, 3), MultiPathDecision::Reject);
628 assert_eq!(decide(0, 1, 4), MultiPathDecision::Reject);
629
630 assert!(is_higher_gravity_replacement(&ps, 3, 100, Some(0), Some(1)));
631 assert!(!is_higher_gravity_replacement(
632 &ps,
633 3,
634 100,
635 Some(1),
636 Some(1)
637 ));
638 assert_eq!(crate::logging::GRAVITY_UPDATE_LOG_LEVEL, log::Level::Trace);
639 assert_eq!(crate::logging::PATHING_LOG_TARGET, "rns::pathing");
640 }
641}