mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01:00 
			
		
		
		
	Merge pull request #196 from kradalby/derp-improvements
Add ability to fetch DERP from url and file
This commit is contained in:
		
						commit
						5aaffaaecb
					
				
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -16,6 +16,7 @@ | ||||
| 
 | ||||
| /headscale | ||||
| config.json | ||||
| config.yaml | ||||
| *.key | ||||
| /db.sqlite | ||||
| *.sqlite3 | ||||
|  | ||||
| @ -12,6 +12,11 @@ RUN test -e /go/bin/headscale | ||||
| 
 | ||||
| FROM ubuntu:20.04 | ||||
| 
 | ||||
| RUN apt-get update \ | ||||
|     && apt-get install -y ca-certificates \ | ||||
|     && update-ca-certificates \ | ||||
|     && rm -rf /var/lib/apt/lists/* | ||||
| 
 | ||||
| COPY --from=build /go/bin/headscale /usr/local/bin/headscale | ||||
| ENV TZ UTC | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										15
									
								
								api.go
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								api.go
									
									
									
									
									
								
							| @ -82,7 +82,10 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) { | ||||
| 
 | ||||
| 	now := time.Now().UTC() | ||||
| 	var m Machine | ||||
| 	if result := h.db.Preload("Namespace").First(&m, "machine_key = ?", mKey.HexString()); errors.Is(result.Error, gorm.ErrRecordNotFound) { | ||||
| 	if result := h.db.Preload("Namespace").First(&m, "machine_key = ?", mKey.HexString()); errors.Is( | ||||
| 		result.Error, | ||||
| 		gorm.ErrRecordNotFound, | ||||
| 	) { | ||||
| 		log.Info().Str("machine", req.Hostinfo.Hostname).Msg("New machine") | ||||
| 		m = Machine{ | ||||
| 			Expiry:               &req.Expiry, | ||||
| @ -270,7 +273,7 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m *Ma | ||||
| 		DNSConfig:    dnsConfig, | ||||
| 		Domain:       h.cfg.BaseDomain, | ||||
| 		PacketFilter: *h.aclRules, | ||||
| 		DERPMap:      h.cfg.DerpMap, | ||||
| 		DERPMap:      h.DERPMap, | ||||
| 		UserProfiles: profiles, | ||||
| 	} | ||||
| 
 | ||||
| @ -329,7 +332,13 @@ func (h *Headscale) getMapKeepAliveResponse(mKey wgkey.Key, req tailcfg.MapReque | ||||
| 	return data, nil | ||||
| } | ||||
| 
 | ||||
| func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgkey.Key, req tailcfg.RegisterRequest, m Machine) { | ||||
| func (h *Headscale) handleAuthKey( | ||||
| 	c *gin.Context, | ||||
| 	db *gorm.DB, | ||||
| 	idKey wgkey.Key, | ||||
| 	req tailcfg.RegisterRequest, | ||||
| 	m Machine, | ||||
| ) { | ||||
| 	log.Debug(). | ||||
| 		Str("func", "handleAuthKey"). | ||||
| 		Str("machine", req.Hostinfo.Hostname). | ||||
|  | ||||
							
								
								
									
										34
									
								
								app.go
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								app.go
									
									
									
									
									
								
							| @ -4,6 +4,7 @@ import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"os" | ||||
| 	"sort" | ||||
| 	"strings" | ||||
| @ -28,11 +29,12 @@ type Config struct { | ||||
| 	ServerURL                      string | ||||
| 	Addr                           string | ||||
| 	PrivateKeyPath                 string | ||||
| 	DerpMap                        *tailcfg.DERPMap | ||||
| 	EphemeralNodeInactivityTimeout time.Duration | ||||
| 	IPPrefix                       netaddr.IPPrefix | ||||
| 	BaseDomain                     string | ||||
| 
 | ||||
| 	DERP DERPConfig | ||||
| 
 | ||||
| 	DBtype string | ||||
| 	DBpath string | ||||
| 	DBhost string | ||||
| @ -55,6 +57,13 @@ type Config struct { | ||||
| 	DNSConfig *tailcfg.DNSConfig | ||||
| } | ||||
| 
 | ||||
| type DERPConfig struct { | ||||
| 	URLs            []url.URL | ||||
| 	Paths           []string | ||||
| 	AutoUpdate      bool | ||||
| 	UpdateFrequency time.Duration | ||||
| } | ||||
| 
 | ||||
| // Headscale represents the base app of the service
 | ||||
| type Headscale struct { | ||||
| 	cfg        Config | ||||
| @ -65,6 +74,8 @@ type Headscale struct { | ||||
| 	publicKey  *wgkey.Key | ||||
| 	privateKey *wgkey.Private | ||||
| 
 | ||||
| 	DERPMap *tailcfg.DERPMap | ||||
| 
 | ||||
| 	aclPolicy *ACLPolicy | ||||
| 	aclRules  *[]tailcfg.FilterRule | ||||
| 
 | ||||
| @ -114,7 +125,7 @@ func NewHeadscale(cfg Config) (*Headscale, error) { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		// we might have routes already from Split DNS
 | ||||
| 		if h.cfg.DNSConfig.Routes == nil {  | ||||
| 		if h.cfg.DNSConfig.Routes == nil { | ||||
| 			h.cfg.DNSConfig.Routes = make(map[string][]dnstype.Resolver) | ||||
| 		} | ||||
| 		for _, d := range magicDNSDomains { | ||||
| @ -153,11 +164,15 @@ func (h *Headscale) expireEphemeralNodesWorker() { | ||||
| 			return | ||||
| 		} | ||||
| 		for _, m := range *machines { | ||||
| 			if m.AuthKey != nil && m.LastSeen != nil && m.AuthKey.Ephemeral && time.Now().After(m.LastSeen.Add(h.cfg.EphemeralNodeInactivityTimeout)) { | ||||
| 			if m.AuthKey != nil && m.LastSeen != nil && m.AuthKey.Ephemeral && | ||||
| 				time.Now().After(m.LastSeen.Add(h.cfg.EphemeralNodeInactivityTimeout)) { | ||||
| 				log.Info().Str("machine", m.Name).Msg("Ephemeral client removed from database") | ||||
| 				err = h.db.Unscoped().Delete(m).Error | ||||
| 				if err != nil { | ||||
| 					log.Error().Err(err).Str("machine", m.Name).Msg("🤮 Cannot delete ephemeral machine from the database") | ||||
| 					log.Error(). | ||||
| 						Err(err). | ||||
| 						Str("machine", m.Name). | ||||
| 						Msg("🤮 Cannot delete ephemeral machine from the database") | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| @ -198,6 +213,15 @@ func (h *Headscale) Serve() error { | ||||
| 	go h.watchForKVUpdates(5000) | ||||
| 	go h.expireEphemeralNodes(5000) | ||||
| 
 | ||||
| 	// Fetch an initial DERP Map before we start serving
 | ||||
| 	h.DERPMap = GetDERPMap(h.cfg.DERP) | ||||
| 
 | ||||
| 	if h.cfg.DERP.AutoUpdate { | ||||
| 		derpMapCancelChannel := make(chan struct{}) | ||||
| 		defer func() { derpMapCancelChannel <- struct{}{} }() | ||||
| 		go h.scheduledDERPMapUpdateWorker(derpMapCancelChannel) | ||||
| 	} | ||||
| 
 | ||||
| 	s := &http.Server{ | ||||
| 		Addr:        h.cfg.Addr, | ||||
| 		Handler:     r, | ||||
| @ -273,7 +297,6 @@ func (h *Headscale) getLastStateChange(namespaces ...string) time.Time { | ||||
| 
 | ||||
| 			times = append(times, lastChange) | ||||
| 		} | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| 	sort.Slice(times, func(i, j int) bool { | ||||
| @ -284,7 +307,6 @@ func (h *Headscale) getLastStateChange(namespaces ...string) time.Time { | ||||
| 
 | ||||
| 	if len(times) == 0 { | ||||
| 		return time.Now().UTC() | ||||
| 
 | ||||
| 	} else { | ||||
| 		return times[0] | ||||
| 	} | ||||
|  | ||||
| @ -4,7 +4,7 @@ import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"net/url" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| @ -13,7 +13,6 @@ import ( | ||||
| 	"github.com/juanfont/headscale" | ||||
| 	"github.com/rs/zerolog/log" | ||||
| 	"github.com/spf13/viper" | ||||
| 	"gopkg.in/yaml.v2" | ||||
| 	"inet.af/netaddr" | ||||
| 	"tailscale.com/tailcfg" | ||||
| 	"tailscale.com/types/dnstype" | ||||
| @ -51,21 +50,26 @@ func LoadConfig(path string) error { | ||||
| 
 | ||||
| 	// Collect any validation errors and return them all at once
 | ||||
| 	var errorText string | ||||
| 	if (viper.GetString("tls_letsencrypt_hostname") != "") && ((viper.GetString("tls_cert_path") != "") || (viper.GetString("tls_key_path") != "")) { | ||||
| 	if (viper.GetString("tls_letsencrypt_hostname") != "") && | ||||
| 		((viper.GetString("tls_cert_path") != "") || (viper.GetString("tls_key_path") != "")) { | ||||
| 		errorText += "Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both\n" | ||||
| 	} | ||||
| 
 | ||||
| 	if (viper.GetString("tls_letsencrypt_hostname") != "") && (viper.GetString("tls_letsencrypt_challenge_type") == "TLS-ALPN-01") && (!strings.HasSuffix(viper.GetString("listen_addr"), ":443")) { | ||||
| 	if (viper.GetString("tls_letsencrypt_hostname") != "") && | ||||
| 		(viper.GetString("tls_letsencrypt_challenge_type") == "TLS-ALPN-01") && | ||||
| 		(!strings.HasSuffix(viper.GetString("listen_addr"), ":443")) { | ||||
| 		// this is only a warning because there could be something sitting in front of headscale that redirects the traffic (e.g. an iptables rule)
 | ||||
| 		log.Warn(). | ||||
| 			Msg("Warning: when using tls_letsencrypt_hostname with TLS-ALPN-01 as challenge type, headscale must be reachable on port 443, i.e. listen_addr should probably end in :443") | ||||
| 	} | ||||
| 
 | ||||
| 	if (viper.GetString("tls_letsencrypt_challenge_type") != "HTTP-01") && (viper.GetString("tls_letsencrypt_challenge_type") != "TLS-ALPN-01") { | ||||
| 	if (viper.GetString("tls_letsencrypt_challenge_type") != "HTTP-01") && | ||||
| 		(viper.GetString("tls_letsencrypt_challenge_type") != "TLS-ALPN-01") { | ||||
| 		errorText += "Fatal config error: the only supported values for tls_letsencrypt_challenge_type are HTTP-01 and TLS-ALPN-01\n" | ||||
| 	} | ||||
| 
 | ||||
| 	if !strings.HasPrefix(viper.GetString("server_url"), "http://") && !strings.HasPrefix(viper.GetString("server_url"), "https://") { | ||||
| 	if !strings.HasPrefix(viper.GetString("server_url"), "http://") && | ||||
| 		!strings.HasPrefix(viper.GetString("server_url"), "https://") { | ||||
| 		errorText += "Fatal config error: server_url must start with https:// or http://\n" | ||||
| 	} | ||||
| 	if errorText != "" { | ||||
| @ -73,7 +77,35 @@ func LoadConfig(path string) error { | ||||
| 	} else { | ||||
| 		return nil | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func GetDERPConfig() headscale.DERPConfig { | ||||
| 	urlStrs := viper.GetStringSlice("derp.urls") | ||||
| 
 | ||||
| 	urls := make([]url.URL, len(urlStrs)) | ||||
| 	for index, urlStr := range urlStrs { | ||||
| 		urlAddr, err := url.Parse(urlStr) | ||||
| 		if err != nil { | ||||
| 			log.Error(). | ||||
| 				Str("url", urlStr). | ||||
| 				Err(err). | ||||
| 				Msg("Failed to parse url, ignoring...") | ||||
| 		} | ||||
| 
 | ||||
| 		urls[index] = *urlAddr | ||||
| 	} | ||||
| 
 | ||||
| 	paths := viper.GetStringSlice("derp.paths") | ||||
| 
 | ||||
| 	autoUpdate := viper.GetBool("derp.auto_update_enabled") | ||||
| 	updateFrequency := viper.GetDuration("derp.update_frequency") | ||||
| 
 | ||||
| 	return headscale.DERPConfig{ | ||||
| 		URLs:            urls, | ||||
| 		Paths:           paths, | ||||
| 		AutoUpdate:      autoUpdate, | ||||
| 		UpdateFrequency: updateFrequency, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func GetDNSConfig() (*tailcfg.DNSConfig, string) { | ||||
| @ -171,33 +203,30 @@ func absPath(path string) string { | ||||
| } | ||||
| 
 | ||||
| func getHeadscaleApp() (*headscale.Headscale, error) { | ||||
| 	derpPath := absPath(viper.GetString("derp_map_path")) | ||||
| 	derpMap, err := loadDerpMap(derpPath) | ||||
| 	if err != nil { | ||||
| 		log.Error(). | ||||
| 			Str("path", derpPath). | ||||
| 			Err(err). | ||||
| 			Msg("Could not load DERP servers map file") | ||||
| 	} | ||||
| 
 | ||||
| 	// Minimum inactivity time out is keepalive timeout (60s) plus a few seconds
 | ||||
| 	// to avoid races
 | ||||
| 	minInactivityTimeout, _ := time.ParseDuration("65s") | ||||
| 	if viper.GetDuration("ephemeral_node_inactivity_timeout") <= minInactivityTimeout { | ||||
| 		err = fmt.Errorf("ephemeral_node_inactivity_timeout (%s) is set too low, must be more than %s\n", viper.GetString("ephemeral_node_inactivity_timeout"), minInactivityTimeout) | ||||
| 		err := fmt.Errorf( | ||||
| 			"ephemeral_node_inactivity_timeout (%s) is set too low, must be more than %s\n", | ||||
| 			viper.GetString("ephemeral_node_inactivity_timeout"), | ||||
| 			minInactivityTimeout, | ||||
| 		) | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	dnsConfig, baseDomain := GetDNSConfig() | ||||
| 	derpConfig := GetDERPConfig() | ||||
| 
 | ||||
| 	cfg := headscale.Config{ | ||||
| 		ServerURL:      viper.GetString("server_url"), | ||||
| 		Addr:           viper.GetString("listen_addr"), | ||||
| 		PrivateKeyPath: absPath(viper.GetString("private_key_path")), | ||||
| 		DerpMap:        derpMap, | ||||
| 		IPPrefix:       netaddr.MustParseIPPrefix(viper.GetString("ip_prefix")), | ||||
| 		BaseDomain:     baseDomain, | ||||
| 
 | ||||
| 		DERP: derpConfig, | ||||
| 
 | ||||
| 		EphemeralNodeInactivityTimeout: viper.GetDuration("ephemeral_node_inactivity_timeout"), | ||||
| 
 | ||||
| 		DBtype: viper.GetString("db_type"), | ||||
| @ -243,21 +272,6 @@ func getHeadscaleApp() (*headscale.Headscale, error) { | ||||
| 	return h, nil | ||||
| } | ||||
| 
 | ||||
| func loadDerpMap(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 JsonOutput(result interface{}, errResult error, outputFormat string) { | ||||
| 	var j []byte | ||||
| 	var err error | ||||
|  | ||||
| @ -25,7 +25,6 @@ func (s *Suite) SetUpSuite(c *check.C) { | ||||
| } | ||||
| 
 | ||||
| func (s *Suite) TearDownSuite(c *check.C) { | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| func (*Suite) TestPostgresConfigLoading(c *check.C) { | ||||
| @ -53,7 +52,6 @@ func (*Suite) TestPostgresConfigLoading(c *check.C) { | ||||
| 	// Test that config file was interpreted correctly
 | ||||
| 	c.Assert(viper.GetString("server_url"), check.Equals, "http://127.0.0.1:8080") | ||||
| 	c.Assert(viper.GetString("listen_addr"), check.Equals, "0.0.0.0:8080") | ||||
| 	c.Assert(viper.GetString("derp_map_path"), check.Equals, "derp.yaml") | ||||
| 	c.Assert(viper.GetString("db_type"), check.Equals, "postgres") | ||||
| 	c.Assert(viper.GetString("db_port"), check.Equals, "5432") | ||||
| 	c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "") | ||||
| @ -86,7 +84,7 @@ func (*Suite) TestSqliteConfigLoading(c *check.C) { | ||||
| 	// Test that config file was interpreted correctly
 | ||||
| 	c.Assert(viper.GetString("server_url"), check.Equals, "http://127.0.0.1:8080") | ||||
| 	c.Assert(viper.GetString("listen_addr"), check.Equals, "0.0.0.0:8080") | ||||
| 	c.Assert(viper.GetString("derp_map_path"), check.Equals, "derp.yaml") | ||||
| 	c.Assert(viper.GetStringSlice("derp.paths")[0], check.Equals, "derp-example.yaml") | ||||
| 	c.Assert(viper.GetString("db_type"), check.Equals, "sqlite3") | ||||
| 	c.Assert(viper.GetString("db_path"), check.Equals, "db.sqlite") | ||||
| 	c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "") | ||||
| @ -128,7 +126,7 @@ func (*Suite) TestDNSConfigLoading(c *check.C) { | ||||
| func writeConfig(c *check.C, tmpDir string, configYaml []byte) { | ||||
| 	// Populate a custom config file
 | ||||
| 	configFile := filepath.Join(tmpDir, "config.yaml") | ||||
| 	err := ioutil.WriteFile(configFile, configYaml, 0644) | ||||
| 	err := ioutil.WriteFile(configFile, configYaml, 0o644) | ||||
| 	if err != nil { | ||||
| 		c.Fatalf("Couldn't write file %s", configFile) | ||||
| 	} | ||||
| @ -139,10 +137,12 @@ func (*Suite) TestTLSConfigValidation(c *check.C) { | ||||
| 	if err != nil { | ||||
| 		c.Fatal(err) | ||||
| 	} | ||||
| 	//defer os.RemoveAll(tmpDir)
 | ||||
| 	// defer os.RemoveAll(tmpDir)
 | ||||
| 	fmt.Println(tmpDir) | ||||
| 
 | ||||
| 	configYaml := []byte("---\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"\"\ntls_cert_path: \"abc.pem\"") | ||||
| 	configYaml := []byte( | ||||
| 		"---\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"\"\ntls_cert_path: \"abc.pem\"", | ||||
| 	) | ||||
| 	writeConfig(c, tmpDir, configYaml) | ||||
| 
 | ||||
| 	// Check configuration validation errors (1)
 | ||||
| @ -150,13 +150,23 @@ func (*Suite) TestTLSConfigValidation(c *check.C) { | ||||
| 	c.Assert(err, check.NotNil) | ||||
| 	// check.Matches can not handle multiline strings
 | ||||
| 	tmp := strings.ReplaceAll(err.Error(), "\n", "***") | ||||
| 	c.Assert(tmp, check.Matches, ".*Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both.*") | ||||
| 	c.Assert(tmp, check.Matches, ".*Fatal config error: the only supported values for tls_letsencrypt_challenge_type are.*") | ||||
| 	c.Assert( | ||||
| 		tmp, | ||||
| 		check.Matches, | ||||
| 		".*Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both.*", | ||||
| 	) | ||||
| 	c.Assert( | ||||
| 		tmp, | ||||
| 		check.Matches, | ||||
| 		".*Fatal config error: the only supported values for tls_letsencrypt_challenge_type are.*", | ||||
| 	) | ||||
| 	c.Assert(tmp, check.Matches, ".*Fatal config error: server_url must start with https:// or http://.*") | ||||
| 	fmt.Println(tmp) | ||||
| 
 | ||||
| 	// Check configuration validation errors (2)
 | ||||
| 	configYaml = []byte("---\nserver_url: \"http://127.0.0.1:8080\"\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"TLS-ALPN-01\"") | ||||
| 	configYaml = []byte( | ||||
| 		"---\nserver_url: \"http://127.0.0.1:8080\"\ntls_letsencrypt_hostname: \"example.com\"\ntls_letsencrypt_challenge_type: \"TLS-ALPN-01\"", | ||||
| 	) | ||||
| 	writeConfig(c, tmpDir, configYaml) | ||||
| 	err = cli.LoadConfig(tmpDir) | ||||
| 	c.Assert(err, check.IsNil) | ||||
|  | ||||
| @ -2,7 +2,6 @@ | ||||
| server_url: http://127.0.0.1:8080 | ||||
| listen_addr: 0.0.0.0:8080 | ||||
| private_key_path: private.key | ||||
| derp_map_path: derp.yaml | ||||
| ephemeral_node_inactivity_timeout: 30m | ||||
| 
 | ||||
| # Postgres config | ||||
|  | ||||
| @ -1,26 +1,43 @@ | ||||
| --- | ||||
| log_level: info | ||||
| server_url: http://127.0.0.1:8080 | ||||
| listen_addr: 0.0.0.0:8080 | ||||
| private_key_path: private.key | ||||
| derp_map_path: derp.yaml | ||||
| ephemeral_node_inactivity_timeout: 30m | ||||
| 
 | ||||
| # SQLite config (uncomment it if you want to use SQLite) | ||||
| db_type: sqlite3 | ||||
| db_path: db.sqlite | ||||
| 
 | ||||
| derp: | ||||
|   # List of externally available DERP maps encoded in JSON | ||||
|   urls: | ||||
|     - https://controlplane.tailscale.com/derpmap/default | ||||
| 
 | ||||
|   # Locally available DERP map files encoded in YAML | ||||
|   paths: | ||||
|     - derp-example.yaml | ||||
| 
 | ||||
|   # If enabled, a worker will be set up to periodically | ||||
|   # refresh the given sources and update the derpmap | ||||
|   # will be set up. | ||||
|   auto_update_enabled: true | ||||
| 
 | ||||
|   # How often should we check for updates? | ||||
|   update_frequency: 24h | ||||
| 
 | ||||
| acme_url: https://acme-v02.api.letsencrypt.org/directory | ||||
| acme_email: '' | ||||
| tls_letsencrypt_hostname: '' | ||||
| acme_email: "" | ||||
| tls_letsencrypt_hostname: "" | ||||
| tls_letsencrypt_listen: ":http" | ||||
| tls_letsencrypt_cache_dir: ".cache" | ||||
| tls_letsencrypt_challenge_type: HTTP-01 | ||||
| tls_cert_path: '' | ||||
| tls_key_path: '' | ||||
| acl_policy_path: '' | ||||
| tls_cert_path: "" | ||||
| tls_key_path: "" | ||||
| acl_policy_path: "" | ||||
| dns_config: | ||||
|   nameservers: | ||||
|   - 1.1.1.1 | ||||
|     - 1.1.1.1 | ||||
|   domains: [] | ||||
|   magic_dns: true | ||||
|   base_domain: example.com | ||||
|  | ||||
							
								
								
									
										15
									
								
								derp-example.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								derp-example.yaml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | ||||
| # If you plan to somehow use headscale, please deploy your own DERP infra: https://tailscale.com/kb/1118/custom-derp-servers/ | ||||
| regions:  | ||||
|   900: | ||||
|     regionid: 900 | ||||
|     regioncode: custom | ||||
|     regionname: My Region | ||||
|     nodes: | ||||
|     - name: 1a | ||||
|       regionid: 1 | ||||
|       hostname: myderp.mydomain.no | ||||
|       ipv4: 123.123.123.123 | ||||
|       ipv6: "2604:a880:400:d1::828:b001" | ||||
|       stunport: 0 | ||||
|       stunonly: false | ||||
|       derptestport: 0 | ||||
							
								
								
									
										152
									
								
								derp.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								derp.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,152 @@ | ||||
| package headscale | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"os" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/rs/zerolog/log" | ||||
| 
 | ||||
| 	"gopkg.in/yaml.v2" | ||||
| 
 | ||||
| 	"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) { | ||||
| 	client := http.Client{ | ||||
| 		Timeout: 10 * time.Second, | ||||
| 	} | ||||
| 	resp, err := client.Get(addr.String()) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	defer resp.Body.Close() | ||||
| 	body, err := ioutil.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 { | ||||
| 		for id, region := range derpMap.Regions { | ||||
| 			result.Regions[id] = region | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return &result | ||||
| } | ||||
| 
 | ||||
| func GetDERPMap(cfg DERPConfig) *tailcfg.DERPMap { | ||||
| 	derpMaps := make([]*tailcfg.DERPMap, 0) | ||||
| 
 | ||||
| 	for _, path := range cfg.Paths { | ||||
| 		log.Debug(). | ||||
| 			Str("func", "GetDERPMap"). | ||||
| 			Str("path", path). | ||||
| 			Msg("Loading DERPMap from path") | ||||
| 		derpMap, err := loadDERPMapFromPath(path) | ||||
| 		if err != nil { | ||||
| 			log.Error(). | ||||
| 				Str("func", "GetDERPMap"). | ||||
| 				Str("path", path). | ||||
| 				Err(err). | ||||
| 				Msg("Could not load DERP map from path") | ||||
| 			break | ||||
| 		} | ||||
| 
 | ||||
| 		derpMaps = append(derpMaps, derpMap) | ||||
| 	} | ||||
| 
 | ||||
| 	for _, addr := range cfg.URLs { | ||||
| 		derpMap, err := loadDERPMapFromURL(addr) | ||||
| 		log.Debug(). | ||||
| 			Str("func", "GetDERPMap"). | ||||
| 			Str("url", addr.String()). | ||||
| 			Msg("Loading DERPMap from path") | ||||
| 		if err != nil { | ||||
| 			log.Error(). | ||||
| 				Str("func", "GetDERPMap"). | ||||
| 				Str("url", addr.String()). | ||||
| 				Err(err). | ||||
| 				Msg("Could not load DERP map from path") | ||||
| 			break | ||||
| 		} | ||||
| 
 | ||||
| 		derpMaps = append(derpMaps, derpMap) | ||||
| 	} | ||||
| 
 | ||||
| 	derpMap := mergeDERPMaps(derpMaps) | ||||
| 
 | ||||
| 	log.Trace().Interface("derpMap", derpMap).Msg("DERPMap loaded") | ||||
| 
 | ||||
| 	if len(derpMap.Regions) == 0 { | ||||
| 		log.Warn(). | ||||
| 			Msg("DERP map is empty, not a single DERP map datasource was loaded correctly or contained a region") | ||||
| 	} | ||||
| 
 | ||||
| 	return derpMap | ||||
| } | ||||
| 
 | ||||
| func (h *Headscale) scheduledDERPMapUpdateWorker(cancelChan <-chan struct{}) { | ||||
| 	log.Info(). | ||||
| 		Dur("frequency", h.cfg.DERP.UpdateFrequency). | ||||
| 		Msg("Setting up a DERPMap update worker") | ||||
| 	ticker := time.NewTicker(h.cfg.DERP.UpdateFrequency) | ||||
| 
 | ||||
| 	for { | ||||
| 		select { | ||||
| 		case <-cancelChan: | ||||
| 			return | ||||
| 
 | ||||
| 		case <-ticker.C: | ||||
| 			log.Info().Msg("Fetching DERPMap updates") | ||||
| 			h.DERPMap = GetDERPMap(h.cfg.DERP) | ||||
| 
 | ||||
| 			namespaces, err := h.ListNamespaces() | ||||
| 			if err != nil { | ||||
| 				log.Error(). | ||||
| 					Err(err). | ||||
| 					Msg("Failed to fetch namespaces") | ||||
| 			} | ||||
| 
 | ||||
| 			for _, namespace := range *namespaces { | ||||
| 				h.setLastStateChangeToNow(namespace.Name) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										146
									
								
								derp.yaml
									
									
									
									
									
								
							
							
						
						
									
										146
									
								
								derp.yaml
									
									
									
									
									
								
							| @ -1,146 +0,0 @@ | ||||
| # This file contains some of the official Tailscale DERP servers,  | ||||
| # shamelessly taken from https://github.com/tailscale/tailscale/blob/main/net/dnsfallback/dns-fallback-servers.json | ||||
| # | ||||
| # If you plan to somehow use headscale, please deploy your own DERP infra: https://tailscale.com/kb/1118/custom-derp-servers/ | ||||
| regions:  | ||||
|   1: | ||||
|     regionid: 1 | ||||
|     regioncode: nyc | ||||
|     regionname: New York City | ||||
|     nodes: | ||||
|     - name: 1a | ||||
|       regionid: 1 | ||||
|       hostname: derp1.tailscale.com | ||||
|       ipv4: 159.89.225.99 | ||||
|       ipv6: "2604:a880:400:d1::828:b001" | ||||
|       stunport: 0 | ||||
|       stunonly: false | ||||
|       derptestport: 0 | ||||
|     - name: 1b | ||||
|       regionid: 1 | ||||
|       hostname: derp1b.tailscale.com | ||||
|       ipv4: 45.55.35.93 | ||||
|       ipv6: "2604:a880:800:a1::f:2001" | ||||
|       stunport: 0 | ||||
|       stunonly: false | ||||
|       derptestport: 0 | ||||
|   2: | ||||
|     regionid: 2 | ||||
|     regioncode: sfo | ||||
|     regionname: San Francisco | ||||
|     nodes: | ||||
|     - name: 2a | ||||
|       regionid: 2 | ||||
|       hostname: derp2.tailscale.com | ||||
|       ipv4: 167.172.206.31 | ||||
|       ipv6: "2604:a880:2:d1::c5:7001" | ||||
|       stunport: 0 | ||||
|       stunonly: false | ||||
|       derptestport: 0 | ||||
|     - name: 2b | ||||
|       regionid: 2 | ||||
|       hostname: derp2b.tailscale.com | ||||
|       ipv4: 64.227.106.23 | ||||
|       ipv6: "2604:a880:4:1d0::29:9000" | ||||
|       stunport: 0 | ||||
|       stunonly: false | ||||
|       derptestport: 0 | ||||
|   3: | ||||
|     regionid: 3 | ||||
|     regioncode: sin | ||||
|     regionname: Singapore | ||||
|     nodes: | ||||
|     - name: 3a | ||||
|       regionid: 3 | ||||
|       hostname: derp3.tailscale.com | ||||
|       ipv4: 68.183.179.66 | ||||
|       ipv6: "2400:6180:0:d1::67d:8001" | ||||
|       stunport: 0 | ||||
|       stunonly: false | ||||
|       derptestport: 0 | ||||
|   4: | ||||
|     regionid: 4 | ||||
|     regioncode: fra | ||||
|     regionname: Frankfurt | ||||
|     nodes: | ||||
|     - name: 4a | ||||
|       regionid: 4 | ||||
|       hostname: derp4.tailscale.com | ||||
|       ipv4: 167.172.182.26 | ||||
|       ipv6: "2a03:b0c0:3:e0::36e:900" | ||||
|       stunport: 0 | ||||
|       stunonly: false | ||||
|       derptestport: 0 | ||||
|     - name: 4b | ||||
|       regionid: 4 | ||||
|       hostname: derp4b.tailscale.com | ||||
|       ipv4: 157.230.25.0 | ||||
|       ipv6: "2a03:b0c0:3:e0::58f:3001" | ||||
|       stunport: 0 | ||||
|       stunonly: false | ||||
|       derptestport: 0 | ||||
|   5: | ||||
|     regionid: 5 | ||||
|     regioncode: syd | ||||
|     regionname: Sydney | ||||
|     nodes: | ||||
|     - name: 5a | ||||
|       regionid: 5 | ||||
|       hostname: derp5.tailscale.com | ||||
|       ipv4: 103.43.75.49 | ||||
|       ipv6: "2001:19f0:5801:10b7:5400:2ff:feaa:284c" | ||||
|       stunport: 0 | ||||
|       stunonly: false | ||||
|       derptestport: 0 | ||||
|   6: | ||||
|     regionid: 6 | ||||
|     regioncode: blr | ||||
|     regionname: Bangalore | ||||
|     nodes: | ||||
|     - name: 6a | ||||
|       regionid: 6 | ||||
|       hostname: derp6.tailscale.com | ||||
|       ipv4: 68.183.90.120 | ||||
|       ipv6: "2400:6180:100:d0::982:d001" | ||||
|       stunport: 0 | ||||
|       stunonly: false | ||||
|       derptestport: 0 | ||||
|   7: | ||||
|     regionid: 7 | ||||
|     regioncode: tok | ||||
|     regionname: Tokyo | ||||
|     nodes: | ||||
|     - name: 7a | ||||
|       regionid: 7 | ||||
|       hostname: derp7.tailscale.com | ||||
|       ipv4: 167.179.89.145 | ||||
|       ipv6: "2401:c080:1000:467f:5400:2ff:feee:22aa" | ||||
|       stunport: 0 | ||||
|       stunonly: false | ||||
|       derptestport: 0 | ||||
|   8: | ||||
|     regionid: 8 | ||||
|     regioncode: lhr | ||||
|     regionname: London | ||||
|     nodes: | ||||
|     - name: 8a | ||||
|       regionid: 8 | ||||
|       hostname: derp8.tailscale.com | ||||
|       ipv4: 167.71.139.179 | ||||
|       ipv6: "2a03:b0c0:1:e0::3cc:e001" | ||||
|       stunport: 0 | ||||
|       stunonly: false | ||||
|       derptestport: 0 | ||||
|   9: | ||||
|     regionid: 9 | ||||
|     regioncode: sao | ||||
|     regionname: São Paulo | ||||
|     nodes: | ||||
|     - name: 9a | ||||
|       regionid: 9 | ||||
|       hostname: derp9.tailscale.com | ||||
|       ipv4: 207.148.3.137 | ||||
|       ipv6: "2001:19f0:6401:1d9c:5400:2ff:feef:bb82" | ||||
|       stunport: 0 | ||||
|       stunonly: false | ||||
|       derptestport: 0 | ||||
| @ -230,7 +230,6 @@ func (s *IntegrationTestSuite) SetupSuite() { | ||||
| 		Name: "headscale", | ||||
| 		Mounts: []string{ | ||||
| 			fmt.Sprintf("%s/integration_test/etc:/etc/headscale", currentPath), | ||||
| 			fmt.Sprintf("%s/derp.yaml:/etc/headscale/derp.yaml", currentPath), | ||||
| 		}, | ||||
| 		Networks: []*dockertest.Network{&network}, | ||||
| 		Cmd:      []string{"headscale", "serve"}, | ||||
| @ -289,7 +288,16 @@ func (s *IntegrationTestSuite) SetupSuite() { | ||||
| 		fmt.Printf("Creating pre auth key for %s\n", namespace) | ||||
| 		authKey, err := executeCommand( | ||||
| 			&headscale, | ||||
| 			[]string{"headscale", "--namespace", namespace, "preauthkeys", "create", "--reusable", "--expiration", "24h"}, | ||||
| 			[]string{ | ||||
| 				"headscale", | ||||
| 				"--namespace", | ||||
| 				namespace, | ||||
| 				"preauthkeys", | ||||
| 				"create", | ||||
| 				"--reusable", | ||||
| 				"--expiration", | ||||
| 				"24h", | ||||
| 			}, | ||||
| 			[]string{}, | ||||
| 		) | ||||
| 		assert.Nil(s.T(), err) | ||||
| @ -298,7 +306,16 @@ func (s *IntegrationTestSuite) SetupSuite() { | ||||
| 
 | ||||
| 		fmt.Printf("Joining tailscale containers to headscale at %s\n", headscaleEndpoint) | ||||
| 		for hostname, tailscale := range scales.tailscales { | ||||
| 			command := []string{"tailscale", "up", "-login-server", headscaleEndpoint, "--authkey", strings.TrimSuffix(authKey, "\n"), "--hostname", hostname} | ||||
| 			command := []string{ | ||||
| 				"tailscale", | ||||
| 				"up", | ||||
| 				"-login-server", | ||||
| 				headscaleEndpoint, | ||||
| 				"--authkey", | ||||
| 				strings.TrimSuffix(authKey, "\n"), | ||||
| 				"--hostname", | ||||
| 				hostname, | ||||
| 			} | ||||
| 
 | ||||
| 			fmt.Println("Join command:", command) | ||||
| 			fmt.Printf("Running join command for %s\n", hostname) | ||||
| @ -661,7 +678,13 @@ func (s *IntegrationTestSuite) TestMagicDNS() { | ||||
| 							fmt.Sprintf("%s.%s.headscale.net", peername, namespace), | ||||
| 						} | ||||
| 
 | ||||
| 						fmt.Printf("Pinging using Hostname (magicdns) from %s (%s) to %s (%s)\n", hostname, ips[hostname], peername, ip) | ||||
| 						fmt.Printf( | ||||
| 							"Pinging using Hostname (magicdns) from %s (%s) to %s (%s)\n", | ||||
| 							hostname, | ||||
| 							ips[hostname], | ||||
| 							peername, | ||||
| 							ip, | ||||
| 						) | ||||
| 						result, err := executeCommand( | ||||
| 							&tailscale, | ||||
| 							command, | ||||
|  | ||||
| @ -1,19 +0,0 @@ | ||||
| { | ||||
|   "server_url": "http://headscale:8080", | ||||
|   "listen_addr": "0.0.0.0:8080", | ||||
|   "private_key_path": "private.key", | ||||
|   "derp_map_path": "derp.yaml", | ||||
|   "ephemeral_node_inactivity_timeout": "30m", | ||||
|   "db_type": "sqlite3", | ||||
|   "db_path": "/tmp/integration_test_db.sqlite3", | ||||
|   "acl_policy_path": "", | ||||
|   "log_level": "trace", | ||||
|   "dns_config": { | ||||
|     "nameservers": [ | ||||
|       "1.1.1.1" | ||||
|     ], | ||||
|     "domains": [], | ||||
|     "magic_dns": true, | ||||
|     "base_domain": "headscale.net" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										20
									
								
								integration_test/etc/config.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								integration_test/etc/config.yaml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | ||||
| log_level: trace | ||||
| acl_policy_path: "" | ||||
| db_type: sqlite3 | ||||
| ephemeral_node_inactivity_timeout: 30m | ||||
| dns_config: | ||||
|   base_domain: headscale.net | ||||
|   magic_dns: true | ||||
|   domains: [] | ||||
|   nameservers: | ||||
|     - 1.1.1.1 | ||||
| db_path: /tmp/integration_test_db.sqlite3 | ||||
| private_key_path: private.key | ||||
| listen_addr: 0.0.0.0:8080 | ||||
| server_url: http://headscale:8080 | ||||
| 
 | ||||
| derp: | ||||
|   urls: | ||||
|     - https://controlplane.tailscale.com/derpmap/default | ||||
|   auto_update_enabled: false | ||||
|   update_frequency: 1m | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user