feat: add prominent warning banner for non-standard IP prefixes

Add a highly visible ASCII-art warning banner that is printed at
startup when the configured IP prefixes fall outside the standard
Tailscale CGNAT (100.64.0.0/10) or ULA (fd7a:115c:a1e0::/48) ranges.

The warning fires once even if both v4 and v6 are non-standard, and
the warnBanner() function is reusable for other critical configuration
warnings in the future.

Also updates config-example.yaml to clarify that subsets of the
default ranges are fine, but ranges outside CGNAT/ULA are not.

Closes #3055
This commit is contained in:
Tanayk07
2026-03-08 12:39:17 +05:30
committed by Kristoffer Dalby
parent 3d53f97c82
commit 5105033224
2 changed files with 73 additions and 26 deletions

View File

@@ -50,12 +50,21 @@ noise:
# List of IP prefixes to allocate tailaddresses from.
# Each prefix consists of either an IPv4 or IPv6 address,
# and the associated prefix length, delimited by a slash.
# It must be within IP ranges supported by the Tailscale
# client - i.e., subnets of 100.64.0.0/10 and fd7a:115c:a1e0::/48.
# See below:
# IPv6: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#LL81C52-L81C71
#
# WARNING: These prefixes MUST be subsets of the standard Tailscale ranges:
# - IPv4: 100.64.0.0/10 (CGNAT range)
# - IPv6: fd7a:115c:a1e0::/48 (Tailscale ULA range)
#
# Using a SUBSET of these ranges is supported and useful if you want to
# limit IP allocation to a smaller block (e.g., 100.64.0.0/24).
#
# Using ranges OUTSIDE of CGNAT/ULA is NOT supported and will cause
# undefined behaviour. The Tailscale client has hard-coded assumptions
# about these ranges and will break in subtle, hard-to-debug ways.
#
# See:
# IPv4: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#L33
# Any other range is NOT supported, and it will cause unexpected issues.
# IPv6: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#LL81C52-L81C71
prefixes:
v4: 100.64.0.0/10
v6: fd7a:115c:a1e0::/48

View File

@@ -842,54 +842,72 @@ func dnsToTailcfgDNS(dns DNSConfig) *tailcfg.DNSConfig {
return &cfg
}
func prefixV4() (*netip.Prefix, error) {
// warnBanner prints a highly visible warning banner to the log output.
// It wraps the provided lines in an ASCII-art box with a "Warning!" header.
// This is intended for critical configuration issues that users must not ignore.
func warnBanner(lines []string) {
var b strings.Builder
b.WriteString("\n")
b.WriteString("################################################################\n")
b.WriteString("### __ __ _ _ ###\n")
b.WriteString("### \\ \\ / / (_) | | ###\n")
b.WriteString("### \\ \\ /\\ / /_ _ _ __ _ __ _ _ __ __ _| | ###\n")
b.WriteString("### \\ \\/ \\/ / _` | '__| '_ \\| | '_ \\ / _` | | ###\n")
b.WriteString("### \\ /\\ / (_| | | | | | | | | | | (_| |_| ###\n")
b.WriteString("### \\/ \\/ \\__,_|_| |_| |_|_|_| |_|\\__, (_) ###\n")
b.WriteString("### __/ | ###\n")
b.WriteString("### |___/ ###\n")
b.WriteString("################################################################\n")
b.WriteString("### ###\n")
for _, line := range lines {
b.WriteString(fmt.Sprintf("### %-56s ###\n", line))
}
b.WriteString("### ###\n")
b.WriteString("################################################################")
log.Warn().Msg(b.String())
}
func prefixV4() (*netip.Prefix, bool, error) {
prefixV4Str := viper.GetString("prefixes.v4")
if prefixV4Str == "" {
return nil, nil //nolint:nilnil // empty prefix is valid, not an error
return nil, false, nil
}
prefixV4, err := netip.ParsePrefix(prefixV4Str)
if err != nil {
return nil, fmt.Errorf("parsing IPv4 prefix from config: %w", err)
return nil, false, fmt.Errorf("parsing IPv4 prefix from config: %w", err)
}
builder := netipx.IPSetBuilder{}
builder.AddPrefix(tsaddr.CGNATRange())
ipSet, _ := builder.IPSet()
if !ipSet.ContainsPrefix(prefixV4) {
log.Warn().
Msgf("Prefix %s is not in the %s range. This is an unsupported configuration.",
prefixV4Str, tsaddr.CGNATRange())
}
return &prefixV4, nil
return &prefixV4, !ipSet.ContainsPrefix(prefixV4), nil
}
func prefixV6() (*netip.Prefix, error) {
func prefixV6() (*netip.Prefix, bool, error) {
prefixV6Str := viper.GetString("prefixes.v6")
if prefixV6Str == "" {
return nil, nil //nolint:nilnil // empty prefix is valid, not an error
return nil, false, nil
}
prefixV6, err := netip.ParsePrefix(prefixV6Str)
if err != nil {
return nil, fmt.Errorf("parsing IPv6 prefix from config: %w", err)
return nil, false, fmt.Errorf("parsing IPv6 prefix from config: %w", err)
}
builder := netipx.IPSetBuilder{}
builder.AddPrefix(tsaddr.TailscaleULARange())
ipSet, _ := builder.IPSet()
if !ipSet.ContainsPrefix(prefixV6) {
log.Warn().
Msgf("Prefix %s is not in the %s range. This is an unsupported configuration.",
prefixV6Str, tsaddr.TailscaleULARange())
}
return &prefixV6, nil
return &prefixV6, !ipSet.ContainsPrefix(prefixV6), nil
}
// LoadCLIConfig returns the needed configuration for the CLI client
@@ -921,12 +939,12 @@ func LoadServerConfig() (*Config, error) {
logConfig := logConfig()
zerolog.SetGlobalLevel(logConfig.Level)
prefix4, err := prefixV4()
prefix4, v4NonStandard, err := prefixV4()
if err != nil {
return nil, err
}
prefix6, err := prefixV6()
prefix6, v6NonStandard, err := prefixV6()
if err != nil {
return nil, err
}
@@ -935,6 +953,26 @@ func LoadServerConfig() (*Config, error) {
return nil, ErrNoPrefixConfigured
}
if v4NonStandard || v6NonStandard {
warnBanner([]string{
"You have overridden the default Headscale IP prefixes",
"with a range outside of the standard CGNAT and/or ULA",
"ranges. This is NOT a supported configuration.",
"",
"Using subsets of the default ranges (100.64.0.0/10 for",
"IPv4, fd7a:115c:a1e0::/48 for IPv6) is fine. Using",
"ranges outside of these will cause undefined behaviour",
"as the Tailscale client is NOT designed to operate on",
"any other ranges.",
"",
"Please revert your prefixes to subsets of the standard",
"ranges as described in the example configuration.",
"",
"Any issue raised using a range outside of the supported",
"range will be labelled as wontfix and closed.",
})
}
allocStr := viper.GetString("prefixes.allocation")
var alloc IPAllocationStrategy