ntkd 0.1.4

The Netsukuku routing daemon: composes the ntk-* library crates into a running node.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
//! Installing one identity's kernel routing state to match QSPN's routing decisions.
//!
//! Replaces `identity_ip_commands.vala`'s `ip route change ... table ntk` calls
//! (`research/impl/vala/ntkd/identity_ip_commands.vala:533-560`) with real, diffed netlink
//! mutations: [`RouteInstaller::apply`] issues only the operations a changed
//! [`ntk_qspn::RouteSnapshot`] actually requires, never a blind reinstall of everything.

use std::collections::BTreeMap;
use std::net::Ipv4Addr;

use ntk_common::{Cost, HCoord, Naddr};
use ntk_netlink::{
    Interface, Ipv4Net, Netlink, Nexthop, RouteKey, RouteSpec, RouteTarget, RuleSelector, RuleSpec,
};
use ntk_qspn::{ArcId, RoutePath, RouteSnapshot};

use crate::kernel::addressing::{self, AddressingError};

/// The interface an identity's own routable address is installed on.
///
/// Upstream installs the identity's global `/32` on `lo` *in addition to* every physical `dev`
/// (`identity_ip_commands.vala:37-49`) β€” the per-device copies exist only to support anonymizing
/// NAT's `POSTROUTING` rewrite, which this daemon deliberately omits (see the batch contract's
/// NAT scope note). Without NAT, the address only needs to exist somewhere the kernel considers
/// local for routing purposes, so `lo` alone is kept.
const IDENTITY_ADDRESS_INTERFACE: &str = "lo";

/// Counts of route mutations one [`RouteInstaller::apply`] call issued.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct AppliedDelta {
    /// Destinations that newly appeared (`add_route`).
    pub added: usize,
    /// Destinations whose target changed (`change_route`).
    pub changed: usize,
    /// Destinations that disappeared (`remove_route`).
    pub removed: usize,
}

/// Owns one identity's kernel routing state and reconciles it against QSPN's published
/// [`RouteSnapshot`]s.
#[derive(Debug)]
pub struct RouteInstaller<K> {
    kernel: K,
    my_naddr: Naddr,
    table: u32,
    rule_priority: u32,
    /// Gateway endpoint for each currently-known local arc, keyed by [`ArcId`] — the arc→(via,
    /// dev) mapping a path's leading arc resolves through to become a real `RouteTarget`.
    arc_endpoints: BTreeMap<ArcId, (Ipv4Addr, Interface)>,
    /// The last route actually installed per destination, so [`RouteInstaller::apply`] can diff
    /// against it instead of blindly reinstalling everything.
    applied: BTreeMap<HCoord, RouteSpec>,
    /// This identity's own address, once [`RouteInstaller::install_identity`] has added it.
    identity_address: Option<Ipv4Net>,
}

impl<K> RouteInstaller<K> {
    /// Test-only accessor to the underlying kernel handle, so integration tests can assert on
    /// [`ntk_netlink::FakeNetlink`]'s recorded operation log directly.
    #[cfg(any(test, feature = "test-util"))]
    pub fn kernel_ref(&self) -> &K {
        &self.kernel
    }
}

impl<K: Netlink> RouteInstaller<K> {
    /// Builds an installer for one identity, over the already-allocated `table`/`rule_priority`
    /// pair (see [`ntk_netlink::TableAllocator::acquire`]).
    pub fn new(kernel: K, my_naddr: Naddr, table: u32, rule_priority: u32) -> Self {
        Self {
            kernel,
            my_naddr,
            table,
            rule_priority,
            arc_endpoints: BTreeMap::new(),
            applied: BTreeMap::new(),
            identity_address: None,
        }
    }

    /// Transitions this installer to `real_naddr` once a migrating identity's negotiated
    /// position is confirmed reachable (`ntkd::node::lifecycle::migrate`'s bootstrap-complete
    /// wait) β€” the counterpart to constructing this installer with a virtual placeholder
    /// [`Naddr`] up front so [`Self::install_identity`]/[`Self::apply`] stay real no-ops for the
    /// whole migration window. Never installs anything itself: the caller still needs its own
    /// follow-up [`Self::install_identity`]/[`Self::apply`] calls once this returns, exactly as
    /// if a fresh, real-`Naddr`-constructed installer had been used from the start.
    pub fn realize(&mut self, real_naddr: Naddr) {
        self.my_naddr = real_naddr;
    }

