From ce050f1ca1371c6b6f59172fb0d746d213a6193d Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 13 Jul 2026 15:53:07 +0000 Subject: [PATCH] ipn/ipnlocal: test the unresolved-exit-node blackhole routes Selecting an exit node that doesn't resolve to any current peer (a nonexistent node, or MDM's "auto:any" placeholder before it is resolved) installs the blackhole default routes, so internet traffic is dropped rather than escaping to the local network. That behavior is documented on ipn.Prefs.ExitNodeID and five-plus years old, but nothing tested it. Lock it in ahead of an upcoming change that moves OS route computation to net/routemanager and must preserve it. Updates #12542 Change-Id: I0f63b0d5ce46061a74c69b75f7f83f115da7c3d4 Signed-off-by: Brad Fitzpatrick --- ipn/ipnlocal/local_test.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go index 97d8d1abc..0de7a9f34 100644 --- a/ipn/ipnlocal/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -9668,3 +9668,38 @@ func TestEnginePeerForIPAdjustsForPrefs(t *testing.T) { }) } } + +// Tests that selecting an exit node that doesn't resolve to any +// current peer (a nonexistent node, or MDM's "auto:any" placeholder +// before it is resolved) still installs the blackhole default routes, +// so internet traffic is dropped rather than escaping to the local +// network. That behavior is documented on [ipn.Prefs.ExitNodeID] and +// must survive the migration of OS route computation to +// net/routemanager. +func TestRouterConfigExitNodeBlackhole(t *testing.T) { + lb := newTestLocalBackend(t) + nm := &netmap.NetworkMap{ + SelfNode: (&tailcfg.Node{ + Name: "test-node", + Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.1.1/32")}, + }).View(), + } + cfg := &wgcfg.Config{} // no peer carries the default routes + + hasDefaults := func(routes []netip.Prefix) bool { + return slices.Contains(routes, tsaddr.AllIPv4()) && slices.Contains(routes, tsaddr.AllIPv6()) + } + for _, exitID := range []tailcfg.StableNodeID{"auto:any", "no-such-node"} { + prefs := ipn.Prefs{ExitNodeID: exitID} + rcfg := lb.routerConfigLocked(cfg, prefs.View(), nm, false) + if !hasDefaults(rcfg.Routes) { + t.Errorf("ExitNodeID=%q: Routes = %v; want blackhole default routes", exitID, rcfg.Routes) + } + } + + // With no exit node selected, there must be no default routes. + rcfg := lb.routerConfigLocked(cfg, new(ipn.Prefs).View(), nm, false) + if hasDefaults(rcfg.Routes) { + t.Errorf("no exit node: Routes = %v; want no default routes", rcfg.Routes) + } +}