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)) +}