mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-04 19:45:14 -04:00
## Describe your changes Move route select/deselect handling from the daemon server into exported routemanager methods (SelectRoutes, DeselectRoutes, SelectAllRoutes, DeselectAllRoutes) so every consumer shares one implementation: v4/v6 exit-pair expansion, exit-node mutual exclusion, and selection triggering. Previously the exit-node exclusivity lived only in the daemon's SelectNetworks RPC, so the Android and iOS bindings could leave two exit nodes selected until the next network map reconciliation. Both bindings now call the shared manager methods and enforce exclusivity at toggle time, matching the desktop behavior. ## 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/__ <!-- codesmith:footer --> --- <a href="https://app.blacksmith.sh/netbirdio/codesmith/netbird/pr/6928"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"><source media="(prefers-color-scheme: light)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v2.svg"><img alt="View with [code]smith" src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"></picture></a> <a href="https://backend.blacksmith.sh/track/enable-autofix?expires=1787772098&installation_model_id=427504&pr_number=6928&repository=netbirdio%2Fnetbird&return_to=https%3A%2F%2Fgithub.com%2Fnetbirdio%2Fnetbird%2Fpull%2F6928&signature=31ad59e1483e1582cd447a8db2fe21e5309230e631cbd0cad0f977cd15fb7b9b"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"><img alt="Autofix with [code]smith" src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"></picture></a> <sup>Need help on this PR? Tag <code>@codesmith-bot</code> with what you need. Autofix is disabled.</sup> <!-- codesmith:autofix:disabled --> <!-- /codesmith:footer --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Route selection/deselection is now handled through shared route-manager APIs for both individual routes and “all routes”. * Exit-node selections automatically enforce mutual exclusivity while keeping non-exit routes unaffected. * **Bug Fixes** * Unknown or unavailable route IDs now return errors, and exclusivity is preserved even when some route IDs fail. * **Tests** * Added route-selection tests covering exclusivity (including IPv4/IPv6), select-all behavior, partial errors, and invalid IDs. * **Refactor / Chores** * Simplified Android, iOS, and server routing flows to delegate to the shared manager; updated mocks and removed redundant routing command logic/dependencies. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
139 lines
4.6 KiB
Go
139 lines
4.6 KiB
Go
package routemanager
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
|
|
"github.com/hashicorp/go-multierror"
|
|
log "github.com/sirupsen/logrus"
|
|
"golang.org/x/exp/maps"
|
|
|
|
nberrors "github.com/netbirdio/netbird/client/errors"
|
|
"github.com/netbirdio/netbird/route"
|
|
)
|
|
|
|
// SelectRoutes selects the routes with the given network IDs and applies the
|
|
// new selection. V4/v6 exit-node pairs are expanded automatically. Exit nodes
|
|
// are mutually exclusive: if the selection activates an exit node, every other
|
|
// available exit node is deselected so two can't be active at once. With
|
|
// appendRoute=false the previous selection is replaced instead of extended.
|
|
func (m *DefaultManager) SelectRoutes(ids []route.NetID, appendRoute bool) error {
|
|
if err := m.selectRoutes(ids, appendRoute); err != nil {
|
|
return err
|
|
}
|
|
m.TriggerSelection(m.GetClientRoutes())
|
|
return nil
|
|
}
|
|
|
|
// DeselectRoutes removes the routes with the given network IDs from the
|
|
// selection and applies the change. V4/v6 exit-node pairs are expanded
|
|
// automatically.
|
|
func (m *DefaultManager) DeselectRoutes(ids []route.NetID) error {
|
|
if err := m.deselectRoutes(ids); err != nil {
|
|
return err
|
|
}
|
|
m.TriggerSelection(m.GetClientRoutes())
|
|
return nil
|
|
}
|
|
|
|
func (m *DefaultManager) deselectRoutes(ids []route.NetID) error {
|
|
routesMap := m.GetClientRoutesWithNetID()
|
|
routes := route.ExpandV6ExitPairs(slices.Clone(ids), routesMap)
|
|
|
|
log.Debugf("deselecting routes with ids: %v", routes)
|
|
|
|
if err := m.routeSelector.DeselectRoutes(routes, maps.Keys(routesMap)); err != nil {
|
|
return fmt.Errorf("deselect routes: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SelectAllRoutes selects every available route and applies the selection.
|
|
// Exit nodes stay mutually exclusive: at most one remains active.
|
|
func (m *DefaultManager) SelectAllRoutes() {
|
|
m.selectAllRoutes()
|
|
m.TriggerSelection(m.GetClientRoutes())
|
|
}
|
|
|
|
func (m *DefaultManager) selectAllRoutes() {
|
|
m.routeSelector.SelectAllRoutes()
|
|
|
|
// Select-all wipes every explicit selection, so exit nodes fall back to
|
|
// management's auto-apply flags — which may mark several at once.
|
|
// Reconcile immediately so at most one exit node stays active instead of
|
|
// waiting for the next network map to enforce it.
|
|
m.mux.Lock()
|
|
defer m.mux.Unlock()
|
|
m.updateRouteSelectorFromManagement(m.clientRoutes)
|
|
}
|
|
|
|
// DeselectAllRoutes deselects every route and applies the change.
|
|
func (m *DefaultManager) DeselectAllRoutes() {
|
|
m.routeSelector.DeselectAllRoutes()
|
|
m.TriggerSelection(m.GetClientRoutes())
|
|
}
|
|
|
|
func (m *DefaultManager) selectRoutes(ids []route.NetID, appendRoute bool) error {
|
|
routesMap := m.GetClientRoutesWithNetID()
|
|
routes := route.ExpandV6ExitPairs(slices.Clone(ids), routesMap)
|
|
allIDs := maps.Keys(routesMap)
|
|
|
|
log.Debugf("selecting routes with ids: %v", routes)
|
|
|
|
// A partial failure (e.g. an unknown ID in the request) still selects the
|
|
// valid routes, so exclusivity below must run regardless of the error.
|
|
var merr *multierror.Error
|
|
if err := m.routeSelector.SelectRoutes(routes, appendRoute, allIDs); err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("select routes: %w", err))
|
|
}
|
|
|
|
// Exit nodes are mutually exclusive: if this selection activates an
|
|
// exit node, deselect every other available exit node so two can't be
|
|
// selected at once. Non-exit route selections are left untouched.
|
|
if requestActivatesExitNode(routes, routesMap) {
|
|
if others := otherExitNodeIDs(routesMap, routes); len(others) > 0 {
|
|
if err := m.routeSelector.DeselectRoutes(others, allIDs); err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("deselect sibling exit nodes: %w", err))
|
|
}
|
|
}
|
|
}
|
|
|
|
return nberrors.FormatErrorOrNil(merr)
|
|
}
|
|
|
|
func isExitNodeRoutes(routes []*route.Route) bool {
|
|
return len(routes) > 0 && (route.IsV4DefaultRoute(routes[0].Network) || route.IsV6DefaultRoute(routes[0].Network))
|
|
}
|
|
|
|
// requestActivatesExitNode reports whether any requested NetID maps to an exit
|
|
// node (default route) in the current route table.
|
|
func requestActivatesExitNode(requested []route.NetID, routesMap map[route.NetID][]*route.Route) bool {
|
|
for _, id := range requested {
|
|
if isExitNodeRoutes(routesMap[id]) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// otherExitNodeIDs returns every available exit-node NetID that is not in the
|
|
// requested set — the siblings to deselect so a single exit node stays active.
|
|
func otherExitNodeIDs(routesMap map[route.NetID][]*route.Route, requested []route.NetID) []route.NetID {
|
|
keep := make(map[route.NetID]struct{}, len(requested))
|
|
for _, id := range requested {
|
|
keep[id] = struct{}{}
|
|
}
|
|
var others []route.NetID
|
|
for id, routes := range routesMap {
|
|
if !isExitNodeRoutes(routes) {
|
|
continue
|
|
}
|
|
if _, ok := keep[id]; ok {
|
|
continue
|
|
}
|
|
others = append(others, id)
|
|
}
|
|
return others
|
|
}
|