From aeb8d29fc967e3cb6cea3ffcc434a4dfa9cd3ca2 Mon Sep 17 00:00:00 2001 From: Saleh Date: Sat, 4 Jul 2026 16:02:56 +0300 Subject: [PATCH] types: lowercase DNS extra record names DNS names are case-insensitive, but clients match extra records against the lowercased query name, so records with mixed-case names (for example "Printer.fritz.box" in an extra_records_path file) never resolved and queries fell through to the global nameserver. Normalize record names to lowercase where the records enter the tailcfg DNS config, covering both dns.extra_records and extra_records_path. Fixes #2782 --- CHANGELOG.md | 8 ++++++++ hscontrol/types/config.go | 20 ++++++++++++++++++-- hscontrol/types/config_test.go | 29 +++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 479b533358..5d28467daf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,14 @@ keys remain all-access. - `headscale users rename` now sends the identifier of the matched user instead of the raw `--identifier` flag value, so renaming by name works again [#3442](https://github.com/juanfont/headscale/pull/3442) +## 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 f1b0298ffb..dabf2524ce 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 d0859758c6..c8dd1b0b78 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) + } +}