[PR #6296] [MERGED] [client] propagate exit-node deselect to synthesized v6 (::/0) route #29284

Closed
opened 2026-08-05 08:07:48 -04:00 by saavagebueno · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/netbirdio/netbird/pull/6296
Author: @pappz
Created: 5/31/2026
Status: Merged
Merged: 6/16/2026
Merged by: @pappz

Base: mainHead: fix/exit-node-v6-deselect-propagation


📝 Commits (9)

  • c7499cf [client] propagate exit-node deselect to synthesized v6 (::/0) route
  • 84867c7 [client] add DIAG logging to trace exit-node v6 (::/0) route filtering
  • 99223a3 [client] clear orphaned v6 exit selection when v4 pair is toggled
  • aa164c9 [ios] compute route connection status in the bridge
  • 8e5130c [client] remove exit-node v6 DIAG logging and tidy routeselector
  • d25c8d8 [client] mirror v4 exit selection onto v6 pair at write time
  • ed76f8f [client] add DIAG logging to trace v6 exit-pair mirror
  • 764642d [client] remove v6 exit-pair mirror DIAG logging
  • 0035ddd Merge branch 'main' into fix/exit-node-v6-deselect-propagation

📊 Changes

6 files changed (+269 additions, -112 deletions)

View changed files

📝 client/internal/routemanager/manager.go (+21 -0)
client/internal/routemanager/manager_v6exit_test.go (+47 -0)
📝 client/internal/routeselector/routeselector.go (+85 -81)
📝 client/internal/routeselector/routeselector_test.go (+65 -31)
📝 client/ios/NetBirdSDK/client.go (+50 -0)
📝 client/ios/NetBirdSDK/routes.go (+1 -0)

📄 Description

When a client deselects an IPv4 exit node, the auto-generated IPv6 default route (::/0) was still selected and pushed onto the tunnel interface, even though the user disabled the exit node. On an exit node without a real IPv6 egress this blackholes IPv6 traffic, and because clients prefer IPv6 (happy eyeballs) it can break general connectivity.

Root cause: the synthesized v6 route gets a different NetID than its v4 base (base + "-v6"). The route selector keys deselects by NetID and defaults unknown NetIDs to selected, so the "-v6" entry was never matched by the v4 deselect. The effectiveNetID() mirror that solves exactly this is used by HasUserSelectionForRoute and FilterSelectedExitNodes, but categorizeUserSelection called the raw IsSelected(), bypassing it and mis-categorizing the v6 pair as user-selected.

Add RouteSelector.IsSelectedForExitNode(), which applies effectiveNetID before the selection check, and use it in categorizeUserSelection. IsSelected() is left untouched so non-exit code paths don't make unrelated "*-v6" routes inherit v4 state. Adds regression tests for the v4/v6 deselect mirror and explicit-v6 override.

  1. The exit node advertises both the 0.0.0.0/0 and ::/0 routes.
  2. UI: you enable the exit node → ExpandV6ExitPairs → selected={v4, v6}. (The -v6 key is persisted at this point.)
  3. On the management side, disable IPv6.
  4. UI: you disable the exit node → ExpandV6ExitPairs can't find the v6 pair (it's no longer in the routesMap) → only the v4 gets added to deselected.
  5. On the management side, enable IPv6 → ::/0 reappears → on the next route update it gets applied to the tunnel, even though the exit node is OFF.

Describe your changes

Stack

Checklist

  • Is it a bug fix
  • Is a typo/documentation fix
  • Is a feature enhancement
  • It is a refactor
  • Created tests that fail without the change (if possible)
  • This change does not modify the public API, gRPC protocols, functionality behavior, CLI / service flags, or introduce a new feature — OR I have discussed it with the NetBird team beforehand (link the issue / Slack thread in the description). See CONTRIBUTING.md.

By submitting this pull request, you confirm that you have read and agree to the terms of the Contributor License Agreement.

Documentation

Select exactly one:

  • I added/updated documentation for this change
  • Documentation is not needed for this change (explain why)

Docs PR URL (required if "docs added" is checked)

Paste the PR link from https://github.com/netbirdio/docs here:

https://github.com/netbirdio/docs/pull/__

