From e836db1eadcf3a517758cf5ac342cdf25d903e3c Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 22 Oct 2021 16:51:19 +0000 Subject: [PATCH 1/9] Add config.yaml to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 95d758a7..610550b9 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ /headscale config.json +config.yaml *.key /db.sqlite *.sqlite3 From aa245c2d06a2dbd9caded3c063c9429d4bc3af78 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 22 Oct 2021 16:52:39 +0000 Subject: [PATCH 2/9] Remove derp.yaml, add selfhosted example This PR will promote fetching the derpmap directly from tailscale, so we will remove our example, as it might easily get outdated. Add a derp-example that shows how a user can also add their own derp server. --- derp-example.yaml | 18 ++++++ derp.yaml | 146 ---------------------------------------------- 2 files changed, 18 insertions(+), 146 deletions(-) create mode 100644 derp-example.yaml delete mode 100644 derp.yaml diff --git a/derp-example.yaml b/derp-example.yaml new file mode 100644 index 00000000..45db5b9a --- /dev/null +++ b/derp-example.yaml @@ -0,0 +1,18 @@ +# 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: + 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 diff --git a/derp.yaml b/derp.yaml deleted file mode 100644 index 9434e712..00000000 --- a/derp.yaml +++ /dev/null @@ -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 From 57f46ded83456cc5abae95db15099abd51773bd8 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 22 Oct 2021 16:55:14 +0000 Subject: [PATCH 3/9] Split derp into its own config struct --- app.go | 34 +++++++++++--- cmd/headscale/cli/utils.go | 78 +++++++++++++++++++-------------- cmd/headscale/headscale_test.go | 28 ++++++++---- config.yaml.postgres.example | 1 - config.yaml.sqlite.example | 31 ++++++++++--- 5 files changed, 117 insertions(+), 55 deletions(-) diff --git a/app.go b/app.go index 66e2a306..546eb866 100644 --- a/app.go +++ b/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] } diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index 52c8d043..0768e1eb 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -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 diff --git a/cmd/headscale/headscale_test.go b/cmd/headscale/headscale_test.go index 0c3add69..eff78496 100644 --- a/cmd/headscale/headscale_test.go +++ b/cmd/headscale/headscale_test.go @@ -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) diff --git a/config.yaml.postgres.example b/config.yaml.postgres.example index 569b42a9..920bdaaa 100644 --- a/config.yaml.postgres.example +++ b/config.yaml.postgres.example @@ -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 diff --git a/config.yaml.sqlite.example b/config.yaml.sqlite.example index 158b1e5b..411a2a77 100644 --- a/config.yaml.sqlite.example +++ b/config.yaml.sqlite.example @@ -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 From 177f1eca06ff3f046ac75c921f01ccf1b0eb80ab Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 22 Oct 2021 16:55:35 +0000 Subject: [PATCH 4/9] Add helper functions for building derp maps from urls and file --- derp.go | 152 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 derp.go diff --git a/derp.go b/derp.go new file mode 100644 index 00000000..39e63210 --- /dev/null +++ b/derp.go @@ -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) + } + } + } +} From 582eb57a092b15ba84e1f69b9bca4a0c485249f7 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 22 Oct 2021 16:56:00 +0000 Subject: [PATCH 5/9] Use the new derp map --- api.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/api.go b/api.go index 6e30cb3a..a31cf529 100644 --- a/api.go +++ b/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). From 0e902fe949cec49aeea5f48da98779993f3b213d Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 22 Oct 2021 16:56:23 +0000 Subject: [PATCH 6/9] Add certificates to docker image so we can get derpmap from tailscale --- Dockerfile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Dockerfile b/Dockerfile index 20bb7dae..6e216aad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 From d875cca69d7919fdeb476b6c5b442ce5439d9e8c Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 22 Oct 2021 16:57:01 +0000 Subject: [PATCH 7/9] move integration to yaml, add new derp configuration --- integration_test/etc/config.json | 19 ------------------- integration_test/etc/config.yaml | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 19 deletions(-) delete mode 100644 integration_test/etc/config.json create mode 100644 integration_test/etc/config.yaml diff --git a/integration_test/etc/config.json b/integration_test/etc/config.json deleted file mode 100644 index dc23652d..00000000 --- a/integration_test/etc/config.json +++ /dev/null @@ -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" - } -} \ No newline at end of file diff --git a/integration_test/etc/config.yaml b/integration_test/etc/config.yaml new file mode 100644 index 00000000..6f68f304 --- /dev/null +++ b/integration_test/etc/config.yaml @@ -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 From aefbd66317d837846e1db8c4fdc4d11633e885cf Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 22 Oct 2021 16:57:51 +0000 Subject: [PATCH 8/9] Remove derpmap volume from integration tests --- integration_test.go | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/integration_test.go b/integration_test.go index 3c51215d..53092423 100644 --- a/integration_test.go +++ b/integration_test.go @@ -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, From a3557694161dfa18aa518df84e73a60d30c001ae Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 22 Oct 2021 23:58:27 +0100 Subject: [PATCH 9/9] Update derp-example.yaml Co-authored-by: Juan Font --- derp-example.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/derp-example.yaml b/derp-example.yaml index 45db5b9a..bbf7cc8d 100644 --- a/derp-example.yaml +++ b/derp-example.yaml @@ -1,6 +1,3 @@ -# 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: 900: