Split derp into its own config struct

This commit is contained in:
Kristoffer Dalby 2021-10-22 16:55:14 +00:00
parent aa245c2d06
commit 57f46ded83
5 changed files with 117 additions and 55 deletions

32
app.go
View file

@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"os" "os"
"sort" "sort"
"strings" "strings"
@ -28,11 +29,12 @@ type Config struct {
ServerURL string ServerURL string
Addr string Addr string
PrivateKeyPath string PrivateKeyPath string
DerpMap *tailcfg.DERPMap
EphemeralNodeInactivityTimeout time.Duration EphemeralNodeInactivityTimeout time.Duration
IPPrefix netaddr.IPPrefix IPPrefix netaddr.IPPrefix
BaseDomain string BaseDomain string
DERP DERPConfig
DBtype string DBtype string
DBpath string DBpath string
DBhost string DBhost string
@ -55,6 +57,13 @@ type Config struct {
DNSConfig *tailcfg.DNSConfig DNSConfig *tailcfg.DNSConfig
} }
type DERPConfig struct {
URLs []url.URL
Paths []string
AutoUpdate bool
UpdateFrequency time.Duration
}
// Headscale represents the base app of the service // Headscale represents the base app of the service
type Headscale struct { type Headscale struct {
cfg Config cfg Config
@ -65,6 +74,8 @@ type Headscale struct {
publicKey *wgkey.Key publicKey *wgkey.Key
privateKey *wgkey.Private privateKey *wgkey.Private
DERPMap *tailcfg.DERPMap
aclPolicy *ACLPolicy aclPolicy *ACLPolicy
aclRules *[]tailcfg.FilterRule aclRules *[]tailcfg.FilterRule
@ -153,11 +164,15 @@ func (h *Headscale) expireEphemeralNodesWorker() {
return return
} }
for _, m := range *machines { 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") log.Info().Str("machine", m.Name).Msg("Ephemeral client removed from database")
err = h.db.Unscoped().Delete(m).Error err = h.db.Unscoped().Delete(m).Error
if err != nil { 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.watchForKVUpdates(5000)
go h.expireEphemeralNodes(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{ s := &http.Server{
Addr: h.cfg.Addr, Addr: h.cfg.Addr,
Handler: r, Handler: r,
@ -273,7 +297,6 @@ func (h *Headscale) getLastStateChange(namespaces ...string) time.Time {
times = append(times, lastChange) times = append(times, lastChange)
} }
} }
sort.Slice(times, func(i, j int) bool { sort.Slice(times, func(i, j int) bool {
@ -284,7 +307,6 @@ func (h *Headscale) getLastStateChange(namespaces ...string) time.Time {
if len(times) == 0 { if len(times) == 0 {
return time.Now().UTC() return time.Now().UTC()
} else { } else {
return times[0] return times[0]
} }

View file

@ -4,7 +4,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -13,7 +13,6 @@ import (
"github.com/juanfont/headscale" "github.com/juanfont/headscale"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/spf13/viper" "github.com/spf13/viper"
"gopkg.in/yaml.v2"
"inet.af/netaddr" "inet.af/netaddr"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/dnstype" "tailscale.com/types/dnstype"
@ -51,21 +50,26 @@ func LoadConfig(path string) error {
// Collect any validation errors and return them all at once // Collect any validation errors and return them all at once
var errorText string 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" 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) // 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(). 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") 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" 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" errorText += "Fatal config error: server_url must start with https:// or http://\n"
} }
if errorText != "" { if errorText != "" {
@ -73,7 +77,35 @@ func LoadConfig(path string) error {
} else { } else {
return nil 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) { func GetDNSConfig() (*tailcfg.DNSConfig, string) {
@ -171,33 +203,30 @@ func absPath(path string) string {
} }
func getHeadscaleApp() (*headscale.Headscale, error) { 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 // Minimum inactivity time out is keepalive timeout (60s) plus a few seconds
// to avoid races // to avoid races
minInactivityTimeout, _ := time.ParseDuration("65s") minInactivityTimeout, _ := time.ParseDuration("65s")
if viper.GetDuration("ephemeral_node_inactivity_timeout") <= minInactivityTimeout { 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 return nil, err
} }
dnsConfig, baseDomain := GetDNSConfig() dnsConfig, baseDomain := GetDNSConfig()
derpConfig := GetDERPConfig()
cfg := headscale.Config{ cfg := headscale.Config{
ServerURL: viper.GetString("server_url"), ServerURL: viper.GetString("server_url"),
Addr: viper.GetString("listen_addr"), Addr: viper.GetString("listen_addr"),
PrivateKeyPath: absPath(viper.GetString("private_key_path")), PrivateKeyPath: absPath(viper.GetString("private_key_path")),
DerpMap: derpMap,
IPPrefix: netaddr.MustParseIPPrefix(viper.GetString("ip_prefix")), IPPrefix: netaddr.MustParseIPPrefix(viper.GetString("ip_prefix")),
BaseDomain: baseDomain, BaseDomain: baseDomain,
DERP: derpConfig,
EphemeralNodeInactivityTimeout: viper.GetDuration("ephemeral_node_inactivity_timeout"), EphemeralNodeInactivityTimeout: viper.GetDuration("ephemeral_node_inactivity_timeout"),
DBtype: viper.GetString("db_type"), DBtype: viper.GetString("db_type"),
@ -243,21 +272,6 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
return h, nil 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) { func JsonOutput(result interface{}, errResult error, outputFormat string) {
var j []byte var j []byte
var err error var err error

View file

@ -25,7 +25,6 @@ func (s *Suite) SetUpSuite(c *check.C) {
} }
func (s *Suite) TearDownSuite(c *check.C) { func (s *Suite) TearDownSuite(c *check.C) {
} }
func (*Suite) TestPostgresConfigLoading(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 // 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("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("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_type"), check.Equals, "postgres")
c.Assert(viper.GetString("db_port"), check.Equals, "5432") c.Assert(viper.GetString("db_port"), check.Equals, "5432")
c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "") 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 // 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("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("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_type"), check.Equals, "sqlite3")
c.Assert(viper.GetString("db_path"), check.Equals, "db.sqlite") c.Assert(viper.GetString("db_path"), check.Equals, "db.sqlite")
c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "") 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) { func writeConfig(c *check.C, tmpDir string, configYaml []byte) {
// Populate a custom config file // Populate a custom config file
configFile := filepath.Join(tmpDir, "config.yaml") configFile := filepath.Join(tmpDir, "config.yaml")
err := ioutil.WriteFile(configFile, configYaml, 0644) err := ioutil.WriteFile(configFile, configYaml, 0o644)
if err != nil { if err != nil {
c.Fatalf("Couldn't write file %s", configFile) c.Fatalf("Couldn't write file %s", configFile)
} }
@ -139,10 +137,12 @@ func (*Suite) TestTLSConfigValidation(c *check.C) {
if err != nil { if err != nil {
c.Fatal(err) c.Fatal(err)
} }
//defer os.RemoveAll(tmpDir) // defer os.RemoveAll(tmpDir)
fmt.Println(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) writeConfig(c, tmpDir, configYaml)
// Check configuration validation errors (1) // Check configuration validation errors (1)
@ -150,13 +150,23 @@ func (*Suite) TestTLSConfigValidation(c *check.C) {
c.Assert(err, check.NotNil) c.Assert(err, check.NotNil)
// check.Matches can not handle multiline strings // check.Matches can not handle multiline strings
tmp := strings.ReplaceAll(err.Error(), "\n", "***") 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(
c.Assert(tmp, check.Matches, ".*Fatal config error: the only supported values for tls_letsencrypt_challenge_type are.*") 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://.*") c.Assert(tmp, check.Matches, ".*Fatal config error: server_url must start with https:// or http://.*")
fmt.Println(tmp) fmt.Println(tmp)
// Check configuration validation errors (2) // 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) writeConfig(c, tmpDir, configYaml)
err = cli.LoadConfig(tmpDir) err = cli.LoadConfig(tmpDir)
c.Assert(err, check.IsNil) c.Assert(err, check.IsNil)

View file

@ -2,7 +2,6 @@
server_url: http://127.0.0.1:8080 server_url: http://127.0.0.1:8080
listen_addr: 0.0.0.0:8080 listen_addr: 0.0.0.0:8080
private_key_path: private.key private_key_path: private.key
derp_map_path: derp.yaml
ephemeral_node_inactivity_timeout: 30m ephemeral_node_inactivity_timeout: 30m
# Postgres config # Postgres config

View file

@ -1,26 +1,43 @@
--- ---
log_level: info
server_url: http://127.0.0.1:8080 server_url: http://127.0.0.1:8080
listen_addr: 0.0.0.0:8080 listen_addr: 0.0.0.0:8080
private_key_path: private.key private_key_path: private.key
derp_map_path: derp.yaml
ephemeral_node_inactivity_timeout: 30m ephemeral_node_inactivity_timeout: 30m
# SQLite config (uncomment it if you want to use SQLite) # SQLite config (uncomment it if you want to use SQLite)
db_type: sqlite3 db_type: sqlite3
db_path: db.sqlite 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_url: https://acme-v02.api.letsencrypt.org/directory
acme_email: '' acme_email: ""
tls_letsencrypt_hostname: '' tls_letsencrypt_hostname: ""
tls_letsencrypt_listen: ":http" tls_letsencrypt_listen: ":http"
tls_letsencrypt_cache_dir: ".cache" tls_letsencrypt_cache_dir: ".cache"
tls_letsencrypt_challenge_type: HTTP-01 tls_letsencrypt_challenge_type: HTTP-01
tls_cert_path: '' tls_cert_path: ""
tls_key_path: '' tls_key_path: ""
acl_policy_path: '' acl_policy_path: ""
dns_config: dns_config:
nameservers: nameservers:
- 1.1.1.1 - 1.1.1.1
domains: [] domains: []
magic_dns: true magic_dns: true
base_domain: example.com base_domain: example.com