pub struct RoutingTable { /* private fields */ }Expand description
Routing table for stream-to-destination mapping
Implementations§
Source§impl RoutingTable
impl RoutingTable
Sourcepub fn add_route(&self, dest_id: u64, next_hop: SocketAddr) -> u64
pub fn add_route(&self, dest_id: u64, next_hop: SocketAddr) -> u64
Add or update the ORDINARY (unauthenticated) route.
Called by routed end-to-end installs and legacy/manual callers.
Unconditionally replaces the ordinary candidate — a direct route
is always preferred over any indirect one (metric 1; pingwave
routes carry hop_count + 2, never below 2) — and refreshes
updated_at.
Non-destructive to authenticated state by construction: this
writes only the ordinary slot, so a routed (connect_via)
install can no longer erase an authenticated learned route to
the same endpoint. A routed end-to-end session contributes no
adjacency evidence, and must not delete stronger evidence.
Returns the transition token this install produced, so a caller
that may later have to undo it can name the exact write rather
than matching on a destination and address a replacement could
already have reused.
Sourcepub fn add_route_with_metric(
&self,
dest_id: u64,
next_hop: SocketAddr,
metric: u16,
)
pub fn add_route_with_metric( &self, dest_id: u64, next_hop: SocketAddr, metric: u16, )
Add or update the ORDINARY route with an explicit metric.
Used by the pingwave-driven route installer. Within the ordinary slot the existing candidate is replaced only if the new metric is strictly better — a peer that crafts an equal metric must not displace an installed route — and on equal/worse metrics the existing candidate is kept but refreshed, since an alternate path’s arrival is evidence the destination is still reachable.
Both of those rules apply ONLY among unauthenticated candidates. This writer cannot see, replace, or refresh the protected candidate: an unauthenticated datagram is not evidence about an authenticated adjacency, in either direction.
Sourcepub fn add_authenticated_route_with_metric(
&self,
dest_id: u64,
next_hop: SocketAddr,
next_hop_id: u64,
metric: u16,
)
pub fn add_authenticated_route_with_metric( &self, dest_id: u64, next_hop: SocketAddr, next_hop_id: u64, metric: u16, )
Add or update a learned route that binds the authenticated
identity of its adjacent next hop (SUBNET_AUTH_PLAN.md D6).
Same precedence contract as Self::add_route_with_metric —
a strictly better metric replaces, anything else keeps the
installed entry — with one addition: an equal-or-worse arrival
that agrees with the installed entry’s next_hop upgrades an
identity-less entry in place. A legacy entry left behind by an
older writer would otherwise pin protected forwarding dead for
that destination forever, because equal-metric refreshes never
replace the entry that could carry the identity.
What an equal-or-worse arrival can never do is rewrite an installed identity — or refresh a route it does not carry. Freshness belongs to the installed adjacent identity/path:
- same address + same identity → refresh;
- same address + no installed identity → upgrade + refresh;
- same address + conflicting identity → no rewrite, no refresh (a conflicting claim is evidence of address reuse, not of reachability — the conflicted entry is left to age out);
- different next hop → no rewrite, no refresh. Another authenticated peer proving an alternate path exists is not evidence the installed path is alive; letting it renew the installed binding would pin a blackholed protected route fresh forever while the table refuses to switch to the alternate. (The ordinary writer’s refresh rule is unchanged — ordinary candidates carry no protected traffic. A future multi-route table may retain the alternate separately.)
The “no identity yet” case is now structural rather than an in-place upgrade: an ordinary candidate lives in its own slot, so an authenticated arrival always has an empty protected slot to land in regardless of what any pingwave installed. That is what keeps a forged legacy route from suppressing protected reachability forever.
Sourcepub fn invalidate_protected_via(&self, identity: u64) -> usize
pub fn invalidate_protected_via(&self, identity: u64) -> usize
Retire every PROTECTED candidate bound to identity, across
all destinations. Returns how many were retired.
Called when an authenticated adjacency ends in a way that does
not replace it with an equivalent one — a direct session
displaced by a routed end-to-end session, say. Every protected
candidate whose next_hop_id is that peer was evidence about an
adjacency that no longer exists, so it is invalidated rather
than migrated: a routed session is not a weaker version of the
direct one, it is different evidence entirely, and it cannot
carry protected forwarding at all.
Ordinary candidates are untouched — they never depended on the adjacency.
Sourcepub fn remove_ordinary_route(&self, dest_id: u64) -> Option<RouteEntry>
pub fn remove_ordinary_route(&self, dest_id: u64) -> Option<RouteEntry>
Remove the ORDINARY candidate — the symmetric counterpart of
Self::add_route, which writes only that slot.
This is what a caller undoing its own manual/legacy install wants. Removing the protected candidate too would let an ordinary-only API delete authenticated state it never created.
Sourcepub fn remove_protected_route_if_owned(
&self,
dest_id: u64,
identity: u64,
) -> Option<RouteEntry>
pub fn remove_protected_route_if_owned( &self, dest_id: u64, identity: u64, ) -> Option<RouteEntry>
Remove the PROTECTED candidate, but only when it is bound to
identity — the owner’s own retraction.
Sourcepub fn remove_destination_all_candidates(
&self,
dest_id: u64,
) -> Option<RouteEntry>
pub fn remove_destination_all_candidates( &self, dest_id: u64, ) -> Option<RouteEntry>
Remove a destination outright — BOTH candidates, regardless of provenance or ownership.
Explicitly administrative: it crosses the provenance boundary
every other operation respects, so it is named for what it does
rather than reading like the inverse of add_route (it is not —
see Self::remove_ordinary_route).
Returns the protected candidate if there was one, else the ordinary one.
Sourcepub fn remove_route(&self, dest_id: u64) -> Option<RouteEntry>
👎Deprecated: asymmetric with add_route: use remove_ordinary_route, remove_protected_route_if_owned, or remove_destination_all_candidates
pub fn remove_route(&self, dest_id: u64) -> Option<RouteEntry>
asymmetric with add_route: use remove_ordinary_route, remove_protected_route_if_owned, or remove_destination_all_candidates
Deprecated alias for Self::remove_destination_all_candidates.
Kept so existing callers keep compiling, but the name is
misleading now that add_route writes one slot and this clears
both. New code should say which it means.
Sourcepub fn remove_route_if_from_hop(
&self,
dest_id: u64,
next_hop: SocketAddr,
identity: u64,
sender_is_direct: bool,
) -> TransitionOutcome
pub fn remove_route_if_from_hop( &self, dest_id: u64, next_hop: SocketAddr, identity: u64, sender_is_direct: bool, ) -> TransitionOutcome
Remove what the authenticated sender identity actually OWNS at
next_hop — the predicate route WITHDRAWAL must use.
Ownership differs by candidate, and neither half is an address match alone:
- protected —
next_hop_id == Some(identity)is sufficient, and the address is deliberately NOT required.install_peerpublishes the peer record and session index before migrating routes, so an authenticated withdrawal can legitimately arrive under the new session while the binding still carries the old address. Requiring the address there made the handler drop the graph edge and then return without removing the route, leaving route and graph state inconsistent until age-out. - ordinary — the address must match AND
sender_is_directmust confirm the sender genuinely owns that address (forward and reverse indexes agree). A routed end-to-end peer records its RELAY’s address; without the confirmation it could remove unrelated legacy routes sitting at the shared relay tuple, which belong to the relay, not to it.
Returns a TransitionOutcome describing what actually
changed, NOT merely whether something was removed.
With two candidates per destination, “a candidate was removed” and “this node can no longer reach the destination” are different facts. A caller that treats the first as the second disrupts sensing and cascades an “unreachable via me” withdrawal while a live alternate candidate is still installed right there. The outcome reports the effective path before and after and whether the destination is still reachable, so the caller can distinguish removing a hidden candidate (nothing to announce), switching the effective path (re-anchor locally), and genuinely losing the destination (promote or cascade).
Sourcepub fn remove_ordinary_route_if_next_hop_is(
&self,
dest_id: u64,
expected_next_hop: SocketAddr,
) -> bool
pub fn remove_ordinary_route_if_next_hop_is( &self, dest_id: u64, expected_next_hop: SocketAddr, ) -> bool
Remove the ORDINARY candidate for dest_id iff its next_hop
still equals expected_next_hop — the provenance-specific
rollback for a caller that installed an ordinary route.
Rollback must undo exactly what it did. Scanning both slots by address lets an ordinary registration’s failure erase a protected candidate that merely shares the address.
Sourcepub fn remove_ordinary_route_if_token_is(
&self,
dest_id: u64,
expected_token: u64,
) -> bool
pub fn remove_ordinary_route_if_token_is( &self, dest_id: u64, expected_token: u64, ) -> bool
Remove the ORDINARY candidate for dest_id iff the destination
still carries the exact transition token expected_token — the
rollback form for a caller undoing a write it can name.
Stronger than the address form, and for a reason address cannot
cover: two registrations for the same endpoint through the same
relay install an identical (dest_id, next_hop) pair, so an
address-keyed rollback of the FIRST one silently deletes the
second one’s route. The token is never reused, so it names the
write and not the shape of the write.
Deliberately conservative: any other observable change to this destination since the install — including one to the protected candidate — moves the token and the rollback declines, leaving the candidate to age out rather than removing state it can no longer prove it owns.
Sourcepub fn remove_route_if_next_hop_is(
&self,
dest_id: u64,
expected_next_hop: SocketAddr,
) -> bool
pub fn remove_route_if_next_hop_is( &self, dest_id: u64, expected_next_hop: SocketAddr, ) -> bool
Remove any candidate for dest_id whose next_hop still equals
expected_next_hop. Used by rollback paths that registered a
specific route and need to undo it without clobbering a newer
concurrently-written entry. Returns true if anything was
removed.
Address-only: correct for undoing a write THIS node just made
(the caller knows exactly what it installed). Withdrawal — a
claim from a REMOTE sender — must use
Self::remove_route_if_from_hop instead.
Sourcepub fn migrate_next_hop(
&self,
old: SocketAddr,
new: SocketAddr,
identity: u64,
) -> usize
pub fn migrate_next_hop( &self, old: SocketAddr, new: SocketAddr, identity: u64, ) -> usize
Repoint the routes that ride through a re-handshaking peer at
its new address, refreshing updated_at so the migrated
entries aren’t immediately swept. Returns the number of
entries migrated.
Called when a peer re-handshakes from a new address (NAT
rebind): multi-hop routes learned through that peer still
carry its previous address as next_hop, and — because
equal-metric refreshes deliberately never overwrite an
installed next_hop — nothing else would ever repoint them.
Without this migration, address-keyed operations such as
Self::remove_route_if_next_hop_is (used by the RT-5 route
withdrawal receive path) silently miss those entries.
An entry moves only when the caller owns BOTH halves of it:
- An entry bound to
identityand still atoldfollows the identity tonew. Requiring the expected old address is what makes a STALE migration harmless: two accepted re-handshakes for the same peer can serialize their peer-map replacement but finish these migrations in the opposite order, and identity equality alone would let the olderA→Broll back the newerA→C. The older caller’s routes no longer point atA, so it matches nothing and returns 0. - An entry bound to a different identity is never touched,
even when its
next_hopequalsold: the address may have been reused, and an address match must not retarget a route that belongs to someone else’s authenticated adjacency. - A legacy entry (no identity) migrates by address match, as before — it carries no protected traffic either way.
Sourcepub fn lookup(&self, dest_id: u64) -> Option<SocketAddr>
pub fn lookup(&self, dest_id: u64) -> Option<SocketAddr>
Look up next hop for destination — the ordinary forwarding lookup, over whichever candidate is currently effective.
Returns None for stale routes — a candidate whose updated_at
is older than the configured max_route_age (default: very
large; call Self::set_max_route_age to enable expiry). Stale
candidates stay in the map until a periodic Self::sweep_stale
call removes them.
Sourcepub fn add_authenticated_route(
&self,
dest_id: u64,
next_hop: SocketAddr,
next_hop_id: u64,
) -> u64
pub fn add_authenticated_route( &self, dest_id: u64, next_hop: SocketAddr, next_hop_id: u64, ) -> u64
Install an identity-bound route for protected forwarding.
Returns the transition token this install produced — see
Self::add_route.
Sourcepub fn lookup_authenticated(&self, dest_id: u64) -> Option<(u64, SocketAddr)>
pub fn lookup_authenticated(&self, dest_id: u64) -> Option<(u64, SocketAddr)>
The PROTECTED candidate’s identity and address — the
protected-forwarding lookup. It answers “who is the next hop”,
where plain lookup answers only “where do I send”.
Reads the protected slot alone: an ordinary candidate, however
good its metric and whoever installed it, resolves to None
rather than to an unauthenticated guess.
Sourcepub fn rebind_authenticated_route(
&self,
dest_id: u64,
identity: u64,
new_addr: SocketAddr,
) -> bool
pub fn rebind_authenticated_route( &self, dest_id: u64, identity: u64, new_addr: SocketAddr, ) -> bool
Move the protected candidate to a new address under the same
identity. Returns false when it is absent or bound to a
different identity.
Sourcepub fn observe(&self, dest_id: u64) -> Option<RouteObservation>
pub fn observe(&self, dest_id: u64) -> Option<RouteObservation>
Read a destination’s full candidate state for a later
conditional write. Pair with
Self::install_metered_if_unchanged or
Self::remove_failed_candidates_if_unchanged.
Sourcepub fn install_metered_if_unchanged(
&self,
dest_id: u64,
observed: RouteObservation,
next_hop: SocketAddr,
provenance: AlternateProvenance,
metric: u16,
) -> Option<TransitionOutcome>
pub fn install_metered_if_unchanged( &self, dest_id: u64, observed: RouteObservation, next_hop: SocketAddr, provenance: AlternateProvenance, metric: u16, ) -> Option<TransitionOutcome>
Install a route with an explicit metric and provenance, ONLY if
the destination has not changed since observed was read — the
compare-and-set form every event-driven rewriter must use.
Those callers read peer state, decide, and only then write. Between the two a fresh authenticated route can land; an unconditional write would clobber it with a decision made about state that no longer exists. Decision-time filtering does not help — the race is at mutation time.
provenance picks the slot; the caller states the metric it is
installing (metric 1 is the claim of an adjacency and nothing
else). Returns the produced TransitionOutcome — including
the new token — when the write happened, None when it was
refused.
Returning the token ATOMICALLY matters: a caller that wrote and then re-read to learn “its” token can observe a third party’s write in between and record that as its own — which is how a conditional undo ends up undoing a newer writer.
Sourcepub fn remove_failed_candidates_if_unchanged(
&self,
dest_id: u64,
observed: RouteObservation,
failed_identity: u64,
failed_addr: SocketAddr,
) -> Option<TransitionOutcome>
pub fn remove_failed_candidates_if_unchanged( &self, dest_id: u64, observed: RouteObservation, failed_identity: u64, failed_addr: SocketAddr, ) -> Option<TransitionOutcome>
Apply a peer-failure transition to ONE destination atomically: verify the observation, remove every candidate the failure invalidates, and keep every candidate it does not.
Removal is ALL failure handling does. A failure invalidates the evidence that depended on the failed peer; it does not know a replacement path, and manufacturing one here converted “some peer is alive” into “that peer may route this destination”. A surviving candidate simply wins the next lookup; a destination left with nothing becomes unreachable, which is the truthful answer until discovery produces fresh evidence.
The transition is still a CANDIDATE event, not a destination event: ownership is per candidate, matching the withdrawal rule — the protected candidate by bound identity (its address may have drifted), the ordinary one by address.
Returns None if the observation is stale (or the token space
is exhausted), in which case nothing was touched.
Sourcepub fn install_metered_if_absent(
&self,
dest_id: u64,
next_hop: SocketAddr,
provenance: AlternateProvenance,
metric: u16,
) -> bool
pub fn install_metered_if_absent( &self, dest_id: u64, next_hop: SocketAddr, provenance: AlternateProvenance, metric: u16, ) -> bool
Install a route into a destination that has NO entry at all — the compare-and-set form for a writer whose observation was “absent”.
Recovery needs this exactly once: a failure that removed a peer’s last candidate removed the destination with it, so the recovery that reinstalls the route to that peer ITSELF (from the peer’s live session — current evidence, not a saved record) observes absence. Declining when ANY entry exists is the same discipline as the token check: presence means another writer has spoken since the observation, and its evidence is newer.
Sourcepub fn sweep_stale(&self, max_age: Duration) -> usize
pub fn sweep_stale(&self, max_age: Duration) -> usize
Drop every candidate older than max_age. Returns the number of
DESTINATIONS left without one (what route_count stops
counting).
Called periodically from the heartbeat loop to keep dead routes out of the table.
A destination emptied here leaves the table with its last candidate: absence means “no current evidence”. A conditional writer whose observation predates the sweep declines — the observation no longer resolves — and can never be wrongly admitted, because a destination re-created by fresh evidence draws a fresh never-reused token.
Sourcepub fn set_max_route_age(&self, age: Duration)
pub fn set_max_route_age(&self, age: Duration)
Configure the maximum route age for lookup staleness checks.
Defaults to Duration::MAX (effectively disabled). MeshNode
sets this to 3 × session_timeout at construction.
Sourcepub fn get_stream_stats(
&self,
stream_id: u64,
) -> Option<Ref<'_, u64, SchedulerStreamStats>>
pub fn get_stream_stats( &self, stream_id: u64, ) -> Option<Ref<'_, u64, SchedulerStreamStats>>
Get stream stats, creating the entry if absent.
Shares the MAX_STREAM_STATS admission gate with the record_*
methods: an existing entry is always returned, but a novel
stream_id is only created (and returned) while the map is below
the cap, returning None once it’s reached. Without this gate,
get_stream_stats was an unbounded-growth hole — it inserted a
fresh entry for any id regardless of the cap the record_* path
enforces.
Sourcepub fn record_out(&self, stream_id: u64, bytes: u64)
pub fn record_out(&self, stream_id: u64, bytes: u64)
Record outgoing packet for stream
Sourcepub fn record_drop(&self, stream_id: u64)
pub fn record_drop(&self, stream_id: u64)
Record dropped packet for stream
Sourcepub fn route_count(&self) -> usize
pub fn route_count(&self) -> usize
Get number of routes
Sourcepub fn stream_count(&self) -> usize
pub fn stream_count(&self) -> usize
Get number of active streams
Sourcepub fn deactivate_route(&self, dest_id: u64)
pub fn deactivate_route(&self, dest_id: u64)
Mark a destination’s candidates inactive (on failure)
Sourcepub fn activate_route(&self, dest_id: u64)
pub fn activate_route(&self, dest_id: u64)
Reactivate a destination’s candidates
Sourcepub fn all_routes(&self) -> Vec<(u64, RouteEntry)>
pub fn all_routes(&self) -> Vec<(u64, RouteEntry)>
Get the effective route per destination (for debugging/stats).
Sourcepub fn all_route_candidates(&self) -> Vec<(u64, RouteEntry)>
pub fn all_route_candidates(&self) -> Vec<(u64, RouteEntry)>
Every candidate, both provenances, per destination.
Callers deciding whether a destination is AFFECTED by an event (a peer failing, say) must consider both slots: a destination can ride through the subject peer on either candidate, and looking only at the effective one would silently miss the other.
Sourcepub fn cleanup_idle_streams(&self, idle_nanos: u64) -> usize
pub fn cleanup_idle_streams(&self, idle_nanos: u64) -> usize
Clean up idle streams (no activity for given duration)
Sourcepub fn aggregate_stats(&self) -> AggregateStats
pub fn aggregate_stats(&self) -> AggregateStats
Get aggregate stats