Skip to content

fix(vpp-offload): follow the router's own addresses while the daemon runs - #355

Open
lunarthegrey wants to merge 1 commit into
mainfrom
fix/vpp-offload-live-self-networks
Open

lunarthegrey wants to merge 1 commit into
mainfrom
fix/vpp-offload-live-self-networks

Conversation

@lunarthegrey

Copy link
Copy Markdown
Contributor

Why

vpp-offload's ConvergenceEngine read the router's own addresses and subnets once, at attach (with_self_networks and its v6 twin), and judged every route via the router against them for the life of the process.

On 2026-10-11 a new IX LAN was addressed on a new bridge after the daemon started. FRR redistributes the connected /24 with the router's own address as next hop. Because the engine's subnet set predated the address, the route fell through to shape 3 (kernel_owns). The kernel's connected route leaves through a bridge VPP can reach over a vlans all trunk, so shape 3 refused it, and the route read unresolvable (mapping): … the kernel sends this prefix out <bridge>, which VPP can reach (a transit route under next-hop-self?). A covering steer-exempt was already configured. While the route stayed unresolvable:

  • fib-synced read Degraded.
  • Every post-resync verify was VerifyIncomplete, so the supervisor parked in Ready.
  • The driver's steer retry needs fib_fit_to_steer(), which refuses while anything is unresolvable, so steering was never re-asserted and the lever was refused. Only a daemon restart rebuilt the engine.

What changes

  • The kernel topology re-reads the addresses every 2 s, on the existing pf-vpp-fdb thread next to the FDB and port VLANs (Topology::self_networks, topology::SelfNetworks). This is a poll, not an RTM_NEWADDR subscription. A full read is a few dozen entries and has no notifications to lose, so there is no overrun to recover from. That is why fix: netlink consumers recover from lost notifications and never wait forever on a reply #332's subscribe-before-dump pattern does not apply. The only existing address subscription is the hand-back path's (v6 only, and only under v6 on).

  • Its rule from fix: netlink consumers recover from lost notifications and never wait forever on a reply #332: an address leaves only once two reads in a row lack it, because a dump taken during churn can skip a live entry (publish_self_networks). A new address is published at once, and a failed read publishes the failure.

  • The engine follows (ConvergenceEngine::refresh_self_networks). The runtime calls it on the placement tick (PLACEMENT_EVERY, 2 s) before apply_changes. It diffs the new sets against the ones in force, both families (v6 only under v6 on, as at bring-up), and hands back to the source every route whose judgement the change can flip:

    • routes through an address that came or went, via requeue_via (shapes 1 and 3 need every next hop to be the router's);
    • routes inside a subnet that came or went, or naming such an address (shapes 1 and 2), via the new RouteSource::requeue_within. On RouteFeed that is a range scan over the ordered mirror, so its cost is the routes inside the subnet, not the table.

    The delta path then judges each route again, either way: kernel-delivered and withdrawn, or back to resolution and the kernel's word. A resync re-reads too, so its walk starts from the addresses held now.

  • A failed read keeps the last good sets. An empty table would otherwise read as every address gone. The reason of every unresolvable route via the router names the failure until a read succeeds.

  • Startup and the refresh share one getifaddrs walk (bringup::kernel_ifaddrs, fallible). The attach-time readers still degrade open, as before.

Once the route is kernel-delivered, nothing new is needed: poll_reverify re-runs the incomplete verdict once the table is clean, and the steer retry follows. vpp_runtime.rs proves that chain through the real loop.

Tests

  • engine.rs unit tests, with a fake topology and a fake feed that serves handed-back routes:
    • an address added after attach makes its subnet kernel-delivered, with exactly the expected requeue scope;
    • an address removed sends its subnet back to the kernel's word;
    • a failed read keeps the sets and is named in the reason;
    • a resync judges against the addresses held now;
    • the v6 half follows under v6 on only.
  • tests/vpp_engine.rs, against the fake VPP: the unresolvable count drops from 1 to 0, VPP is told to delete the prefix and nothing is installed, the first-steer gate opens with the steer-exempt, verify goes from incomplete to passed, and removing the address brings the count back with the same name.
  • tests/vpp_runtime.rs, through Driver + Runtime + the real RouteFeed: a module parked in Ready on an incomplete verdict recovers by itself after the address appears, with no resync. The test waits ~2 s of real time, because placement is paced on the real clock.
  • feed.rs: requeue_within queues exactly the routes inside the nets (both families, the subnet's own route included, a newer queued withdrawal wins), through the Arc the loader boxes.
  • topology.rs: the two-reads publish rule.

Mutation checks: removing requeue_within from the refresh fails the unit tests and the fake-VPP test, which then reports the production symptom string verbatim. Removing the runtime call fails the runtime test. Removing the resync re-read fails its unit test.

Local gates: cargo fmt --check; host clippy and clippy for all four Linux targets with --workspace --all-targets --all-features -D warnings; host cargo test --workspace; and fmt, clippy and cargo test --workspace on Linux arm64 in Docker. Docs: CHANGELOG, and the runbook's kernel-delivered section, the new-VLAN bullet and the unresolvable-reason table.

🤖 Generated with Claude Code

…runs

The engine read the router's addresses and subnets once, at attach, and
judged every route via the router against them for the life of the
process. A subnet addressed later, such as a new IX LAN on a new bridge
whose connected route FRR redistributes with the router as next hop, fell
through to the kernel FIB check. That check refused it, because the
connected route leaves through a bridge VPP can reach, so the route read
unresolvable until a restart. With it, fib-synced read Degraded, every
verify came back incomplete and parked the module in Ready, and the
first-steer FIB gate refused both the steer retry and the lever, even
with a covering steer-exempt in place.

The kernel topology's background thread now re-reads the addresses
every 2 s, alongside the FDB and port VLANs, and the runtime's placement
tick hands any change to the engine. The engine hands back to the
source every route whose judgement the change can flip: routes through
an address that came or went (requeue_via), and routes inside a subnet
that came or went or naming such an address (the new requeue_within, a
range over the feed's ordered mirror). The delta path then judges them
again, both ways. A resync re-reads too, so its walk starts from the
addresses held now.

The addresses are polled rather than subscribed to: a full read every
2 s is a few dozen entries and has no notifications to lose, so there
is no overrun to recover from. A dump taken during churn can still skip
a live entry, so an address leaves only once two reads in a row lack it,
the rule neigh-snoop applies to its neighbour rows since #332. A failed
read keeps the last good sets, rather than reading an empty table as
every address gone, and the reason of every unresolvable route via the
router names the failure. Startup and the refresh share one getifaddrs
walk, so they cannot disagree about what an address is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-11T08:24:02.628598Z 119222d PR opened
🔒 Security Review ✅ Completed 2026-10-11T08:26:03.137517Z 119222d PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant