From 75f3e1fb03cfa99621925cba2164d64f1a298d60 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Thu, 21 Oct 2021 21:38:02 +0000
Subject: [PATCH 01/11] docs(README): update contributors
---
README.md | 151 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 151 insertions(+)
diff --git a/README.md b/README.md
index 19742d8e..3b411ecb 100644
--- a/README.md
+++ b/README.md
@@ -61,4 +61,155 @@ Please have a look at the documentation under [`docs/`](docs/).
## Contributors
+
+
From 5420347d2454855865c322314a0f5a30228b6da5 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri, 22 Oct 2021 06:58:20 +0000
Subject: [PATCH 02/11] docs(README): update contributors
---
README.md | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/README.md b/README.md
index 3b411ecb..a3c09396 100644
--- a/README.md
+++ b/README.md
@@ -195,6 +195,13 @@ Please have a look at the documentation under [`docs/`](docs/).
+
+
+
+
+ derelm
+
+ |
From e836db1eadcf3a517758cf5ac342cdf25d903e3c Mon Sep 17 00:00:00 2001
From: Kristoffer Dalby
Date: Fri, 22 Oct 2021 16:51:19 +0000
Subject: [PATCH 03/11] 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 04/11] 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 05/11] 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 06/11] 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 07/11] 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 08/11] 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 09/11] 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 10/11] 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 11/11] 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:
|