    /// Records the gateway endpoint of `arc`: the neighbour address and outgoing interface a
    /// path whose leading arc is `arc` should route through.
    pub fn set_arc_endpoint(&mut self, arc: ArcId, via: Ipv4Addr, dev: Interface) {
        self.arc_endpoints.insert(arc, (via, dev));
    }

    /// Forgets `arc`'s endpoint. Any not-yet-reapplied path leading with `arc` becomes
    /// unreachable the next time [`RouteInstaller::apply`] runs.
    pub fn clear_arc_endpoint(&mut self, arc: ArcId) {
        self.arc_endpoints.remove(&arc);
    }

    /// Installs this identity's own address (on `IDENTITY_ADDRESS_INTERFACE`) and its
    /// catch-all [`RuleSelector::Any`] rule at `rule_priority`.
    ///
    /// A no-op, deliberately, while [`Naddr::is_virtual`] β€” a migrating identity holds a
    /// virtual position for exactly the window between "the Coordinator resolved a slot" and
    /// "qspn has verified it is actually reachable", and a virtual position's own out-of-range
    /// (`pos >= gsize(level)`) encoding is not a valid bit pattern for
    /// [`addressing::host_address`]'s packing β€” installing it would not fail loudly, it would
    /// silently claim a wrong `/32` (the out-of-range value corrupting adjacent levels' bits in
    /// the packed accumulator) or a right-looking one that stops being right the moment a real
    /// position is finally assigned. Suppressing here rather than erroring keeps a caller that
    /// constructs a [`RouteInstaller`] before a position resolves working exactly as if it had
    /// waited to construct one at all β€” `Self::identity_address` simply stays `None` until a
    /// caller re-invokes this once [`Naddr::is_virtual`] is false (a fresh, real [`Naddr`]).
    ///
    /// # Errors
    /// [`RouteError::Addressing`] if this identity's [`Naddr`] does not fit the `10.0.0.0/8`
    /// address space; [`RouteError::Netlink`] if either kernel mutation fails.
    pub async fn install_identity(&mut self) -> Result<(), RouteError> {
        if self.my_naddr.is_virtual() {
            return Ok(());
        }
        let address = addressing::host_address(&self.my_naddr)?;
        self.kernel
            .add_address(&Interface::name(IDENTITY_ADDRESS_INTERFACE), address)
            .await?;
        self.kernel
            .add_rule(&RuleSpec {
                table: self.table,
                priority: self.rule_priority,
                selector: RuleSelector::Any,
            })
            .await?;
        self.identity_address = Some(address);
        Ok(())
    }

