derp/server: resolve the live DERP map in /bootstrap-dns

The handler captured the startup map, so hostnames added by
derp.auto_update never resolved.
This commit is contained in:
Kristoffer Dalby
2026-09-30 15:27:52 +00:00
committed by Kristoffer Dalby
parent 57358b7430
commit 84cf38ce24
3 changed files with 33 additions and 3 deletions
+1 -1
View File
@@ -501,7 +501,7 @@ func (h *Headscale) createRouter(apiV1Mux, apiV2Mux http.Handler) *chi.Mux {
r.HandleFunc("/derp", h.DERPServer.DERPHandler)
r.HandleFunc("/derp/probe", derpServer.DERPProbeHandler)
r.HandleFunc("/derp/latency-check", derpServer.DERPProbeHandler)
r.HandleFunc("/bootstrap-dns", derpServer.DERPBootstrapDNSHandler(h.state.DERPMap()))
r.HandleFunc("/bootstrap-dns", derpServer.DERPBootstrapDNSHandler(h.state.DERPMap))
}
// Auth is enforced inside each Huma mux per-operation, so the whole API
+3 -2
View File
@@ -334,8 +334,9 @@ func DERPProbeHandler(
// They have a cache, but not clear if that is really necessary at Headscale, uh, scale.
// An example implementation is found here https://derp.tailscale.com/bootstrap-dns
// Coordination server is included automatically, since local DERP is using the same DNS Name in d.serverURL.
// derpMap is called per request so DERP map updates are served.
func DERPBootstrapDNSHandler(
derpMap tailcfg.DERPMapView,
derpMap func() tailcfg.DERPMapView,
) func(http.ResponseWriter, *http.Request) {
return func(
writer http.ResponseWriter,
@@ -348,7 +349,7 @@ func DERPBootstrapDNSHandler(
var resolver net.Resolver
for _, region := range derpMap.Regions().All() { //nolint:unqueryvet // not SQLBoiler, tailcfg iterator
for _, region := range derpMap().Regions().All() { //nolint:unqueryvet // not SQLBoiler, tailcfg iterator
for _, node := range region.Nodes().All() { //nolint:unqueryvet // not SQLBoiler, tailcfg iterator
addrs, err := resolver.LookupIP(resolvCtx, "ip", node.HostName())
if err != nil {
+29
View File
@@ -1,12 +1,18 @@
package server
import (
"encoding/json"
"net"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"tailscale.com/envknob"
"tailscale.com/tailcfg"
)
// TestGenerateRegionInsecureTLS pins the contract nix/testkit.nix relies on:
@@ -58,3 +64,26 @@ func TestGenerateRegionInsecureTLS(t *testing.T) {
})
}
}
// TestDERPBootstrapDNSHandlerFollowsDERPMapUpdates guards against the handler
// resolving a DERP map captured at startup: hostnames that a later
// auto-update adds must be served.
func TestDERPBootstrapDNSHandlerFollowsDERPMapUpdates(t *testing.T) {
var current atomic.Pointer[tailcfg.DERPMap]
current.Store(&tailcfg.DERPMap{})
handler := DERPBootstrapDNSHandler(func() tailcfg.DERPMapView {
return current.Load().View()
})
current.Store(&tailcfg.DERPMap{Regions: map[tailcfg.DERPRegionID]*tailcfg.DERPRegion{
1: {RegionID: 1, Nodes: []*tailcfg.DERPNode{{Name: "1a", RegionID: 1, HostName: "localhost"}}},
}})
rec := httptest.NewRecorder()
handler(rec, httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/bootstrap-dns", nil))
var got map[string][]net.IP
require.NoError(t, json.NewDecoder(rec.Body).Decode(&got))
assert.Contains(t, got, "localhost")
}