mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 00:01:40 +01:00 
			
		
		
		
	Discovered this while investigating the following issue; I think it's unrelated, but might as well fix it. Also, add a test helper for checking things that have an IsZero method using the reflect package. Updates tailscale/support-escalations#55 Signed-off-by: Andrew Dunham <andrew@du.nham.ca> Change-Id: I57b7adde43bcef9483763b561da173b4c35f49e2
		
			
				
	
	
		
			197 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			197 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) Tailscale Inc & AUTHORS
 | |
| // SPDX-License-Identifier: BSD-3-Clause
 | |
| 
 | |
| package dns
 | |
| 
 | |
| import (
 | |
| 	"bufio"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"net/netip"
 | |
| 	"slices"
 | |
| 	"strings"
 | |
| 
 | |
| 	"tailscale.com/types/logger"
 | |
| 	"tailscale.com/util/dnsname"
 | |
| )
 | |
| 
 | |
| // An OSConfigurator applies DNS settings to the operating system.
 | |
| type OSConfigurator interface {
 | |
| 	// SetDNS updates the OS's DNS configuration to match cfg.
 | |
| 	// If cfg is the zero value, all Tailscale-related DNS
 | |
| 	// configuration is removed.
 | |
| 	// SetDNS must not be called after Close.
 | |
| 	// SetDNS takes ownership of cfg.
 | |
| 	SetDNS(cfg OSConfig) error
 | |
| 	// SupportsSplitDNS reports whether the configurator is capable of
 | |
| 	// installing a resolver only for specific DNS suffixes. If false,
 | |
| 	// the configurator can only set a global resolver.
 | |
| 	SupportsSplitDNS() bool
 | |
| 	// GetBaseConfig returns the OS's "base" configuration, i.e. the
 | |
| 	// resolver settings the OS would use without Tailscale
 | |
| 	// contributing any configuration.
 | |
| 	// GetBaseConfig must return the tailscale-free base config even
 | |
| 	// after SetDNS has been called to set a Tailscale configuration.
 | |
| 	// Only works when SupportsSplitDNS=false.
 | |
| 
 | |
| 	// Implementations that don't support getting the base config must
 | |
| 	// return ErrGetBaseConfigNotSupported.
 | |
| 	GetBaseConfig() (OSConfig, error)
 | |
| 	// Close removes Tailscale-related DNS configuration from the OS.
 | |
| 	Close() error
 | |
| }
 | |
| 
 | |
| // HostEntry represents a single line in the OS's hosts file.
 | |
| type HostEntry struct {
 | |
| 	Addr  netip.Addr
 | |
| 	Hosts []string
 | |
| }
 | |
| 
 | |
| // OSConfig is an OS DNS configuration.
 | |
| type OSConfig struct {
 | |
| 	// Hosts is a map of DNS FQDNs to their IPs, which should be added to the
 | |
| 	// OS's hosts file. Currently, (2022-08-12) it is only populated for Windows
 | |
| 	// in SplitDNS mode and with Smart Name Resolution turned on.
 | |
| 	Hosts []*HostEntry
 | |
| 	// Nameservers are the IP addresses of the nameservers to use.
 | |
| 	Nameservers []netip.Addr
 | |
| 	// SearchDomains are the domain suffixes to use when expanding
 | |
| 	// single-label name queries. SearchDomains is additive to
 | |
| 	// whatever non-Tailscale search domains the OS has.
 | |
| 	SearchDomains []dnsname.FQDN
 | |
| 	// MatchDomains are the DNS suffixes for which Nameservers should
 | |
| 	// be used. If empty, Nameservers is installed as the "primary" resolver.
 | |
| 	// A non-empty MatchDomains requests a "split DNS" configuration
 | |
| 	// from the OS, which will only work with OSConfigurators that
 | |
| 	// report SupportsSplitDNS()=true.
 | |
| 	MatchDomains []dnsname.FQDN
 | |
| }
 | |
| 
 | |
| func (o *OSConfig) WriteToBufioWriter(w *bufio.Writer) {
 | |
| 	if o == nil {
 | |
| 		w.WriteString("<nil>")
 | |
| 		return
 | |
| 	}
 | |
| 	w.WriteString("{")
 | |
| 	if len(o.Hosts) > 0 {
 | |
| 		fmt.Fprintf(w, "Hosts:%v ", o.Hosts)
 | |
| 	}
 | |
| 	if len(o.Nameservers) > 0 {
 | |
| 		fmt.Fprintf(w, "Nameservers:%v ", o.Nameservers)
 | |
| 	}
 | |
| 	if len(o.SearchDomains) > 0 {
 | |
| 		fmt.Fprintf(w, "SearchDomains:%v ", o.SearchDomains)
 | |
| 	}
 | |
| 	if len(o.MatchDomains) > 0 {
 | |
| 		w.WriteString("MatchDomains:[")
 | |
| 		sp := ""
 | |
| 		var numARPA int
 | |
| 		for _, s := range o.MatchDomains {
 | |
| 			if strings.HasSuffix(string(s), ".arpa.") {
 | |
| 				numARPA++
 | |
| 				continue
 | |
| 			}
 | |
| 			w.WriteString(sp)
 | |
| 			w.WriteString(string(s))
 | |
| 			sp = " "
 | |
| 		}
 | |
| 		w.WriteString("]")
 | |
| 		if numARPA > 0 {
 | |
| 			fmt.Fprintf(w, "+%darpa", numARPA)
 | |
| 		}
 | |
| 	}
 | |
| 	w.WriteString("}")
 | |
| }
 | |