    /// Diffs `snapshot` against the last applied one and issues only the resulting deltas:
    /// `add_route` for a newly-reachable destination, `change_route` for one whose target
    /// changed, `remove_route` for one that dropped out of the snapshot, and nothing for an
    /// unchanged destination.
    ///
    /// A no-op, deliberately, while [`Naddr::is_virtual`] β€” see [`Self::install_identity`]'s
    /// doc for why: [`addressing::gnode_destination`] reads every level above a destination's
    /// own from this identity's `my_naddr`, so a virtual position anywhere in it can corrupt an
    /// silently drops a *previously* applied real route, either β€” with nothing new to diff
    /// against, the last-known-good `self.applied` set is left exactly as it was rather than
    /// torn down, so a caller must still call [`Self::teardown`] explicitly to remove it.
    ///
    /// A destination whose [`HCoord`] [`addressing::gnode_destination`] cannot encode (an
    /// out-of-range position that reached this far β€” `ntk_qspn::check_incoming_message` rejects
    /// it before it is ever admitted into routing state, but this is still the last line of
    /// defense) is logged via `tracing::warn!` and dropped from the diff, never propagated as an
    /// error: one such entry must not abort every other, unrelated destination's route
    /// maintenance for this identity, or a single malformed snapshot becomes a denial of
    /// service. Because the dropped entry is simply absent from the diffed set, any route it
    /// previously had installed is withdrawn by the same "destination missing from the new
    /// snapshot" path below that already handles a destination going unreachable β€” a stale route
    /// this update can no longer vouch for (the destination may have moved) is left installed
    /// nowhere rather than left pointing at a CIDR nothing currently corroborates.
    ///
    /// `self.applied` is updated one mutation at a time, immediately after the kernel call that
    /// mutation corresponds to succeeds β€” never wholesale after the whole batch. This makes the
    /// invariant "`self.applied` always describes what is actually in the kernel" hold even
    /// across a mid-batch failure: if a netlink call fails partway through, every earlier
    /// success in *this* call is already recorded, this call returns `Err` with `self.applied`
    /// accurately reflecting the ops that did land, and the next call re-diffs from that
    /// ground truth β€” retrying only the remainder instead of replaying already-applied
    /// mutations into `AlreadyExists` forever. It also closes a second-order leak this
    /// per-batch update previously had: because a *successful* add/change now lands in
    /// `self.applied` the instant its kernel call returns, a destination can no longer succeed
    /// in the kernel while staying unrecorded here β€” so a destination that later vanishes from
    /// a snapshot is always visible to the `self.applied`-vs-`next` diff and gets its
    /// `remove_route` issued, instead of being invisible to both sides and orphaned forever.
    ///
    /// # Errors
    /// [`RouteError::Netlink`] if a kernel mutation fails.
    pub async fn apply(&mut self, snapshot: &RouteSnapshot) -> Result<AppliedDelta, RouteError> {
        if self.my_naddr.is_virtual() {
            return Ok(AppliedDelta::default());
        }
        let src = self.identity_address.map(|net| net.address());
        let mut next = BTreeMap::new();
        for entry in snapshot.levels.iter().flatten() {
            let destination = match addressing::gnode_destination(&self.my_naddr, entry.destination)
            {
                Ok(destination) => destination,
                Err(error) => {
                    tracing::warn!(
                        level = entry.destination.level,
                        pos = entry.destination.pos,
                        %error,
                        "route snapshot entry dropped: destination cannot be encoded"
                    );
                    continue;
                }
            };
            let target = route_target(&entry.paths, &self.arc_endpoints, src);
            next.insert(
                entry.destination,
                RouteSpec {
                    destination,
                    table: self.table,
                    target,
                },
            );
        }

        let mut delta = AppliedDelta::default();
        for (destination, spec) in &next {
            let is_new = self
                .applied
                .get(destination)
                .is_none_or(|previous| previous != spec);
            if !is_new {
                continue;
            }
            if self.applied.contains_key(destination) {
                self.kernel.change_route(spec).await?;
                delta.changed += 1;
            } else {
                self.kernel.add_route(spec).await?;
                delta.added += 1;
            }
            self.applied.insert(*destination, spec.clone());
        }

        let stale: Vec<HCoord> = self
            .applied
            .keys()
            .filter(|destination| !next.contains_key(destination))
            .copied()
            .collect();
        for destination in stale {
            let previous = self
                .applied
                .get(&destination)
                .expect("destination came from self.applied's own keys")
                .clone();
            self.kernel
                .remove_route(RouteKey {
                    destination: previous.destination,
                    table: previous.table,
                })
                .await?;
            self.applied.remove(&destination);
            delta.removed += 1;
        }

        Ok(delta)
    }

    /// Removes everything this installer added: every currently-applied route, then the
    /// catch-all rule, then the identity address β€” the reverse of the order
    /// [`RouteInstaller::install_identity`]/[`RouteInstaller::apply`] added them in.
    ///
    /// # Errors
    /// [`RouteError::Netlink`] if a kernel mutation fails.
    pub async fn teardown(&mut self) -> Result<(), RouteError> {
        for (_, spec) in std::mem::take(&mut self.applied) {
            self.kernel
                .remove_route(RouteKey {
                    destination: spec.destination,
                    table: spec.table,
                })
                .await?;
        }
        if let Some(address) = self.identity_address.take() {
            self.kernel
                .remove_rule(&RuleSpec {
                    table: self.table,
                    priority: self.rule_priority,
                    selector: RuleSelector::Any,
                })
                .await?;
            self.kernel
                .remove_address(&Interface::name(IDENTITY_ADDRESS_INTERFACE), address)
                .await?;
        }
        Ok(())
    }
}

/// A usable path: its leading arc resolved to a real gateway/interface.
struct Usable {
    via: Ipv4Addr,
    dev: Interface,
    cost: Cost,
}

