From 90732bdaaf47f601742539f7d6ce63d1c3133a55 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Mon, 28 Sep 2026 13:31:11 +0000 Subject: [PATCH] derp: drop regions set to null in derp.paths 19d5d9de cloned regions while merging and skipped nil ones, so the documented null-removal recipe silently kept the region. --- CHANGELOG.md | 1 + hscontrol/derp/derp.go | 9 +++++++-- hscontrol/derp/derp_merge_test.go | 17 +++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 644ffba7..2e4bb433 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,7 @@ removed on this schedule: - Fix `headscale users destroy`/`rename` reporting "multiple users match query" when no user matches; an ambiguous match now lists the matching users [#3476](https://github.com/juanfont/headscale/pull/3476) - Deleting a user that still owns nodes now lists the nodes (ID and hostname) that must be deleted first [#3475](https://github.com/juanfont/headscale/pull/3475) - Headscale now requires Go 1.27 to build +- A `derp.paths` region set to `null` removes that region again, as documented ## 0.29.4 (2026-09-23) diff --git a/hscontrol/derp/derp.go b/hscontrol/derp/derp.go index c7c2c5ad..03b67171 100644 --- a/hscontrol/derp/derp.go +++ b/hscontrol/derp/derp.go @@ -82,9 +82,14 @@ func mergeDERPMaps(derpMaps []*tailcfg.DERPMap) *tailcfg.DERPMap { // shuffle alias regions shared with the source map or a previously // served map, racing concurrent readers. for id, region := range derpMap.Regions { - if cloned := region.Clone(); cloned != nil { - result.Regions[id] = cloned + // A null region removes one an earlier map added, the documented + // way to drop a region from derp.urls via derp.paths. + if region == nil { + delete(result.Regions, id) + continue } + + result.Regions[id] = region.Clone() } } diff --git a/hscontrol/derp/derp_merge_test.go b/hscontrol/derp/derp_merge_test.go index 330bcf59..804c7f7a 100644 --- a/hscontrol/derp/derp_merge_test.go +++ b/hscontrol/derp/derp_merge_test.go @@ -26,3 +26,20 @@ func TestMergeDERPMapsClonesRegions(t *testing.T) { assert.Equal(t, "a", src.Regions[1].Nodes[0].Name, "source region was mutated through a shared pointer") } + +// TestMergeDERPMapsNullRemovesRegion pins docs/ref/derp.md's recipe: a later +// map setting a region to null drops it from the result. +func TestMergeDERPMapsNullRemovesRegion(t *testing.T) { + base := &tailcfg.DERPMap{ + Regions: map[tailcfg.DERPRegionID]*tailcfg.DERPRegion{ + 1: {RegionID: 1, RegionCode: "nyc"}, + 2: {RegionID: 2, RegionCode: "sfo"}, + }, + } + drop := &tailcfg.DERPMap{Regions: map[tailcfg.DERPRegionID]*tailcfg.DERPRegion{1: nil}} + + merged := mergeDERPMaps([]*tailcfg.DERPMap{base, drop}) + + assert.NotContains(t, merged.Regions, tailcfg.DERPRegionID(1)) + assert.Contains(t, merged.Regions, tailcfg.DERPRegionID(2)) +}