From bd29c4ab40d8a01b0249ca1fe936179a1191544a Mon Sep 17 00:00:00 2001 From: Kyle McMartin Date: Sat, 3 May 2025 18:26:19 -0700 Subject: [PATCH 1/2] net/dns/resolvd: prevent overwriting our backup resolv.conf (#12360) When SetDNS is called multiple times, such as on start up, the current implementation will create backupConf, and then subsequent calls will overwrite our backup with the last generated resolv.conf file. Guard against this by only copying the backupConf the first time. Write the sentinel comments that allow us to check whether we "own" the resolv.conf as resolvconffile does. Also do some clean up of the resolvd nameserver entries and whitespace, to enable a follow up commit to transition to writing the config with resolvconffile instead of open coding it. Fixes #12360 Signed-off-by: Kyle McMartin --- net/dns/resolvd.go | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/net/dns/resolvd.go b/net/dns/resolvd.go index ad1a99c11..bfd9a2b6e 100644 --- a/net/dns/resolvd.go +++ b/net/dns/resolvd.go @@ -38,11 +38,31 @@ func (m *resolvdManager) SetDNS(config OSConfig) error { m.ifName, } - origResolv, err := m.readAndCopy(resolvConf, backupConf, 0644) + // first, read in the resolvConf + origResolv, err := m.fs.ReadFile(resolvConf) if err != nil { return err } - newResolvConf := removeSearchLines(origResolv) + + // search for our sentinel in resolvConf, if it's not present + // it's our first call to SetDNS, and we should make backupConf + resolvConfOwned := bytes.Contains(origResolv, []byte("generated by tailscale")) + if !resolvConfOwned { + _, err = m.fs.Stat(backupConf) + if err != nil && os.IsNotExist(err) { + _, err = m.readAndCopy(resolvConf, backupConf, 0644) + if err != nil { + return err + } + } + } + + // remove resolvd nameservers and search lines + origResolv = removeSearchLines(origResolv) + origResolv = removeResolvdNameserverLines(origResolv) + // and clean up whitespace + removeBlanks := regexp.MustCompile(`(?m)^\s*\n`) + origResolv = removeBlanks.ReplaceAll(origResolv, []byte("")) for _, ns := range config.Nameservers { args = append(args, ns.String()) @@ -55,6 +75,15 @@ func (m *resolvdManager) SetDNS(config OSConfig) error { newSearch = append(newSearch, s.WithoutTrailingDot()) } + var resolvConfHeader = []byte("") + if !resolvConfOwned { + // if we don't have a header, insert one + resolvConfHeader = []byte( + "# resolv.conf(5) file generated by tailscale\n" + + "# For more info, see https://tailscale.com/s/resolvconf-overwrite\n" + + "# DO NOT EDIT THIS FILE BY HAND -- CHANGES WILL BE OVERWRITTEN\n\n") + } + newResolvConf := append(resolvConfHeader, origResolv...) if len(newSearch) > 1 { newResolvConf = append(newResolvConf, []byte(strings.Join(newSearch, " "))...) newResolvConf = append(newResolvConf, '\n') @@ -123,6 +152,11 @@ func (m resolvdManager) readResolvConf() (config OSConfig, err error) { }, nil } +func removeResolvdNameserverLines(orig []byte) []byte { + re := regexp.MustCompile(`(?m)^nameserver\s+[^\s]+\s+#\s+resolvd:.*$`) + return re.ReplaceAll(orig, []byte("")) +} + func removeSearchLines(orig []byte) []byte { re := regexp.MustCompile(`(?ms)^search\s+.+$`) return re.ReplaceAll(orig, []byte("")) From f655b98db642d2623cadb96488ad6e02055c620b Mon Sep 17 00:00:00 2001 From: Kyle McMartin Date: Sun, 4 May 2025 19:23:25 -0700 Subject: [PATCH 2/2] Deduplicate OSConfig.SearchDomains before SetDNS (#12360) Rather than do this in each implementation, filter the unified Config.SearchDomains list before calling into the OSConfig layer. This ensures that when GetBaseConfig rereads that already written resolv.conf search line, we don't end up growing the same tailnet entry multiple times. Updates #12360 Signed-off-by: Kyle McMartin --- net/dns/manager.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/net/dns/manager.go b/net/dns/manager.go index 64bf12c6b..a463a842a 100644 --- a/net/dns/manager.go +++ b/net/dns/manager.go @@ -394,7 +394,18 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig defaultRoutes = append(defaultRoutes, &dnstype.Resolver{Addr: ip.String()}) } rcfg.Routes["."] = defaultRoutes - ocfg.SearchDomains = append(ocfg.SearchDomains, baseCfg.SearchDomains...) + + seen := map[dnsname.FQDN]bool{} + SearchDomains := append(ocfg.SearchDomains, baseCfg.SearchDomains...) + // deduplicate our list of SearchDomains + ocfg.SearchDomains = []dnsname.FQDN{} + for _, sd := range SearchDomains { + if seen[sd] { + continue + } + ocfg.SearchDomains = append(ocfg.SearchDomains, sd) + seen[sd] = true + } } return rcfg, ocfg, nil