mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-31 13:09:55 +00:00
Compare commits
2 Commits
fix/ctx-en
...
fix/exit-n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84867c7e45 | ||
|
|
c7499cf8fc |
@@ -428,10 +428,16 @@ func (m *DefaultManager) UpdateRoutes(
|
||||
var merr *multierror.Error
|
||||
if !m.disableClientRoutes {
|
||||
|
||||
log.Debugf("DIAG UpdateRoutes: incoming %d client networks: %v", len(clientRoutes), haKeysList(clientRoutes))
|
||||
log.Debugf("DIAG UpdateRoutes: selector BEFORE management update: %s", m.routeSelector.MarshalSummary())
|
||||
|
||||
// Update route selector based on management server's isSelected status
|
||||
m.updateRouteSelectorFromManagement(clientRoutes)
|
||||
|
||||
log.Debugf("DIAG UpdateRoutes: selector AFTER management update: %s", m.routeSelector.MarshalSummary())
|
||||
|
||||
filteredClientRoutes := m.routeSelector.FilterSelectedExitNodes(clientRoutes)
|
||||
log.Debugf("DIAG UpdateRoutes: AFTER filter, %d networks remain: %v", len(filteredClientRoutes), haKeysList(filteredClientRoutes))
|
||||
|
||||
if err := m.updateSystemRoutes(filteredClientRoutes); err != nil {
|
||||
merr = multierror.Append(merr, fmt.Errorf("update system routes: %w", err))
|
||||
@@ -745,7 +751,10 @@ func (m *DefaultManager) isExitNodeRoute(routes []*route.Route) bool {
|
||||
}
|
||||
|
||||
func (m *DefaultManager) categorizeUserSelection(netID route.NetID, info *exitNodeInfo) {
|
||||
if m.routeSelector.IsSelected(netID) {
|
||||
// Use the exit-node-aware check so a synthesized "-v6" route inherits the v4
|
||||
// base's selection: deselecting the v4 exit node must also drop its ::/0 pair,
|
||||
// otherwise the v6 default route leaks into the tunnel despite the deselect.
|
||||
if m.routeSelector.IsSelectedForExitNode(netID) {
|
||||
info.userSelected = append(info.userSelected, netID)
|
||||
} else {
|
||||
info.userDeselected = append(info.userDeselected, netID)
|
||||
@@ -763,6 +772,8 @@ func (m *DefaultManager) checkManagementSelection(routes []*route.Route, netID r
|
||||
|
||||
func (m *DefaultManager) updateExitNodeSelections(info exitNodeInfo) {
|
||||
routesToDeselect := m.getRoutesToDeselect(info.allIDs)
|
||||
log.Debugf("DIAG updateExitNodeSelections: allIDs=%v userSelected=%v userDeselected=%v selectedByManagement=%v -> routesToDeselect(no user selection)=%v",
|
||||
info.allIDs, info.userSelected, info.userDeselected, info.selectedByManagement, routesToDeselect)
|
||||
m.deselectExitNodes(routesToDeselect)
|
||||
m.selectExitNodesByManagement(info.selectedByManagement, info.allIDs)
|
||||
}
|
||||
@@ -802,4 +813,14 @@ func (m *DefaultManager) selectExitNodesByManagement(selectedByManagement []rout
|
||||
func (m *DefaultManager) logExitNodeUpdate(info exitNodeInfo) {
|
||||
log.Debugf("Updated route selector: %d exit nodes available, %d selected by management, %d user-selected, %d user-deselected",
|
||||
len(info.allIDs), len(info.selectedByManagement), len(info.userSelected), len(info.userDeselected))
|
||||
log.Debugf("DIAG logExitNodeUpdate: allIDs=%v selectedByManagement=%v userSelected=%v userDeselected=%v",
|
||||
info.allIDs, info.selectedByManagement, info.userSelected, info.userDeselected)
|
||||
}
|
||||
|
||||
func haKeysList(m route.HAMap) []route.HAUniqueID {
|
||||
out := make([]route.HAUniqueID, 0, len(m))
|
||||
for k := range m {
|
||||
out = append(out, k)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/client/errors"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
@@ -124,6 +125,24 @@ func (rs *RouteSelector) IsSelected(routeID route.NetID) bool {
|
||||
return rs.isSelectedLocked(routeID)
|
||||
}
|
||||
|
||||
// IsSelectedForExitNode checks if an exit-node route is selected, mirroring the
|
||||
// v4/v6 pair: a synthesized "-v6" entry with no explicit state of its own inherits
|
||||
// its v4 base's selection, so a deselect on the v4 base also deselects the v6 entry.
|
||||
// Only call this from exit-node code paths (see effectiveNetID).
|
||||
func (rs *RouteSelector) IsSelectedForExitNode(routeID route.NetID) bool {
|
||||
rs.mu.RLock()
|
||||
defer rs.mu.RUnlock()
|
||||
|
||||
return rs.isSelectedLocked(rs.effectiveNetID(routeID))
|
||||
}
|
||||
|
||||
// MarshalSummary returns a short human-readable description of the selector state for diagnostics.
|
||||
func (rs *RouteSelector) MarshalSummary() string {
|
||||
rs.mu.RLock()
|
||||
defer rs.mu.RUnlock()
|
||||
return fmt.Sprintf("deselectAll=%v selected=%v deselected=%v", rs.deselectAll, keysOf(rs.selectedRoutes), keysOf(rs.deselectedRoutes))
|
||||
}
|
||||
|
||||
// FilterSelected removes unselected routes from the provided map.
|
||||
func (rs *RouteSelector) FilterSelected(routes route.HAMap) route.HAMap {
|
||||
rs.mu.RLock()
|
||||
@@ -161,24 +180,47 @@ func (rs *RouteSelector) FilterSelectedExitNodes(routes route.HAMap) route.HAMap
|
||||
return route.HAMap{}
|
||||
}
|
||||
|
||||
log.Debugf("DIAG FilterSelectedExitNodes: incoming %d networks, deselected=%v selected=%v deselectAll=%v",
|
||||
len(routes), keysOf(rs.deselectedRoutes), keysOf(rs.selectedRoutes), rs.deselectAll)
|
||||
|
||||
filtered := make(route.HAMap, len(routes))
|
||||
for id, rt := range routes {
|
||||
netID := id.NetID()
|
||||
if rs.isDeselectedLocked(netID) {
|
||||
log.Debugf("DIAG FilterSelectedExitNodes: SKIP id=%q netID=%q (literally deselected)", id, netID)
|
||||
continue
|
||||
}
|
||||
|
||||
if !isExitNode(rt) {
|
||||
log.Debugf("DIAG FilterSelectedExitNodes: KEEP id=%q netID=%q (not an exit node)", id, netID)
|
||||
filtered[id] = rt
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debugf("DIAG FilterSelectedExitNodes: EXITNODE id=%q netID=%q -> applyExitNodeFilter", id, netID)
|
||||
rs.applyExitNodeFilter(id, netID, rt, filtered)
|
||||
}
|
||||
|
||||
log.Debugf("DIAG FilterSelectedExitNodes: result keeps %d networks: %v", len(filtered), haKeysOf(filtered))
|
||||
return filtered
|
||||
}
|
||||
|
||||
func keysOf(m map[route.NetID]struct{}) []route.NetID {
|
||||
out := make([]route.NetID, 0, len(m))
|
||||
for k := range m {
|
||||
out = append(out, k)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func haKeysOf(m route.HAMap) []route.HAUniqueID {
|
||||
out := make([]route.HAUniqueID, 0, len(m))
|
||||
for k := range m {
|
||||
out = append(out, k)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// effectiveNetID returns the v4 base for a "-v6" exit pair entry that has no explicit
|
||||
// state of its own, so selections made on the v4 entry govern the v6 entry automatically.
|
||||
// Only call this from exit-node-specific code paths: applying it to a non-exit "-v6" route
|
||||
@@ -232,15 +274,22 @@ func (rs *RouteSelector) applyExitNodeFilter(
|
||||
// Exit-node path: apply the v4/v6 pair mirror so a deselect on the v4 base also
|
||||
// drops the synthesized v6 entry that lacks its own explicit state.
|
||||
effective := rs.effectiveNetID(netID)
|
||||
log.Debugf("DIAG applyExitNodeFilter: id=%q netID=%q effective=%q hasUserSel=%v isSelected=%v",
|
||||
id, netID, effective, rs.hasUserSelectionForRouteLocked(effective), rs.isSelectedLocked(effective))
|
||||
if rs.hasUserSelectionForRouteLocked(effective) {
|
||||
if rs.isSelectedLocked(effective) {
|
||||
log.Debugf("DIAG applyExitNodeFilter: KEEP id=%q (effective %q is selected)", id, effective)
|
||||
out[id] = rt
|
||||
} else {
|
||||
log.Debugf("DIAG applyExitNodeFilter: DROP id=%q (effective %q is deselected)", id, effective)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// no explicit selection for this route: defer to management's SkipAutoApply flag
|
||||
sel := collectSelected(rt)
|
||||
log.Debugf("DIAG applyExitNodeFilter: no user selection for effective %q; SkipAutoApply filter kept %d/%d routes for id=%q",
|
||||
effective, len(sel), len(rt), id)
|
||||
if len(sel) > 0 {
|
||||
out[id] = sel
|
||||
}
|
||||
|
||||
@@ -359,6 +359,30 @@ func TestRouteSelector_V6ExitPairInherits(t *testing.T) {
|
||||
assert.True(t, rs.IsSelected("corp-v6"), "non-exit *-v6 routes must not inherit unrelated v4 state")
|
||||
})
|
||||
|
||||
t.Run("IsSelectedForExitNode mirrors deselected v4 base", func(t *testing.T) {
|
||||
rs := routeselector.NewRouteSelector()
|
||||
require.NoError(t, rs.DeselectRoutes([]route.NetID{"exit1"}, all))
|
||||
|
||||
// Regression: deselecting the v4 exit node must also report the synthesized
|
||||
// "-v6" pair as not selected, otherwise the ::/0 route leaks into the tunnel.
|
||||
assert.False(t, rs.IsSelectedForExitNode("exit1"))
|
||||
assert.False(t, rs.IsSelectedForExitNode("exit1-v6"), "v6 pair inherits v4 base deselect")
|
||||
|
||||
// An exit node with no user selection at all stays selected by default.
|
||||
assert.True(t, rs.IsSelectedForExitNode("exit2"))
|
||||
assert.True(t, rs.IsSelectedForExitNode("exit2-v6"))
|
||||
})
|
||||
|
||||
t.Run("IsSelectedForExitNode respects explicit v6 state", func(t *testing.T) {
|
||||
rs := routeselector.NewRouteSelector()
|
||||
require.NoError(t, rs.DeselectRoutes([]route.NetID{"exit1"}, all))
|
||||
require.NoError(t, rs.SelectRoutes([]route.NetID{"exit1-v6"}, true, all))
|
||||
|
||||
// Explicit selection on the v6 entry overrides the v4 base's deselect.
|
||||
assert.False(t, rs.IsSelectedForExitNode("exit1"))
|
||||
assert.True(t, rs.IsSelectedForExitNode("exit1-v6"), "explicit v6 select wins over v4 base")
|
||||
})
|
||||
|
||||
t.Run("explicit v6 state overrides v4 base in filter", func(t *testing.T) {
|
||||
rs := routeselector.NewRouteSelector()
|
||||
require.NoError(t, rs.DeselectRoutes([]route.NetID{"exit1"}, all))
|
||||
|
||||
Reference in New Issue
Block a user