Skip to content

pkg/routing: unbind WireGuard TUN from VRF on teardown/rebind (#5120)#5408

Merged
psaab merged 1 commit into
masterfrom
fix/5120-wg-vrf-unbind
Jul 10, 2026
Merged

pkg/routing: unbind WireGuard TUN from VRF on teardown/rebind (#5120)#5408
psaab merged 1 commit into
masterfrom
fix/5120-wg-vrf-unbind

Conversation

@psaab

@psaab psaab commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Closes #5120

Problem

applyWireguardTunLocked (pkg/routing/tunnel.go) bound the persistent
wgN TUN to a VRF only in the non-empty routing-instance case and
had no else/unbind branch. The WG config-removal prune in Apply
reconciled only addresses (#1919), never VRF membership. WireGuard also
bypassed the identity-gated appliedRI claim machinery every other
tunnel (GRE/IPIP/anchor) uses.

Result: removing the routing-instance stanza from a still-configured
WG tunnel — or removing the whole persistent tunnel — left wgN
mastered to the old vrf-<name> indefinitely. Traffic then stays
isolated in / leaks through the wrong routing table after a successful
commit, carried by the persistent interface. The source comment on
origin/master explicitly conceded the gap (boundary (3), #1434).

Verified GENUINE on current origin/master (b6703ee9a): the WG bind at
tunnel.go was a bare if tc.RoutingInstance != "" { Bind... } with no
unbind, and the prune loop only called pruneAppliedAddrsLocked.

Fix

Route WireGuard through the same identity-gated claim machinery as
other tunnels:

  • applyWireguardTunLocked now calls reconcileVRFClaimLocked (records
    appliedRI on a successful bind; identity-gated-unbinds when the
    desired RI is empty on a still-configured tunnel).
  • Extract the unbind tail of reconcileVRFClaimLocked into a shared
    unbindVRFClaimLocked(name, link) -> retry. It clears the master only
    when the link's CURRENT master is the claimed RI's vrf- device
    (never a foreign bind), clears the claim on success / master==0 /
    identity-mismatch / VRF-not-found, and retains the claim
    (retry=true) on a transient VRF-lookup or LinkSetNoMaster error.
  • The persistent-link removal prune in Apply calls
    unbindVRFClaimLocked on the KEPT wgN link and folds its retry into
    the existing address-prune retry (retaining the wgConfigured entry
    for the next Apply). A not-found device clears the claim.
  • Preserve appliedRI across the non-WG→WG same-name handoff so a
    reused anchor TUN's prior VRF claim carries into the WG reconcile
    (else an anchor→WG-no-RI device would strand its old master). A
    GRE→WG handoff recreates the link (master==0), so the carried claim
    self-clears.

The wgN link itself is still KEPT on removal (#1432 S2a — tearing it
flaps the live peer/session); only the stale VRF enslavement is cleared.

Tests (RED-on-revert proven)

Four new tests in tunnel_reconcile_test.go:

  • TestWireguardVRFUnboundOnConfigRemoval — bind, then whole-tunnel
    removal → wgN kept, LinkSetNoMaster issued, master 0, claim
    cleared, idempotent.
  • TestWireguardVRFUnboundOnRoutingInstanceRemoval — RI stanza removed
    from a still-configured tunnel → unbound.
  • TestWireguardVRFForeignMasterNeverUnbound — a claim-less WG tunnel
    with a foreign master is never unbound (identity gate).
  • TestWireguardVRFUnbindTransientRetried — a transient
    LinkSetNoMaster failure retains the claim + wgConfigured and
    retries to success.

RED-on-revert: git stash of tunnel.go alone (tests kept) →
the removal, RI-removal, and retry tests FAIL (appliedRI never
recorded, noMaster=[]). The foreign-master safety guard passes both
ways by design. Restored → all four PASS.

Validation

  • go build ./... exit 0
  • go test ./pkg/routing/... PASS (full package, not just new tests)
  • go vet ./pkg/routing/... clean
  • gofmt -l clean on touched files
  • Updated pkg/routing/README.md (WG-removal-prune + VRF-claims
    sections; dropped the Implement Multi-Tunnel WireGuard Support #1434 "VRF membership is not unbound" residual)

No cluster smoke (per task scope — control-plane netlink reconcile,
covered by the seam tests).

🤖 Generated with Claude Code

https://claude.ai/code/session_015oARShYtiJJ2H4UB4nXGqi

The WireGuard apply path bound the persistent wgN TUN to a VRF only in
the non-empty routing-instance case and had no else/unbind branch, and
the WG config-removal prune in Apply reconciled only addresses. So
removing the `routing-instance` stanza from a still-configured tunnel,
or removing the whole persistent tunnel, left wgN mastered to the old
vrf-<name> indefinitely. Traffic then stays isolated in — or leaks
through — the wrong routing table after a successful commit, carried
indefinitely by the persistent interface. WG bypassed the identity-gated
appliedRI claim machinery every other tunnel uses, so there was no
symmetric unbind for a successful bind.

Route WireGuard through the same claim machinery as GRE/IPIP:

  - applyWireguardTunLocked now calls reconcileVRFClaimLocked instead of
    a bind-only block. A successful bind records appliedRI; an empty
    desired RI on a still-configured tunnel identity-gated-unbinds.
  - Extract the identity-gated unbind tail of reconcileVRFClaimLocked
    into unbindVRFClaimLocked(name, link) -> retry. It clears the master
    only when the link's CURRENT master IS the claimed RI's vrf- device
    (never a foreign bind we do not own), clears the claim on success /
    master==0 / identity-mismatch / VRF-not-found, and RETAINS the claim
    (returns retry=true) on a transient VRF lookup or LinkSetNoMaster
    error.
  - The WG config-removal prune in Apply calls unbindVRFClaimLocked on
    the KEPT persistent link and folds its retry into the existing
    address-prune retry (retaining the wgConfigured entry so the next
    Apply retries). A not-found device clears the claim (the binding
    died with the device).
  - Preserve appliedRI across the non-WG->WG same-name handoff: a reused
    anchor TUN keeps its kernel master, so carrying the prior claim lets
    the WG reconcile unbind an anchor->WG-no-RI device instead of
    stranding it. A GRE->WG handoff recreates the link (master==0), so
    the carried claim self-clears.

Add four fail-on-revert tests covering the bind + config-removal unbind,
the routing-instance-removal unbind, the foreign-master veto, and the
transient-unbind retry. Verified RED on reverted source (the removal,
RI-removal, and retry tests fail without the fix; the foreign-master
safety guard passes both ways by design). go build ./... and
go test ./pkg/routing/... are green; gofmt clean.

Update pkg/routing/README.md: the WG-removal-prune and VRF-claims
sections now document the shared unbind and drop the #1434 "VRF
membership is not unbound" residual.

Closes #5120

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015oARShYtiJJ2H4UB4nXGqi
Copilot AI review requested due to automatic review settings July 10, 2026 18:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@psaab

psaab commented Jul 10, 2026

Copy link
Copy Markdown
Owner Author

@copilot review

Copilot finished work on behalf of psaab July 10, 2026 18:53
@psaab
psaab merged commit 781e35c into master Jul 10, 2026
1 check passed
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.

routing/wireguard: WireGuard TUN is never unbound from its VRF when the routing-instance is removed/changed

2 participants