mirror of
https://github.com/juanfont/headscale.git
synced 2026-09-25 17:54:54 +09:00
types: validate listener address collisions
Reject configurations where two configured TCP listeners would bind the same kernel socket. Covers every pair of listen_addr, grpc_listen_addr, metrics_listen_addr, and tls_letsencrypt_listen (when HTTP-01 ACME is in use). Each violation renders as a structured ConfigError naming both YAML keys, the values the operator wrote, and a hint pointing at the canonical setup. Closes the symptom in #3227: the misconfiguration is now rejected at config-load time with a self-explaining error, instead of failing at runtime with a misleading "address already in use" log because the ACME challenge listener and the main HTTP listener competed for the same port inside the same process. Fixes #3227
This commit is contained in:
@@ -631,6 +631,9 @@ func validateServerConfig() error {
|
||||
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")
|
||||
}
|
||||
|
||||
v := &configValidator{}
|
||||
validateListenerCollisions(v)
|
||||
|
||||
if (viper.GetString("tls_letsencrypt_challenge_type") != HTTP01ChallengeType) &&
|
||||
(viper.GetString("tls_letsencrypt_challenge_type") != TLSALPN01ChallengeType) {
|
||||
errorText += "Fatal config error: the only supported values for tls_letsencrypt_challenge_type are HTTP-01 and TLS-ALPN-01\n"
|
||||
@@ -705,10 +708,10 @@ func validateServerConfig() error {
|
||||
|
||||
if errorText != "" {
|
||||
// nolint
|
||||
return errors.New(strings.TrimSuffix(errorText, "\n"))
|
||||
v.AddErr(errors.New(strings.TrimSuffix(errorText, "\n")))
|
||||
}
|
||||
|
||||
return nil
|
||||
return v.Err()
|
||||
}
|
||||
|
||||
func tlsConfig() TLSConfig {
|
||||
|
||||
@@ -511,6 +511,55 @@ oidc:` + tt.oidcBlock + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTLSLetsEncryptListenAddrCollision(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
listenAddr string
|
||||
leListen string
|
||||
hostname string
|
||||
challengeType string
|
||||
wantErr string // empty = expect no error
|
||||
}{
|
||||
{"collision-numeric", ":80", ":80", "example.com", "HTTP-01", "would bind the same TCP socket"},
|
||||
{"collision-named-vs-numeric", "0.0.0.0:80", ":http", "example.com", "HTTP-01", "would bind the same TCP socket"},
|
||||
{"collision-https-named", ":443", ":https", "example.com", "HTTP-01", "would bind the same TCP socket"},
|
||||
{"canonical", "0.0.0.0:443", ":http", "example.com", "HTTP-01", ""},
|
||||
{"no-hostname-skipped", "0.0.0.0:80", ":http", "", "HTTP-01", ""},
|
||||
{"tls-alpn-01-skipped", "0.0.0.0:80", ":http", "example.com", "TLS-ALPN-01", ""},
|
||||
{"different-numeric", ":8080", ":8081", "example.com", "HTTP-01", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
viper.Reset()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
cfg := fmt.Sprintf(`---
|
||||
server_url: https://example.com
|
||||
listen_addr: %q
|
||||
tls_letsencrypt_hostname: %q
|
||||
tls_letsencrypt_challenge_type: %q
|
||||
tls_letsencrypt_listen: %q
|
||||
noise:
|
||||
private_key_path: noise_private.key
|
||||
dns:
|
||||
override_local_dns: false
|
||||
`, tt.listenAddr, tt.hostname, tt.challengeType, tt.leListen)
|
||||
require.NoError(t, os.WriteFile(
|
||||
filepath.Join(tmpDir, "config.yaml"), []byte(cfg), 0o600))
|
||||
require.NoError(t, LoadConfig(tmpDir, false))
|
||||
|
||||
err := validateServerConfig()
|
||||
if tt.wantErr == "" {
|
||||
require.NoError(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// OK
|
||||
// server_url: headscale.com, base: clients.headscale.com
|
||||
// server_url: headscale.com, base: headscale.net
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var errEmptyListenAddr = errors.New("address is empty")
|
||||
@@ -78,3 +80,63 @@ func isWildcardHost(h string) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// validateListenerCollisions records a *ConfigError for each pair of
|
||||
// configured TCP listeners that would bind the same kernel socket. The
|
||||
// ACME HTTP-01 challenge listener is only considered when a hostname is
|
||||
// set and the HTTP-01 challenge is selected.
|
||||
func validateListenerCollisions(v *configValidator) {
|
||||
type spec struct {
|
||||
key, addr string
|
||||
active bool
|
||||
}
|
||||
|
||||
listenAddr := viper.GetString("listen_addr")
|
||||
grpcAddr := viper.GetString("grpc_listen_addr")
|
||||
metricsAddr := viper.GetString("metrics_listen_addr")
|
||||
acmeAddr := viper.GetString("tls_letsencrypt_listen")
|
||||
|
||||
listeners := []spec{
|
||||
{"listen_addr", listenAddr, listenAddr != ""},
|
||||
{"grpc_listen_addr", grpcAddr, grpcAddr != ""},
|
||||
{"metrics_listen_addr", metricsAddr, metricsAddr != ""},
|
||||
{
|
||||
key: "tls_letsencrypt_listen",
|
||||
addr: acmeAddr,
|
||||
active: acmeAddr != "" &&
|
||||
viper.GetString("tls_letsencrypt_hostname") != "" &&
|
||||
viper.GetString("tls_letsencrypt_challenge_type") == HTTP01ChallengeType,
|
||||
},
|
||||
}
|
||||
|
||||
for i := range listeners {
|
||||
for j := i + 1; j < len(listeners); j++ {
|
||||
a, b := listeners[i], listeners[j]
|
||||
if !a.active || !b.active {
|
||||
continue
|
||||
}
|
||||
|
||||
overlap, err := listenersOverlap(a.addr, b.addr)
|
||||
if err != nil {
|
||||
v.Add(&ConfigError{
|
||||
Reason: "cannot parse " + a.key,
|
||||
Current: []KV{{a.key, a.addr}},
|
||||
Detail: err.Error(),
|
||||
Hint: `use host:port form, e.g. "0.0.0.0:8080"`,
|
||||
})
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if overlap {
|
||||
v.Add(&ConfigError{
|
||||
Reason: fmt.Sprintf("%s and %s would bind the same TCP socket", a.key, b.key),
|
||||
Current: []KV{{a.key, a.addr}},
|
||||
ConflictsWith: []KV{{b.key, b.addr}},
|
||||
Hint: "give each listener a distinct port, or bind them to different non-wildcard hosts",
|
||||
See: "https://headscale.net/stable/ref/tls/",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user