Files
headscale/hscontrol/derp/derp.go
T
Simon Law 3aece65014 build: update tailscale.com to v1.103.0-pre.0.20260819232550-0e84b4a3a0a0
PR tailscale/tailscale#20646 changed the representation of DERP region
IDs from plain ints to a tailcfg.DERPRegionID type. This patch
requires that version of the tailscale.com library and updates our
code to also use the new type.

Updates: tailscale/tailscale#20165
(cherry picked from commit 79695ab51b)
2026-08-25 21:59:00 +02:00

174 lines
3.8 KiB
Go

package derp
import (
"cmp"
"context"
"encoding/json"
"hash/crc64"
"io"
"math/rand"
"net/http"
"net/url"
"os"
"reflect"
"slices"
"sync"
"time"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/spf13/viper"
"gopkg.in/yaml.v3"
"tailscale.com/tailcfg"
)
func loadDERPMapFromPath(path string) (*tailcfg.DERPMap, error) {
b, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var derpMap tailcfg.DERPMap
err = yaml.Unmarshal(b, &derpMap)
return &derpMap, err
}
func loadDERPMapFromURL(addr url.URL) (*tailcfg.DERPMap, error) {
ctx, cancel := context.WithTimeout(context.Background(), types.HTTPTimeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, addr.String(), nil)
if err != nil {
return nil, err
}
client := http.Client{
Timeout: types.HTTPTimeout,
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var derpMap tailcfg.DERPMap
err = json.Unmarshal(body, &derpMap)
return &derpMap, err
}
// mergeDERPMaps naively merges a list of [tailcfg.DERPMap] values into a single
// [tailcfg.DERPMap], it will _only_ look at the Regions, an integer.
// If a region exists in two of the given [tailcfg.DERPMap] values, the region
// form the _last_ [tailcfg.DERPMap] will be preserved.
// An empty [tailcfg.DERPMap] list will result in a [tailcfg.DERPMap] with no regions.
func mergeDERPMaps(derpMaps []*tailcfg.DERPMap) *tailcfg.DERPMap {
result := tailcfg.DERPMap{
OmitDefaultRegions: false,
Regions: map[tailcfg.DERPRegionID]*tailcfg.DERPRegion{},
}
for _, derpMap := range derpMaps {
// Clone each region: copying the pointer would let a later in-place
// 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
}
}
}
return &result
}
func GetDERPMap(cfg types.DERPConfig) (*tailcfg.DERPMap, error) {
var derpMaps []*tailcfg.DERPMap
if cfg.DERPMap != nil {
derpMaps = append(derpMaps, cfg.DERPMap)
}
for _, addr := range cfg.URLs {
derpMap, err := loadDERPMapFromURL(addr)
if err != nil {
return nil, err
}
derpMaps = append(derpMaps, derpMap)
}
for _, path := range cfg.Paths {
derpMap, err := loadDERPMapFromPath(path)
if err != nil {
return nil, err
}
derpMaps = append(derpMaps, derpMap)
}
derpMap := mergeDERPMaps(derpMaps)
shuffleDERPMap(derpMap)
return derpMap, nil
}
func shuffleDERPMap(dm *tailcfg.DERPMap) {
if dm == nil || len(dm.Regions) == 0 {
return
}
// Collect region IDs and sort them to ensure deterministic iteration order.
// Map iteration order is non-deterministic in Go, which would cause the
// shuffle to be non-deterministic even with a fixed seed.
ids := make([]tailcfg.DERPRegionID, 0, len(dm.Regions))
for id := range dm.Regions {
ids = append(ids, id)
}
slices.Sort(ids)
for _, id := range ids {
region := dm.Regions[id]
if len(region.Nodes) == 0 {
continue
}
derpRandom().Shuffle(len(region.Nodes), reflect.Swapper(region.Nodes))
}
}
var crc64Table = crc64.MakeTable(crc64.ISO)
var (
derpRandomInst *rand.Rand
derpRandomMu sync.Mutex
)
func derpRandom() *rand.Rand {
derpRandomMu.Lock()
defer derpRandomMu.Unlock()
if derpRandomInst == nil {
seed := cmp.Or(viper.GetString("dns.base_domain"), time.Now().String())
derpRandomInst = rand.New(rand.NewSource(int64(crc64.Checksum([]byte(seed), crc64Table)))) //nolint:gosec // weak random is fine for DERP scrambling
}
return derpRandomInst
}
func resetDerpRandomForTesting() {
derpRandomMu.Lock()
defer derpRandomMu.Unlock()
derpRandomInst = nil
}