types: move sub-builder log.Fatal sites into validators

derpConfig, databaseConfig, and dnsToTailcfgDNS used log.Fatal to
reject invalid combinations the moment they were observed. Lift those
checks into modular validators (validateDERPConfig,
validateDatabaseConfig, validateMagicDNSConfig) called from
validateServerConfig so each violation lands in the same configValidator
collector and renders as a structured ConfigError next to all other
config feedback. The sub-builder functions trust validation has run
and no longer crash the process.

Updates #3227
This commit is contained in:
Kristoffer Dalby
2026-04-30 08:23:41 +00:00
parent 1b4b79901a
commit 7865430419
2 changed files with 77 additions and 17 deletions
+74 -17
View File
@@ -749,9 +749,83 @@ func validateServerConfig() error {
})
}
validateDERPConfig(v)
validateDatabaseConfig(v)
validateMagicDNSConfig(v)
return v.Err()
}
// validateDERPConfig records ConfigErrors when the embedded DERP server
// is enabled without the addresses or paths it needs.
func validateDERPConfig(v *configValidator) {
if !viper.GetBool("derp.server.enabled") {
return
}
if viper.GetString("derp.server.stun_listen_addr") == "" {
v.Add(&ConfigError{
Reason: "derp.server.stun_listen_addr is required when the embedded DERP server is enabled",
Current: []KV{
{"derp.server.enabled", true},
{"derp.server.stun_listen_addr", ""},
},
Hint: `set derp.server.stun_listen_addr (e.g. "0.0.0.0:3478"), or set derp.server.enabled: false`,
See: "https://headscale.net/stable/ref/integration/derp/",
})
}
if !viper.GetBool("derp.server.automatically_add_embedded_derp_region") &&
len(viper.GetStringSlice("derp.paths")) == 0 {
v.Add(&ConfigError{
Reason: "derp.paths is required when derp.server.automatically_add_embedded_derp_region is false",
Current: []KV{
{"derp.server.automatically_add_embedded_derp_region", false},
{"derp.paths", "[]"},
},
Hint: "list at least one DERP map JSON file in derp.paths, or set automatically_add_embedded_derp_region: true",
})
}
}
// validateDatabaseConfig records a ConfigError when database.type is not
// one of the supported backends.
func validateDatabaseConfig(v *configValidator) {
t := viper.GetString("database.type")
switch t {
case DatabaseSqlite, DatabasePostgres, "sqlite":
return
}
v.Add(&ConfigError{
Reason: "database.type has an unsupported value",
Current: []KV{{"database.type", t}},
Allowed: []string{"sqlite", "sqlite3", "postgres"},
Hint: "pick one of the allowed values; sqlite is the default for single-host deployments",
See: "https://headscale.net/stable/ref/database/",
})
}
// validateMagicDNSConfig records a ConfigError when MagicDNS is enabled
// without a base_domain.
func validateMagicDNSConfig(v *configValidator) {
if !viper.GetBool("dns.magic_dns") {
return
}
if viper.GetString("dns.base_domain") == "" {
v.Add(&ConfigError{
Reason: "dns.base_domain is required when dns.magic_dns is true",
Current: []KV{
{"dns.magic_dns", true},
{"dns.base_domain", ""},
},
Hint: `set dns.base_domain to a domain you control (e.g. "ts.example.net"), or set dns.magic_dns: false`,
See: "https://headscale.net/stable/ref/dns/",
})
}
}
func tlsConfig() TLSConfig {
return TLSConfig{
LetsEncrypt: LetsEncryptConfig{
@@ -787,11 +861,6 @@ func derpConfig() DERPConfig {
"derp.server.automatically_add_embedded_derp_region",
)
if serverEnabled && stunAddr == "" {
log.Fatal().
Msg("derp.server.stun_listen_addr must be set if derp.server.enabled is true")
}
urlStrs := viper.GetStringSlice("derp.urls")
urls := make([]url.URL, 0, len(urlStrs))
@@ -812,11 +881,6 @@ func derpConfig() DERPConfig {
paths := viper.GetStringSlice("derp.paths")
if serverEnabled && !automaticallyAddEmbeddedDerpRegion && len(paths) == 0 {
log.Fatal().
Msg("Disabling derp.server.automatically_add_embedded_derp_region requires to configure the derp server in derp.paths")
}
autoUpdate := viper.GetBool("derp.auto_update_enabled")
updateFrequency := viper.GetDuration("derp.update_frequency")
@@ -903,9 +967,6 @@ func databaseConfig() DatabaseConfig {
break
case "sqlite":
type_ = "sqlite3"
default:
log.Fatal().
Msgf("invalid database type %q, must be sqlite, sqlite3 or postgres", type_)
}
return DatabaseConfig{
@@ -1029,10 +1090,6 @@ func (d *DNSConfig) splitResolvers() map[string][]*dnstype.Resolver {
func dnsToTailcfgDNS(dns DNSConfig) *tailcfg.DNSConfig {
cfg := tailcfg.DNSConfig{}
if dns.BaseDomain == "" && dns.MagicDNS {
log.Fatal().Msg("dns.base_domain must be set when using MagicDNS (dns.magic_dns)")
}
cfg.Proxied = dns.MagicDNS
cfg.ExtraRecords = lowercaseRecordNames(dns.ExtraRecords)
+3
View File
@@ -542,7 +542,10 @@ tls_letsencrypt_challenge_type: %q
tls_letsencrypt_listen: %q
noise:
private_key_path: noise_private.key
database:
type: sqlite3
dns:
magic_dns: false
override_local_dns: false
`, tt.listenAddr, tt.hostname, tt.challengeType, tt.leListen)
require.NoError(t, os.WriteFile(