mirror of
https://github.com/juanfont/headscale.git
synced 2025-10-19 11:15:48 +02:00
170 lines
3.4 KiB
Go
170 lines
3.4 KiB
Go
package derp
|
|
|
|
import (
|
|
"cmp"
|
|
"context"
|
|
"encoding/json"
|
|
"hash/crc64"
|
|
"io"
|
|
"maps"
|
|
"math/rand"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"reflect"
|
|
"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) {
|
|
derpFile, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer derpFile.Close()
|
|
var derpMap tailcfg.DERPMap
|
|
b, err := io.ReadAll(derpFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
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 DERPMaps into a single
|
|
// DERPMap, it will _only_ look at the Regions, an integer.
|
|
// If a region exists in two of the given DERPMaps, the region
|
|
// form the _last_ DERPMap will be preserved.
|
|
// An empty DERPMap list will result in a DERPMap with no regions.
|
|
func mergeDERPMaps(derpMaps []*tailcfg.DERPMap) *tailcfg.DERPMap {
|
|
result := tailcfg.DERPMap{
|
|
OmitDefaultRegions: false,
|
|
Regions: map[int]*tailcfg.DERPRegion{},
|
|
}
|
|
|
|
for _, derpMap := range derpMaps {
|
|
maps.Copy(result.Regions, derpMap.Regions)
|
|
}
|
|
|
|
for id, region := range result.Regions {
|
|
if region == nil {
|
|
delete(result.Regions, id)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
for id, region := range dm.Regions {
|
|
if len(region.Nodes) == 0 {
|
|
continue
|
|
}
|
|
|
|
dm.Regions[id] = shuffleRegionNoClone(region)
|
|
}
|
|
}
|
|
|
|
var crc64Table = crc64.MakeTable(crc64.ISO)
|
|
|
|
var (
|
|
derpRandomOnce sync.Once
|
|
derpRandomInst *rand.Rand
|
|
derpRandomMu sync.Mutex
|
|
)
|
|
|
|
func derpRandom() *rand.Rand {
|
|
derpRandomMu.Lock()
|
|
defer derpRandomMu.Unlock()
|
|
|
|
derpRandomOnce.Do(func() {
|
|
seed := cmp.Or(viper.GetString("dns.base_domain"), time.Now().String())
|
|
rnd := rand.New(rand.NewSource(0))
|
|
rnd.Seed(int64(crc64.Checksum([]byte(seed), crc64Table)))
|
|
derpRandomInst = rnd
|
|
})
|
|
return derpRandomInst
|
|
}
|
|
|
|
func resetDerpRandomForTesting() {
|
|
derpRandomMu.Lock()
|
|
defer derpRandomMu.Unlock()
|
|
derpRandomOnce = sync.Once{}
|
|
derpRandomInst = nil
|
|
}
|
|
|
|
func shuffleRegionNoClone(r *tailcfg.DERPRegion) *tailcfg.DERPRegion {
|
|
derpRandom().Shuffle(len(r.Nodes), reflect.Swapper(r.Nodes))
|
|
return r
|
|
}
|