/// Resolves every path's leading arc against `endpoints`, dropping any path whose arc has no
/// recorded endpoint or whose cost is [`Cost::Dead`] (unreachable regardless of endpoint
/// knowledge). `paths` is ascending cost per [`ntk_qspn::RouteSnapshot`]'s contract, so the
/// filtered order stays best-first.
fn resolve_usable(
    paths: &[RoutePath],
    endpoints: &BTreeMap<ArcId, (Ipv4Addr, Interface)>,
) -> Vec<Usable> {
    paths
        .iter()
        .filter(|path| path.cost != Cost::Dead)
        .filter_map(|path| {
            let (via, dev) = endpoints.get(&path.arc)?;
            Some(Usable {
                via: *via,
                dev: dev.clone(),
                cost: path.cost,
            })
        })
        .collect()
}

/// Builds the [`RouteTarget`] for one destination's paths: [`RouteTarget::Unreachable`] if no
/// path's leading arc is known, [`RouteTarget::Gateway`] for exactly one usable path (after
/// [`dedupe_by_gateway`] collapses any sharing a kernel-visible gateway), or
/// [`RouteTarget::Multipath`] for several β€” see [`nexthop_weight`] for the weight derivation.
fn route_target(
    paths: &[RoutePath],
    endpoints: &BTreeMap<ArcId, (Ipv4Addr, Interface)>,
    src: Option<Ipv4Addr>,
) -> RouteTarget {
    let usable = dedupe_by_gateway(resolve_usable(paths, endpoints));
    match usable.as_slice() {
        [] => RouteTarget::Unreachable,
        [only] => RouteTarget::Gateway {
            via: only.via,
            dev: only.dev.clone(),
            src,
        },
        several => {
            let best = several
                .iter()
                .map(|u| effective_cost(u.cost))
                .min()
                .expect("non-empty slice");
            RouteTarget::Multipath(
                several
                    .iter()
                    .map(|u| Nexthop {
                        via: u.via,
                        dev: u.dev.clone(),
                        weight: nexthop_weight(u.cost, best),
                    })
                    .collect(),
            )
        }
    }
}

/// Collapses [`Usable`] paths that resolve to the same kernel-visible gateway `(via, dev)` into
/// one entry each.
///
/// A kernel FIB nexthop is identified by `(via, dev)` alone β€” qspn, however, may legitimately
/// admit several [`RoutePath`]s to the same destination that share a *leading* arc and diverge
/// only afterwards (their full paths differ, but this node's next hop is identical). Installing
/// one nexthop per such path would register the same `(via, dev)` pair twice in one
/// [`RouteTarget::Multipath`], which every netlink backend this crate targets either rejects or
/// silently collapses itself β€” either way, not the deliberate weight split this crate computes.
///
/// `usable` arrives best-first (ascending cost, [`resolve_usable`]'s doc), so keeping the first
/// occurrence of each `(via, dev)` keeps its cheapest path β€” the most favorable weight that
/// gateway can offer. Merging by *summing* the colliding paths' weights was considered and
/// rejected: once traffic is handed to a gateway it leaves this node's control at that first
/// hop, so two qspn paths sharing one only ever describe one physical link's worth of egress
/// capacity, not two β€” summing would double-count it and unfairly starve a genuinely distinct
/// gateway's share of the split.
fn dedupe_by_gateway(usable: Vec<Usable>) -> Vec<Usable> {
    let mut seen = std::collections::HashSet::new();
    usable
        .into_iter()
        .filter(|u| seen.insert((u.via, u.dev.clone())))
        .collect()
}

/// A path's cost as a strictly positive magnitude for weight-ratio purposes:
/// [`Cost::Null`] (a trivial zero-cost path) is treated as `1` so it still receives the maximum
/// share rather than triggering a division by zero, and larger [`Cost::Finite`] magnitudes are
/// used as-is (also floored at `1`).
fn effective_cost(cost: Cost) -> u64 {
    match cost {
        Cost::Null => 1,
        Cost::Finite(magnitude) => magnitude.max(1),
        Cost::Dead => unreachable!("Dead-cost paths are filtered out by resolve_usable"),
    }
}

/// Derives a [`Nexthop::weight`] from `cost` relative to the cheapest usable path's cost `best`.
///
/// [`Nexthop::weight`] is the raw kernel field, where the *real* ECMP share the kernel gives a
/// nexthop is `weight + 1` (0..=255 stored, 1..=256 real). This function gives the cheapest path
/// the maximum real weight (256) and scales every other path's real weight down by
/// `best / effective_cost(cost)`, floored at a real weight of 1 (stored `0`) so no reachable path
/// is ever starved to zero traffic β€” upstream never installed multipath routes at all
/// (`research/notes/03-specs-and-rfcs.md` RFC 0013), so this ratio is this crate's own design
/// choice: proportional-to-inverse-cost is the standard weighted-ECMP heuristic (lower latency/
/// metric gets more traffic), applied here as the simplest ratio that is monotonic, bounded, and
/// exactly reproduces equal weights for equal costs.
fn nexthop_weight(cost: Cost, best: u64) -> u8 {
    let effective = effective_cost(cost);
    let real_weight = (256u128 * u128::from(best) / u128::from(effective)).clamp(1, 256);
    (real_weight - 1) as u8
}

