From e9a47f3c04133cc62ff3dd8d8c6ca7e287373856 Mon Sep 17 00:00:00 2001 From: chaosinthecrd Date: Tue, 9 Dec 2025 14:00:24 +0000 Subject: [PATCH 1/3] ipn,ipn/local: always accept routes for Tailscale Services (cgnat range) Updates #18198 Co-authored-by: James Tucker Signed-off-by: chaosinthecrd --- ipn/ipnlocal/local.go | 48 +++++++++++++++--- ipn/ipnlocal/local_test.go | 99 ++++++++++++++++++++++++++++++++++++++ types/netmap/netmap.go | 8 ++- 3 files changed, 145 insertions(+), 10 deletions(-) diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index cebb96130..83cd09c25 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -5051,7 +5051,6 @@ func (b *LocalBackend) authReconfig() { // // b.mu must be held. func (b *LocalBackend) authReconfigLocked() { - if b.shutdownCalled { b.logf("[v1] authReconfig: skipping because in shutdown") return @@ -5083,9 +5082,9 @@ func (b *LocalBackend) authReconfigLocked() { } var flags netmap.WGConfigFlags - if prefs.RouteAll() { - flags |= netmap.AllowSubnetRoutes - } + // NOTE: we want to always allow subnet routes for the sake of enabling services + flags |= netmap.AllowSubnetRoutes + if hasPAC && disableSubnetsIfPAC { if flags&netmap.AllowSubnetRoutes != 0 { b.logf("authReconfig: have PAC; disabling subnet routes") @@ -5429,6 +5428,34 @@ func peerRoutes(logf logger.Logf, peers []wgcfg.Peer, cgnatThreshold int) (route return routes } +func (b *LocalBackend) servicesRoutes(routes []netip.Prefix) []netip.Prefix { + var servicesRangeBuilder netipx.IPSetBuilder + servicesRangeBuilder.AddPrefix(tsaddr.CGNATRange()) + servicesRangeBuilder.AddPrefix(tsaddr.TailscaleULARange()) + + services, err := servicesRangeBuilder.IPSet() + if err != nil { + b.logf("accept routes filter: failed to build filtered set, all routes will be accepted: %v (check accept-routes flag)", err) + return routes + } + + var builder netipx.IPSetBuilder + for _, r := range routes { + builder.AddPrefix(r) + } + + builder.Intersect(services) + + set, err := builder.IPSet() + if err != nil { + b.logf("accept routes filter: failed to build filtered set, all routes will be accepted: %v (check accept-routes flag)", err) + return routes + } + + b.logf("accept routes filter: accepting routes: %v", set.Ranges()) + return set.Prefixes() +} + // routerConfig produces a router.Config from a wireguard config and IPN prefs. // // b.mu must be held. @@ -5455,13 +5482,20 @@ func (b *LocalBackend) routerConfigLocked(cfg *wgcfg.Config, prefs ipn.PrefsView doStatefulFiltering = true } + var routes []netip.Prefix + if prefs.RouteAll() { + routes = peerRoutes(b.logf, cfg.Peers, singleRouteThreshold) + } else { + routes = b.servicesRoutes(peerRoutes(b.logf, cfg.Peers, singleRouteThreshold)) + } + rs := &router.Config{ LocalAddrs: unmapIPPrefixes(cfg.Addresses), SubnetRoutes: unmapIPPrefixes(prefs.AdvertiseRoutes().AsSlice()), SNATSubnetRoutes: !prefs.NoSNAT(), StatefulFiltering: doStatefulFiltering, NetfilterMode: prefs.NetfilterMode(), - Routes: peerRoutes(b.logf, cfg.Peers, singleRouteThreshold), + Routes: routes, NetfilterKind: netfilterKind, } @@ -7774,9 +7808,7 @@ func maybeUsernameOf(actor ipnauth.Actor) string { return username } -var ( - metricCurrentWatchIPNBus = clientmetric.NewGauge("localbackend_current_watch_ipn_bus") -) +var metricCurrentWatchIPNBus = clientmetric.NewGauge("localbackend_current_watch_ipn_bus") func (b *LocalBackend) stateEncrypted() opt.Bool { switch runtime.GOOS { diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go index 02997a0e1..12a3fb685 100644 --- a/ipn/ipnlocal/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -7295,3 +7295,102 @@ func TestStripKeysFromPrefs(t *testing.T) { }) } } + +func TestRouteAllDisabled(t *testing.T) { + pp := netip.MustParsePrefix + + tests := []struct { + name string + peers []wgcfg.Peer + wantEndpoints []netip.Prefix + routeAll bool + }{ + { + name: "route_all_disabled", + routeAll: false, + peers: []wgcfg.Peer{ + { + AllowedIPs: []netip.Prefix{ + // if one ip in the Tailscale ULA range is added, the entire range is added to the router config + pp("fd7a:115c:a1e0::2501:9b83/128"), + pp("100.80.207.38/32"), + pp("100.80.207.56/32"), + pp("100.80.207.40/32"), + pp("100.94.122.93/32"), + pp("100.79.141.115/32"), + + // ips outside the tailscale cgnat/ula range are not added to the router config + pp("192.168.0.45/32"), + pp("fd7a:115c:b1e0::2501:9b83/128"), + pp("fdf8:f966:e27c:0:5:0:0:10/128"), + }, + }, + }, + wantEndpoints: []netip.Prefix{ + pp("100.80.207.38/32"), + pp("100.80.207.56/32"), + pp("100.80.207.40/32"), + pp("100.94.122.93/32"), + pp("100.79.141.115/32"), + pp("fd7a:115c:a1e0::/48"), + }, + }, + { + name: "route_all_enabled", + routeAll: true, + peers: []wgcfg.Peer{ + { + AllowedIPs: []netip.Prefix{ + // if one ip in the Tailscale ULA range is added, the entire range is added to the router config + pp("fd7a:115c:a1e0::2501:9b83/128"), + pp("100.80.207.38/32"), + pp("100.80.207.56/32"), + pp("100.80.207.40/32"), + pp("100.94.122.93/32"), + pp("100.79.141.115/32"), + + // ips outside the tailscale cgnat/ula range are not added to the router config + pp("192.168.0.45/32"), + pp("fd7a:115c:b1e0::2501:9b83/128"), + pp("fdf8:f966:e27c:0:5:0:0:10/128"), + }, + }, + }, + wantEndpoints: []netip.Prefix{ + pp("100.80.207.38/32"), + pp("100.80.207.56/32"), + pp("100.80.207.40/32"), + pp("100.94.122.93/32"), + pp("100.79.141.115/32"), + pp("192.168.0.45/32"), + pp("fd7a:115c:a1e0::/48"), + pp("fd7a:115c:b1e0::2501:9b83/128"), + pp("fdf8:f966:e27c:0:5:0:0:10/128"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + prefs := ipn.Prefs{RouteAll: tt.routeAll} + lb := newTestLocalBackend(t) + cfg := &wgcfg.Config{ + Peers: tt.peers, + } + + rcfg := lb.routerConfigLocked(cfg, prefs.View(), false) + for _, p := range rcfg.Routes { + found := false + for _, r := range tt.wantEndpoints { + if p.Addr() == r.Addr() { + found = true + break + } + } + if !found { + t.Errorf("unexpected prefix %q in router config", p.String()) + } + } + }) + } +} diff --git a/types/netmap/netmap.go b/types/netmap/netmap.go index c54562f4d..60dc38d48 100644 --- a/types/netmap/netmap.go +++ b/types/netmap/netmap.go @@ -13,6 +13,7 @@ import ( "strings" "time" + "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" "tailscale.com/tka" "tailscale.com/types/key" @@ -154,8 +155,11 @@ func (nm *NetworkMap) SelfNodeOrZero() tailcfg.NodeView { // AnyPeersAdvertiseRoutes reports whether any peer is advertising non-exit node routes. func (nm *NetworkMap) AnyPeersAdvertiseRoutes() bool { for _, p := range nm.Peers { - if p.PrimaryRoutes().Len() > 0 { - return true + // NOTE: (ChaosInTheCRD) if the peer being advertised is a tailscale ip, we ignore it in this check + for _, r := range p.PrimaryRoutes().AsSlice() { + if !tsaddr.IsTailscaleIP(r.Addr()) { + return true + } } } return false From e10c12141d8bf0a3783dee32aaa91237db9269fc Mon Sep 17 00:00:00 2001 From: chaosinthecrd Date: Tue, 6 Jan 2026 16:18:12 +0000 Subject: [PATCH 2/3] ipn: addressing comments Signed-off-by: chaosinthecrd --- ipn/ipnlocal/local.go | 50 +++++++---------------------------- ipn/ipnlocal/local_test.go | 2 +- types/netmap/netmap.go | 4 +-- wgengine/wgcfg/nmcfg/nmcfg.go | 4 +++ 4 files changed, 16 insertions(+), 44 deletions(-) diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 83cd09c25..cf4e1a1b0 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -5082,8 +5082,9 @@ func (b *LocalBackend) authReconfigLocked() { } var flags netmap.WGConfigFlags - // NOTE: we want to always allow subnet routes for the sake of enabling services - flags |= netmap.AllowSubnetRoutes + if prefs.RouteAll() { + flags |= netmap.AllowSubnetRoutes + } if hasPAC && disableSubnetsIfPAC { if flags&netmap.AllowSubnetRoutes != 0 { @@ -5382,7 +5383,7 @@ func magicDNSRootDomains(nm *netmap.NetworkMap) []dnsname.FQDN { // peerRoutes returns the routerConfig.Routes to access peers. // If there are over cgnatThreshold CGNAT routes, one big CGNAT route // is used instead. -func peerRoutes(logf logger.Logf, peers []wgcfg.Peer, cgnatThreshold int) (routes []netip.Prefix) { +func peerRoutes(logf logger.Logf, peers []wgcfg.Peer, cgnatThreshold int, routeAll bool) (routes []netip.Prefix) { tsULA := tsaddr.TailscaleULARange() cgNAT := tsaddr.CGNATRange() var didULA bool @@ -5412,7 +5413,7 @@ func peerRoutes(logf logger.Logf, peers []wgcfg.Peer, cgnatThreshold int) (route } if aip.IsSingleIP() && cgNAT.Contains(aip.Addr()) { cgNATIPs = append(cgNATIPs, aip) - } else { + } else if routeAll { routes = append(routes, aip) } } @@ -5428,34 +5429,6 @@ func peerRoutes(logf logger.Logf, peers []wgcfg.Peer, cgnatThreshold int) (route return routes } -func (b *LocalBackend) servicesRoutes(routes []netip.Prefix) []netip.Prefix { - var servicesRangeBuilder netipx.IPSetBuilder - servicesRangeBuilder.AddPrefix(tsaddr.CGNATRange()) - servicesRangeBuilder.AddPrefix(tsaddr.TailscaleULARange()) - - services, err := servicesRangeBuilder.IPSet() - if err != nil { - b.logf("accept routes filter: failed to build filtered set, all routes will be accepted: %v (check accept-routes flag)", err) - return routes - } - - var builder netipx.IPSetBuilder - for _, r := range routes { - builder.AddPrefix(r) - } - - builder.Intersect(services) - - set, err := builder.IPSet() - if err != nil { - b.logf("accept routes filter: failed to build filtered set, all routes will be accepted: %v (check accept-routes flag)", err) - return routes - } - - b.logf("accept routes filter: accepting routes: %v", set.Ranges()) - return set.Prefixes() -} - // routerConfig produces a router.Config from a wireguard config and IPN prefs. // // b.mu must be held. @@ -5482,20 +5455,13 @@ func (b *LocalBackend) routerConfigLocked(cfg *wgcfg.Config, prefs ipn.PrefsView doStatefulFiltering = true } - var routes []netip.Prefix - if prefs.RouteAll() { - routes = peerRoutes(b.logf, cfg.Peers, singleRouteThreshold) - } else { - routes = b.servicesRoutes(peerRoutes(b.logf, cfg.Peers, singleRouteThreshold)) - } - rs := &router.Config{ LocalAddrs: unmapIPPrefixes(cfg.Addresses), SubnetRoutes: unmapIPPrefixes(prefs.AdvertiseRoutes().AsSlice()), SNATSubnetRoutes: !prefs.NoSNAT(), StatefulFiltering: doStatefulFiltering, NetfilterMode: prefs.NetfilterMode(), - Routes: routes, + Routes: peerRoutes(b.logf, cfg.Peers, singleRouteThreshold, prefs.RouteAll()), NetfilterKind: netfilterKind, } @@ -7808,7 +7774,9 @@ func maybeUsernameOf(actor ipnauth.Actor) string { return username } -var metricCurrentWatchIPNBus = clientmetric.NewGauge("localbackend_current_watch_ipn_bus") +var ( + metricCurrentWatchIPNBus = clientmetric.NewGauge("localbackend_current_watch_ipn_bus") +) func (b *LocalBackend) stateEncrypted() opt.Bool { switch runtime.GOOS { diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go index 12a3fb685..457bc297b 100644 --- a/ipn/ipnlocal/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -306,7 +306,7 @@ func TestPeerRoutes(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := peerRoutes(t.Logf, tt.peers, 2) + got := peerRoutes(t.Logf, tt.peers, 2, true) if !reflect.DeepEqual(got, tt.want) { t.Errorf("got = %v; want %v", got, tt.want) } diff --git a/types/netmap/netmap.go b/types/netmap/netmap.go index 60dc38d48..d83407750 100644 --- a/types/netmap/netmap.go +++ b/types/netmap/netmap.go @@ -156,8 +156,8 @@ func (nm *NetworkMap) SelfNodeOrZero() tailcfg.NodeView { func (nm *NetworkMap) AnyPeersAdvertiseRoutes() bool { for _, p := range nm.Peers { // NOTE: (ChaosInTheCRD) if the peer being advertised is a tailscale ip, we ignore it in this check - for _, r := range p.PrimaryRoutes().AsSlice() { - if !tsaddr.IsTailscaleIP(r.Addr()) { + for _, r := range p.PrimaryRoutes().All() { + if !tsaddr.IsTailscaleIP(r.Addr()) && r.IsSingleIP() { return true } } diff --git a/wgengine/wgcfg/nmcfg/nmcfg.go b/wgengine/wgcfg/nmcfg/nmcfg.go index 487e78d81..4b52482f0 100644 --- a/wgengine/wgcfg/nmcfg/nmcfg.go +++ b/wgengine/wgcfg/nmcfg/nmcfg.go @@ -11,6 +11,7 @@ import ( "net/netip" "strings" + "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" "tailscale.com/types/key" "tailscale.com/types/logger" @@ -30,6 +31,9 @@ func cidrIsSubnet(node tailcfg.NodeView, cidr netip.Prefix) bool { if cidr.Bits() == 0 { return false } + if tsaddr.IsTailscaleIP(cidr.Addr()) { + return false + } if !cidr.IsSingleIP() { return true } From 665f41931cfb0a5c4d106444284c4aba9e5f0beb Mon Sep 17 00:00:00 2001 From: chaosinthecrd Date: Wed, 7 Jan 2026 17:35:54 +0000 Subject: [PATCH 3/3] ipn: adding to test data Signed-off-by: chaosinthecrd --- ipn/ipnlocal/local_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go index 457bc297b..bcc5ebaf2 100644 --- a/ipn/ipnlocal/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -7319,6 +7319,9 @@ func TestRouteAllDisabled(t *testing.T) { pp("100.94.122.93/32"), pp("100.79.141.115/32"), + // a /28 range will not be added, since this is not a Service IP range (which is always /32, a single IP) + pp("100.64.0.0/28"), + // ips outside the tailscale cgnat/ula range are not added to the router config pp("192.168.0.45/32"), pp("fd7a:115c:b1e0::2501:9b83/128"),