Summary by CodeRabbit

  • Bug Fixes

    • v6 exit-route selections now mirror their v4 counterparts so deselected v4 routes no longer leave orphaned v6 selections or cause routing leaks.
  • New Features

    • iOS: per-route connection status added to route selection details (Connected / Idle).
    • Route selector: explicit v4↔v6 pairing sync to keep selection state consistent.
  • Tests

    • Added tests covering v4/v6 pairing, sync semantics, orphaned v6 regression, and filtering behavior.

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/netbirdio/netbird/pull/6296 **Author:** [@pappz](https://github.com/pappz) **Created:** 5/31/2026 **Status:** ✅ Merged **Merged:** 6/16/2026 **Merged by:** [@pappz](https://github.com/pappz) **Base:** `main` ← **Head:** `fix/exit-node-v6-deselect-propagation` --- ### 📝 Commits (9) - [`c7499cf`](https://github.com/netbirdio/netbird/commit/c7499cf8fc4cd098cb4f20c3448701fe9f495938) [client] propagate exit-node deselect to synthesized v6 (::/0) route - [`84867c7`](https://github.com/netbirdio/netbird/commit/84867c7e45dfced9d034c6e8431000b4d8120eac) [client] add DIAG logging to trace exit-node v6 (::/0) route filtering - [`99223a3`](https://github.com/netbirdio/netbird/commit/99223a310dcfa5aa2f38bed66ddb818732f7bede) [client] clear orphaned v6 exit selection when v4 pair is toggled - [`aa164c9`](https://github.com/netbirdio/netbird/commit/aa164c93cfb16c8b48af2e31c52fc7a9d2eddf44) [ios] compute route connection status in the bridge - [`8e5130c`](https://github.com/netbirdio/netbird/commit/8e5130cda726f93d5115945448f41118081ba26e) [client] remove exit-node v6 DIAG logging and tidy routeselector - [`d25c8d8`](https://github.com/netbirdio/netbird/commit/d25c8d881d45cf0002ed8f76aeac2b9ef024511d) [client] mirror v4 exit selection onto v6 pair at write time - [`ed76f8f`](https://github.com/netbirdio/netbird/commit/ed76f8f06554b4eb367292e6a7e5d978abaebdc3) [client] add DIAG logging to trace v6 exit-pair mirror - [`764642d`](https://github.com/netbirdio/netbird/commit/764642d8f25f2327ace59176c942696813ff10d8) [client] remove v6 exit-pair mirror DIAG logging - [`0035ddd`](https://github.com/netbirdio/netbird/commit/0035ddde8c47c0107c2f48b85b00cf1a19c3fdb7) Merge branch 'main' into fix/exit-node-v6-deselect-propagation ### 📊 Changes **6 files changed** (+269 additions, -112 deletions) <details> <summary>View changed files</summary> 📝 `client/internal/routemanager/manager.go` (+21 -0) ➕ `client/internal/routemanager/manager_v6exit_test.go` (+47 -0) 📝 `client/internal/routeselector/routeselector.go` (+85 -81) 📝 `client/internal/routeselector/routeselector_test.go` (+65 -31) 📝 `client/ios/NetBirdSDK/client.go` (+50 -0) 📝 `client/ios/NetBirdSDK/routes.go` (+1 -0) </details> ### 📄 Description When a client deselects an IPv4 exit node, the auto-generated IPv6 default route (::/0) was still selected and pushed onto the tunnel interface, even though the user disabled the exit node. On an exit node without a real IPv6 egress this blackholes IPv6 traffic, and because clients prefer IPv6 (happy eyeballs) it can break general connectivity. Root cause: the synthesized v6 route gets a different NetID than its v4 base (base + "-v6"). The route selector keys deselects by NetID and defaults unknown NetIDs to selected, so the "-v6" entry was never matched by the v4 deselect. The effectiveNetID() mirror that solves exactly this is used by HasUserSelectionForRoute and FilterSelectedExitNodes, but categorizeUserSelection called the raw IsSelected(), bypassing it and mis-categorizing the v6 pair as user-selected. Add RouteSelector.IsSelectedForExitNode(), which applies effectiveNetID before the selection check, and use it in categorizeUserSelection. IsSelected() is left untouched so non-exit code paths don't make unrelated "*-v6" routes inherit v4 state. Adds regression tests for the v4/v6 deselect mirror and explicit-v6 override. 1. The exit node advertises both the 0.0.0.0/0 and ::/0 routes. 2. UI: you enable the exit node → ExpandV6ExitPairs → selected={v4, v6}. (The -v6 key is persisted at this point.) 3. On the management side, disable IPv6. 4. UI: you disable the exit node → ExpandV6ExitPairs can't find the v6 pair (it's no longer in the routesMap) → only the v4 gets added to deselected. 5. On the management side, enable IPv6 → ::/0 reappears → on the next route update it gets applied to the tunnel, even though the exit node is OFF. ## Describe your changes ## Issue ticket number and link ## Stack <!-- branch-stack --> ### Checklist - [x] Is it a bug fix - [ ] Is a typo/documentation fix - [ ] Is a feature enhancement - [ ] It is a refactor - [ ] Created tests that fail without the change (if possible) - [ ] This change does **not** modify the public API, gRPC protocols, functionality behavior, CLI / service flags, or introduce a new feature — **OR** I have discussed it with the NetBird team beforehand (link the issue / Slack thread in the description). See [CONTRIBUTING.md](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTING.md#discuss-changes-with-the-netbird-team-first). > By submitting this pull request, you confirm that you have read and agree to the terms of the [Contributor License Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md). ## Documentation Select exactly one: - [ ] I added/updated documentation for this change - [x] Documentation is **not needed** for this change (explain why) ### Docs PR URL (required if "docs added" is checked) Paste the PR link from https://github.com/netbirdio/docs here: https://github.com/netbirdio/docs/pull/__ <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * v6 exit-route selections now mirror their v4 counterparts so deselected v4 routes no longer leave orphaned v6 selections or cause routing leaks. * **New Features** * iOS: per-route connection status added to route selection details (Connected / Idle). * Route selector: explicit v4↔v6 pairing sync to keep selection state consistent. * **Tests** * Added tests covering v4/v6 pairing, sync semantics, orphaned v6 regression, and filtering behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
saavagebueno added the pull-request label 2026-08-05 08:07:48 -04:00
Sign in to join this conversation.
No Label pull-request
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: DYNR/netbird#29284