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
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"maps"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
"tailscale.com/net/routecheck/peernode"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/util/eventbus"
|
||||
"tailscale.com/util/mak"
|
||||
@@ -271,3 +273,83 @@ var _ RouteCheckReport = *new(routecheckReport)
|
||||
func (rp routecheckReport) IsReachable(_ tailcfg.NodeID) peernode.Reachability {
|
||||
return peernode.Reachability(rp)
|
||||
}
|
||||
|
||||
func TestNodeBackendRouteManager(t *testing.T) {
|
||||
nb := newNodeBackend(t.Context(), tstest.WhileTestRunningLogger(t), eventbus.New())
|
||||
|
||||
mkPeer := func(id tailcfg.NodeID, stableID tailcfg.StableNodeID, addr4 string, extra ...string) tailcfg.NodeView {
|
||||
n := &tailcfg.Node{
|
||||
ID: id,
|
||||
StableID: stableID,
|
||||
Key: key.NewNode().Public(),
|
||||
HomeDERP: 1, // required by the route manager's reachability filter
|
||||
Addresses: []netip.Prefix{
|
||||
netip.MustParsePrefix(addr4),
|
||||
},
|
||||
}
|
||||
n.AllowedIPs = append(n.AllowedIPs, n.Addresses...)
|
||||
for _, s := range extra {
|
||||
n.AllowedIPs = append(n.AllowedIPs, netip.MustParsePrefix(s))
|
||||
}
|
||||
return n.View()
|
||||
}
|
||||
wantPeerFor := func(ip string, want tailcfg.NodeView) {
|
||||
t.Helper()
|
||||
got, ok := nb.routeMgr.Outbound().Lookup(netip.MustParseAddr(ip))
|
||||
if !want.Valid() {
|
||||
if ok {
|
||||
t.Errorf("Outbound lookup %s = %v; want no match", ip, got)
|
||||
}
|
||||
return
|
||||
}
|
||||
if !ok || got.Key != want.Key() {
|
||||
t.Errorf("Outbound lookup %s = %v, %v; want %v", ip, got, ok, want.Key())
|
||||
}
|
||||
}
|
||||
|
||||
p1 := mkPeer(1, "stable1", "100.64.0.1/32")
|
||||
p2 := mkPeer(2, "stable2", "100.64.0.2/32", "0.0.0.0/0", "::/0")
|
||||
|
||||
// A full netmap populates the route manager.
|
||||
nb.SetNetMap(&netmap.NetworkMap{Peers: []tailcfg.NodeView{p1, p2}})
|
||||
wantPeerFor("100.64.0.1", p1)
|
||||
wantPeerFor("100.64.0.2", p2)
|
||||
wantPeerFor("8.8.8.8", tailcfg.NodeView{}) // exit node not selected
|
||||
|
||||
// Selecting peer 2 as the exit node resolves its stable ID and
|
||||
// installs its /0 routes.
|
||||
nb.updateRouteManagerPrefs(routePrefs{ExitNodeID: "stable2", ExitNodeSelected: true})
|
||||
wantPeerFor("8.8.8.8", p2)
|
||||
|
||||
// A selected exit node that resolves to no current peer must
|
||||
// blackhole internet traffic, not fall back to "no exit node":
|
||||
// the default routes stay in the OS route set with no outbound
|
||||
// peer to carry them.
|
||||
nb.updateRouteManagerPrefs(routePrefs{ExitNodeID: "no-such-node", ExitNodeSelected: true})
|
||||
wantPeerFor("8.8.8.8", tailcfg.NodeView{})
|
||||
if !nb.routeMgr.OSRoutes().Get(netip.MustParsePrefix("0.0.0.0/0")) {
|
||||
t.Error("unresolved exit node: OSRoutes missing 0.0.0.0/0 blackhole route")
|
||||
}
|
||||
|
||||
nb.updateRouteManagerPrefs(routePrefs{})
|
||||
wantPeerFor("8.8.8.8", tailcfg.NodeView{})
|
||||
if nb.routeMgr.OSRoutes().Get(netip.MustParsePrefix("0.0.0.0/0")) {
|
||||
t.Error("no exit node: OSRoutes unexpectedly contains 0.0.0.0/0")
|
||||
}
|
||||
|
||||
// Incremental deltas: add peer 3, remove peer 1.
|
||||
p3 := mkPeer(3, "stable3", "100.64.0.3/32")
|
||||
if !nb.UpdateNetmapDelta([]netmap.NodeMutation{
|
||||
netmap.NodeMutationUpsert{Node: p3},
|
||||
netmap.MakeNodeMutationRemove(1),
|
||||
}) {
|
||||
t.Fatal("UpdateNetmapDelta not handled")
|
||||
}
|
||||
wantPeerFor("100.64.0.3", p3)
|
||||
wantPeerFor("100.64.0.1", tailcfg.NodeView{})
|
||||
|
||||
// A full netmap that drops a peer removes it from the route manager.
|
||||
nb.SetNetMap(&netmap.NetworkMap{Peers: []tailcfg.NodeView{p2}})
|
||||
wantPeerFor("100.64.0.3", tailcfg.NodeView{})
|
||||
wantPeerFor("100.64.0.2", p2)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user