mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01: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