diff --git a/CHANGELOG.md b/CHANGELOG.md index f325397f5..509b4a2ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,14 @@ keys remain all-access. - Fix tailsql not shutting down with headscale, leaving the process hanging on graceful shutdown [#3400](https://github.com/juanfont/headscale/pull/3400) - Fix tvOS setup instructions: install the VPN configuration before setting the coordination server URL [#3431](https://github.com/juanfont/headscale/pull/3431) +## 0.29.4 (202x-xx-xx) + +**Minimum supported Tailscale client version: v1.80.0** + +### Changes + +- Lowercase DNS extra record names so mixed-case records resolve [#3366](https://github.com/juanfont/headscale/pull/3366) + ## 0.29.3 (2026-07-29) **Minimum supported Tailscale client version: v1.80.0** diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go index f1b0298ff..dabf2524c 100644 --- a/hscontrol/types/config.go +++ b/hscontrol/types/config.go @@ -993,7 +993,7 @@ func dnsToTailcfgDNS(dns DNSConfig) *tailcfg.DNSConfig { cfg.Proxied = dns.MagicDNS - cfg.ExtraRecords = dns.ExtraRecords + cfg.ExtraRecords = lowercaseRecordNames(dns.ExtraRecords) if dns.OverrideLocalDNS { cfg.Resolvers = dns.globalResolvers() } else { @@ -1475,6 +1475,22 @@ func (c *Config) SetExtraRecords(records []tailcfg.DNSRecord) { defer tailcfgDNSMu.Unlock() if c.TailcfgDNSConfig != nil { - c.TailcfgDNSConfig.ExtraRecords = records + c.TailcfgDNSConfig.ExtraRecords = lowercaseRecordNames(records) } } + +// lowercaseRecordNames normalizes DNS record names to lowercase, as DNS names +// are case-insensitive and clients match extra records by exact name. +func lowercaseRecordNames(records []tailcfg.DNSRecord) []tailcfg.DNSRecord { + if len(records) == 0 { + return records + } + + normalized := make([]tailcfg.DNSRecord, len(records)) + for i, record := range records { + record.Name = strings.ToLower(record.Name) + normalized[i] = record + } + + return normalized +} diff --git a/hscontrol/types/config_test.go b/hscontrol/types/config_test.go index d0859758c..c8dd1b0b7 100644 --- a/hscontrol/types/config_test.go +++ b/hscontrol/types/config_test.go @@ -741,3 +741,32 @@ func TestTrustedProxies(t *testing.T) { }) } } + +// DNS names are case-insensitive, but MagicDNS resolution in clients matches +// extra records by exact name, so mixed-case record names never resolve. +func TestExtraRecordsAreLowercased(t *testing.T) { + mixed := []tailcfg.DNSRecord{ + {Name: "Printer.fritz.box", Type: "A", Value: "192.168.1.2"}, + {Name: "NAS.FRITZ.BOX", Type: "A", Value: "192.168.1.3"}, + } + want := []tailcfg.DNSRecord{ + {Name: "printer.fritz.box", Type: "A", Value: "192.168.1.2"}, + {Name: "nas.fritz.box", Type: "A", Value: "192.168.1.3"}, + } + + tcfg := dnsToTailcfgDNS(DNSConfig{ + MagicDNS: true, + BaseDomain: "example.com", + ExtraRecords: mixed, + }) + if diff := cmp.Diff(want, tcfg.ExtraRecords); diff != "" { + t.Errorf("dnsToTailcfgDNS extra records mismatch (-want +got):\n%s", diff) + } + + cfg := &Config{TailcfgDNSConfig: &tailcfg.DNSConfig{}} + cfg.SetExtraRecords(mixed) + + if diff := cmp.Diff(want, cfg.TailcfgDNSConfig.ExtraRecords); diff != "" { + t.Errorf("SetExtraRecords mismatch (-want +got):\n%s", diff) + } +}