mirror of
https://github.com/juanfont/headscale.git
synced 2025-08-24 13:46:53 +02:00
derp: increase update frequency and harden on failures (#2741)
This commit is contained in:
parent
51c6367bb1
commit
b87567628a
15
CHANGELOG.md
15
CHANGELOG.md
@ -26,6 +26,7 @@ Please read the [PR description](https://github.com/juanfont/headscale/pull/2617
|
|||||||
for more technical details about the issues and solutions.
|
for more technical details about the issues and solutions.
|
||||||
|
|
||||||
**SQLite Database Backup Example:**
|
**SQLite Database Backup Example:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Stop headscale
|
# Stop headscale
|
||||||
systemctl stop headscale
|
systemctl stop headscale
|
||||||
@ -41,6 +42,13 @@ cp /var/lib/headscale/db.sqlite-shm /var/lib/headscale/db.sqlite-shm.backup
|
|||||||
systemctl start headscale
|
systemctl start headscale
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### DERPMap update frequency
|
||||||
|
|
||||||
|
The default DERPMap update frequency has been changed from 24 hours to 3 hours.
|
||||||
|
If you set the `derp.update_frequency` configuration option, it is recommended to change
|
||||||
|
it to `3h` to ensure that the headscale instance gets the latest DERPMap updates when
|
||||||
|
upstream is changed.
|
||||||
|
|
||||||
### BREAKING
|
### BREAKING
|
||||||
|
|
||||||
- Remove support for 32-bit binaries
|
- Remove support for 32-bit binaries
|
||||||
@ -55,6 +63,11 @@ systemctl start headscale
|
|||||||
- **IMPORTANT: Backup your SQLite database before upgrading**
|
- **IMPORTANT: Backup your SQLite database before upgrading**
|
||||||
- Introduces safer table renaming migration strategy
|
- Introduces safer table renaming migration strategy
|
||||||
- Addresses longstanding database integrity issues
|
- Addresses longstanding database integrity issues
|
||||||
|
- DERPmap update frequency default changed from 24h to 3h
|
||||||
|
[#2741](https://github.com/juanfont/headscale/pull/2741)
|
||||||
|
- DERPmap update mechanism has been improved with retry,
|
||||||
|
and is now failing conservatively, preserving the old map upon failure.
|
||||||
|
[#2741](https://github.com/juanfont/headscale/pull/2741)
|
||||||
- Add support for `autogroup:member`, `autogroup:tagged`
|
- Add support for `autogroup:member`, `autogroup:tagged`
|
||||||
[#2572](https://github.com/juanfont/headscale/pull/2572)
|
[#2572](https://github.com/juanfont/headscale/pull/2572)
|
||||||
- Remove policy v1 code [#2600](https://github.com/juanfont/headscale/pull/2600)
|
- Remove policy v1 code [#2600](https://github.com/juanfont/headscale/pull/2600)
|
||||||
@ -72,7 +85,7 @@ systemctl start headscale
|
|||||||
[#2643](https://github.com/juanfont/headscale/pull/2643)
|
[#2643](https://github.com/juanfont/headscale/pull/2643)
|
||||||
- OIDC: Use group claim from UserInfo
|
- OIDC: Use group claim from UserInfo
|
||||||
[#2663](https://github.com/juanfont/headscale/pull/2663)
|
[#2663](https://github.com/juanfont/headscale/pull/2663)
|
||||||
- OIDC: Update user with claims from UserInfo *before* comparing with allowed
|
- OIDC: Update user with claims from UserInfo _before_ comparing with allowed
|
||||||
groups, email and domain [#2663](https://github.com/juanfont/headscale/pull/2663)
|
groups, email and domain [#2663](https://github.com/juanfont/headscale/pull/2663)
|
||||||
|
|
||||||
## 0.26.1 (2025-06-06)
|
## 0.26.1 (2025-06-06)
|
||||||
|
@ -128,7 +128,7 @@ derp:
|
|||||||
auto_update_enabled: true
|
auto_update_enabled: true
|
||||||
|
|
||||||
# How often should we check for DERP updates?
|
# How often should we check for DERP updates?
|
||||||
update_frequency: 24h
|
update_frequency: 3h
|
||||||
|
|
||||||
# Disables the automatic check for headscale updates on startup
|
# Disables the automatic check for headscale updates on startup
|
||||||
disable_check_updates: false
|
disable_check_updates: false
|
||||||
@ -293,8 +293,7 @@ dns:
|
|||||||
|
|
||||||
# Split DNS (see https://tailscale.com/kb/1054/dns/),
|
# Split DNS (see https://tailscale.com/kb/1054/dns/),
|
||||||
# a map of domains and which DNS server to use for each.
|
# a map of domains and which DNS server to use for each.
|
||||||
split:
|
split: {}
|
||||||
{}
|
|
||||||
# foo.bar.com:
|
# foo.bar.com:
|
||||||
# - 1.1.1.1
|
# - 1.1.1.1
|
||||||
# darp.headscale.net:
|
# darp.headscale.net:
|
||||||
|
@ -17,6 +17,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/cenkalti/backoff/v5"
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
grpcRuntime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
grpcRuntime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||||
@ -284,12 +285,24 @@ func (h *Headscale) scheduledTasks(ctx context.Context) {
|
|||||||
|
|
||||||
case <-derpTickerChan:
|
case <-derpTickerChan:
|
||||||
log.Info().Msg("Fetching DERPMap updates")
|
log.Info().Msg("Fetching DERPMap updates")
|
||||||
derpMap := derp.GetDERPMap(h.cfg.DERP)
|
derpMap, err := backoff.Retry(ctx, func() (*tailcfg.DERPMap, error) {
|
||||||
|
derpMap, err := derp.GetDERPMap(h.cfg.DERP)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if h.cfg.DERP.ServerEnabled && h.cfg.DERP.AutomaticallyAddEmbeddedDerpRegion {
|
if h.cfg.DERP.ServerEnabled && h.cfg.DERP.AutomaticallyAddEmbeddedDerpRegion {
|
||||||
region, _ := h.DERPServer.GenerateRegion()
|
region, _ := h.DERPServer.GenerateRegion()
|
||||||
derpMap.Regions[region.RegionID] = ®ion
|
derpMap.Regions[region.RegionID] = ®ion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return derpMap, nil
|
||||||
|
}, backoff.WithBackOff(backoff.NewExponentialBackOff()))
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("failed to build new DERPMap, retrying later")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
h.state.SetDERPMap(derpMap)
|
||||||
|
|
||||||
h.Change(change.DERPSet)
|
h.Change(change.DERPSet)
|
||||||
|
|
||||||
case records, ok := <-extraRecordsUpdate:
|
case records, ok := <-extraRecordsUpdate:
|
||||||
@ -516,29 +529,31 @@ func (h *Headscale) Serve() error {
|
|||||||
h.mapBatcher.Start()
|
h.mapBatcher.Start()
|
||||||
defer h.mapBatcher.Close()
|
defer h.mapBatcher.Close()
|
||||||
|
|
||||||
// TODO(kradalby): fix state part.
|
|
||||||
if h.cfg.DERP.ServerEnabled {
|
if h.cfg.DERP.ServerEnabled {
|
||||||
// When embedded DERP is enabled we always need a STUN server
|
// When embedded DERP is enabled we always need a STUN server
|
||||||
if h.cfg.DERP.STUNAddr == "" {
|
if h.cfg.DERP.STUNAddr == "" {
|
||||||
return errSTUNAddressNotSet
|
return errSTUNAddressNotSet
|
||||||
}
|
}
|
||||||
|
|
||||||
region, err := h.DERPServer.GenerateRegion()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("generating DERP region for embedded server: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.cfg.DERP.AutomaticallyAddEmbeddedDerpRegion {
|
|
||||||
h.state.DERPMap().Regions[region.RegionID] = ®ion
|
|
||||||
}
|
|
||||||
|
|
||||||
go h.DERPServer.ServeSTUN()
|
go h.DERPServer.ServeSTUN()
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(h.state.DERPMap().Regions) == 0 {
|
derpMap, err := derp.GetDERPMap(h.cfg.DERP)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get DERPMap: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.cfg.DERP.ServerEnabled && h.cfg.DERP.AutomaticallyAddEmbeddedDerpRegion {
|
||||||
|
region, _ := h.DERPServer.GenerateRegion()
|
||||||
|
derpMap.Regions[region.RegionID] = ®ion
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(derpMap.Regions) == 0 {
|
||||||
return errEmptyInitialDERPMap
|
return errEmptyInitialDERPMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.state.SetDERPMap(derpMap)
|
||||||
|
|
||||||
// Start ephemeral node garbage collector and schedule all nodes
|
// Start ephemeral node garbage collector and schedule all nodes
|
||||||
// that are already in the database and ephemeral. If they are still
|
// that are already in the database and ephemeral. If they are still
|
||||||
// around between restarts, they will reconnect and the GC will
|
// around between restarts, they will reconnect and the GC will
|
||||||
|
@ -1,16 +1,22 @@
|
|||||||
package derp
|
package derp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"hash/crc64"
|
||||||
"io"
|
"io"
|
||||||
"maps"
|
"maps"
|
||||||
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/juanfont/headscale/hscontrol/types"
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/spf13/viper"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
)
|
)
|
||||||
@ -79,26 +85,16 @@ func mergeDERPMaps(derpMaps []*tailcfg.DERPMap) *tailcfg.DERPMap {
|
|||||||
return &result
|
return &result
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetDERPMap(cfg types.DERPConfig) *tailcfg.DERPMap {
|
func GetDERPMap(cfg types.DERPConfig) (*tailcfg.DERPMap, error) {
|
||||||
var derpMaps []*tailcfg.DERPMap
|
var derpMaps []*tailcfg.DERPMap
|
||||||
if cfg.DERPMap != nil {
|
if cfg.DERPMap != nil {
|
||||||
derpMaps = append(derpMaps, cfg.DERPMap)
|
derpMaps = append(derpMaps, cfg.DERPMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, path := range cfg.Paths {
|
for _, path := range cfg.Paths {
|
||||||
log.Debug().
|
|
||||||
Str("func", "GetDERPMap").
|
|
||||||
Str("path", path).
|
|
||||||
Msg("Loading DERPMap from path")
|
|
||||||
derpMap, err := loadDERPMapFromPath(path)
|
derpMap, err := loadDERPMapFromPath(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
return nil, err
|
||||||
Str("func", "GetDERPMap").
|
|
||||||
Str("path", path).
|
|
||||||
Err(err).
|
|
||||||
Msg("Could not load DERP map from path")
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
derpMaps = append(derpMaps, derpMap)
|
derpMaps = append(derpMaps, derpMap)
|
||||||
@ -106,26 +102,59 @@ func GetDERPMap(cfg types.DERPConfig) *tailcfg.DERPMap {
|
|||||||
|
|
||||||
for _, addr := range cfg.URLs {
|
for _, addr := range cfg.URLs {
|
||||||
derpMap, err := loadDERPMapFromURL(addr)
|
derpMap, err := loadDERPMapFromURL(addr)
|
||||||
log.Debug().
|
|
||||||
Str("func", "GetDERPMap").
|
|
||||||
Str("url", addr.String()).
|
|
||||||
Msg("Loading DERPMap from path")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
return nil, err
|
||||||
Str("func", "GetDERPMap").
|
|
||||||
Str("url", addr.String()).
|
|
||||||
Err(err).
|
|
||||||
Msg("Could not load DERP map from path")
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
derpMaps = append(derpMaps, derpMap)
|
derpMaps = append(derpMaps, derpMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
derpMap := mergeDERPMaps(derpMaps)
|
derpMap := mergeDERPMaps(derpMaps)
|
||||||
|
shuffleDERPMap(derpMap)
|
||||||
|
|
||||||
log.Trace().Interface("derpMap", derpMap).Msg("DERPMap loaded")
|
return derpMap, nil
|
||||||
|
}
|
||||||
return derpMap
|
|
||||||
|
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.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func derpRandom() *rand.Rand {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
284
hscontrol/derp/derp_test.go
Normal file
284
hscontrol/derp/derp_test.go
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
package derp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestShuffleDERPMapDeterministic(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
baseDomain string
|
||||||
|
derpMap *tailcfg.DERPMap
|
||||||
|
expected *tailcfg.DERPMap
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "single region with 4 nodes",
|
||||||
|
baseDomain: "test1.example.com",
|
||||||
|
derpMap: &tailcfg.DERPMap{
|
||||||
|
Regions: map[int]*tailcfg.DERPRegion{
|
||||||
|
1: {
|
||||||
|
RegionID: 1,
|
||||||
|
RegionCode: "nyc",
|
||||||
|
RegionName: "New York City",
|
||||||
|
Nodes: []*tailcfg.DERPNode{
|
||||||
|
{Name: "1f", RegionID: 1, HostName: "derp1f.tailscale.com"},
|
||||||
|
{Name: "1g", RegionID: 1, HostName: "derp1g.tailscale.com"},
|
||||||
|
{Name: "1h", RegionID: 1, HostName: "derp1h.tailscale.com"},
|
||||||
|
{Name: "1i", RegionID: 1, HostName: "derp1i.tailscale.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: &tailcfg.DERPMap{
|
||||||
|
Regions: map[int]*tailcfg.DERPRegion{
|
||||||
|
1: {
|
||||||
|
RegionID: 1,
|
||||||
|
RegionCode: "nyc",
|
||||||
|
RegionName: "New York City",
|
||||||
|
Nodes: []*tailcfg.DERPNode{
|
||||||
|
{Name: "1g", RegionID: 1, HostName: "derp1g.tailscale.com"},
|
||||||
|
{Name: "1f", RegionID: 1, HostName: "derp1f.tailscale.com"},
|
||||||
|
{Name: "1i", RegionID: 1, HostName: "derp1i.tailscale.com"},
|
||||||
|
{Name: "1h", RegionID: 1, HostName: "derp1h.tailscale.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple regions with nodes",
|
||||||
|
baseDomain: "test2.example.com",
|
||||||
|
derpMap: &tailcfg.DERPMap{
|
||||||
|
Regions: map[int]*tailcfg.DERPRegion{
|
||||||
|
10: {
|
||||||
|
RegionID: 10,
|
||||||
|
RegionCode: "sea",
|
||||||
|
RegionName: "Seattle",
|
||||||
|
Nodes: []*tailcfg.DERPNode{
|
||||||
|
{Name: "10b", RegionID: 10, HostName: "derp10b.tailscale.com"},
|
||||||
|
{Name: "10c", RegionID: 10, HostName: "derp10c.tailscale.com"},
|
||||||
|
{Name: "10d", RegionID: 10, HostName: "derp10d.tailscale.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
RegionID: 2,
|
||||||
|
RegionCode: "sfo",
|
||||||
|
RegionName: "San Francisco",
|
||||||
|
Nodes: []*tailcfg.DERPNode{
|
||||||
|
{Name: "2d", RegionID: 2, HostName: "derp2d.tailscale.com"},
|
||||||
|
{Name: "2e", RegionID: 2, HostName: "derp2e.tailscale.com"},
|
||||||
|
{Name: "2f", RegionID: 2, HostName: "derp2f.tailscale.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: &tailcfg.DERPMap{
|
||||||
|
Regions: map[int]*tailcfg.DERPRegion{
|
||||||
|
10: {
|
||||||
|
RegionID: 10,
|
||||||
|
RegionCode: "sea",
|
||||||
|
RegionName: "Seattle",
|
||||||
|
Nodes: []*tailcfg.DERPNode{
|
||||||
|
{Name: "10b", RegionID: 10, HostName: "derp10b.tailscale.com"},
|
||||||
|
{Name: "10c", RegionID: 10, HostName: "derp10c.tailscale.com"},
|
||||||
|
{Name: "10d", RegionID: 10, HostName: "derp10d.tailscale.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
RegionID: 2,
|
||||||
|
RegionCode: "sfo",
|
||||||
|
RegionName: "San Francisco",
|
||||||
|
Nodes: []*tailcfg.DERPNode{
|
||||||
|
{Name: "2f", RegionID: 2, HostName: "derp2f.tailscale.com"},
|
||||||
|
{Name: "2e", RegionID: 2, HostName: "derp2e.tailscale.com"},
|
||||||
|
{Name: "2d", RegionID: 2, HostName: "derp2d.tailscale.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "large region with many nodes",
|
||||||
|
baseDomain: "test3.example.com",
|
||||||
|
derpMap: &tailcfg.DERPMap{
|
||||||
|
Regions: map[int]*tailcfg.DERPRegion{
|
||||||
|
4: {
|
||||||
|
RegionID: 4,
|
||||||
|
RegionCode: "fra",
|
||||||
|
RegionName: "Frankfurt",
|
||||||
|
Nodes: []*tailcfg.DERPNode{
|
||||||
|
{Name: "4f", RegionID: 4, HostName: "derp4f.tailscale.com"},
|
||||||
|
{Name: "4g", RegionID: 4, HostName: "derp4g.tailscale.com"},
|
||||||
|
{Name: "4h", RegionID: 4, HostName: "derp4h.tailscale.com"},
|
||||||
|
{Name: "4i", RegionID: 4, HostName: "derp4i.tailscale.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: &tailcfg.DERPMap{
|
||||||
|
Regions: map[int]*tailcfg.DERPRegion{
|
||||||
|
4: {
|
||||||
|
RegionID: 4,
|
||||||
|
RegionCode: "fra",
|
||||||
|
RegionName: "Frankfurt",
|
||||||
|
Nodes: []*tailcfg.DERPNode{
|
||||||
|
{Name: "4f", RegionID: 4, HostName: "derp4f.tailscale.com"},
|
||||||
|
{Name: "4h", RegionID: 4, HostName: "derp4h.tailscale.com"},
|
||||||
|
{Name: "4g", RegionID: 4, HostName: "derp4g.tailscale.com"},
|
||||||
|
{Name: "4i", RegionID: 4, HostName: "derp4i.tailscale.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same region different base domain",
|
||||||
|
baseDomain: "different.example.com",
|
||||||
|
derpMap: &tailcfg.DERPMap{
|
||||||
|
Regions: map[int]*tailcfg.DERPRegion{
|
||||||
|
4: {
|
||||||
|
RegionID: 4,
|
||||||
|
RegionCode: "fra",
|
||||||
|
RegionName: "Frankfurt",
|
||||||
|
Nodes: []*tailcfg.DERPNode{
|
||||||
|
{Name: "4f", RegionID: 4, HostName: "derp4f.tailscale.com"},
|
||||||
|
{Name: "4g", RegionID: 4, HostName: "derp4g.tailscale.com"},
|
||||||
|
{Name: "4h", RegionID: 4, HostName: "derp4h.tailscale.com"},
|
||||||
|
{Name: "4i", RegionID: 4, HostName: "derp4i.tailscale.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: &tailcfg.DERPMap{
|
||||||
|
Regions: map[int]*tailcfg.DERPRegion{
|
||||||
|
4: {
|
||||||
|
RegionID: 4,
|
||||||
|
RegionCode: "fra",
|
||||||
|
RegionName: "Frankfurt",
|
||||||
|
Nodes: []*tailcfg.DERPNode{
|
||||||
|
{Name: "4g", RegionID: 4, HostName: "derp4g.tailscale.com"},
|
||||||
|
{Name: "4i", RegionID: 4, HostName: "derp4i.tailscale.com"},
|
||||||
|
{Name: "4f", RegionID: 4, HostName: "derp4f.tailscale.com"},
|
||||||
|
{Name: "4h", RegionID: 4, HostName: "derp4h.tailscale.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
viper.Set("dns.base_domain", tt.baseDomain)
|
||||||
|
defer viper.Reset()
|
||||||
|
resetDerpRandomForTesting()
|
||||||
|
|
||||||
|
testMap := tt.derpMap.View().AsStruct()
|
||||||
|
shuffleDERPMap(testMap)
|
||||||
|
|
||||||
|
if diff := cmp.Diff(tt.expected, testMap); diff != "" {
|
||||||
|
t.Errorf("Shuffled DERP map doesn't match expected (-expected +actual):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShuffleDERPMapEdgeCases(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
derpMap *tailcfg.DERPMap
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil derp map",
|
||||||
|
derpMap: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty derp map",
|
||||||
|
derpMap: &tailcfg.DERPMap{
|
||||||
|
Regions: map[int]*tailcfg.DERPRegion{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "region with no nodes",
|
||||||
|
derpMap: &tailcfg.DERPMap{
|
||||||
|
Regions: map[int]*tailcfg.DERPRegion{
|
||||||
|
1: {
|
||||||
|
RegionID: 1,
|
||||||
|
RegionCode: "empty",
|
||||||
|
RegionName: "Empty Region",
|
||||||
|
Nodes: []*tailcfg.DERPNode{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "region with single node",
|
||||||
|
derpMap: &tailcfg.DERPMap{
|
||||||
|
Regions: map[int]*tailcfg.DERPRegion{
|
||||||
|
1: {
|
||||||
|
RegionID: 1,
|
||||||
|
RegionCode: "single",
|
||||||
|
RegionName: "Single Node Region",
|
||||||
|
Nodes: []*tailcfg.DERPNode{
|
||||||
|
{Name: "1a", RegionID: 1, HostName: "derp1a.tailscale.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
shuffleDERPMap(tt.derpMap)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func TestShuffleDERPMapWithoutBaseDomain(t *testing.T) {
|
||||||
|
viper.Reset()
|
||||||
|
resetDerpRandomForTesting()
|
||||||
|
|
||||||
|
derpMap := &tailcfg.DERPMap{
|
||||||
|
Regions: map[int]*tailcfg.DERPRegion{
|
||||||
|
1: {
|
||||||
|
RegionID: 1,
|
||||||
|
RegionCode: "test",
|
||||||
|
RegionName: "Test Region",
|
||||||
|
Nodes: []*tailcfg.DERPNode{
|
||||||
|
{Name: "1a", RegionID: 1, HostName: "derp1a.test.com"},
|
||||||
|
{Name: "1b", RegionID: 1, HostName: "derp1b.test.com"},
|
||||||
|
{Name: "1c", RegionID: 1, HostName: "derp1c.test.com"},
|
||||||
|
{Name: "1d", RegionID: 1, HostName: "derp1d.test.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
original := derpMap.View().AsStruct()
|
||||||
|
shuffleDERPMap(derpMap)
|
||||||
|
|
||||||
|
if len(derpMap.Regions) != 1 || len(derpMap.Regions[1].Nodes) != 4 {
|
||||||
|
t.Error("Shuffle corrupted DERP map structure")
|
||||||
|
}
|
||||||
|
|
||||||
|
originalNodes := make(map[string]bool)
|
||||||
|
for _, node := range original.Regions[1].Nodes {
|
||||||
|
originalNodes[node.Name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
shuffledNodes := make(map[string]bool)
|
||||||
|
for _, node := range derpMap.Regions[1].Nodes {
|
||||||
|
shuffledNodes[node.Name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(originalNodes, shuffledNodes); diff != "" {
|
||||||
|
t.Errorf("Shuffle changed node set (-original +shuffled):\n%s", diff)
|
||||||
|
}
|
||||||
|
}
|
@ -276,7 +276,7 @@ func DERPProbeHandler(
|
|||||||
// An example implementation is found here https://derp.tailscale.com/bootstrap-dns
|
// 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.
|
// Coordination server is included automatically, since local DERP is using the same DNS Name in d.serverURL.
|
||||||
func DERPBootstrapDNSHandler(
|
func DERPBootstrapDNSHandler(
|
||||||
derpMap *tailcfg.DERPMap,
|
derpMap tailcfg.DERPMapView,
|
||||||
) func(http.ResponseWriter, *http.Request) {
|
) func(http.ResponseWriter, *http.Request) {
|
||||||
return func(
|
return func(
|
||||||
writer http.ResponseWriter,
|
writer http.ResponseWriter,
|
||||||
@ -287,18 +287,18 @@ func DERPBootstrapDNSHandler(
|
|||||||
resolvCtx, cancel := context.WithTimeout(req.Context(), time.Minute)
|
resolvCtx, cancel := context.WithTimeout(req.Context(), time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
var resolver net.Resolver
|
var resolver net.Resolver
|
||||||
for _, region := range derpMap.Regions {
|
for _, region := range derpMap.Regions().All() {
|
||||||
for _, node := range region.Nodes { // we don't care if we override some nodes
|
for _, node := range region.Nodes().All() { // we don't care if we override some nodes
|
||||||
addrs, err := resolver.LookupIP(resolvCtx, "ip", node.HostName)
|
addrs, err := resolver.LookupIP(resolvCtx, "ip", node.HostName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Trace().
|
log.Trace().
|
||||||
Caller().
|
Caller().
|
||||||
Err(err).
|
Err(err).
|
||||||
Msgf("bootstrap DNS lookup failed %q", node.HostName)
|
Msgf("bootstrap DNS lookup failed %q", node.HostName())
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
dnsEntries[node.HostName] = addrs
|
dnsEntries[node.HostName()] = addrs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
writer.Header().Set("Content-Type", "application/json")
|
writer.Header().Set("Content-Type", "application/json")
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/juanfont/headscale/hscontrol/db"
|
"github.com/juanfont/headscale/hscontrol/db"
|
||||||
|
"github.com/juanfont/headscale/hscontrol/derp"
|
||||||
"github.com/juanfont/headscale/hscontrol/state"
|
"github.com/juanfont/headscale/hscontrol/state"
|
||||||
"github.com/juanfont/headscale/hscontrol/types"
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
"github.com/juanfont/headscale/hscontrol/types/change"
|
"github.com/juanfont/headscale/hscontrol/types/change"
|
||||||
@ -167,6 +168,12 @@ func setupBatcherWithTestData(t *testing.T, bf batcherFunc, userCount, nodesPerU
|
|||||||
t.Fatalf("Failed to create state: %v", err)
|
t.Fatalf("Failed to create state: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
derpMap, err := derp.GetDERPMap(cfg.DERP)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, derpMap)
|
||||||
|
|
||||||
|
state.SetDERPMap(derpMap)
|
||||||
|
|
||||||
// Set up a permissive policy that allows all communication for testing
|
// Set up a permissive policy that allows all communication for testing
|
||||||
allowAllPolicy := `{
|
allowAllPolicy := `{
|
||||||
"acls": [
|
"acls": [
|
||||||
|
@ -79,7 +79,7 @@ func (b *MapResponseBuilder) WithSelfNode() *MapResponseBuilder {
|
|||||||
|
|
||||||
// WithDERPMap adds the DERP map to the response
|
// WithDERPMap adds the DERP map to the response
|
||||||
func (b *MapResponseBuilder) WithDERPMap() *MapResponseBuilder {
|
func (b *MapResponseBuilder) WithDERPMap() *MapResponseBuilder {
|
||||||
b.resp.DERPMap = b.mapper.state.DERPMap()
|
b.resp.DERPMap = b.mapper.state.DERPMap().AsStruct()
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,10 +9,10 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
hsdb "github.com/juanfont/headscale/hscontrol/db"
|
hsdb "github.com/juanfont/headscale/hscontrol/db"
|
||||||
"github.com/juanfont/headscale/hscontrol/derp"
|
|
||||||
"github.com/juanfont/headscale/hscontrol/policy"
|
"github.com/juanfont/headscale/hscontrol/policy"
|
||||||
"github.com/juanfont/headscale/hscontrol/policy/matcher"
|
"github.com/juanfont/headscale/hscontrol/policy/matcher"
|
||||||
"github.com/juanfont/headscale/hscontrol/routes"
|
"github.com/juanfont/headscale/hscontrol/routes"
|
||||||
@ -55,7 +55,7 @@ type State struct {
|
|||||||
// ipAlloc manages IP address allocation for nodes
|
// ipAlloc manages IP address allocation for nodes
|
||||||
ipAlloc *hsdb.IPAllocator
|
ipAlloc *hsdb.IPAllocator
|
||||||
// derpMap contains the current DERP relay configuration
|
// derpMap contains the current DERP relay configuration
|
||||||
derpMap *tailcfg.DERPMap
|
derpMap atomic.Pointer[tailcfg.DERPMap]
|
||||||
// polMan handles policy evaluation and management
|
// polMan handles policy evaluation and management
|
||||||
polMan policy.PolicyManager
|
polMan policy.PolicyManager
|
||||||
// registrationCache caches node registration data to reduce database load
|
// registrationCache caches node registration data to reduce database load
|
||||||
@ -86,8 +86,6 @@ func NewState(cfg *types.Config) (*State, error) {
|
|||||||
return nil, fmt.Errorf("init ip allocatior: %w", err)
|
return nil, fmt.Errorf("init ip allocatior: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
derpMap := derp.GetDERPMap(cfg.DERP)
|
|
||||||
|
|
||||||
nodes, err := db.ListNodes()
|
nodes, err := db.ListNodes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("loading nodes: %w", err)
|
return nil, fmt.Errorf("loading nodes: %w", err)
|
||||||
@ -107,17 +105,17 @@ func NewState(cfg *types.Config) (*State, error) {
|
|||||||
return nil, fmt.Errorf("init policy manager: %w", err)
|
return nil, fmt.Errorf("init policy manager: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &State{
|
s := &State{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
|
|
||||||
db: db,
|
db: db,
|
||||||
ipAlloc: ipAlloc,
|
ipAlloc: ipAlloc,
|
||||||
// TODO(kradalby): Update DERPMap
|
|
||||||
derpMap: derpMap,
|
|
||||||
polMan: polMan,
|
polMan: polMan,
|
||||||
registrationCache: registrationCache,
|
registrationCache: registrationCache,
|
||||||
primaryRoutes: routes.New(),
|
primaryRoutes: routes.New(),
|
||||||
}, nil
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close gracefully shuts down the State instance and releases all resources.
|
// Close gracefully shuts down the State instance and releases all resources.
|
||||||
@ -170,9 +168,14 @@ func policyBytes(db *hsdb.HSDatabase, cfg *types.Config) ([]byte, error) {
|
|||||||
return nil, fmt.Errorf("%w: %s", ErrUnsupportedPolicyMode, cfg.Policy.Mode)
|
return nil, fmt.Errorf("%w: %s", ErrUnsupportedPolicyMode, cfg.Policy.Mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetDERPMap updates the DERP relay configuration.
|
||||||
|
func (s *State) SetDERPMap(dm *tailcfg.DERPMap) {
|
||||||
|
s.derpMap.Store(dm)
|
||||||
|
}
|
||||||
|
|
||||||
// DERPMap returns the current DERP relay configuration for peer-to-peer connectivity.
|
// DERPMap returns the current DERP relay configuration for peer-to-peer connectivity.
|
||||||
func (s *State) DERPMap() *tailcfg.DERPMap {
|
func (s *State) DERPMap() tailcfg.DERPMapView {
|
||||||
return s.derpMap
|
return s.derpMap.Load().View()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReloadPolicy reloads the access control policy and triggers auto-approval if changed.
|
// ReloadPolicy reloads the access control policy and triggers auto-approval if changed.
|
||||||
@ -209,7 +212,6 @@ func (s *State) CreateUser(user types.User) (*types.User, bool, error) {
|
|||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
|
||||||
if err := s.db.DB.Save(&user).Error; err != nil {
|
if err := s.db.DB.Save(&user).Error; err != nil {
|
||||||
return nil, false, fmt.Errorf("creating user: %w", err)
|
return nil, false, fmt.Errorf("creating user: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -300,6 +300,7 @@ func LoadConfig(path string, isFile bool) error {
|
|||||||
viper.SetDefault("derp.server.verify_clients", true)
|
viper.SetDefault("derp.server.verify_clients", true)
|
||||||
viper.SetDefault("derp.server.stun.enabled", true)
|
viper.SetDefault("derp.server.stun.enabled", true)
|
||||||
viper.SetDefault("derp.server.automatically_add_embedded_derp_region", true)
|
viper.SetDefault("derp.server.automatically_add_embedded_derp_region", true)
|
||||||
|
viper.SetDefault("derp.update_frequency", "3h")
|
||||||
|
|
||||||
viper.SetDefault("unix_socket", "/var/run/headscale/headscale.sock")
|
viper.SetDefault("unix_socket", "/var/run/headscale/headscale.sock")
|
||||||
viper.SetDefault("unix_socket_permission", "0o770")
|
viper.SetDefault("unix_socket_permission", "0o770")
|
||||||
|
Loading…
Reference in New Issue
Block a user