From e63dcfd2073c791ba91208c386d3a476190634d1 Mon Sep 17 00:00:00 2001 From: Saleh Date: Sat, 4 Jul 2026 16:02:56 +0300 Subject: [PATCH] fix: 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 | 1 + hscontrol/types/config.go | 20 ++++++++++++++++++-- hscontrol/types/config_test.go | 30 ++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 785d7d855..270e9240c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ keys remain all-access. - Expiring or deleting a non-existent pre-auth key now returns an error instead of silently succeeding [#3324](https://github.com/juanfont/headscale/pull/3324) - Improve systemd service file hardening [#3341](https://github.com/juanfont/headscale/pull/3341) +- Lowercase DNS extra record names so mixed-case records resolve [#2782](https://github.com/juanfont/headscale/issues/2782) ## 0.29.2 (2026-07-01) diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go index 4f0b4ecfa..b4a97a2b5 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..876cdc817 100644 --- a/hscontrol/types/config_test.go +++ b/hscontrol/types/config_test.go @@ -741,3 +741,33 @@ 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. +// See https://github.com/juanfont/headscale/issues/2782. +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) + } +}