ipn/ipnlocal,net/routemanager: keep a routemanager.RouteManager updated per node
Give nodeBackend a RouteManager and keep it in sync as routing inputs change: full netmaps resync the whole peer set (removals plus no-op-cheap upserts), incremental netmap deltas mirror their peer upserts and removes into the same mutation batch, and authReconfigLocked pushes the routing-relevant prefs (exit node, subnet route acceptance, OneCGNAT) after resolving the exit node's stable ID to its current numeric node ID. A selected exit node that doesn't resolve to a current peer (a nonexistent node, or MDM's "auto:any" placeholder awaiting resolution) is not the same as no exit node: per the long-standing ipn.Prefs.ExitNodeID contract, it blackholes internet traffic rather than letting it escape to the local network. RouteManager's Prefs gains an ExitNodeSelected bit so its OS route set keeps the default routes in that case, with no outbound peer to carry them, matching what routerConfigLocked does today, as pinned by TestRouterConfigExitNodeBlackhole in the previous commit. All mutations happen with nodeBackend.mu held, satisfying the RouteManager's serialized Begin/Commit contract. Nothing consumes its snapshots yet; the wgengine data plane and OS router wiring come next. Updates #12542 Change-Id: I677b6b2c9efb8e41b3d27071bd9db73e01640d3b Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
committed by
Brad Fitzpatrick
parent
ce050f1ca1
commit
ff1c7ef23c
@@ -106,10 +106,22 @@ func (p peerView) hasDataPlaneAttrs() bool {
|
||||
// Prefs is the subset of ipn.Prefs that affects routing.
|
||||
type Prefs struct {
|
||||
// ExitNodeID is the node ID of the peer selected as this
|
||||
// node's exit node, or zero if no exit node is selected.
|
||||
// node's exit node, or zero if no exit node is selected or the
|
||||
// selected exit node does not resolve to a current peer.
|
||||
// (Callers resolve ipn.Prefs's stable node ID to a NodeID.)
|
||||
ExitNodeID tailcfg.NodeID
|
||||
|
||||
// ExitNodeSelected is whether the prefs select any exit node at
|
||||
// all, even one that doesn't resolve to a current peer (in which
|
||||
// case ExitNodeID is zero). When set, the exit routes are always
|
||||
// in the OS route set: with a resolved exit node they carry its
|
||||
// traffic, and without one they blackhole internet traffic
|
||||
// rather than let it escape to the local network, per the
|
||||
// [tailscale.com/ipn.Prefs.ExitNodeID] docs. MDM's "auto:any"
|
||||
// placeholder relies on the blackhole while an exit node is
|
||||
// still being chosen.
|
||||
ExitNodeSelected bool
|
||||
|
||||
// RouteAll is whether advertised subnet routes (non-exit
|
||||
// routes) from peers are accepted.
|
||||
RouteAll bool
|
||||
@@ -899,6 +911,13 @@ func (rm *RouteManager) applyDirty(dirty set.Set[netip.Prefix], res *Result) {
|
||||
var cgnatDirty []netip.Prefix
|
||||
for pfx := range dirty {
|
||||
want, wantOS := rm.desiredFor(pfx)
|
||||
if rm.prefs.ExitNodeSelected && tsaddr.IsExitRoute(pfx) {
|
||||
// The exit routes stay in the OS route set as long as an
|
||||
// exit node is selected, even with no eligible
|
||||
// contributor, to blackhole rather than leak internet
|
||||
// traffic. See [Prefs.ExitNodeSelected].
|
||||
wantOS = true
|
||||
}
|
||||
|
||||
if cur, ok := out.Get(pfx); (want != nil) != ok || (ok && cur != want) {
|
||||
if want != nil {
|
||||
@@ -984,6 +1003,13 @@ func (rm *RouteManager) rebuildAll(res *Result) {
|
||||
for _, pfx := range plain {
|
||||
osr.Insert(pfx)
|
||||
}
|
||||
if rm.prefs.ExitNodeSelected {
|
||||
// Blackhole (or carry) internet traffic while any exit node
|
||||
// is selected, resolved or not. See [Prefs.ExitNodeSelected].
|
||||
for _, pfx := range tsaddr.ExitRoutes() {
|
||||
osr.Insert(pfx)
|
||||
}
|
||||
}
|
||||
if len(rm.ulaPfxs) > 0 {
|
||||
osr.Insert(tsaddr.TailscaleULARange())
|
||||
}
|
||||
|
||||
@@ -895,3 +895,48 @@ func TestExtraAllowedIPsPeerLifecycle(t *testing.T) {
|
||||
wantOutbound(t, rm, "fe80::1234", k1, true)
|
||||
wantOSRoutes(t, rm, "100.64.0.1/32", "fd7a:115c:a1e0::/48")
|
||||
}
|
||||
|
||||
// Tests that the exit routes stay in the OS route set whenever an
|
||||
// exit node is selected, even one that resolves to no current peer
|
||||
// (Prefs.ExitNodeSelected with a zero ExitNodeID), so internet
|
||||
// traffic is blackholed rather than escaping to the local network.
|
||||
// This mirrors the long-standing behavior documented on
|
||||
// ipn.Prefs.ExitNodeID and relied on by MDM's "auto:any" placeholder.
|
||||
func TestExitNodeBlackhole(t *testing.T) {
|
||||
rm := New(t.Logf)
|
||||
exitPeer := peer1()
|
||||
exitPeer.Routes = tsaddr.ExitRoutes()
|
||||
commit(rm, func(m *Mutation) { m.upsertPeer(exitPeer) })
|
||||
|
||||
// No exit node selected: no default routes, no outbound winner.
|
||||
wantOSRoutes(t, rm, "100.64.0.1/32", "fd7a:115c:a1e0::/48")
|
||||
wantOutbound(t, rm, "8.8.8.8", key.NodePublic{}, false)
|
||||
|
||||
// A resolved exit node carries the default routes.
|
||||
commit(rm, func(m *Mutation) {
|
||||
m.SetPrefs(Prefs{ExitNodeID: 1, ExitNodeSelected: true})
|
||||
})
|
||||
wantOSRoutes(t, rm, "100.64.0.1/32", "fd7a:115c:a1e0::/48", "0.0.0.0/0", "::/0")
|
||||
wantOutbound(t, rm, "8.8.8.8", k1, true)
|
||||
|
||||
// An unresolved exit node (nonexistent, or not yet resolved)
|
||||
// keeps the default routes with no outbound winner: a blackhole.
|
||||
commit(rm, func(m *Mutation) {
|
||||
m.SetPrefs(Prefs{ExitNodeID: 0, ExitNodeSelected: true})
|
||||
})
|
||||
wantOSRoutes(t, rm, "100.64.0.1/32", "fd7a:115c:a1e0::/48", "0.0.0.0/0", "::/0")
|
||||
wantOutbound(t, rm, "8.8.8.8", key.NodePublic{}, false)
|
||||
|
||||
// Removing the exit-capable peer while still selected keeps the
|
||||
// blackhole in place via the incremental path.
|
||||
commit(rm, func(m *Mutation) {
|
||||
m.SetPrefs(Prefs{ExitNodeID: 1, ExitNodeSelected: true})
|
||||
})
|
||||
commit(rm, func(m *Mutation) { m.RemovePeer(1) })
|
||||
wantOSRoutes(t, rm, "0.0.0.0/0", "::/0")
|
||||
wantOutbound(t, rm, "8.8.8.8", key.NodePublic{}, false)
|
||||
|
||||
// Deselecting the exit node removes the default routes.
|
||||
commit(rm, func(m *Mutation) { m.SetPrefs(Prefs{}) })
|
||||
wantOSRoutes(t, rm)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user