Skip to content

Stale-node detection calls remove_stale_links but never remove_stale_nodes, leaving orphan nodes in node_index #243

Description

@forkwright

Finding

run_stale_detection calls proc.topology_mut().remove_stale_links(stale_timeout * 3) but never calls remove_stale_nodes. remove_stale_links removes edges from the petgraph and leaves node_index entries intact; node eviction only happens in remove_stale_nodes. The two methods have deliberately different semantics, and skipping the node-removal step means node_count(), contains_node(), neighbors(), shortest_path(), and connected_components() all continue to see dead nodes indefinitely. There is no call to remove_stale_nodes anywhere in non-test production code.

Evidence

crates/kerykeion/src/discovery.rs:209 — only edges are pruned:

proc.topology_mut().remove_stale_links(stale_timeout * 3);

crates/kerykeion/src/topology.rs:88remove_stale_nodes is the only path that evicts node_index entries, and it is never called in production:

pub fn remove_stale_nodes(&mut self, timeout: Duration) {
    let cutoff = Instant::now() - timeout;
    let stale: Vec<NodeNum> = self
        .node_index
        .iter()

Why this matters

A node that goes offline persists as a phantom in node_index forever. connected_components() then returns a singleton component for every dead node, triggering spurious PartitionDetected signals that propagate to consumers and corrupt the operator's picture of the mesh during the exact moments (nodes dropping out) that matter most under a counter-surveillance threat model. The accumulation is unbounded over long uptimes, and an adversary flooding unique source node IDs can inflate the phantom node count and the false-partition signal rate at will.

Desired correction

After remove_stale_links, call remove_stale_nodes with the same or a shorter timeout so node_index stays consistent with the edge set. Preferably unify both steps into a single prune_stale(timeout) method on MeshTopology that always removes edges first then orphan nodes, making it impossible to call one without the other.

Done when: run_stale_detection (or its equivalent) removes both stale edges and stale nodes in the correct order, and a test verifies that a node with no live edges is absent from node_count() after a cleanup cycle.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions