mapper: send a router all of its own routes

Its AllowedIPs held only the primaries the policy let it reach. It now
holds every approved route, and PrimaryRoutes the ones it serves.

Fixes #3626
This commit is contained in:
Kristoffer Dalby committed 2026-11-02 17:46:52 +00:00
1 parent a49f9122f7
commit e0c9ff425e
4 files changed
+19 -204

No files matched your search

+11 -8
View File
@@ -8,7 +8,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"
@@ -88,15 +87,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()),
@@ -106,6 +102,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
@@ -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,
+8 -48
View File
@@ -373,50 +373,7 @@ var routeOverlapCompatTests = []string{
// routeOverlapSkipReasons lists checks where headscale differs from SaaS,
// keyed <test_id>/<node>/<check>. 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)
-135
View File
@@ -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) {