diff --git a/hscontrol/mapper/builder.go b/hscontrol/mapper/builder.go index 5ea2f5a45..0ab69ac17 100644 --- a/hscontrol/mapper/builder.go +++ b/hscontrol/mapper/builder.go @@ -6,7 +6,6 @@ import ( "slices" "time" - "github.com/juanfont/headscale/hscontrol/policy" policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2" "github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/util/zlog/zf" @@ -86,15 +85,12 @@ func (b *MapResponseBuilder) WithSelfNode() *MapResponseBuilder { return b } - _, matchers := b.mapper.state.Filter() - tailnode, err := nv.TailNode( b.capVer, - func(id types.NodeID) []netip.Prefix { - // Self node: include own primaries + exit routes (no via steering for self). - primaries := policy.ReduceRoutes(nv, b.mapper.state.GetNodePrimaryRoutes(id), matchers) - - return slices.Concat(primaries, nv.ExitRoutes()) + func(types.NodeID) []netip.Prefix { + // A router holds every route it is approved for, whatever + // its own policy reaches. + return nv.AllApprovedRoutes() }, b.mapper.cfg, b.mapper.state.NodeCapMap(nv.ID()), @@ -104,6 +100,13 @@ func (b *MapResponseBuilder) WithSelfNode() *MapResponseBuilder { return b } + // PrimaryRoutes is what the router serves: the routes it is primary + // for, under the rule its peers get them by. + tailnode.PrimaryRoutes = slices.DeleteFunc( + b.mapper.state.GetNodePrimaryRoutes(nv.ID()), + func(p netip.Prefix) bool { return !b.mapper.cfg.ServesRoute(p) }, + ) + b.resp.Node = tailnode return b diff --git a/hscontrol/servertest/mapstream_compat_test.go b/hscontrol/servertest/mapstream_compat_test.go index 41faedeb0..7f5523e31 100644 --- a/hscontrol/servertest/mapstream_compat_test.go +++ b/hscontrol/servertest/mapstream_compat_test.go @@ -114,19 +114,6 @@ var mapStreamSkipReasons = map[string]string{ "route-announce/RA9/srv/presence": policyResponseToEveryNode, "route-announce/RA9/wit/presence": policyResponseToEveryNode, - "route-announce/RA0/rtr/self-routes": selfRoutesReduced, - "route-announce/RA1/rtr/self-routes": selfRoutesReduced, - "route-announce/RA2/rtr/self-routes": selfRoutesReduced, - "route-announce/RA3/rtr/self-routes": selfRoutesReduced, - "route-announce/RA4/rtr/self-routes": selfRoutesReduced, - "route-announce/RA5/rtr/self-routes": selfRoutesReduced, - "route-announce/RA6/rtr/self-routes": selfRoutesReduced, - "route-announce/RA7/rtr/self-routes": selfRoutesReduced, - "route-announce/RA8/rtr/self-routes": selfRoutesReduced, - "route-announce/RA9/rtr/self-routes": selfRoutesReduced, - "route-announce/RA10/rtr/self-routes": selfRoutesReduced, - "route-announce/RA11/rtr/self-routes": selfRoutesReduced, - "policy-generation/G2/thor-1/peers": sameUserPeers, "policy-generation/G2/thor-2/peers": sameUserPeers, "policy-generation/G3/thor-1/peers": sameUserPeers, diff --git a/hscontrol/servertest/routes_test.go b/hscontrol/servertest/routes_test.go index 6c3f8529a..de304f7aa 100644 --- a/hscontrol/servertest/routes_test.go +++ b/hscontrol/servertest/routes_test.go @@ -373,50 +373,7 @@ var routeOverlapCompatTests = []string{ // routeOverlapSkipReasons lists checks where headscale differs from SaaS, // keyed //. A listed check that matches fails, so a fix // must delete its entry. -var routeOverlapSkipReasons = map[string]string{ - // Once a router is sent its own routes, its self-primary also needs the - // approved singles in the tailnet range left unserved. - "routes-t1-auto-v4-default/ovl-r-ctrl/self-allowed": selfRoutesReduced, - "routes-t1-auto-v4-default/ovl-r-ctrl/self-primary": selfRoutesReduced, - "routes-t1-auto-v4-default/ovl-r-host4/self-allowed": selfRoutesReduced, - "routes-t1-auto-v4-default/ovl-r-narrow/self-allowed": selfRoutesReduced, - "routes-t1-auto-v4-default/ovl-r-narrow/self-primary": selfRoutesReduced, - "routes-t2-auto-v6-default/ovl-r-host6/self-allowed": selfRoutesReduced, - "routes-t2-auto-v6-default/ovl-r-mapped/self-allowed": selfRoutesReduced, - "routes-t2-auto-v6-default/ovl-r-mapped/self-primary": selfRoutesReduced, - "routes-t2-auto-v6-default/ovl-r-narrow/self-allowed": selfRoutesReduced, - "routes-t2-auto-v6-default/ovl-r-narrow/self-primary": selfRoutesReduced, - "routes-t2-auto-v6-default/ovl-r-via/self-allowed": selfRoutesReduced, - "routes-t2-auto-v6-default/ovl-r-via/self-primary": selfRoutesReduced, - "routes-t3-auto-narrow-keys/ovl-r-ctrl/self-allowed": selfRoutesReduced, - "routes-t3-auto-narrow-keys/ovl-r-ctrl/self-primary": selfRoutesReduced, - "routes-t3-auto-narrow-keys/ovl-r-host4/self-allowed": selfRoutesReduced, - "routes-t3-auto-narrow-keys/ovl-r-host6/self-allowed": selfRoutesReduced, - "routes-t3-auto-narrow-keys/ovl-r-narrow/self-allowed": selfRoutesReduced, - "routes-t3-auto-narrow-keys/ovl-r-narrow/self-primary": selfRoutesReduced, - "routes-t3-auto-narrow-keys/ovl-r-via/self-allowed": selfRoutesReduced, - "routes-t3-auto-narrow-keys/ovl-r-via/self-primary": selfRoutesReduced, - "routes-t4-admin/ovl-r-ctrl/self-allowed": selfRoutesReduced, - "routes-t4-admin/ovl-r-ctrl/self-primary": selfRoutesReduced, - "routes-t4-admin/ovl-r-host4/self-allowed": selfRoutesReduced, - "routes-t4-admin/ovl-r-host6/self-allowed": selfRoutesReduced, - "routes-t4-admin/ovl-r-mapped/self-allowed": selfRoutesReduced, - "routes-t4-admin/ovl-r-mapped/self-primary": selfRoutesReduced, - "routes-t4-admin/ovl-r-narrow/self-allowed": selfRoutesReduced, - "routes-t4-admin/ovl-r-narrow/self-primary": selfRoutesReduced, - "routes-t4-admin/ovl-r-via/self-allowed": selfRoutesReduced, - "routes-t4-admin/ovl-r-via/self-primary": selfRoutesReduced, - "routes-t5-oauth-routes-scope/ovl-r-ctrl/self-allowed": selfRoutesReduced, - "routes-t5-oauth-routes-scope/ovl-r-ctrl/self-primary": selfRoutesReduced, - "routes-t5-oauth-routes-scope/ovl-r-host4/self-allowed": selfRoutesReduced, - "routes-t5-oauth-routes-scope/ovl-r-host6/self-allowed": selfRoutesReduced, - "routes-t5-oauth-routes-scope/ovl-r-mapped/self-allowed": selfRoutesReduced, - "routes-t5-oauth-routes-scope/ovl-r-mapped/self-primary": selfRoutesReduced, - "routes-t5-oauth-routes-scope/ovl-r-narrow/self-allowed": selfRoutesReduced, - "routes-t5-oauth-routes-scope/ovl-r-narrow/self-primary": selfRoutesReduced, - "routes-t5-oauth-routes-scope/ovl-r-via/self-allowed": selfRoutesReduced, - "routes-t5-oauth-routes-scope/ovl-r-via/self-primary": selfRoutesReduced, -} +var routeOverlapSkipReasons = map[string]string{} // routeOverlapNotCompared lists, by key prefix, checks left out: SaaS's // answer there is in doubt, so it is no reference. @@ -643,8 +600,9 @@ func sortedStrings(s []string) string { // TestRoutesNodeRangeSingleNotServed checks that a router's approved single // addresses inside the node ranges, in either spelling, reach no viewer -// through a plain grant or a via grant. They stay approved, the router's own -// entry keeps them, and its wider route is still served. +// through a plain grant or a via grant. They stay approved and in the +// router's own AllowedIPs, never in its PrimaryRoutes, and its wider route +// is still served. func TestRoutesNodeRangeSingleNotServed(t *testing.T) { t.Parallel() @@ -704,9 +662,9 @@ func TestRoutesNodeRangeSingleNotServed(t *testing.T) { return ok && views.SliceContains(p.PrimaryRoutes(), subnet) }) - router.WaitForCondition(t, "router holds its routes", 15*time.Second, + router.WaitForCondition(t, "router serves its subnet", 15*time.Second, func(nm *netmap.NetworkMap) bool { - return views.SliceContains(nm.SelfNode.AllowedIPs(), subnet) + return views.SliceContains(nm.SelfNode.PrimaryRoutes(), subnet) }) peer, _ := viewer.PeerByName("router") @@ -719,6 +677,8 @@ func TestRoutesNodeRangeSingleNotServed(t *testing.T) { "%s served in PrimaryRoutes", single) assert.Truef(t, views.SliceContains(self.AllowedIPs(), single), "%s missing from the router's own AllowedIPs", single) + assert.Falsef(t, views.SliceContains(self.PrimaryRoutes(), single), + "%s in the router's own PrimaryRoutes", single) } node, ok := srv.State().GetNodeByID(routerID) diff --git a/hscontrol/servertest/via_compat_test.go b/hscontrol/servertest/via_compat_test.go index fe774ab5f..13bdad274 100644 --- a/hscontrol/servertest/via_compat_test.go +++ b/hscontrol/servertest/via_compat_test.go @@ -107,139 +107,6 @@ var routesMapCompatTests = []struct { var viaCompatSkipReasons = map[string]string{ "tagged-self": "TAGGED_SELF_PROFILE: SaaS gives a tagged node a profile of its own, headscale the shared tagged-devices one", - // SaaS sends a router every approved route in its own AllowedIPs and - // every route it is primary for in PrimaryRoutes. - "grant-v11/blastoise/self-routes": selfRoutesReduced, - "grant-v11/squirtle/self-routes": selfRoutesReduced, - "grant-v12/blastoise/self-routes": selfRoutesReduced, - "grant-v12/squirtle/self-routes": selfRoutesReduced, - "grant-v13/blastoise/self-routes": selfRoutesReduced, - "grant-v13/squirtle/self-routes": selfRoutesReduced, - - "routes-a1-wildcard-acl-includes-routes-in-srcips/butterfree/self-routes": selfRoutesReduced, - "routes-a2-tag-based-acl-excludes-routes/butterfree/self-routes": selfRoutesReduced, - "routes-a2-tag-based-acl-excludes-routes/caterpie/self-routes": selfRoutesReduced, - "routes-a2-tag-based-acl-excludes-routes/metapod/self-routes": selfRoutesReduced, - "routes-a2-tag-based-acl-excludes-routes/squirtle/self-routes": selfRoutesReduced, - "routes-a2-tag-based-acl-excludes-routes/wartortle/self-routes": selfRoutesReduced, - "routes-a3-explicit-subnet-filter-to-router/butterfree/self-routes": selfRoutesReduced, - "routes-a3-explicit-subnet-filter-to-router/metapod/self-routes": selfRoutesReduced, - "routes-a3-explicit-subnet-filter-to-router/wartortle/self-routes": selfRoutesReduced, - "routes-a4-multiple-routes-same-router/butterfree/self-routes": selfRoutesReduced, - "routes-a4-multiple-routes-same-router/caterpie/self-routes": selfRoutesReduced, - "routes-a4-multiple-routes-same-router/metapod/self-routes": selfRoutesReduced, - "routes-a4-multiple-routes-same-router/squirtle/self-routes": selfRoutesReduced, - "routes-e1-ha-two-routers-same-subnet/butterfree/self-routes": selfRoutesReduced, - "routes-e1-ha-two-routers-same-subnet/caterpie/self-routes": selfRoutesReduced, - "routes-e1-ha-two-routers-same-subnet/squirtle/self-routes": selfRoutesReduced, - "routes-e1-ha-two-routers-same-subnet/wartortle/self-routes": selfRoutesReduced, - "routes-e2-ha-primary-in-allowedips/butterfree/self-routes": selfRoutesReduced, - "routes-e2-ha-primary-in-allowedips/caterpie/self-routes": selfRoutesReduced, - "routes-e2-ha-primary-in-allowedips/squirtle/self-routes": selfRoutesReduced, - "routes-e2-ha-primary-in-allowedips/wartortle/self-routes": selfRoutesReduced, - "routes-f2-subnet-as-acl-source/butterfree/self-routes": selfRoutesReduced, - "routes-f2-subnet-as-acl-source/caterpie/self-routes": selfRoutesReduced, - "routes-f2-subnet-as-acl-source/metapod/self-routes": selfRoutesReduced, - "routes-f2-subnet-as-acl-source/squirtle/self-routes": selfRoutesReduced, - "routes-f2-subnet-as-acl-source/wartortle/self-routes": selfRoutesReduced, - "routes-f3-wildcard-src-specific-dst/butterfree/self-routes": selfRoutesReduced, - "routes-f3-wildcard-src-specific-dst/metapod/self-routes": selfRoutesReduced, - "routes-f3-wildcard-src-specific-dst/wartortle/self-routes": selfRoutesReduced, - "routes-h1-wildcard-srcips-format/butterfree/self-routes": selfRoutesReduced, - "routes-h1-wildcard-srcips-format/metapod/self-routes": selfRoutesReduced, - "routes-pa1-partial-route-approval/butterfree/self-routes": selfRoutesReduced, - "routes-m1-wildcard-src-multi-range/r-exit/self-routes": selfRoutesReduced, - "routes-m1-wildcard-src-multi-range/r-multi/self-routes": selfRoutesReduced, - "routes-m2-member-partial-subnet/r-exit/self-routes": selfRoutesReduced, - "routes-m2-member-partial-subnet/r-multi/self-routes": selfRoutesReduced, - "routes-m3-subnet-route-as-src/r-exit/self-routes": selfRoutesReduced, - "routes-m3-subnet-route-as-src/r-multi/self-routes": selfRoutesReduced, - "routes-m4-member-self/r-exit/self-routes": selfRoutesReduced, - "routes-m4-member-self/r-multi/self-routes": selfRoutesReduced, - "routes-m6-subnet-to-subnet-multi-route/r-exit/self-routes": selfRoutesReduced, - "routes-m6-subnet-to-subnet-multi-route/r-multi/self-routes": selfRoutesReduced, - - "via-grant-v14/blastoise/self-routes": selfRoutesReduced, - "via-grant-v14/fearow/self-routes": selfRoutesReduced, - "via-grant-v14/spearow/self-routes": selfRoutesReduced, - "via-grant-v14/squirtle/self-routes": selfRoutesReduced, - "via-grant-v15/blastoise/self-routes": selfRoutesReduced, - "via-grant-v15/fearow/self-routes": selfRoutesReduced, - "via-grant-v15/spearow/self-routes": selfRoutesReduced, - "via-grant-v15/squirtle/self-routes": selfRoutesReduced, - "via-grant-v16/blastoise/self-routes": selfRoutesReduced, - "via-grant-v16/fearow/self-routes": selfRoutesReduced, - "via-grant-v16/spearow/self-routes": selfRoutesReduced, - "via-grant-v16/squirtle/self-routes": selfRoutesReduced, - "via-grant-v26/blastoise/self-routes": selfRoutesReduced, - "via-grant-v26/fearow/self-routes": selfRoutesReduced, - "via-grant-v26/spearow/self-routes": selfRoutesReduced, - "via-grant-v26/squirtle/self-routes": selfRoutesReduced, - "via-grant-v27/blastoise/self-routes": selfRoutesReduced, - "via-grant-v27/fearow/self-routes": selfRoutesReduced, - "via-grant-v27/spearow/self-routes": selfRoutesReduced, - "via-grant-v27/squirtle/self-routes": selfRoutesReduced, - "via-grant-v28/blastoise/self-routes": selfRoutesReduced, - "via-grant-v28/fearow/self-routes": selfRoutesReduced, - "via-grant-v28/spearow/self-routes": selfRoutesReduced, - "via-grant-v28/squirtle/self-routes": selfRoutesReduced, - "via-grant-v29/blastoise/self-routes": selfRoutesReduced, - "via-grant-v29/fearow/self-routes": selfRoutesReduced, - "via-grant-v29/spearow/self-routes": selfRoutesReduced, - "via-grant-v29/squirtle/self-routes": selfRoutesReduced, - "via-grant-v30/blastoise/self-routes": selfRoutesReduced, - "via-grant-v30/fearow/self-routes": selfRoutesReduced, - "via-grant-v30/spearow/self-routes": selfRoutesReduced, - "via-grant-v30/squirtle/self-routes": selfRoutesReduced, - "via-grant-v31/blastoise/self-routes": selfRoutesReduced, - "via-grant-v31/fearow/self-routes": selfRoutesReduced, - "via-grant-v31/spearow/self-routes": selfRoutesReduced, - "via-grant-v31/squirtle/self-routes": selfRoutesReduced, - "via-grant-v32/blastoise/self-routes": selfRoutesReduced, - "via-grant-v32/fearow/self-routes": selfRoutesReduced, - "via-grant-v32/spearow/self-routes": selfRoutesReduced, - "via-grant-v32/squirtle/self-routes": selfRoutesReduced, - "via-grant-v33/blastoise/self-routes": selfRoutesReduced, - "via-grant-v33/fearow/self-routes": selfRoutesReduced, - "via-grant-v33/spearow/self-routes": selfRoutesReduced, - "via-grant-v33/squirtle/self-routes": selfRoutesReduced, - "via-grant-v34/blastoise/self-routes": selfRoutesReduced, - "via-grant-v34/fearow/self-routes": selfRoutesReduced, - "via-grant-v34/spearow/self-routes": selfRoutesReduced, - "via-grant-v34/squirtle/self-routes": selfRoutesReduced, - "via-grant-v35/blastoise/self-routes": selfRoutesReduced, - "via-grant-v35/fearow/self-routes": selfRoutesReduced, - "via-grant-v35/spearow/self-routes": selfRoutesReduced, - "via-grant-v35/squirtle/self-routes": selfRoutesReduced, - "via-grant-v36/blastoise/self-routes": selfRoutesReduced, - "via-grant-v36/fearow/self-routes": selfRoutesReduced, - "via-grant-v36/spearow/self-routes": selfRoutesReduced, - "via-grant-v36/squirtle/self-routes": selfRoutesReduced, - "via-grant-v47/blastoise/self-routes": selfRoutesReduced, - "via-grant-v47/fearow/self-routes": selfRoutesReduced, - "via-grant-v47/spearow/self-routes": selfRoutesReduced, - "via-grant-v47/squirtle/self-routes": selfRoutesReduced, - "via-grant-v48/blastoise/self-routes": selfRoutesReduced, - "via-grant-v48/fearow/self-routes": selfRoutesReduced, - "via-grant-v48/spearow/self-routes": selfRoutesReduced, - "via-grant-v48/squirtle/self-routes": selfRoutesReduced, - "via-grant-v49/blastoise/self-routes": selfRoutesReduced, - "via-grant-v49/fearow/self-routes": selfRoutesReduced, - "via-grant-v49/spearow/self-routes": selfRoutesReduced, - "via-grant-v49/squirtle/self-routes": selfRoutesReduced, - "via-grant-v50/blastoise/self-routes": selfRoutesReduced, - "via-grant-v50/fearow/self-routes": selfRoutesReduced, - "via-grant-v50/spearow/self-routes": selfRoutesReduced, - "via-grant-v50/squirtle/self-routes": selfRoutesReduced, - "via-grant-v63/blastoise/self-routes": selfRoutesReduced, - "via-grant-v63/fearow/self-routes": selfRoutesReduced, - "via-grant-v63/spearow/self-routes": selfRoutesReduced, - "via-grant-v63/squirtle/self-routes": selfRoutesReduced, - "via-grant-v66/blastoise/self-routes": selfRoutesReduced, - "via-grant-v66/squirtle/self-routes": selfRoutesReduced, - "via-grant-v67/blastoise/self-routes": selfRoutesReduced, - "via-grant-v67/squirtle/self-routes": selfRoutesReduced, - "routes-m1-wildcard-src-multi-range/srv/peer-allowed-ips": exitRoutesOnOverlap, "routes-m6-subnet-to-subnet-multi-route/r-multi/peer-allowed-ips": exitRoutesOnOverlap, @@ -257,8 +124,6 @@ const exitRoutesOnOverlap = "EXIT_ROUTES_ON_OVERLAP: headscale offers a peer's e const sameUserPeers = "SAME_USER_PEERS: SaaS lists a user's own devices as each other's peers with no rule linking them; headscale peers only what the policy links" -const selfRoutesReduced = "SELF_ROUTES_REDUCED: headscale sends a router only the primary routes its policy lets it reach" - // TestRoutesMapCompat replays routesMapCompatTests through // [runViaMapCompat]. func TestRoutesMapCompat(t *testing.T) {