Repository: juanfont/headscale

Issue:
When does derp.yaml support the InsecureForTests configuration item
When does derp.yaml support the InsecureForTests configuration item

Discussion:
@github-actions: This issue is stale because it has been open for 90 days with no activity.@github-actions: This issue was closed because it has been inactive for 14 days since being marked as stale.@WhaleFell: 2025 Still unable to support InsecureForTests

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6460d7e4..a5441a5a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -43,6 +43,7 @@ Fix hang on SIGTERM [#1492](https://github.com/juanfont/headscale/pull/1492) tak
 Send logs to stderr by default [#1524](https://github.com/juanfont/headscale/pull/1524)
 Fix [TS-2023-006](https://tailscale.com/security-bulletins/#ts-2023-006) security UPnP issue [#1563](https://github.com/juanfont/headscale/pull/1563)
 Turn off gRPC logging [#1640](https://github.com/juanfont/headscale/pull/1640) fixes [#1259](https://github.com/juanfont/headscale/issues/1259)
+Added the possibility to manually create a DERP-map entry which can be customized, instead of automatically creating it. [#1565](https://github.com/juanfont/headscale/pull/1565)
 
 ## 0.22.3 (2023-05-12)
 
diff --git a/config-example.yaml b/config-example.yaml
index 5105dcd8..96a654a4 100644
--- a/config-example.yaml
+++ b/config-example.yaml
@@ -94,6 +94,16 @@ derp:
     #
     private_key_path: /var/lib/headscale/derp_server_private.key
 
+    # This flag can be used, so the DERP map entry for the embedded DERP server is not written automatically,
+    # it enables the creation of your very own DERP map entry using a locally available file with the parameter DERP.paths
+    # If you enable the DERP server and set this to false, it is required to add the DERP server to the DERP map using DERP.paths
+    automatically_add_embedded_derp_region: true
+
+    # For better connection stability (especially when using an Exit-Node and DNS is not working),
+    # it is possible to optionall add the public IPv4 and IPv6 address to the Derp-Map using:
+    ipv4: 1.2.3.4
+    ipv6: 2001:db8::1
+
   # List of externally available DERP maps encoded in JSON
   urls:
     - https://controlplane.tailscale.com/derpmap/default
diff --git a/hscontrol/app.go b/hscontrol/app.go
index 5327d6f8..75dfddee 100644
--- a/hscontrol/app.go
+++ b/hscontrol/app.go
@@ -268,7 +268,7 @@ func (h *Headscale) scheduledDERPMapUpdateWorker(cancelChan <-chan struct{}) {
 		case <-ticker.C:
 			log.Info().Msg("Fetching DERPMap updates")
 			h.DERPMap = derp.GetDERPMap(h.cfg.DERP)
-			if h.cfg.DERP.ServerEnabled {
+			if h.cfg.DERP.ServerEnabled && h.cfg.DERP.AutomaticallyAddEmbeddedDerpRegion {
 				region, _ := h.DERPServer.GenerateRegion()
 				h.DERPMap.Regions[region.RegionID] = &region
 			}
@@ -501,7 +501,9 @@ func (h *Headscale) Serve() error {
 			return err
 		}
 
-		h.DERPMap.Regions[region.RegionID] = &region
+		if h.cfg.DERP.AutomaticallyAddEmbeddedDerpRegion {
+			h.DERPMap.Regions[region.RegionID] = &region
+		}
 
 		go h.DERPServer.ServeSTUN()
 	}
diff --git a/hscontrol/db/routes.go b/hscontrol/db/routes.go
index 51c7f3bc..aed9776d 100644
--- a/hscontrol/db/routes.go
+++ b/hscontrol/db/routes.go
@@ -349,7 +349,7 @@ func (hsdb *HSDatabase) GetNodePrimaryRoutes(node *types.Node) (types.Routes, er
 
 // SaveNodeRoutes takes a node and updates the database with
 // the new routes.
-// It returns a bool wheter an update should be sent as the
+// It returns a bool whether an update should be sent as the
 // saved route impacts nodes.
 func (hsdb *HSDatabase) SaveNodeRoutes(node *types.Node) (bool, error) {
 	hsdb.mu.Lock()
diff --git a/hscontrol/derp/server/derp_server.go b/hscontrol/derp/server/derp_server.go
index 59e40285..ad325c7a 100644
--- a/hscontrol/derp/server/derp_server.go
+++ b/hscontrol/derp/server/derp_server.go
@@ -84,6 +84,8 @@ func (d *DERPServer) GenerateRegion() (tailcfg.DERPRegion, error) {
 				RegionID: d.cfg.ServerRegionID,
 				HostName: host,
 				DERPPort: port,
+				IPv4:     d.cfg.IPv4,
+				IPv6:     d.cfg.IPv6,
 			},
 		},
 	}
@@ -99,6 +101,7 @@ func (d *DERPServer) GenerateRegion() (tailcfg.DERPRegion, error) {
 	localDERPregion.Nodes[0].STUNPort = portSTUN
 
 	log.Info().Caller().Msgf("DERP region: %+v", localDERPregion)
+	log.Info().Caller().Msgf("DERP Nodes[0]: %+v", localDERPregion.Nodes[0])
 
 	return localDERPregion, nil
 }
@@ -208,6 +211,7 @@ func DERPProbeHandler(
 // The initial implementation is here https://github.com/tailscale/tailscale/pull/1406
 // 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
 func DERPBootstrapDNSHandler(
 	derpMap *tailcfg.DERPMap,
 ) func(http.ResponseWriter, *http.Request) {
diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go
index 4b29c4b7..8e619733 100644
--- a/hscontrol/types/config.go
+++ b/hscontrol/types/config.go
@@ -107,16 +107,19 @@ type OIDCConfig struct {
 }
 
 type DERPConfig struct {
-	ServerEnabled        bool
-	ServerRegionID       int
-	ServerRegionCode     string
-	ServerRegionName     string
-	ServerPrivateKeyPath string
-	STUNAddr             string
-	URLs                 []url.URL
-	Paths                []string
-	AutoUpdate           bool
-	UpdateFrequency      time.Duration
+	ServerEnabled                      bool
+	AutomaticallyAddEmbeddedDerpRegion bool
+	ServerRegionID                     int
+	ServerRegionCode                   string
+	ServerRegionName                   string
+	ServerPrivateKeyPath               string
+	STUNAddr                           string
+	URLs                               []url.URL
+	Paths                              []string
+	AutoUpdate                         bool
+	UpdateFrequency                    time.Duration
+	IPv4                               string
+	IPv6                               string
 }
 
 type LogTailConfig struct {
@@ -169,6 +172,7 @@ func LoadConfig(path string, isFile bool) error {
 
 	viper.SetDefault("derp.server.enabled", false)
 	viper.SetDefault("derp.server.stun.enabled", true)
+	viper.SetDefault("derp.server.automatically_add_embedded_derp_region", true)
 
 	viper.SetDefault("unix_socket", "/var/run/headscale/headscale.sock")
 	viper.SetDefault("unix_socket_permission", "0o770")
@@ -286,8 +290,14 @@ func GetDERPConfig() DERPConfig {
 	serverRegionCode := viper.GetString("derp.server.region_code")
 	serverRegionName := viper.GetString("derp.server.region_name")
 	stunAddr := viper.GetString("derp.server.stun_listen_addr")
-	privateKeyPath := util.AbsolutePathFromConfigPath(viper.GetString("derp.server.private_key_path"))
-
+	privateKeyPath := util.AbsolutePathFromConfigPath(
+		viper.GetString("derp.server.private_key_path"),
+	)
+	ipv4 := viper.GetString("derp.server.ipv4")
+	ipv6 := viper.GetString("derp.server.ipv6")
+	automaticallyAddEmbeddedDerpRegion := viper.GetBool(
+		"derp.server.automatically_add_embedded_derp_region",
+	)
 	if serverEnabled && stunAddr == "" {
 		log.Fatal().
 			Msg("derp.server.stun_listen_addr must be set if derp.server.enabled is true")
@@ -310,20 +320,28 @@ func GetDERPConfig() DERPConfig {
 
 	paths := viper.GetStringSlice("derp.paths")
 
+	if serverEnabled && !automaticallyAddEmbeddedDerpRegion && len(paths) == 0 {
+		log.Fatal().
+			Msg("Disabling derp.server.automatically_add_embedded_derp_region requires to configure the derp server in derp.paths")
+	}
+
 	autoUpdate := viper.GetBool("derp.auto_update_enabled")
 	updateFrequency := viper.GetDuration("derp.update_frequency")
 
 	return DERPConfig{
-		ServerEnabled:        serverEnabled,
-		ServerRegionID:       serverRegionID,
-		ServerRegionCode:     serverRegionCode,
-		ServerRegionName:     serverRegionName,
-		ServerPrivateKeyPath: privateKeyPath,
-		STUNAddr:             stunAddr,
-		URLs:                 urls,
-		Paths:                paths,
-		AutoUpdate:           autoUpdate,
-		UpdateFrequency:      updateFrequency,
+		ServerEnabled:                      serverEnabled,
+		ServerRegionID:                     serverRegionID,
+		ServerRegionCode:                   serverRegionCode,
+		ServerRegionName:                   serverRegionName,
+		ServerPrivateKeyPath:               privateKeyPath,
+		STUNAddr:                           stunAddr,
+		URLs:                               urls,
+		Paths:                              paths,
+		AutoUpdate:                         autoUpdate,
+		UpdateFrequency:                    updateFrequency,
+		IPv4:                               ipv4,
+		IPv6:                               ipv6,
+		AutomaticallyAddEmbeddedDerpRegion: automaticallyAddEmbeddedDerpRegion,
 	}
 }
 
diff --git a/hscontrol/types/node.go b/hscontrol/types/node.go
index 9b2ba769..44342646 100644
--- a/hscontrol/types/node.go
+++ b/hscontrol/types/node.go
@@ -383,7 +383,7 @@ func (node *Node) GetFQDN(dnsConfig *tailcfg.DNSConfig, baseDomain string) (stri
 // inform peers about smaller changes to the node.
 // When a field is added to this function, remember to also add it to:
 // - node.ApplyPeerChange
-// - logTracePeerChange in poll.go
+// - logTracePeerChange in poll.go.
 func (node *Node) PeerChangeFromMapRequest(req tailcfg.MapRequest) tailcfg.PeerChange {
 	ret := tailcfg.PeerChange{
 		NodeID: tailcfg.NodeID(node.ID),



Test patch:
diff --git a/integration/general_test.go b/integration/general_test.go
index c0928445..15c3a72c 100644
--- a/integration/general_test.go
+++ b/integration/general_test.go
@@ -320,7 +320,6 @@ func TestTaildrop(t *testing.T) {
 			if err != nil {
 				t.Fatalf("failed to install curl on %s, err: %s", client.Hostname(), err)
 			}
-
 		}
 		curlCommand := []string{"curl", "--unix-socket", "/var/run/tailscale/tailscaled.sock", "http://local-tailscaled.sock/localapi/v0/file-targets"}
 		err = retry(10, 1*time.Second, func() error {