/// Everything that can go wrong installing or diffing one identity's kernel routing state.
#[derive(Debug, thiserror::Error)]
pub enum RouteError {
    /// A kernel-state mutation failed.
    #[error(transparent)]
    Netlink(#[from] ntk_netlink::NetlinkError),
    /// A [`Naddr`]/[`HCoord`] could not be translated into the `10.0.0.0/8` address space.
    #[error(transparent)]
    Addressing(#[from] AddressingError),
}

#[cfg(test)]
mod tests {
    use super::*;
    use ntk_qspn::RouteEntry;

    #[test]
    fn nexthop_weight_gives_cheapest_path_max_weight() {
        let best = effective_cost(Cost::Finite(10));
        assert_eq!(nexthop_weight(Cost::Finite(10), best), 255);
        // 256 * 10 / 20 = 128 real weight -> stored 127.
        assert_eq!(nexthop_weight(Cost::Finite(20), best), 127);
        // 256 * 10 / 1000 rounds down to 2 real weight -> stored 1, never starved to 0.
        assert_eq!(nexthop_weight(Cost::Finite(1000), best), 1);
    }

    #[test]
    fn nexthop_weight_treats_null_cost_as_best() {
        let best = effective_cost(Cost::Null);
        assert_eq!(nexthop_weight(Cost::Null, best), 255);
    }

    fn path(arc: u32, cost: u64) -> RoutePath {
        RoutePath {
            arc: ArcId::from(arc),
            hops: Vec::new(),
            cost: Cost::Finite(cost),
            nodes_inside: 0,
        }
    }

    /// Two paths sharing a leading arc (they diverge only downstream of it) must collapse into
    /// exactly one kernel nexthop β€” a FIB nexthop is `(via, dev)`, not "which qspn path chose
    /// it" β€” while a path over a genuinely distinct arc keeps its own nexthop.
    #[test]
    fn route_target_dedupes_nexthops_sharing_a_gateway() {
        let via1: Ipv4Addr = "169.254.1.1".parse().unwrap();
        let via2: Ipv4Addr = "169.254.2.2".parse().unwrap();
        let dev1 = Interface::name("eth0");
        let dev2 = Interface::name("eth1");
        let mut endpoints = BTreeMap::new();
        endpoints.insert(ArcId::from(1), (via1, dev1.clone()));
        endpoints.insert(ArcId::from(2), (via2, dev2.clone()));

        // Ascending cost, as `RouteSnapshot` guarantees: arc 1 twice (a cheaper and a costlier
        // path sharing that same first hop), then arc 2 once.
        let paths = vec![path(1, 10), path(1, 15), path(2, 20)];

        let target = route_target(&paths, &endpoints, None);
        let RouteTarget::Multipath(nexthops) = target else {
            panic!("expected a multipath target, got {target:?}");
        };
        assert_eq!(
            nexthops.len(),
            2,
            "the two arc-1 paths must collapse into a single nexthop: {nexthops:?}"
        );
        let best = effective_cost(Cost::Finite(10));
        assert_eq!(
            nexthops,
            vec![
                Nexthop {
                    via: via1,
                    dev: dev1,
                    weight: nexthop_weight(Cost::Finite(10), best),
                },
                Nexthop {
                    via: via2,
                    dev: dev2,
                    weight: nexthop_weight(Cost::Finite(20), best),
                },
            ]
        );
    }

    /// Once dedupe collapses every path down to one gateway, the target must be a plain
    /// [`RouteTarget::Gateway`], never a one-element `Multipath`.
    #[test]
    fn route_target_collapses_single_gateway_to_plain_gateway() {
        let via: Ipv4Addr = "169.254.9.9".parse().unwrap();
        let dev = Interface::name("eth0");
        let mut endpoints = BTreeMap::new();
        endpoints.insert(ArcId::from(1), (via, dev.clone()));

        let paths = vec![path(1, 10), path(1, 25)];
        let target = route_target(&paths, &endpoints, None);
        assert_eq!(
            target,
            RouteTarget::Gateway {
                via,
                dev,
                src: None,
            }
        );
    }

    /// A migrating identity's [`RouteInstaller`] must never touch the kernel while
    /// [`Naddr::is_virtual`] β€” see [`RouteInstaller::install_identity`]/[`RouteInstaller::apply`]'s
    /// own docs for why a virtual position's out-of-range encoding would otherwise corrupt the
    /// packed `10.0.0.0/8` bit pattern rather than merely fail.
    #[tokio::test]
    async fn virtual_naddr_suppresses_both_identity_and_route_installation() {
        let topology = ntk_common::Topology::new([4, 2]).unwrap();
        // Level 0's position is out of range (gsize(0) == 4) β€” virtual by construction.
        let virtual_naddr = Naddr::new_allowing_virtual(topology.clone(), [4, 0]).unwrap();
        assert!(virtual_naddr.is_virtual());

        let kernel = ntk_netlink::FakeNetlink::with_links(vec![ntk_netlink::LinkInfo {
            index: 1,
            name: "lo".into(),
            is_up: true,
        }]);
        let mut installer = RouteInstaller::new(kernel, virtual_naddr.clone(), 200, 9_990);

        installer
            .install_identity()
            .await
            .expect("a virtual naddr must not error, just skip");
        assert!(
            installer.kernel_ref().operations().is_empty(),
            "no address/rule must ever be installed for a virtual position"
        );

        let snapshot = RouteSnapshot {
            levels: vec![
                vec![RouteEntry {
                    destination: HCoord::new(0, 1),
                    paths: vec![RoutePath {
                        arc: ArcId::from(1),
                        hops: Vec::new(),
                        cost: Cost::Finite(10),
                        nodes_inside: 1,
                    }],
                }],
                Vec::new(),
            ],
        };
        let delta = installer
            .apply(&snapshot)
            .await
            .expect("a virtual naddr must not error, just skip");
        assert_eq!(
            delta,
            AppliedDelta::default(),
            "no route diff must ever be computed against a virtual position"
        );
        assert!(
            installer.kernel_ref().operations().is_empty(),
            "apply must not touch the kernel while the position is virtual, even with real \
             destinations in the snapshot"
        );

        // Once a real (non-virtual) naddr is known, a fresh installer behaves normally.
        let real_naddr = Naddr::new(topology, [1, 0]).unwrap();
        let mut real_installer = RouteInstaller::new(
            ntk_netlink::FakeNetlink::with_links(vec![ntk_netlink::LinkInfo {
                index: 1,
                name: "lo".into(),
                is_up: true,
            }]),
            real_naddr,
            200,
            9_990,
        );
        real_installer
            .install_identity()
            .await
            .expect("a real naddr installs normally");
        assert!(!real_installer.kernel_ref().operations().is_empty());
    }

    /// [`RouteInstaller::realize`] must let the *same* installer transition from virtual
    /// (no-op) to real (actually installs) in place, keeping arc endpoints recorded while still
    /// virtual β€” [`ntkd::node::lifecycle::migrate`]'s own "confirm bootstrap, then realize"
    /// sequence depends on this rather than discarding and rebuilding the installer.
    #[tokio::test]
    async fn realize_transitions_a_virtual_installer_to_real_in_place() {
        let topology = ntk_common::Topology::new([4, 2]).unwrap();
        let virtual_naddr = Naddr::new_allowing_virtual(topology.clone(), [4, 0]).unwrap();
        let kernel = ntk_netlink::FakeNetlink::with_links(vec![ntk_netlink::LinkInfo {
            index: 1,
            name: "lo".into(),
            is_up: true,
        }]);
        let mut installer = RouteInstaller::new(kernel, virtual_naddr, 200, 9_990);
        let via: Ipv4Addr = "169.254.1.1".parse().unwrap();
        installer.set_arc_endpoint(ArcId::from(1), via, Interface::name("eth0"));

        installer
            .install_identity()
            .await
            .expect("still virtual: must not error");
        assert!(
            installer.kernel_ref().operations().is_empty(),
            "still virtual: no kernel writes yet"
        );

        let real_naddr = Naddr::new(topology, [1, 0]).unwrap();
        installer.realize(real_naddr);
        installer
            .install_identity()
            .await
            .expect("now real: installs for the first time");
        assert!(
            !installer.kernel_ref().operations().is_empty(),
            "realize must let install_identity actually write kernel state"
        );

        let snapshot = RouteSnapshot {
            levels: vec![
                vec![RouteEntry {
                    destination: HCoord::new(0, 1),
                    paths: vec![RoutePath {
                        arc: ArcId::from(1),
                        hops: Vec::new(),
                        cost: Cost::Finite(10),
                        nodes_inside: 1,
                    }],
                }],
                Vec::new(),
            ],
        };
        let delta = installer.apply(&snapshot).await.unwrap();
        assert_eq!(
            delta.added, 1,
            "the arc endpoint recorded while virtual must resolve now that the position is real"
        );
    }

    /// A snapshot with one out-of-range destination alongside several valid ones must not
    /// abort β€” every valid destination's diff still applies, and the malformed one is dropped
    /// (never surfaced as an `Err`, per [`RouteInstaller::apply`]'s doc). This is the DoS-vs-
    /// integrity tradeoff this test exists to pin: before this behavior, a single bad entry
    /// aborted the whole snapshot and *zero* routes were installed or updated for it.
    #[tokio::test]
    async fn apply_skips_unencodable_entry_and_withdraws_its_previous_route() {
        let topology = ntk_common::Topology::new([4, 2, 2, 2]).unwrap();
        let naddr = Naddr::new(topology, [1, 0, 1, 0]).unwrap();
        let kernel = ntk_netlink::FakeNetlink::with_links(vec![ntk_netlink::LinkInfo {
            index: 1,
            name: "lo".into(),
            is_up: true,
        }]);
        let mut installer = RouteInstaller::new(kernel, naddr, 200, 9_990);

        let stable = HCoord::new(1, 1); // gsize(1) == 2: valid.
        let stale = HCoord::new(2, 0); // gsize(2) == 2: valid, but dropped in round 2.

        let baseline = RouteSnapshot {
            levels: vec![
                Vec::new(),
                vec![RouteEntry {
                    destination: stable,
                    paths: vec![path(1, 10)],
                }],
                vec![RouteEntry {
                    destination: stale,
                    paths: vec![path(2, 10)],
                }],
                Vec::new(),
            ],
        };
        let baseline_delta = installer.apply(&baseline).await.unwrap();
        assert_eq!(baseline_delta.added, 2, "both valid destinations install");
        installer.kernel_ref().clear_operations();

        // Round 2: `stable` is unchanged, `stale`'s slot is replaced by an out-of-range
        // position (pos 7 needs 3 bits, level 2's field only has 1 -- gsize(2) == 2), and a
        // brand-new valid destination `fresh` is added.
        let fresh = HCoord::new(0, 3); // gsize(0) == 4: valid.
        let corrupt = HCoord::new(2, 7); // gsize(2) == 2: out of range.
        let next_snapshot = RouteSnapshot {
            levels: vec![
                vec![RouteEntry {
                    destination: fresh,
                    paths: vec![path(3, 10)],
                }],
                vec![RouteEntry {
                    destination: stable,
                    paths: vec![path(1, 10)],
                }],
                vec![RouteEntry {
                    destination: corrupt,
                    paths: vec![path(4, 10)],
                }],
                Vec::new(),
            ],
        };
        let delta = installer
            .apply(&next_snapshot)
            .await
            .expect("one unencodable entry must not abort the whole snapshot");
        assert_eq!(delta.added, 1, "fresh is added");
        assert_eq!(delta.changed, 0, "stable is unchanged, no op issued for it");
        assert_eq!(
            delta.removed, 1,
            "stale's previously-installed route is withdrawn: a corrupt update can no longer \
             vouch for a CIDR it can no longer even name, so the safer state is unrouted, not a \
             route nothing currently corroborates"
        );

        let ops = installer.kernel_ref().operations();
        assert_eq!(
            ops.len(),
            2,
            "exactly the good destinations' ops, never zero and never the bad one: {ops:?}"
        );
        assert!(
            matches!(&ops[0], ntk_netlink::Operation::AddRoute(spec) if spec.table == 200),
            "fresh must still be added: {ops:?}"
        );
        assert!(
            matches!(&ops[1], ntk_netlink::Operation::RemoveRoute(key) if key.table == 200),
            "stale's stale route must be withdrawn: {ops:?}"
        );
    }
}