| 
 | |
| func (o OSConfig) IsZero() bool {
 | |
| 	return len(o.Hosts) == 0 &&
 | |
| 		len(o.Nameservers) == 0 &&
 | |
| 		len(o.SearchDomains) == 0 &&
 | |
| 		len(o.MatchDomains) == 0
 | |
| }
 | |
| 
 | |
| func (a OSConfig) Equal(b OSConfig) bool {
 | |
| 	if len(a.Hosts) != len(b.Hosts) {
 | |
| 		return false
 | |
| 	}
 | |
| 	if len(a.Nameservers) != len(b.Nameservers) {
 | |
| 		return false
 | |
| 	}
 | |
| 	if len(a.SearchDomains) != len(b.SearchDomains) {
 | |
| 		return false
 | |
| 	}
 | |
| 	if len(a.MatchDomains) != len(b.MatchDomains) {
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	for i := range a.Hosts {
 | |
| 		ha, hb := a.Hosts[i], b.Hosts[i]
 | |
| 		if ha.Addr != hb.Addr {
 | |
| 			return false
 | |
| 		}
 | |
| 		if !slices.Equal(ha.Hosts, hb.Hosts) {
 | |
| 			return false
 | |
| 		}
 | |
| 	}
 | |
| 	for i := range a.Nameservers {
 | |
| 		if a.Nameservers[i] != b.Nameservers[i] {
 | |
| 			return false
 | |
| 		}
 | |
| 	}
 | |
| 	for i := range a.SearchDomains {
 | |
| 		if a.SearchDomains[i] != b.SearchDomains[i] {
 | |
| 			return false
 | |
| 		}
 | |
| 	}
 | |
| 	for i := range a.MatchDomains {
 | |
| 		if a.MatchDomains[i] != b.MatchDomains[i] {
 | |
| 			return false
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| // Format implements the fmt.Formatter interface to ensure that Hosts is
 | |
| // printed correctly (i.e. not as a bunch of pointers).
 | |
| //
 | |
| // Fixes https://github.com/tailscale/tailscale/issues/5669
 | |
| func (a OSConfig) Format(f fmt.State, verb rune) {
 | |
| 	logger.ArgWriter(func(w *bufio.Writer) {
 | |
| 		w.WriteString(`{Nameservers:[`)
 | |
| 		for i, ns := range a.Nameservers {
 | |
| 			if i != 0 {
 | |
| 				w.WriteString(" ")
 | |
| 			}
 | |
| 			fmt.Fprintf(w, "%+v", ns)
 | |
| 		}
 | |
| 		w.WriteString(`] SearchDomains:[`)
 | |
| 		for i, domain := range a.SearchDomains {
 | |
| 			if i != 0 {
 | |
| 				w.WriteString(" ")
 | |
| 			}
 | |
| 			fmt.Fprintf(w, "%+v", domain)
 | |
| 		}
 | |
| 		w.WriteString(`] MatchDomains:[`)
 | |
| 		for i, domain := range a.MatchDomains {
 | |
| 			if i != 0 {
 | |
| 				w.WriteString(" ")
 | |
| 			}
 | |
| 			fmt.Fprintf(w, "%+v", domain)
 | |
| 		}
 | |
| 		w.WriteString(`] Hosts:[`)
 | |
| 		for i, host := range a.Hosts {
 | |
| 			if i != 0 {
 | |
| 				w.WriteString(" ")
 | |
| 			}
 | |
| 			fmt.Fprintf(w, "%+v", host)
 | |
| 		}
 | |
| 		w.WriteString(`]}`)
 | |
| 	}).Format(f, verb)
 | |
| }
 | |
| 
 | |
| // ErrGetBaseConfigNotSupported is the error
 | |
| // OSConfigurator.GetBaseConfig returns when the OSConfigurator
 | |
| // doesn't support reading the underlying configuration out of the OS.
 | |
| var ErrGetBaseConfigNotSupported = errors.New("getting OS base config is not supported")
 |