Update dependencies
This commit is contained in:
182
vendor/tailscale.com/net/dns/config.go
generated
vendored
Normal file
182
vendor/tailscale.com/net/dns/config.go
generated
vendored
Normal file
@@ -0,0 +1,182 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package dns contains code to configure and manage DNS settings.
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"sort"
|
||||
|
||||
"tailscale.com/net/dns/publicdns"
|
||||
"tailscale.com/net/dns/resolver"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
// Config is a DNS configuration.
|
||||
type Config struct {
|
||||
// DefaultResolvers are the DNS resolvers to use for DNS names
|
||||
// which aren't covered by more specific per-domain routes below.
|
||||
// If empty, the OS's default resolvers (the ones that predate
|
||||
// Tailscale altering the configuration) are used.
|
||||
DefaultResolvers []*dnstype.Resolver
|
||||
// Routes maps a DNS suffix to the resolvers that should be used
|
||||
// for queries that fall within that suffix.
|
||||
// If a query doesn't match any entry in Routes, the
|
||||
// DefaultResolvers are used.
|
||||
// A Routes entry with no resolvers means the route should be
|
||||
// authoritatively answered using the contents of Hosts.
|
||||
Routes map[dnsname.FQDN][]*dnstype.Resolver
|
||||
// SearchDomains are DNS suffixes to try when expanding
|
||||
// single-label queries.
|
||||
SearchDomains []dnsname.FQDN
|
||||
// Hosts maps DNS FQDNs to their IPs, which can be a mix of IPv4
|
||||
// and IPv6.
|
||||
// Queries matching entries in Hosts are resolved locally by
|
||||
// 100.100.100.100 without leaving the machine.
|
||||
// Adding an entry to Hosts merely creates the record. If you want
|
||||
// it to resolve, you also need to add appropriate routes to
|
||||
// Routes.
|
||||
Hosts map[dnsname.FQDN][]netip.Addr
|
||||
// OnlyIPv6, if true, uses the IPv6 service IP (for MagicDNS)
|
||||
// instead of the IPv4 version (100.100.100.100).
|
||||
OnlyIPv6 bool
|
||||
}
|
||||
|
||||
func (c *Config) serviceIP() netip.Addr {
|
||||
if c.OnlyIPv6 {
|
||||
return tsaddr.TailscaleServiceIPv6()
|
||||
}
|
||||
return tsaddr.TailscaleServiceIP()
|
||||
}
|
||||
|
||||
// WriteToBufioWriter write a debug version of c for logs to w, omitting
|
||||
// spammy stuff like *.arpa entries and replacing it with a total count.
|
||||
func (c *Config) WriteToBufioWriter(w *bufio.Writer) {
|
||||
w.WriteString("{DefaultResolvers:")
|
||||
resolver.WriteDNSResolvers(w, c.DefaultResolvers)
|
||||
|
||||
w.WriteString(" Routes:")
|
||||
resolver.WriteRoutes(w, c.Routes)
|
||||
|
||||
fmt.Fprintf(w, " SearchDomains:%v", c.SearchDomains)
|
||||
fmt.Fprintf(w, " Hosts:%v", len(c.Hosts))
|
||||
w.WriteString("}")
|
||||
}
|
||||
|
||||
// needsAnyResolvers reports whether c requires a resolver to be set
|
||||
// at the OS level.
|
||||
func (c Config) needsOSResolver() bool {
|
||||
return c.hasDefaultResolvers() || c.hasRoutes()
|
||||
}
|
||||
|
||||
func (c Config) hasRoutes() bool {
|
||||
return len(c.Routes) > 0
|
||||
}
|
||||
|
||||
// hasDefaultIPResolversOnly reports whether the only resolvers in c are
|
||||
// DefaultResolvers, and that those resolvers are simple IP addresses
|
||||
// that speak regular port 53 DNS.
|
||||
func (c Config) hasDefaultIPResolversOnly() bool {
|
||||
if !c.hasDefaultResolvers() || c.hasRoutes() {
|
||||
return false
|
||||
}
|
||||
for _, r := range c.DefaultResolvers {
|
||||
if ipp, ok := r.IPPort(); !ok || ipp.Port() != 53 || publicdns.IPIsDoHOnlyServer(ipp.Addr()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// hasHostsWithoutSplitDNSRoutes reports whether c contains any Host entries
|
||||
// that aren't covered by a SplitDNS route suffix.
|
||||
func (c Config) hasHostsWithoutSplitDNSRoutes() bool {
|
||||
// TODO(bradfitz): this could be more efficient, but we imagine
|
||||
// the number of SplitDNS routes and/or hosts will be small.
|
||||
for host := range c.Hosts {
|
||||
if !c.hasSplitDNSRouteForHost(host) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// hasSplitDNSRouteForHost reports whether c contains a SplitDNS route
|
||||
// that contains hosts.
|
||||
func (c Config) hasSplitDNSRouteForHost(host dnsname.FQDN) bool {
|
||||
for route := range c.Routes {
|
||||
if route.Contains(host) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c Config) hasDefaultResolvers() bool {
|
||||
return len(c.DefaultResolvers) > 0
|
||||
}
|
||||
|
||||
// singleResolverSet returns the resolvers used by c.Routes if all
|
||||
// routes use the same resolvers, or nil if multiple sets of resolvers
|
||||
// are specified.
|
||||
func (c Config) singleResolverSet() []*dnstype.Resolver {
|
||||
var (
|
||||
prev []*dnstype.Resolver
|
||||
prevInitialized bool
|
||||
)
|
||||
for _, resolvers := range c.Routes {
|
||||
if !prevInitialized {
|
||||
prev = resolvers
|
||||
prevInitialized = true
|
||||
continue
|
||||
}
|
||||
if !sameResolverNames(prev, resolvers) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return prev
|
||||
}
|
||||
|
||||
// matchDomains returns the list of match suffixes needed by Routes.
|
||||
func (c Config) matchDomains() []dnsname.FQDN {
|
||||
ret := make([]dnsname.FQDN, 0, len(c.Routes))
|
||||
for suffix := range c.Routes {
|
||||
ret = append(ret, suffix)
|
||||
}
|
||||
sort.Slice(ret, func(i, j int) bool {
|
||||
return ret[i].WithTrailingDot() < ret[j].WithTrailingDot()
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
func sameResolverNames(a, b []*dnstype.Resolver) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i].Addr != b[i].Addr {
|
||||
return false
|
||||
}
|
||||
if !sameIPs(a[i].BootstrapResolution, b[i].BootstrapResolution) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func sameIPs(a, b []netip.Addr) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
184
vendor/tailscale.com/net/dns/debian_resolvconf.go
generated
vendored
Normal file
184
vendor/tailscale.com/net/dns/debian_resolvconf.go
generated
vendored
Normal file
@@ -0,0 +1,184 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux || freebsd || openbsd
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
//go:embed resolvconf-workaround.sh
|
||||
var workaroundScript []byte
|
||||
|
||||
// resolvconfConfigName is the name of the config submitted to
|
||||
// resolvconf.
|
||||
// The name starts with 'tun' in order to match the hardcoded
|
||||
// interface order in debian resolvconf, which will place this
|
||||
// configuration ahead of regular network links. In theory, this
|
||||
// doesn't matter because we then fix things up to ensure our config
|
||||
// is the only one in use, but in case that fails, this will make our
|
||||
// configuration slightly preferred.
|
||||
// The 'inet' suffix has no specific meaning, but conventionally
|
||||
// resolvconf implementations encourage adding a suffix roughly
|
||||
// indicating where the config came from, and "inet" is the "none of
|
||||
// the above" value (rather than, say, "ppp" or "dhcp").
|
||||
const resolvconfConfigName = "tun-tailscale.inet"
|
||||
|
||||
// resolvconfLibcHookPath is the directory containing libc update
|
||||
// scripts, which are run by Debian resolvconf when /etc/resolv.conf
|
||||
// has been updated.
|
||||
const resolvconfLibcHookPath = "/etc/resolvconf/update-libc.d"
|
||||
|
||||
// resolvconfHookPath is the name of the libc hook script we install
|
||||
// to force Tailscale's DNS config to take effect.
|
||||
var resolvconfHookPath = filepath.Join(resolvconfLibcHookPath, "tailscale")
|
||||
|
||||
// resolvconfManager manages DNS configuration using the Debian
|
||||
// implementation of the `resolvconf` program, written by Thomas Hood.
|
||||
type resolvconfManager struct {
|
||||
logf logger.Logf
|
||||
listRecordsPath string
|
||||
interfacesDir string
|
||||
scriptInstalled bool // libc update script has been installed
|
||||
}
|
||||
|
||||
func newDebianResolvconfManager(logf logger.Logf) (*resolvconfManager, error) {
|
||||
ret := &resolvconfManager{
|
||||
logf: logf,
|
||||
listRecordsPath: "/lib/resolvconf/list-records",
|
||||
interfacesDir: "/etc/resolvconf/run/interface", // panic fallback if nothing seems to work
|
||||
}
|
||||
|
||||
if _, err := os.Stat(ret.listRecordsPath); os.IsNotExist(err) {
|
||||
// This might be a Debian system from before the big /usr
|
||||
// merge, try /usr instead.
|
||||
ret.listRecordsPath = "/usr" + ret.listRecordsPath
|
||||
}
|
||||
// The runtime directory is currently (2020-04) canonically
|
||||
// /etc/resolvconf/run, but the manpage is making noise about
|
||||
// switching to /run/resolvconf and dropping the /etc path. So,
|
||||
// let's probe the possible directories and use the first one
|
||||
// that works.
|
||||
for _, path := range []string{
|
||||
"/etc/resolvconf/run/interface",
|
||||
"/run/resolvconf/interface",
|
||||
"/var/run/resolvconf/interface",
|
||||
} {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
ret.interfacesDir = path
|
||||
break
|
||||
}
|
||||
}
|
||||
if ret.interfacesDir == "" {
|
||||
// None of the paths seem to work, use the canonical location
|
||||
// that the current manpage says to use.
|
||||
ret.interfacesDir = "/etc/resolvconf/run/interfaces"
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (m *resolvconfManager) deleteTailscaleConfig() error {
|
||||
cmd := exec.Command("resolvconf", "-d", resolvconfConfigName)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("running %s: %s", cmd, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *resolvconfManager) SetDNS(config OSConfig) error {
|
||||
if !m.scriptInstalled {
|
||||
m.logf("injecting resolvconf workaround script")
|
||||
if err := os.MkdirAll(resolvconfLibcHookPath, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := atomicfile.WriteFile(resolvconfHookPath, workaroundScript, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
m.scriptInstalled = true
|
||||
}
|
||||
|
||||
if config.IsZero() {
|
||||
if err := m.deleteTailscaleConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
stdin := new(bytes.Buffer)
|
||||
writeResolvConf(stdin, config.Nameservers, config.SearchDomains) // dns_direct.go
|
||||
|
||||
// This resolvconf implementation doesn't support exclusive
|
||||
// mode or interface priorities, so it will end up blending
|
||||
// our configuration with other sources. However, this will
|
||||
// get fixed up by the script we injected above.
|
||||
cmd := exec.Command("resolvconf", "-a", resolvconfConfigName)
|
||||
cmd.Stdin = stdin
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("running %s: %s", cmd, out)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *resolvconfManager) SupportsSplitDNS() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *resolvconfManager) GetBaseConfig() (OSConfig, error) {
|
||||
var bs bytes.Buffer
|
||||
|
||||
cmd := exec.Command(m.listRecordsPath)
|
||||
// list-records assumes it's being run with CWD set to the
|
||||
// interfaces runtime dir, and returns nonsense otherwise.
|
||||
cmd.Dir = m.interfacesDir
|
||||
cmd.Stdout = &bs
|
||||
if err := cmd.Run(); err != nil {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
|
||||
var conf bytes.Buffer
|
||||
sc := bufio.NewScanner(&bs)
|
||||
for sc.Scan() {
|
||||
if sc.Text() == resolvconfConfigName {
|
||||
continue
|
||||
}
|
||||
bs, err := os.ReadFile(filepath.Join(m.interfacesDir, sc.Text()))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// Probably raced with a deletion, that's okay.
|
||||
continue
|
||||
}
|
||||
return OSConfig{}, err
|
||||
}
|
||||
conf.Write(bs)
|
||||
conf.WriteByte('\n')
|
||||
}
|
||||
|
||||
return readResolv(&conf)
|
||||
}
|
||||
|
||||
func (m *resolvconfManager) Close() error {
|
||||
if err := m.deleteTailscaleConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if m.scriptInstalled {
|
||||
m.logf("removing resolvconf workaround script")
|
||||
os.Remove(resolvconfHookPath) // Best-effort
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
551
vendor/tailscale.com/net/dns/direct.go
generated
vendored
Normal file
551
vendor/tailscale.com/net/dns/direct.go
generated
vendored
Normal file
@@ -0,0 +1,551 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/dns/resolvconffile"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
// writeResolvConf writes DNS configuration in resolv.conf format to the given writer.
|
||||
func writeResolvConf(w io.Writer, servers []netip.Addr, domains []dnsname.FQDN) error {
|
||||
c := &resolvconffile.Config{
|
||||
Nameservers: servers,
|
||||
SearchDomains: domains,
|
||||
}
|
||||
return c.Write(w)
|
||||
}
|
||||
|
||||
func readResolv(r io.Reader) (OSConfig, error) {
|
||||
c, err := resolvconffile.Parse(r)
|
||||
if err != nil {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
return OSConfig{
|
||||
Nameservers: c.Nameservers,
|
||||
SearchDomains: c.SearchDomains,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// resolvOwner returns the apparent owner of the resolv.conf
|
||||
// configuration in bs - one of "resolvconf", "systemd-resolved" or
|
||||
// "NetworkManager", or "" if no known owner was found.
|
||||
//
|
||||
//lint:ignore U1000 used in linux and freebsd code
|
||||
func resolvOwner(bs []byte) string {
|
||||
likely := ""
|
||||
b := bytes.NewBuffer(bs)
|
||||
for {
|
||||
line, err := b.ReadString('\n')
|
||||
if err != nil {
|
||||
return likely
|
||||
}
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
if line[0] != '#' {
|
||||
// First non-empty, non-comment line. Assume the owner
|
||||
// isn't hiding further down.
|
||||
return likely
|
||||
}
|
||||
|
||||
if strings.Contains(line, "systemd-resolved") {
|
||||
likely = "systemd-resolved"
|
||||
} else if strings.Contains(line, "NetworkManager") {
|
||||
likely = "NetworkManager"
|
||||
} else if strings.Contains(line, "resolvconf") {
|
||||
likely = "resolvconf"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isResolvedRunning reports whether systemd-resolved is running on the system,
|
||||
// even if it is not managing the system DNS settings.
|
||||
func isResolvedRunning() bool {
|
||||
if runtime.GOOS != "linux" {
|
||||
return false
|
||||
}
|
||||
|
||||
// systemd-resolved is never installed without systemd.
|
||||
_, err := exec.LookPath("systemctl")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
err = exec.CommandContext(ctx, "systemctl", "is-active", "systemd-resolved.service").Run()
|
||||
|
||||
// is-active exits with code 3 if the service is not active.
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func restartResolved() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
return exec.CommandContext(ctx, "systemctl", "restart", "systemd-resolved.service").Run()
|
||||
}
|
||||
|
||||
// directManager is an OSConfigurator which replaces /etc/resolv.conf with a file
|
||||
// generated from the given configuration, creating a backup of its old state.
|
||||
//
|
||||
// This way of configuring DNS is precarious, since it does not react
|
||||
// to the disappearance of the Tailscale interface.
|
||||
// The caller must call Down before program shutdown
|
||||
// or as cleanup if the program terminates unexpectedly.
|
||||
type directManager struct {
|
||||
logf logger.Logf
|
||||
health *health.Tracker
|
||||
fs wholeFileFS
|
||||
// renameBroken is set if fs.Rename to or from /etc/resolv.conf
|
||||
// fails. This can happen in some container runtimes, where
|
||||
// /etc/resolv.conf is bind-mounted from outside the container,
|
||||
// and therefore /etc and /etc/resolv.conf are different
|
||||
// filesystems as far as rename(2) is concerned.
|
||||
//
|
||||
// In those situations, we fall back to emulating rename with file
|
||||
// copies and truncations, which is not as good (opens up a race
|
||||
// where a reader can see an empty or partial /etc/resolv.conf),
|
||||
// but is better than having non-functioning DNS.
|
||||
renameBroken bool
|
||||
|
||||
ctx context.Context // valid until Close
|
||||
ctxClose context.CancelFunc // closes ctx
|
||||
|
||||
mu sync.Mutex
|
||||
wantResolvConf []byte // if non-nil, what we expect /etc/resolv.conf to contain
|
||||
//lint:ignore U1000 used in direct_linux.go
|
||||
lastWarnContents []byte // last resolv.conf contents that we warned about
|
||||
}
|
||||
|
||||
//lint:ignore U1000 used in manager_{freebsd,openbsd}.go
|
||||
func newDirectManager(logf logger.Logf, health *health.Tracker) *directManager {
|
||||
return newDirectManagerOnFS(logf, health, directFS{})
|
||||
}
|
||||
|
||||
func newDirectManagerOnFS(logf logger.Logf, health *health.Tracker, fs wholeFileFS) *directManager {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
m := &directManager{
|
||||
logf: logf,
|
||||
health: health,
|
||||
fs: fs,
|
||||
ctx: ctx,
|
||||
ctxClose: cancel,
|
||||
}
|
||||
go m.runFileWatcher()
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *directManager) readResolvFile(path string) (OSConfig, error) {
|
||||
b, err := m.fs.ReadFile(path)
|
||||
if err != nil {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
return readResolv(bytes.NewReader(b))
|
||||
}
|
||||
|
||||
// ownedByTailscale reports whether /etc/resolv.conf seems to be a
|
||||
// tailscale-managed file.
|
||||
func (m *directManager) ownedByTailscale() (bool, error) {
|
||||
isRegular, err := m.fs.Stat(resolvConf)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
if !isRegular {
|
||||
return false, nil
|
||||
}
|
||||
bs, err := m.fs.ReadFile(resolvConf)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if bytes.Contains(bs, []byte("generated by tailscale")) {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// backupConfig creates or updates a backup of /etc/resolv.conf, if
|
||||
// resolv.conf does not currently contain a Tailscale-managed config.
|
||||
func (m *directManager) backupConfig() error {
|
||||
if _, err := m.fs.Stat(resolvConf); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// No resolv.conf, nothing to back up. Also get rid of any
|
||||
// existing backup file, to avoid restoring something old.
|
||||
m.fs.Remove(backupConf)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
owned, err := m.ownedByTailscale()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if owned {
|
||||
return nil
|
||||
}
|
||||
|
||||
return m.rename(resolvConf, backupConf)
|
||||
}
|
||||
|
||||
func (m *directManager) restoreBackup() (restored bool, err error) {
|
||||
if _, err := m.fs.Stat(backupConf); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// No backup, nothing we can do.
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
owned, err := m.ownedByTailscale()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
_, err = m.fs.Stat(resolvConf)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return false, err
|
||||
}
|
||||
resolvConfExists := !os.IsNotExist(err)
|
||||
|
||||
if resolvConfExists && !owned {
|
||||
// There's already a non-tailscale config in place, get rid of
|
||||
// our backup.
|
||||
m.fs.Remove(backupConf)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// We own resolv.conf, and a backup exists.
|
||||
if err := m.rename(backupConf, resolvConf); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// rename tries to rename old to new using m.fs.Rename, and falls back
|
||||
// to hand-copying bytes and truncating old if that fails.
|
||||
//
|
||||
// This is a workaround to /etc/resolv.conf being a bind-mounted file
|
||||
// some container environments, which cannot be moved elsewhere in
|
||||
// /etc (because that would be a cross-filesystem move) or deleted
|
||||
// (because that would break the bind in surprising ways).
|
||||
func (m *directManager) rename(old, new string) error {
|
||||
if !m.renameBroken {
|
||||
err := m.fs.Rename(old, new)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if runtime.GOOS == "linux" && distro.Get() == distro.Synology {
|
||||
// Fail fast. The fallback case below won't work anyway.
|
||||
return err
|
||||
}
|
||||
m.logf("rename of %q to %q failed (%v), falling back to copy+delete", old, new, err)
|
||||
m.renameBroken = true
|
||||
}
|
||||
|
||||
bs, err := m.fs.ReadFile(old)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading %q to rename: %w", old, err)
|
||||
}
|
||||
if err := m.fs.WriteFile(new, bs, 0644); err != nil {
|
||||
return fmt.Errorf("writing to %q in rename of %q: %w", new, old, err)
|
||||
}
|
||||
|
||||
// Explicitly set the permissions on the new file. This ensures that
|
||||
// if we have a umask set which prevents creating world-readable files,
|
||||
// the file will still have the correct permissions once it's renamed
|
||||
// into place. See #12609.
|
||||
if err := m.fs.Chmod(new, 0644); err != nil {
|
||||
return fmt.Errorf("chmod %q in rename of %q: %w", new, old, err)
|
||||
}
|
||||
|
||||
if err := m.fs.Remove(old); err != nil {
|
||||
err2 := m.fs.Truncate(old)
|
||||
if err2 != nil {
|
||||
return fmt.Errorf("remove of %q failed (%w) and so did truncate: %v", old, err, err2)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setWant sets the expected contents of /etc/resolv.conf, if any.
|
||||
//
|
||||
// A value of nil means no particular value is expected.
|
||||
//
|
||||
// m takes ownership of want.
|
||||
func (m *directManager) setWant(want []byte) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.wantResolvConf = want
|
||||
}
|
||||
|
||||
func (m *directManager) SetDNS(config OSConfig) (err error) {
|
||||
defer func() {
|
||||
if err != nil && errors.Is(err, fs.ErrPermission) && runtime.GOOS == "linux" &&
|
||||
distro.Get() == distro.Synology && os.Geteuid() != 0 {
|
||||
// On Synology (notably DSM7 where we don't run as root), ignore all
|
||||
// DNS configuration errors for now. We don't have permission.
|
||||
// See https://github.com/tailscale/tailscale/issues/4017
|
||||
m.logf("ignoring SetDNS permission error on Synology (Issue 4017); was: %v", err)
|
||||
err = nil
|
||||
}
|
||||
}()
|
||||
m.setWant(nil) // reset our expectations before any work
|
||||
var changed bool
|
||||
if config.IsZero() {
|
||||
changed, err = m.restoreBackup()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
changed = true
|
||||
if err := m.backupConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
writeResolvConf(buf, config.Nameservers, config.SearchDomains)
|
||||
if err := m.atomicWriteFile(m.fs, resolvConf, buf.Bytes(), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Now that we've successfully written to the file, lock it in.
|
||||
// If we see /etc/resolv.conf with different contents, we know somebody
|
||||
// else trampled on it.
|
||||
m.setWant(buf.Bytes())
|
||||
}
|
||||
|
||||
// We might have taken over a configuration managed by resolved,
|
||||
// in which case it will notice this on restart and gracefully
|
||||
// start using our configuration. This shouldn't happen because we
|
||||
// try to manage DNS through resolved when it's around, but as a
|
||||
// best-effort fallback if we messed up the detection, try to
|
||||
// restart resolved to make the system configuration consistent.
|
||||
//
|
||||
// We take care to only kick systemd-resolved if we've made some
|
||||
// change to the system's DNS configuration, because this codepath
|
||||
// can end up running in cases where the user has manually
|
||||
// configured /etc/resolv.conf to point to systemd-resolved (but
|
||||
// it's not managed explicitly by systemd-resolved), *and* has
|
||||
// --accept-dns=false, meaning we pass an empty configuration to
|
||||
// the running DNS manager. In that very edge-case scenario, we
|
||||
// cause a disruptive DNS outage each time we reset an empty
|
||||
// OS configuration.
|
||||
if changed && isResolvedRunning() && !runningAsGUIDesktopUser() {
|
||||
t0 := time.Now()
|
||||
err := restartResolved()
|
||||
d := time.Since(t0).Round(time.Millisecond)
|
||||
if err != nil {
|
||||
m.logf("error restarting resolved after %v: %v", d, err)
|
||||
} else {
|
||||
m.logf("restarted resolved after %v", d)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *directManager) SupportsSplitDNS() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *directManager) GetBaseConfig() (OSConfig, error) {
|
||||
owned, err := m.ownedByTailscale()
|
||||
if err != nil {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
fileToRead := resolvConf
|
||||
if owned {
|
||||
fileToRead = backupConf
|
||||
}
|
||||
|
||||
oscfg, err := m.readResolvFile(fileToRead)
|
||||
if err != nil {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
|
||||
// On some systems, the backup configuration file is actually a
|
||||
// symbolic link to something owned by another DNS service (commonly,
|
||||
// resolved). Thus, it can be updated out from underneath us to contain
|
||||
// the Tailscale service IP, which results in an infinite loop of us
|
||||
// trying to send traffic to resolved, which sends back to us, and so
|
||||
// on. To solve this, drop the Tailscale service IP from the base
|
||||
// configuration; we do this in all situations since there's
|
||||
// essentially no world where we want to forward to ourselves.
|
||||
//
|
||||
// See: https://github.com/tailscale/tailscale/issues/7816
|
||||
var removed bool
|
||||
oscfg.Nameservers = slices.DeleteFunc(oscfg.Nameservers, func(ip netip.Addr) bool {
|
||||
if ip == tsaddr.TailscaleServiceIP() || ip == tsaddr.TailscaleServiceIPv6() {
|
||||
removed = true
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
if removed {
|
||||
m.logf("[v1] dropped Tailscale IP from base config that was a symlink")
|
||||
}
|
||||
return oscfg, nil
|
||||
}
|
||||
|
||||
func (m *directManager) Close() error {
|
||||
m.ctxClose()
|
||||
|
||||
// We used to keep a file for the tailscale config and symlinked
|
||||
// to it, but then we stopped because /etc/resolv.conf being a
|
||||
// symlink to surprising places breaks snaps and other sandboxing
|
||||
// things. Clean it up if it's still there.
|
||||
m.fs.Remove("/etc/resolv.tailscale.conf")
|
||||
|
||||
if _, err := m.fs.Stat(backupConf); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// No backup, nothing we can do.
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
owned, err := m.ownedByTailscale()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = m.fs.Stat(resolvConf)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
resolvConfExists := !os.IsNotExist(err)
|
||||
|
||||
if resolvConfExists && !owned {
|
||||
// There's already a non-tailscale config in place, get rid of
|
||||
// our backup.
|
||||
m.fs.Remove(backupConf)
|
||||
return nil
|
||||
}
|
||||
|
||||
// We own resolv.conf, and a backup exists.
|
||||
if err := m.rename(backupConf, resolvConf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isResolvedRunning() && !runningAsGUIDesktopUser() {
|
||||
m.logf("restarting systemd-resolved...")
|
||||
if err := restartResolved(); err != nil {
|
||||
m.logf("restart of systemd-resolved failed: %v", err)
|
||||
} else {
|
||||
m.logf("restarted systemd-resolved")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *directManager) atomicWriteFile(fs wholeFileFS, filename string, data []byte, perm os.FileMode) error {
|
||||
var randBytes [12]byte
|
||||
if _, err := rand.Read(randBytes[:]); err != nil {
|
||||
return fmt.Errorf("atomicWriteFile: %w", err)
|
||||
}
|
||||
|
||||
tmpName := fmt.Sprintf("%s.%x.tmp", filename, randBytes[:])
|
||||
defer fs.Remove(tmpName)
|
||||
|
||||
if err := fs.WriteFile(tmpName, data, perm); err != nil {
|
||||
return fmt.Errorf("atomicWriteFile: %w", err)
|
||||
}
|
||||
// Explicitly set the permissions on the temporary file before renaming
|
||||
// it. This ensures that if we have a umask set which prevents creating
|
||||
// world-readable files, the file will still have the correct
|
||||
// permissions once it's renamed into place. See #12609.
|
||||
if err := fs.Chmod(tmpName, perm); err != nil {
|
||||
return fmt.Errorf("atomicWriteFile: Chmod: %w", err)
|
||||
}
|
||||
|
||||
return m.rename(tmpName, filename)
|
||||
}
|
||||
|
||||
// wholeFileFS is a high-level file system abstraction designed just for use
|
||||
// by directManager, with the goal that it is easy to implement over wsl.exe.
|
||||
//
|
||||
// All name parameters are absolute paths.
|
||||
type wholeFileFS interface {
|
||||
Chmod(name string, mode os.FileMode) error
|
||||
ReadFile(name string) ([]byte, error)
|
||||
Remove(name string) error
|
||||
Rename(oldName, newName string) error
|
||||
Stat(name string) (isRegular bool, err error)
|
||||
Truncate(name string) error
|
||||
WriteFile(name string, contents []byte, perm os.FileMode) error
|
||||
}
|
||||
|
||||
// directFS is a wholeFileFS implemented directly on the OS.
|
||||
type directFS struct {
|
||||
// prefix is file path prefix.
|
||||
//
|
||||
// All name parameters are absolute paths so this is typically a
|
||||
// testing temporary directory like "/tmp".
|
||||
prefix string
|
||||
}
|
||||
|
||||
func (fs directFS) path(name string) string { return filepath.Join(fs.prefix, name) }
|
||||
|
||||
func (fs directFS) Stat(name string) (isRegular bool, err error) {
|
||||
fi, err := os.Stat(fs.path(name))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return fi.Mode().IsRegular(), nil
|
||||
}
|
||||
|
||||
func (fs directFS) Chmod(name string, mode os.FileMode) error {
|
||||
return os.Chmod(fs.path(name), mode)
|
||||
}
|
||||
|
||||
func (fs directFS) Rename(oldName, newName string) error {
|
||||
return os.Rename(fs.path(oldName), fs.path(newName))
|
||||
}
|
||||
|
||||
func (fs directFS) Remove(name string) error { return os.Remove(fs.path(name)) }
|
||||
|
||||
func (fs directFS) ReadFile(name string) ([]byte, error) {
|
||||
return os.ReadFile(fs.path(name))
|
||||
}
|
||||
|
||||
func (fs directFS) Truncate(name string) error {
|
||||
return os.Truncate(fs.path(name), 0)
|
||||
}
|
||||
|
||||
func (fs directFS) WriteFile(name string, contents []byte, perm os.FileMode) error {
|
||||
return os.WriteFile(fs.path(name), contents, perm)
|
||||
}
|
||||
|
||||
// runningAsGUIDesktopUser reports whether it seems that this code is
|
||||
// being run as a regular user on a Linux desktop. This is a quick
|
||||
// hack to fix Issue 2672 where PolicyKit pops up a GUI dialog asking
|
||||
// to proceed we do a best effort attempt to restart
|
||||
// systemd-resolved.service. There's surely a better way.
|
||||
func runningAsGUIDesktopUser() bool {
|
||||
return os.Getuid() != 0 && os.Getenv("DISPLAY") != ""
|
||||
}
|
||||
108
vendor/tailscale.com/net/dns/direct_linux.go
generated
vendored
Normal file
108
vendor/tailscale.com/net/dns/direct_linux.go
generated
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"github.com/illarion/gonotify/v2"
|
||||
"tailscale.com/health"
|
||||
)
|
||||
|
||||
func (m *directManager) runFileWatcher() {
|
||||
ctx, cancel := context.WithCancel(m.ctx)
|
||||
defer cancel()
|
||||
in, err := gonotify.NewInotify(ctx)
|
||||
if err != nil {
|
||||
// Oh well, we tried. This is all best effort for now, to
|
||||
// surface warnings to users.
|
||||
m.logf("dns: inotify new: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
const events = gonotify.IN_ATTRIB |
|
||||
gonotify.IN_CLOSE_WRITE |
|
||||
gonotify.IN_CREATE |
|
||||
gonotify.IN_DELETE |
|
||||
gonotify.IN_MODIFY |
|
||||
gonotify.IN_MOVE
|
||||
|
||||
if err := in.AddWatch("/etc/", events); err != nil {
|
||||
m.logf("dns: inotify addwatch: %v", err)
|
||||
return
|
||||
}
|
||||
for {
|
||||
events, err := in.Read()
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
m.logf("dns: inotify read: %v", err)
|
||||
return
|
||||
}
|
||||
var match bool
|
||||
for _, ev := range events {
|
||||
if ev.Name == resolvConf {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
m.checkForFileTrample()
|
||||
}
|
||||
}
|
||||
|
||||
var resolvTrampleWarnable = health.Register(&health.Warnable{
|
||||
Code: "resolv-conf-overwritten",
|
||||
Severity: health.SeverityMedium,
|
||||
Title: "Linux DNS configuration issue",
|
||||
Text: health.StaticMessage("Linux DNS config not ideal. /etc/resolv.conf overwritten. See https://tailscale.com/s/dns-fight"),
|
||||
})
|
||||
|
||||
// checkForFileTrample checks whether /etc/resolv.conf has been trampled
|
||||
// by another program on the system. (e.g. a DHCP client)
|
||||
func (m *directManager) checkForFileTrample() {
|
||||
m.mu.Lock()
|
||||
want := m.wantResolvConf
|
||||
lastWarn := m.lastWarnContents
|
||||
m.mu.Unlock()
|
||||
|
||||
if want == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cur, err := m.fs.ReadFile(resolvConf)
|
||||
if err != nil {
|
||||
m.logf("trample: read error: %v", err)
|
||||
return
|
||||
}
|
||||
if bytes.Equal(cur, want) {
|
||||
m.health.SetHealthy(resolvTrampleWarnable)
|
||||
if lastWarn != nil {
|
||||
m.mu.Lock()
|
||||
m.lastWarnContents = nil
|
||||
m.mu.Unlock()
|
||||
m.logf("trample: resolv.conf again matches expected content")
|
||||
}
|
||||
return
|
||||
}
|
||||
if bytes.Equal(cur, lastWarn) {
|
||||
// We already logged about this, so not worth doing it again.
|
||||
return
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
m.lastWarnContents = cur
|
||||
m.mu.Unlock()
|
||||
|
||||
show := cur
|
||||
if len(show) > 1024 {
|
||||
show = show[:1024]
|
||||
}
|
||||
m.logf("trample: resolv.conf changed from what we expected. did some other program interfere? current contents: %q", show)
|
||||
m.health.SetUnhealthy(resolvTrampleWarnable, nil)
|
||||
}
|
||||
10
vendor/tailscale.com/net/dns/direct_notlinux.go
generated
vendored
Normal file
10
vendor/tailscale.com/net/dns/direct_notlinux.go
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !linux
|
||||
|
||||
package dns
|
||||
|
||||
func (m *directManager) runFileWatcher() {
|
||||
// Not implemented on other platforms. Maybe it could resort to polling.
|
||||
}
|
||||
10
vendor/tailscale.com/net/dns/flush_default.go
generated
vendored
Normal file
10
vendor/tailscale.com/net/dns/flush_default.go
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !windows
|
||||
|
||||
package dns
|
||||
|
||||
func flushCaches() error {
|
||||
return nil
|
||||
}
|
||||
34
vendor/tailscale.com/net/dns/flush_windows.go
generated
vendored
Normal file
34
vendor/tailscale.com/net/dns/flush_windows.go
generated
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func flushCaches() error {
|
||||
cmd := exec.Command("ipconfig", "/flushdns")
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
CreationFlags: windows.DETACHED_PROCESS,
|
||||
}
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%v (output: %s)", err, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Flush clears the local resolver cache.
|
||||
//
|
||||
// Only Windows has a public dns.Flush, needed in router_windows.go. Other
|
||||
// platforms like Linux need a different flush implementation depending on
|
||||
// the DNS manager. There is a FlushCaches method on the manager which
|
||||
// can be used on all platforms.
|
||||
func Flush() error {
|
||||
return flushCaches()
|
||||
}
|
||||
30
vendor/tailscale.com/net/dns/ini.go
generated
vendored
Normal file
30
vendor/tailscale.com/net/dns/ini.go
generated
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build windows
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// parseIni parses a basic .ini file, used for wsl.conf.
|
||||
func parseIni(data string) map[string]map[string]string {
|
||||
sectionRE := regexp.MustCompile(`^\[([^]]+)\]`)
|
||||
kvRE := regexp.MustCompile(`^\s*(\w+)\s*=\s*([^#]*)`)
|
||||
|
||||
ini := map[string]map[string]string{}
|
||||
var section string
|
||||
for _, line := range strings.Split(data, "\n") {
|
||||
if res := sectionRE.FindStringSubmatch(line); len(res) > 1 {
|
||||
section = res[1]
|
||||
ini[section] = map[string]string{}
|
||||
} else if res := kvRE.FindStringSubmatch(line); len(res) > 2 {
|
||||
k, v := strings.TrimSpace(res[1]), strings.TrimSpace(res[2])
|
||||
ini[section][k] = v
|
||||
}
|
||||
}
|
||||
return ini
|
||||
}
|
||||
564
vendor/tailscale.com/net/dns/manager.go
generated
vendored
Normal file
564
vendor/tailscale.com/net/dns/manager.go
generated
vendored
Normal file
@@ -0,0 +1,564 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
xmaps "golang.org/x/exp/maps"
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/dns/resolver"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tstime/rate"
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
var (
|
||||
errFullQueue = errors.New("request queue full")
|
||||
)
|
||||
|
||||
// maxActiveQueries returns the maximal number of DNS requests that can
|
||||
// be running.
|
||||
const maxActiveQueries = 256
|
||||
|
||||
// We use file-ignore below instead of ignore because on some platforms,
|
||||
// the lint exception is necessary and on others it is not,
|
||||
// and plain ignore complains if the exception is unnecessary.
|
||||
|
||||
// Manager manages system DNS settings.
|
||||
type Manager struct {
|
||||
logf logger.Logf
|
||||
health *health.Tracker
|
||||
|
||||
activeQueriesAtomic int32
|
||||
|
||||
ctx context.Context // good until Down
|
||||
ctxCancel context.CancelFunc // closes ctx
|
||||
|
||||
resolver *resolver.Resolver
|
||||
os OSConfigurator
|
||||
knobs *controlknobs.Knobs // or nil
|
||||
goos string // if empty, gets set to runtime.GOOS
|
||||
|
||||
mu sync.Mutex // guards following
|
||||
// config is the last configuration we successfully compiled or nil if there
|
||||
// was any failure applying the last configuration.
|
||||
config *Config
|
||||
}
|
||||
|
||||
// NewManagers created a new manager from the given config.
|
||||
//
|
||||
// knobs may be nil.
|
||||
func NewManager(logf logger.Logf, oscfg OSConfigurator, health *health.Tracker, dialer *tsdial.Dialer, linkSel resolver.ForwardLinkSelector, knobs *controlknobs.Knobs, goos string) *Manager {
|
||||
if dialer == nil {
|
||||
panic("nil Dialer")
|
||||
}
|
||||
if dialer.NetMon() == nil {
|
||||
panic("Dialer has nil NetMon")
|
||||
}
|
||||
logf = logger.WithPrefix(logf, "dns: ")
|
||||
if goos == "" {
|
||||
goos = runtime.GOOS
|
||||
}
|
||||
|
||||
m := &Manager{
|
||||
logf: logf,
|
||||
resolver: resolver.New(logf, linkSel, dialer, health, knobs),
|
||||
os: oscfg,
|
||||
health: health,
|
||||
knobs: knobs,
|
||||
goos: goos,
|
||||
}
|
||||
|
||||
// Rate limit our attempts to correct our DNS configuration.
|
||||
limiter := rate.NewLimiter(1.0/5.0, 1)
|
||||
|
||||
// This will recompile the DNS config, which in turn will requery the system
|
||||
// DNS settings. The recovery func should triggered only when we are missing
|
||||
// upstream nameservers and require them to forward a query.
|
||||
m.resolver.SetMissingUpstreamRecovery(func() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.config == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if limiter.Allow() {
|
||||
m.logf("DNS resolution failed due to missing upstream nameservers. Recompiling DNS configuration.")
|
||||
m.setLocked(*m.config)
|
||||
}
|
||||
})
|
||||
|
||||
m.ctx, m.ctxCancel = context.WithCancel(context.Background())
|
||||
m.logf("using %T", m.os)
|
||||
return m
|
||||
}
|
||||
|
||||
// Resolver returns the Manager's DNS Resolver.
|
||||
func (m *Manager) Resolver() *resolver.Resolver { return m.resolver }
|
||||
|
||||
func (m *Manager) Set(cfg Config) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.setLocked(cfg)
|
||||
}
|
||||
|
||||
// GetBaseConfig returns the current base OS DNS configuration as provided by the OSConfigurator.
|
||||
func (m *Manager) GetBaseConfig() (OSConfig, error) {
|
||||
return m.os.GetBaseConfig()
|
||||
}
|
||||
|
||||
// setLocked sets the DNS configuration.
|
||||
//
|
||||
// m.mu must be held.
|
||||
func (m *Manager) setLocked(cfg Config) error {
|
||||
syncs.AssertLocked(&m.mu)
|
||||
|
||||
// On errors, the 'set' config is cleared.
|
||||
m.config = nil
|
||||
|
||||
m.logf("Set: %v", logger.ArgWriter(func(w *bufio.Writer) {
|
||||
cfg.WriteToBufioWriter(w)
|
||||
}))
|
||||
|
||||
rcfg, ocfg, err := m.compileConfig(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.logf("Resolvercfg: %v", logger.ArgWriter(func(w *bufio.Writer) {
|
||||
rcfg.WriteToBufioWriter(w)
|
||||
}))
|
||||
m.logf("OScfg: %v", logger.ArgWriter(func(w *bufio.Writer) {
|
||||
ocfg.WriteToBufioWriter(w)
|
||||
}))
|
||||
|
||||
if err := m.resolver.SetConfig(rcfg); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.os.SetDNS(ocfg); err != nil {
|
||||
m.health.SetDNSOSHealth(err)
|
||||
return err
|
||||
}
|
||||
|
||||
m.health.SetDNSOSHealth(nil)
|
||||
m.config = &cfg
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// compileHostEntries creates a list of single-label resolutions possible
|
||||
// from the configured hosts and search domains.
|
||||
// The entries are compiled in the order of the search domains, then the hosts.
|
||||
// The returned list is sorted by the first hostname in each entry.
|
||||
func compileHostEntries(cfg Config) (hosts []*HostEntry) {
|
||||
didLabel := make(map[string]bool, len(cfg.Hosts))
|
||||
hostsMap := make(map[netip.Addr]*HostEntry, len(cfg.Hosts))
|
||||
for _, sd := range cfg.SearchDomains {
|
||||
for h, ips := range cfg.Hosts {
|
||||
if !sd.Contains(h) || h.NumLabels() != (sd.NumLabels()+1) {
|
||||
continue
|
||||
}
|
||||
ipHosts := []string{string(h.WithTrailingDot())}
|
||||
if label := dnsname.FirstLabel(string(h)); !didLabel[label] {
|
||||
didLabel[label] = true
|
||||
ipHosts = append(ipHosts, label)
|
||||
}
|
||||
for _, ip := range ips {
|
||||
if cfg.OnlyIPv6 && ip.Is4() {
|
||||
continue
|
||||
}
|
||||
if e := hostsMap[ip]; e != nil {
|
||||
e.Hosts = append(e.Hosts, ipHosts...)
|
||||
} else {
|
||||
hostsMap[ip] = &HostEntry{
|
||||
Addr: ip,
|
||||
Hosts: ipHosts,
|
||||
}
|
||||
}
|
||||
// Only add IPv4 or IPv6 per host, like we do in the resolver.
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(hostsMap) == 0 {
|
||||
return nil
|
||||
}
|
||||
hosts = xmaps.Values(hostsMap)
|
||||
slices.SortFunc(hosts, func(a, b *HostEntry) int {
|
||||
if len(a.Hosts) == 0 && len(b.Hosts) == 0 {
|
||||
return 0
|
||||
} else if len(a.Hosts) == 0 {
|
||||
return -1
|
||||
} else if len(b.Hosts) == 0 {
|
||||
return 1
|
||||
}
|
||||
return strings.Compare(a.Hosts[0], b.Hosts[0])
|
||||
})
|
||||
return hosts
|
||||
}
|
||||
|
||||
// compileConfig converts cfg into a quad-100 resolver configuration
|
||||
// and an OS-level configuration.
|
||||
func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig, err error) {
|
||||
// The internal resolver always gets MagicDNS hosts and
|
||||
// authoritative suffixes, even if we don't propagate MagicDNS to
|
||||
// the OS.
|
||||
rcfg.Hosts = cfg.Hosts
|
||||
routes := map[dnsname.FQDN][]*dnstype.Resolver{} // assigned conditionally to rcfg.Routes below.
|
||||
for suffix, resolvers := range cfg.Routes {
|
||||
if len(resolvers) == 0 {
|
||||
rcfg.LocalDomains = append(rcfg.LocalDomains, suffix)
|
||||
} else {
|
||||
routes[suffix] = resolvers
|
||||
}
|
||||
}
|
||||
|
||||
// Similarly, the OS always gets search paths.
|
||||
ocfg.SearchDomains = cfg.SearchDomains
|
||||
if m.goos == "windows" {
|
||||
ocfg.Hosts = compileHostEntries(cfg)
|
||||
}
|
||||
|
||||
// Deal with trivial configs first.
|
||||
switch {
|
||||
case !cfg.needsOSResolver():
|
||||
// Set search domains, but nothing else. This also covers the
|
||||
// case where cfg is entirely zero, in which case these
|
||||
// configs clear all Tailscale DNS settings.
|
||||
return rcfg, ocfg, nil
|
||||
case cfg.hasDefaultIPResolversOnly() && !cfg.hasHostsWithoutSplitDNSRoutes():
|
||||
// Trivial CorpDNS configuration, just override the OS resolver.
|
||||
//
|
||||
// If there are hosts (ExtraRecords) that are not covered by an existing
|
||||
// SplitDNS route, then we don't go into this path so that we fall into
|
||||
// the next case and send the extra record hosts queries through
|
||||
// 100.100.100.100 instead where we can answer them.
|
||||
//
|
||||
// TODO: for OSes that support it, pass IP:port and DoH
|
||||
// addresses directly to OS.
|
||||
// https://github.com/tailscale/tailscale/issues/1666
|
||||
ocfg.Nameservers = toIPsOnly(cfg.DefaultResolvers)
|
||||
return rcfg, ocfg, nil
|
||||
case cfg.hasDefaultResolvers():
|
||||
// Default resolvers plus other stuff always ends up proxying
|
||||
// through quad-100.
|
||||
rcfg.Routes = routes
|
||||
rcfg.Routes["."] = cfg.DefaultResolvers
|
||||
ocfg.Nameservers = []netip.Addr{cfg.serviceIP()}
|
||||
return rcfg, ocfg, nil
|
||||
}
|
||||
|
||||
// From this point on, we're figuring out split DNS
|
||||
// configurations. The possible cases don't return directly any
|
||||
// more, because as a final step we have to handle the case where
|
||||
// the OS can't do split DNS.
|
||||
|
||||
// Workaround for
|
||||
// https://github.com/tailscale/corp/issues/1662. Even though
|
||||
// Windows natively supports split DNS, it only configures linux
|
||||
// containers using whatever the primary is, and doesn't apply
|
||||
// NRPT rules to DNS traffic coming from WSL.
|
||||
//
|
||||
// In order to make WSL work okay when the host Windows is using
|
||||
// Tailscale, we need to set up quad-100 as a "full proxy"
|
||||
// resolver, regardless of whether Windows itself can do split
|
||||
// DNS. We still make Windows do split DNS itself when it can, but
|
||||
// quad-100 will still have the full split configuration as well,
|
||||
// and so can service WSL requests correctly.
|
||||
//
|
||||
// This bool is used in a couple of places below to implement this
|
||||
// workaround.
|
||||
isWindows := m.goos == "windows"
|
||||
isApple := (m.goos == "darwin" || m.goos == "ios")
|
||||
if len(cfg.singleResolverSet()) > 0 && m.os.SupportsSplitDNS() && !isWindows && !isApple {
|
||||
// Split DNS configuration requested, where all split domains
|
||||
// go to the same resolvers. We can let the OS do it.
|
||||
ocfg.Nameservers = toIPsOnly(cfg.singleResolverSet())
|
||||
ocfg.MatchDomains = cfg.matchDomains()
|
||||
return rcfg, ocfg, nil
|
||||
}
|
||||
|
||||
// Split DNS configuration with either multiple upstream routes,
|
||||
// or routes + MagicDNS, or just MagicDNS, or on an OS that cannot
|
||||
// split-DNS. Install a split config pointing at quad-100.
|
||||
rcfg.Routes = routes
|
||||
ocfg.Nameservers = []netip.Addr{cfg.serviceIP()}
|
||||
|
||||
var baseCfg *OSConfig // base config; non-nil if/when known
|
||||
|
||||
// Even though Apple devices can do split DNS, they don't provide a way to
|
||||
// selectively answer ExtraRecords, and ignore other DNS traffic. As a
|
||||
// workaround, we read the existing default resolver configuration and use
|
||||
// that as the forwarder for all DNS traffic that quad-100 doesn't handle.
|
||||
if isApple || !m.os.SupportsSplitDNS() {
|
||||
// If the OS can't do native split-dns, read out the underlying
|
||||
// resolver config and blend it into our config.
|
||||
cfg, err := m.os.GetBaseConfig()
|
||||
if err == nil {
|
||||
baseCfg = &cfg
|
||||
} else if isApple && err == ErrGetBaseConfigNotSupported {
|
||||
// This is currently (2022-10-13) expected on certain iOS and macOS
|
||||
// builds.
|
||||
} else {
|
||||
m.health.SetDNSOSHealth(err)
|
||||
return resolver.Config{}, OSConfig{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if baseCfg == nil {
|
||||
// If there was no base config, then we need to fallback to SplitDNS mode.
|
||||
ocfg.MatchDomains = cfg.matchDomains()
|
||||
} else {
|
||||
// On iOS only (for now), check if all route names point to resources inside the tailnet.
|
||||
// If so, we can set those names as MatchDomains to enable a split DNS configuration
|
||||
// which will help preserve battery life.
|
||||
// Because on iOS MatchDomains must equal SearchDomains, we cannot do this when
|
||||
// we have any Routes outside the tailnet. Otherwise when app connectors are enabled,
|
||||
// a query for 'work-laptop' might lead to search domain expansion, resolving
|
||||
// as 'work-laptop.aws.com' for example.
|
||||
if m.goos == "ios" && rcfg.RoutesRequireNoCustomResolvers() {
|
||||
if !m.disableSplitDNSOptimization() {
|
||||
for r := range rcfg.Routes {
|
||||
ocfg.MatchDomains = append(ocfg.MatchDomains, r)
|
||||
}
|
||||
} else {
|
||||
m.logf("iOS split DNS is disabled by nodeattr")
|
||||
}
|
||||
}
|
||||
var defaultRoutes []*dnstype.Resolver
|
||||
for _, ip := range baseCfg.Nameservers {
|
||||
defaultRoutes = append(defaultRoutes, &dnstype.Resolver{Addr: ip.String()})
|
||||
}
|
||||
rcfg.Routes["."] = defaultRoutes
|
||||
ocfg.SearchDomains = append(ocfg.SearchDomains, baseCfg.SearchDomains...)
|
||||
}
|
||||
|
||||
return rcfg, ocfg, nil
|
||||
}
|
||||
|
||||
func (m *Manager) disableSplitDNSOptimization() bool {
|
||||
return m.knobs != nil && m.knobs.DisableSplitDNSWhenNoCustomResolvers.Load()
|
||||
}
|
||||
|
||||
// toIPsOnly returns only the IP portion of dnstype.Resolver.
|
||||
// Only safe to use if the resolvers slice has been cleared of
|
||||
// DoH or custom-port entries with something like hasDefaultIPResolversOnly.
|
||||
func toIPsOnly(resolvers []*dnstype.Resolver) (ret []netip.Addr) {
|
||||
for _, r := range resolvers {
|
||||
if ipp, ok := r.IPPort(); ok && ipp.Port() == 53 {
|
||||
ret = append(ret, ipp.Addr())
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Query executes a DNS query received from the given address. The query is
|
||||
// provided in bs as a wire-encoded DNS query without any transport header.
|
||||
// This method is called for requests arriving over UDP and TCP.
|
||||
//
|
||||
// The "family" parameter should indicate what type of DNS query this is:
|
||||
// either "tcp" or "udp".
|
||||
func (m *Manager) Query(ctx context.Context, bs []byte, family string, from netip.AddrPort) ([]byte, error) {
|
||||
select {
|
||||
case <-m.ctx.Done():
|
||||
return nil, net.ErrClosed
|
||||
default:
|
||||
// continue
|
||||
}
|
||||
|
||||
if n := atomic.AddInt32(&m.activeQueriesAtomic, 1); n > maxActiveQueries {
|
||||
atomic.AddInt32(&m.activeQueriesAtomic, -1)
|
||||
metricDNSQueryErrorQueue.Add(1)
|
||||
return nil, errFullQueue
|
||||
}
|
||||
defer atomic.AddInt32(&m.activeQueriesAtomic, -1)
|
||||
return m.resolver.Query(ctx, bs, family, from)
|
||||
}
|
||||
|
||||
const (
|
||||
// RFC 7766 6.2 recommends connection reuse & request pipelining
|
||||
// be undertaken, and the connection be closed by the server
|
||||
// using an idle timeout on the order of seconds.
|
||||
idleTimeoutTCP = 45 * time.Second
|
||||
// The RFCs don't specify the max size of a TCP-based DNS query,
|
||||
// but we want to keep this reasonable. Given payloads are typically
|
||||
// much larger and all known client send a single query, I've arbitrarily
|
||||
// chosen 4k.
|
||||
maxReqSizeTCP = 4096
|
||||
)
|
||||
|
||||
// dnsTCPSession services DNS requests sent over TCP.
|
||||
type dnsTCPSession struct {
|
||||
m *Manager
|
||||
|
||||
conn net.Conn
|
||||
srcAddr netip.AddrPort
|
||||
|
||||
readClosing chan struct{}
|
||||
responses chan []byte // DNS replies pending writing
|
||||
|
||||
ctx context.Context
|
||||
closeCtx context.CancelFunc
|
||||
}
|
||||
|
||||
func (s *dnsTCPSession) handleWrites() {
|
||||
defer s.conn.Close()
|
||||
defer s.closeCtx()
|
||||
|
||||
// NOTE(andrew): we explicitly do not close the 'responses' channel
|
||||
// when this function exits. If we hit an error and return, we could
|
||||
// still have outstanding 'handleQuery' goroutines running, and if we
|
||||
// closed this channel they'd end up trying to send on a closed channel
|
||||
// when they finish.
|
||||
//
|
||||
// Because we call closeCtx, those goroutines will not hang since they
|
||||
// select on <-s.ctx.Done() as well as s.responses.
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.readClosing:
|
||||
return // connection closed or timeout, teardown time
|
||||
|
||||
case resp := <-s.responses:
|
||||
s.conn.SetWriteDeadline(time.Now().Add(idleTimeoutTCP))
|
||||
if err := binary.Write(s.conn, binary.BigEndian, uint16(len(resp))); err != nil {
|
||||
s.m.logf("tcp write (len): %v", err)
|
||||
return
|
||||
}
|
||||
if _, err := s.conn.Write(resp); err != nil {
|
||||
s.m.logf("tcp write (response): %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *dnsTCPSession) handleQuery(q []byte) {
|
||||
resp, err := s.m.Query(s.ctx, q, "tcp", s.srcAddr)
|
||||
if err != nil {
|
||||
s.m.logf("tcp query: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// See note in handleWrites (above) regarding this select{}
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
case s.responses <- resp:
|
||||
}
|
||||
}
|
||||
|
||||
func (s *dnsTCPSession) handleReads() {
|
||||
defer s.conn.Close()
|
||||
defer close(s.readClosing)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
|
||||
default:
|
||||
s.conn.SetReadDeadline(time.Now().Add(idleTimeoutTCP))
|
||||
var reqLen uint16
|
||||
if err := binary.Read(s.conn, binary.BigEndian, &reqLen); err != nil {
|
||||
if err == io.EOF || err == io.ErrClosedPipe {
|
||||
return // connection closed nominally, we gucci
|
||||
}
|
||||
s.m.logf("tcp read (len): %v", err)
|
||||
return
|
||||
}
|
||||
if int(reqLen) > maxReqSizeTCP {
|
||||
s.m.logf("tcp request too large (%d > %d)", reqLen, maxReqSizeTCP)
|
||||
return
|
||||
}
|
||||
|
||||
buf := make([]byte, int(reqLen))
|
||||
if _, err := io.ReadFull(s.conn, buf); err != nil {
|
||||
s.m.logf("tcp read (payload): %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
default:
|
||||
// NOTE: by kicking off the query handling in a
|
||||
// new goroutine, it is possible that we'll
|
||||
// deliver responses out-of-order. This is
|
||||
// explicitly allowed by RFC7766, Section
|
||||
// 6.2.1.1 ("Query Pipelining").
|
||||
go s.handleQuery(buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HandleTCPConn implements magicDNS over TCP, taking a connection and
|
||||
// servicing DNS requests sent down it.
|
||||
func (m *Manager) HandleTCPConn(conn net.Conn, srcAddr netip.AddrPort) {
|
||||
s := dnsTCPSession{
|
||||
m: m,
|
||||
conn: conn,
|
||||
srcAddr: srcAddr,
|
||||
responses: make(chan []byte),
|
||||
readClosing: make(chan struct{}),
|
||||
}
|
||||
s.ctx, s.closeCtx = context.WithCancel(m.ctx)
|
||||
go s.handleReads()
|
||||
s.handleWrites()
|
||||
}
|
||||
|
||||
func (m *Manager) Down() error {
|
||||
m.ctxCancel()
|
||||
if err := m.os.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
m.resolver.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) FlushCaches() error {
|
||||
return flushCaches()
|
||||
}
|
||||
|
||||
// CleanUp restores the system DNS configuration to its original state
|
||||
// in case the Tailscale daemon terminated without closing the router.
|
||||
// No other state needs to be instantiated before this runs.
|
||||
//
|
||||
// health must not be nil
|
||||
func CleanUp(logf logger.Logf, netMon *netmon.Monitor, health *health.Tracker, interfaceName string) {
|
||||
oscfg, err := NewOSConfigurator(logf, nil, nil, interfaceName)
|
||||
if err != nil {
|
||||
logf("creating dns cleanup: %v", err)
|
||||
return
|
||||
}
|
||||
d := &tsdial.Dialer{Logf: logf}
|
||||
d.SetNetMon(netMon)
|
||||
dns := NewManager(logf, oscfg, health, d, nil, nil, runtime.GOOS)
|
||||
if err := dns.Down(); err != nil {
|
||||
logf("dns down: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
metricDNSQueryErrorQueue = clientmetric.NewCounter("dns_query_local_error_queue")
|
||||
)
|
||||
156
vendor/tailscale.com/net/dns/manager_darwin.go
generated
vendored
Normal file
156
vendor/tailscale.com/net/dns/manager_darwin.go
generated
vendored
Normal file
@@ -0,0 +1,156 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
|
||||
"go4.org/mem"
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/dns/resolvconffile"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
// NewOSConfigurator creates a new OS configurator.
|
||||
//
|
||||
// The health tracker and the knobs may be nil and are ignored on this platform.
|
||||
func NewOSConfigurator(logf logger.Logf, _ *health.Tracker, _ *controlknobs.Knobs, ifName string) (OSConfigurator, error) {
|
||||
return &darwinConfigurator{logf: logf, ifName: ifName}, nil
|
||||
}
|
||||
|
||||
// darwinConfigurator is the tailscaled-on-macOS DNS OS configurator that
|
||||
// maintains the Split DNS nameserver entries pointing MagicDNS DNS suffixes
|
||||
// to 100.100.100.100 using the macOS /etc/resolver/$SUFFIX files.
|
||||
type darwinConfigurator struct {
|
||||
logf logger.Logf
|
||||
ifName string
|
||||
}
|
||||
|
||||
func (c *darwinConfigurator) Close() error {
|
||||
c.removeResolverFiles(func(domain string) bool { return true })
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *darwinConfigurator) SupportsSplitDNS() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *darwinConfigurator) SetDNS(cfg OSConfig) error {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(macResolverFileHeader)
|
||||
for _, ip := range cfg.Nameservers {
|
||||
buf.WriteString("nameserver ")
|
||||
buf.WriteString(ip.String())
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
|
||||
if err := os.MkdirAll("/etc/resolver", 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var keep map[string]bool
|
||||
|
||||
// Add a dummy file to /etc/resolver with a "search ..." directive if we have
|
||||
// search suffixes to add.
|
||||
if len(cfg.SearchDomains) > 0 {
|
||||
const searchFile = "search.tailscale" // fake DNS suffix+TLD to put our search
|
||||
mak.Set(&keep, searchFile, true)
|
||||
var sbuf bytes.Buffer
|
||||
sbuf.WriteString(macResolverFileHeader)
|
||||
sbuf.WriteString("search")
|
||||
for _, d := range cfg.SearchDomains {
|
||||
sbuf.WriteString(" ")
|
||||
sbuf.WriteString(string(d.WithoutTrailingDot()))
|
||||
}
|
||||
sbuf.WriteString("\n")
|
||||
if err := os.WriteFile("/etc/resolver/"+searchFile, sbuf.Bytes(), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, d := range cfg.MatchDomains {
|
||||
fileBase := string(d.WithoutTrailingDot())
|
||||
mak.Set(&keep, fileBase, true)
|
||||
fullPath := "/etc/resolver/" + fileBase
|
||||
|
||||
if err := os.WriteFile(fullPath, buf.Bytes(), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return c.removeResolverFiles(func(domain string) bool { return !keep[domain] })
|
||||
}
|
||||
|
||||
// GetBaseConfig returns the current OS DNS configuration, extracting it from /etc/resolv.conf.
|
||||
// We should really be using the SystemConfiguration framework to get this information, as this
|
||||
// is not a stable public API, and is provided mostly as a compatibility effort with Unix
|
||||
// tools. Apple might break this in the future. But honestly, parsing the output of `scutil --dns`
|
||||
// is *even more* likely to break in the future.
|
||||
func (c *darwinConfigurator) GetBaseConfig() (OSConfig, error) {
|
||||
cfg := OSConfig{}
|
||||
|
||||
resolvConf, err := resolvconffile.ParseFile("/etc/resolv.conf")
|
||||
if err != nil {
|
||||
c.logf("failed to parse /etc/resolv.conf: %v", err)
|
||||
return cfg, ErrGetBaseConfigNotSupported
|
||||
}
|
||||
|
||||
for _, ns := range resolvConf.Nameservers {
|
||||
if ns == tsaddr.TailscaleServiceIP() || ns == tsaddr.TailscaleServiceIPv6() {
|
||||
// If we find Quad100 in /etc/resolv.conf, we should ignore it
|
||||
c.logf("ignoring 100.100.100.100 resolver IP found in /etc/resolv.conf")
|
||||
continue
|
||||
}
|
||||
cfg.Nameservers = append(cfg.Nameservers, ns)
|
||||
}
|
||||
cfg.SearchDomains = resolvConf.SearchDomains
|
||||
|
||||
if len(cfg.Nameservers) == 0 {
|
||||
// Log a warning in case we couldn't find any nameservers in /etc/resolv.conf.
|
||||
c.logf("no nameservers found in /etc/resolv.conf, DNS resolution might fail")
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
const macResolverFileHeader = "# Added by tailscaled\n"
|
||||
|
||||
// removeResolverFiles deletes all files in /etc/resolver for which the shouldDelete
|
||||
// func returns true.
|
||||
func (c *darwinConfigurator) removeResolverFiles(shouldDelete func(domain string) bool) error {
|
||||
dents, err := os.ReadDir("/etc/resolver")
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, de := range dents {
|
||||
if !de.Type().IsRegular() {
|
||||
continue
|
||||
}
|
||||
name := de.Name()
|
||||
if !shouldDelete(name) {
|
||||
continue
|
||||
}
|
||||
fullPath := "/etc/resolver/" + name
|
||||
contents, err := os.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) { // race?
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !mem.HasPrefix(mem.B(contents), mem.S(macResolverFileHeader)) {
|
||||
continue
|
||||
}
|
||||
if err := os.Remove(fullPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
19
vendor/tailscale.com/net/dns/manager_default.go
generated
vendored
Normal file
19
vendor/tailscale.com/net/dns/manager_default.go
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !linux && !freebsd && !openbsd && !windows && !darwin
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// NewOSConfigurator creates a new OS configurator.
|
||||
//
|
||||
// The health tracker and the knobs may be nil and are ignored on this platform.
|
||||
func NewOSConfigurator(logger.Logf, *health.Tracker, *controlknobs.Knobs, string) (OSConfigurator, error) {
|
||||
return NewNoopManager()
|
||||
}
|
||||
43
vendor/tailscale.com/net/dns/manager_freebsd.go
generated
vendored
Normal file
43
vendor/tailscale.com/net/dns/manager_freebsd.go
generated
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// NewOSConfigurator creates a new OS configurator.
|
||||
//
|
||||
// The health tracker may be nil; the knobs may be nil and are ignored on this platform.
|
||||
func NewOSConfigurator(logf logger.Logf, health *health.Tracker, _ *controlknobs.Knobs, _ string) (OSConfigurator, error) {
|
||||
bs, err := os.ReadFile("/etc/resolv.conf")
|
||||
if os.IsNotExist(err) {
|
||||
return newDirectManager(logf, health), nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading /etc/resolv.conf: %w", err)
|
||||
}
|
||||
|
||||
switch resolvOwner(bs) {
|
||||
case "resolvconf":
|
||||
switch resolvconfStyle() {
|
||||
case "":
|
||||
return newDirectManager(logf, health), nil
|
||||
case "debian":
|
||||
return newDebianResolvconfManager(logf)
|
||||
case "openresolv":
|
||||
return newOpenresolvManager(logf)
|
||||
default:
|
||||
logf("[unexpected] got unknown flavor of resolvconf %q, falling back to direct manager", resolvconfStyle())
|
||||
return newDirectManager(logf, health), nil
|
||||
}
|
||||
default:
|
||||
return newDirectManager(logf, health), nil
|
||||
}
|
||||
}
|
||||
431
vendor/tailscale.com/net/dns/manager_linux.go
generated
vendored
Normal file
431
vendor/tailscale.com/net/dns/manager_linux.go
generated
vendored
Normal file
@@ -0,0 +1,431 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/cmpver"
|
||||
)
|
||||
|
||||
type kv struct {
|
||||
k, v string
|
||||
}
|
||||
|
||||
func (kv kv) String() string {
|
||||
return fmt.Sprintf("%s=%s", kv.k, kv.v)
|
||||
}
|
||||
|
||||
var publishOnce sync.Once
|
||||
|
||||
// NewOSConfigurator created a new OS configurator.
|
||||
//
|
||||
// The health tracker may be nil; the knobs may be nil and are ignored on this platform.
|
||||
func NewOSConfigurator(logf logger.Logf, health *health.Tracker, _ *controlknobs.Knobs, interfaceName string) (ret OSConfigurator, err error) {
|
||||
env := newOSConfigEnv{
|
||||
fs: directFS{},
|
||||
dbusPing: dbusPing,
|
||||
dbusReadString: dbusReadString,
|
||||
nmIsUsingResolved: nmIsUsingResolved,
|
||||
nmVersionBetween: nmVersionBetween,
|
||||
resolvconfStyle: resolvconfStyle,
|
||||
}
|
||||
mode, err := dnsMode(logf, health, env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
publishOnce.Do(func() {
|
||||
sanitizedMode := strings.ReplaceAll(mode, "-", "_")
|
||||
m := clientmetric.NewGauge(fmt.Sprintf("dns_manager_linux_mode_%s", sanitizedMode))
|
||||
m.Set(1)
|
||||
})
|
||||
logf("dns: using %q mode", mode)
|
||||
switch mode {
|
||||
case "direct":
|
||||
return newDirectManagerOnFS(logf, health, env.fs), nil
|
||||
case "systemd-resolved":
|
||||
return newResolvedManager(logf, health, interfaceName)
|
||||
case "network-manager":
|
||||
return newNMManager(interfaceName)
|
||||
case "debian-resolvconf":
|
||||
return newDebianResolvconfManager(logf)
|
||||
case "openresolv":
|
||||
return newOpenresolvManager(logf)
|
||||
default:
|
||||
logf("[unexpected] detected unknown DNS mode %q, using direct manager as last resort", mode)
|
||||
return newDirectManagerOnFS(logf, health, env.fs), nil
|
||||
}
|
||||
}
|
||||
|
||||
// newOSConfigEnv are the funcs newOSConfigurator needs, pulled out for testing.
|
||||
type newOSConfigEnv struct {
|
||||
fs wholeFileFS
|
||||
dbusPing func(string, string) error
|
||||
dbusReadString func(string, string, string, string) (string, error)
|
||||
nmIsUsingResolved func() error
|
||||
nmVersionBetween func(v1, v2 string) (safe bool, err error)
|
||||
resolvconfStyle func() string
|
||||
}
|
||||
|
||||
func dnsMode(logf logger.Logf, health *health.Tracker, env newOSConfigEnv) (ret string, err error) {
|
||||
var debug []kv
|
||||
dbg := func(k, v string) {
|
||||
debug = append(debug, kv{k, v})
|
||||
}
|
||||
defer func() {
|
||||
if ret != "" {
|
||||
dbg("ret", ret)
|
||||
}
|
||||
logf("dns: %v", debug)
|
||||
}()
|
||||
|
||||
// In all cases that we detect systemd-resolved, try asking it what it
|
||||
// thinks the current resolv.conf mode is so we can add it to our logs.
|
||||
defer func() {
|
||||
if ret != "systemd-resolved" {
|
||||
return
|
||||
}
|
||||
|
||||
// Try to ask systemd-resolved what it thinks the current
|
||||
// status of resolv.conf is. This is documented at:
|
||||
// https://www.freedesktop.org/software/systemd/man/org.freedesktop.resolve1.html
|
||||
mode, err := env.dbusReadString("org.freedesktop.resolve1", "/org/freedesktop/resolve1", "org.freedesktop.resolve1.Manager", "ResolvConfMode")
|
||||
if err != nil {
|
||||
logf("dns: ResolvConfMode error: %v", err)
|
||||
dbg("resolv-conf-mode", "error")
|
||||
} else {
|
||||
dbg("resolv-conf-mode", mode)
|
||||
}
|
||||
}()
|
||||
|
||||
// Before we read /etc/resolv.conf (which might be in a broken
|
||||
// or symlink-dangling state), try to ping the D-Bus service
|
||||
// for systemd-resolved. If it's active on the machine, this
|
||||
// will make it start up and write the /etc/resolv.conf file
|
||||
// before it replies to the ping. (see how systemd's
|
||||
// src/resolve/resolved.c calls manager_write_resolv_conf
|
||||
// before the sd_event_loop starts)
|
||||
resolvedUp := env.dbusPing("org.freedesktop.resolve1", "/org/freedesktop/resolve1") == nil
|
||||
if resolvedUp {
|
||||
dbg("resolved-ping", "yes")
|
||||
}
|
||||
|
||||
bs, err := env.fs.ReadFile(resolvConf)
|
||||
if os.IsNotExist(err) {
|
||||
dbg("rc", "missing")
|
||||
return "direct", nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("reading /etc/resolv.conf: %w", err)
|
||||
}
|
||||
|
||||
switch resolvOwner(bs) {
|
||||
case "systemd-resolved":
|
||||
dbg("rc", "resolved")
|
||||
|
||||
// Some systems, for reasons known only to them, have a
|
||||
// resolv.conf that has the word "systemd-resolved" in its
|
||||
// header, but doesn't actually point to resolved. We mustn't
|
||||
// try to program resolved in that case.
|
||||
// https://github.com/tailscale/tailscale/issues/2136
|
||||
if err := resolvedIsActuallyResolver(logf, env, dbg, bs); err != nil {
|
||||
logf("dns: resolvedIsActuallyResolver error: %v", err)
|
||||
dbg("resolved", "not-in-use")
|
||||
return "direct", nil
|
||||
}
|
||||
if err := env.dbusPing("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/DnsManager"); err != nil {
|
||||
dbg("nm", "no")
|
||||
return "systemd-resolved", nil
|
||||
}
|
||||
dbg("nm", "yes")
|
||||
if err := env.nmIsUsingResolved(); err != nil {
|
||||
dbg("nm-resolved", "no")
|
||||
return "systemd-resolved", nil
|
||||
}
|
||||
dbg("nm-resolved", "yes")
|
||||
|
||||
// Version of NetworkManager before 1.26.6 programmed resolved
|
||||
// incorrectly, such that NM's settings would always take
|
||||
// precedence over other settings set by other resolved
|
||||
// clients.
|
||||
//
|
||||
// If we're dealing with such a version, we have to set our
|
||||
// DNS settings through NM to have them take.
|
||||
//
|
||||
// However, versions 1.26.6 later both fixed the resolved
|
||||
// programming issue _and_ started ignoring DNS settings for
|
||||
// "unmanaged" interfaces - meaning NM 1.26.6 and later
|
||||
// actively ignore DNS configuration we give it. So, for those
|
||||
// NM versions, we can and must use resolved directly.
|
||||
//
|
||||
// Even more fun, even-older versions of NM won't let us set
|
||||
// DNS settings if the interface isn't managed by NM, with a
|
||||
// hard failure on DBus requests. Empirically, NM 1.22 does
|
||||
// this. Based on the versions popular distros shipped, we
|
||||
// conservatively decree that only 1.26.0 through 1.26.5 are
|
||||
// "safe" to use for our purposes. This roughly matches
|
||||
// distros released in the latter half of 2020.
|
||||
//
|
||||
// In a perfect world, we'd avoid this by replacing
|
||||
// configuration out from under NM entirely (e.g. using
|
||||
// directManager to overwrite resolv.conf), but in a world
|
||||
// where resolved runs, we need to get correct configuration
|
||||
// into resolved regardless of what's in resolv.conf (because
|
||||
// resolved can also be queried over dbus, or via an NSS
|
||||
// module that bypasses /etc/resolv.conf). Given that we must
|
||||
// get correct configuration into resolved, we have no choice
|
||||
// but to use NM, and accept the loss of IPv6 configuration
|
||||
// that comes with it (see
|
||||
// https://github.com/tailscale/tailscale/issues/1699,
|
||||
// https://github.com/tailscale/tailscale/pull/1945)
|
||||
safe, err := env.nmVersionBetween("1.26.0", "1.26.5")
|
||||
if err != nil {
|
||||
// Failed to figure out NM's version, can't make a correct
|
||||
// decision.
|
||||
return "", fmt.Errorf("checking NetworkManager version: %v", err)
|
||||
}
|
||||
if safe {
|
||||
dbg("nm-safe", "yes")
|
||||
return "network-manager", nil
|
||||
}
|
||||
dbg("nm-safe", "no")
|
||||
return "systemd-resolved", nil
|
||||
case "resolvconf":
|
||||
dbg("rc", "resolvconf")
|
||||
style := env.resolvconfStyle()
|
||||
switch style {
|
||||
case "":
|
||||
dbg("resolvconf", "no")
|
||||
return "direct", nil
|
||||
case "debian":
|
||||
dbg("resolvconf", "debian")
|
||||
return "debian-resolvconf", nil
|
||||
case "openresolv":
|
||||
dbg("resolvconf", "openresolv")
|
||||
return "openresolv", nil
|
||||
default:
|
||||
// Shouldn't happen, that means we updated flavors of
|
||||
// resolvconf without updating here.
|
||||
dbg("resolvconf", style)
|
||||
logf("[unexpected] got unknown flavor of resolvconf %q, falling back to direct manager", env.resolvconfStyle())
|
||||
return "direct", nil
|
||||
}
|
||||
case "NetworkManager":
|
||||
dbg("rc", "nm")
|
||||
// Sometimes, NetworkManager owns the configuration but points
|
||||
// it at systemd-resolved.
|
||||
if err := resolvedIsActuallyResolver(logf, env, dbg, bs); err != nil {
|
||||
logf("dns: resolvedIsActuallyResolver error: %v", err)
|
||||
dbg("resolved", "not-in-use")
|
||||
// You'd think we would use newNMManager here. However, as
|
||||
// explained in
|
||||
// https://github.com/tailscale/tailscale/issues/1699 ,
|
||||
// using NetworkManager for DNS configuration carries with
|
||||
// it the cost of losing IPv6 configuration on the
|
||||
// Tailscale network interface. So, when we can avoid it,
|
||||
// we bypass NetworkManager by replacing resolv.conf
|
||||
// directly.
|
||||
//
|
||||
// If you ever try to put NMManager back here, keep in mind
|
||||
// that versions >=1.26.6 will ignore DNS configuration
|
||||
// anyway, so you still need a fallback path that uses
|
||||
// directManager.
|
||||
return "direct", nil
|
||||
}
|
||||
dbg("nm-resolved", "yes")
|
||||
|
||||
// See large comment above for reasons we'd use NM rather than
|
||||
// resolved. systemd-resolved is actually in charge of DNS
|
||||
// configuration, but in some cases we might need to configure
|
||||
// it via NetworkManager. All the logic below is probing for
|
||||
// that case: is NetworkManager running? If so, is it one of
|
||||
// the versions that requires direct interaction with it?
|
||||
if err := env.dbusPing("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/DnsManager"); err != nil {
|
||||
dbg("nm", "no")
|
||||
return "systemd-resolved", nil
|
||||
}
|
||||
safe, err := env.nmVersionBetween("1.26.0", "1.26.5")
|
||||
if err != nil {
|
||||
// Failed to figure out NM's version, can't make a correct
|
||||
// decision.
|
||||
return "", fmt.Errorf("checking NetworkManager version: %v", err)
|
||||
}
|
||||
if safe {
|
||||
dbg("nm-safe", "yes")
|
||||
return "network-manager", nil
|
||||
}
|
||||
if err := env.nmIsUsingResolved(); err != nil {
|
||||
// If systemd-resolved is not running at all, then we don't have any
|
||||
// other choice: we take direct control of DNS.
|
||||
dbg("nm-resolved", "no")
|
||||
return "direct", nil
|
||||
}
|
||||
|
||||
health.SetDNSManagerHealth(errors.New("systemd-resolved and NetworkManager are wired together incorrectly; MagicDNS will probably not work. For more info, see https://tailscale.com/s/resolved-nm"))
|
||||
dbg("nm-safe", "no")
|
||||
return "systemd-resolved", nil
|
||||
default:
|
||||
dbg("rc", "unknown")
|
||||
return "direct", nil
|
||||
}
|
||||
}
|
||||
|
||||
func nmVersionBetween(first, last string) (bool, error) {
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
// DBus probably not running.
|
||||
return false, err
|
||||
}
|
||||
|
||||
nm := conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager"))
|
||||
v, err := nm.GetProperty("org.freedesktop.NetworkManager.Version")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
version, ok := v.Value().(string)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("unexpected type %T for NM version", v.Value())
|
||||
}
|
||||
|
||||
outside := cmpver.Compare(version, first) < 0 || cmpver.Compare(version, last) > 0
|
||||
return !outside, nil
|
||||
}
|
||||
|
||||
func nmIsUsingResolved() error {
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
// DBus probably not running.
|
||||
return err
|
||||
}
|
||||
|
||||
nm := conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager/DnsManager"))
|
||||
v, err := nm.GetProperty("org.freedesktop.NetworkManager.DnsManager.Mode")
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting NM mode: %w", err)
|
||||
}
|
||||
mode, ok := v.Value().(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for NM DNS mode", v.Value())
|
||||
}
|
||||
if mode != "systemd-resolved" {
|
||||
return errors.New("NetworkManager is not using systemd-resolved for DNS")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolvedIsActuallyResolver reports whether the system is using
|
||||
// systemd-resolved as the resolver. There are two different ways to
|
||||
// use systemd-resolved:
|
||||
// - libnss_resolve, which requires adding `resolve` to the "hosts:"
|
||||
// line in /etc/nsswitch.conf
|
||||
// - setting the only nameserver configured in `resolv.conf` to
|
||||
// systemd-resolved IP (127.0.0.53)
|
||||
//
|
||||
// Returns an error if the configuration is something other than
|
||||
// exclusively systemd-resolved, or nil if the config is only
|
||||
// systemd-resolved.
|
||||
func resolvedIsActuallyResolver(logf logger.Logf, env newOSConfigEnv, dbg func(k, v string), bs []byte) error {
|
||||
if err := isLibnssResolveUsed(env); err == nil {
|
||||
dbg("resolved", "nss")
|
||||
return nil
|
||||
}
|
||||
|
||||
cfg, err := readResolv(bytes.NewBuffer(bs))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// We've encountered at least one system where the line
|
||||
// "nameserver 127.0.0.53" appears twice, so we look exhaustively
|
||||
// through all of them and allow any number of repeated mentions
|
||||
// of the systemd-resolved stub IP.
|
||||
if len(cfg.Nameservers) == 0 {
|
||||
return errors.New("resolv.conf has no nameservers")
|
||||
}
|
||||
for _, ns := range cfg.Nameservers {
|
||||
if ns != netaddr.IPv4(127, 0, 0, 53) {
|
||||
return fmt.Errorf("resolv.conf doesn't point to systemd-resolved; points to %v", cfg.Nameservers)
|
||||
}
|
||||
}
|
||||
dbg("resolved", "file")
|
||||
return nil
|
||||
}
|
||||
|
||||
// isLibnssResolveUsed reports whether libnss_resolve is used
|
||||
// for resolving names. Returns nil if it is, and an error otherwise.
|
||||
func isLibnssResolveUsed(env newOSConfigEnv) error {
|
||||
bs, err := env.fs.ReadFile("/etc/nsswitch.conf")
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading /etc/resolv.conf: %w", err)
|
||||
}
|
||||
for _, line := range strings.Split(string(bs), "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 || fields[0] != "hosts:" {
|
||||
continue
|
||||
}
|
||||
for _, module := range fields[1:] {
|
||||
if module == "dns" {
|
||||
return fmt.Errorf("dns with a higher priority than libnss_resolve")
|
||||
}
|
||||
if module == "resolve" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("libnss_resolve not used")
|
||||
}
|
||||
|
||||
func dbusPing(name, objectPath string) error {
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
// DBus probably not running.
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
obj := conn.Object(name, dbus.ObjectPath(objectPath))
|
||||
call := obj.CallWithContext(ctx, "org.freedesktop.DBus.Peer.Ping", 0)
|
||||
return call.Err
|
||||
}
|
||||
|
||||
// dbusReadString reads a string property from the provided name and object
|
||||
// path. property must be in "interface.member" notation.
|
||||
func dbusReadString(name, objectPath, iface, member string) (string, error) {
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
// DBus probably not running.
|
||||
return "", err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
obj := conn.Object(name, dbus.ObjectPath(objectPath))
|
||||
|
||||
var result dbus.Variant
|
||||
err = obj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, iface, member).Store(&result)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if s, ok := result.Value().(string); ok {
|
||||
return s, nil
|
||||
}
|
||||
return result.String(), nil
|
||||
}
|
||||
78
vendor/tailscale.com/net/dns/manager_openbsd.go
generated
vendored
Normal file
78
vendor/tailscale.com/net/dns/manager_openbsd.go
generated
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
type kv struct {
|
||||
k, v string
|
||||
}
|
||||
|
||||
func (kv kv) String() string {
|
||||
return fmt.Sprintf("%s=%s", kv.k, kv.v)
|
||||
}
|
||||
|
||||
// NewOSConfigurator created a new OS configurator.
|
||||
//
|
||||
// The health tracker may be nil; the knobs may be nil and are ignored on this platform.
|
||||
func NewOSConfigurator(logf logger.Logf, health *health.Tracker, _ *controlknobs.Knobs, interfaceName string) (OSConfigurator, error) {
|
||||
return newOSConfigurator(logf, health, interfaceName,
|
||||
newOSConfigEnv{
|
||||
rcIsResolvd: rcIsResolvd,
|
||||
fs: directFS{},
|
||||
})
|
||||
}
|
||||
|
||||
// newOSConfigEnv are the funcs newOSConfigurator needs, pulled out for testing.
|
||||
type newOSConfigEnv struct {
|
||||
fs directFS
|
||||
rcIsResolvd func(resolvConfContents []byte) bool
|
||||
}
|
||||
|
||||
func newOSConfigurator(logf logger.Logf, health *health.Tracker, interfaceName string, env newOSConfigEnv) (ret OSConfigurator, err error) {
|
||||
var debug []kv
|
||||
dbg := func(k, v string) {
|
||||
debug = append(debug, kv{k, v})
|
||||
}
|
||||
defer func() {
|
||||
if ret != nil {
|
||||
dbg("ret", fmt.Sprintf("%T", ret))
|
||||
}
|
||||
logf("dns: %v", debug)
|
||||
}()
|
||||
|
||||
bs, err := env.fs.ReadFile(resolvConf)
|
||||
if os.IsNotExist(err) {
|
||||
dbg("rc", "missing")
|
||||
return newDirectManager(logf, health), nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading /etc/resolv.conf: %w", err)
|
||||
}
|
||||
|
||||
if env.rcIsResolvd(bs) {
|
||||
dbg("resolvd", "yes")
|
||||
return newResolvdManager(logf, interfaceName)
|
||||
}
|
||||
|
||||
dbg("resolvd", "missing")
|
||||
return newDirectManager(logf, health), nil
|
||||
}
|
||||
|
||||
func rcIsResolvd(resolvConfContents []byte) bool {
|
||||
// If we have the string "# resolvd:" in resolv.conf resolvd(8) is
|
||||
// managing things.
|
||||
if bytes.Contains(resolvConfContents, []byte("# resolvd:")) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
617
vendor/tailscale.com/net/dns/manager_windows.go
generated
vendored
Normal file
617
vendor/tailscale.com/net/dns/manager_windows.go
generated
vendored
Normal file
@@ -0,0 +1,617 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/winutil"
|
||||
)
|
||||
|
||||
const (
|
||||
versionKey = `SOFTWARE\Microsoft\Windows NT\CurrentVersion`
|
||||
)
|
||||
|
||||
var configureWSL = envknob.RegisterBool("TS_DEBUG_CONFIGURE_WSL")
|
||||
|
||||
type windowsManager struct {
|
||||
logf logger.Logf
|
||||
guid string
|
||||
knobs *controlknobs.Knobs // or nil
|
||||
nrptDB *nrptRuleDatabase
|
||||
wslManager *wslManager
|
||||
|
||||
mu sync.Mutex
|
||||
closing bool
|
||||
}
|
||||
|
||||
// NewOSConfigurator created a new OS configurator.
|
||||
//
|
||||
// The health tracker and the knobs may be nil.
|
||||
func NewOSConfigurator(logf logger.Logf, health *health.Tracker, knobs *controlknobs.Knobs, interfaceName string) (OSConfigurator, error) {
|
||||
ret := &windowsManager{
|
||||
logf: logf,
|
||||
guid: interfaceName,
|
||||
knobs: knobs,
|
||||
wslManager: newWSLManager(logf, health),
|
||||
}
|
||||
|
||||
if isWindows10OrBetter() {
|
||||
ret.nrptDB = newNRPTRuleDatabase(logf)
|
||||
}
|
||||
|
||||
go func() {
|
||||
// Log WSL status once at startup.
|
||||
if distros, err := wslDistros(); err != nil {
|
||||
logf("WSL: could not list distributions: %v", err)
|
||||
} else {
|
||||
logf("WSL: found %d distributions", len(distros))
|
||||
}
|
||||
}()
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (m *windowsManager) openInterfaceKey(pfx winutil.RegistryPathPrefix) (registry.Key, error) {
|
||||
var key registry.Key
|
||||
var err error
|
||||
path := pfx.WithSuffix(m.guid)
|
||||
|
||||
m.mu.Lock()
|
||||
closing := m.closing
|
||||
m.mu.Unlock()
|
||||
if closing {
|
||||
// Do not wait for the interface key to appear if the manager is being closed.
|
||||
// If it's being closed due to the removal of the wintun adapter,
|
||||
// the key would already be gone by now and will not reappear until tailscaled is restarted.
|
||||
key, err = registry.OpenKey(registry.LOCAL_MACHINE, string(path), registry.SET_VALUE)
|
||||
} else {
|
||||
key, err = winutil.OpenKeyWait(registry.LOCAL_MACHINE, path, registry.SET_VALUE)
|
||||
}
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("opening %s: %w", path, err)
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (m *windowsManager) muteKeyNotFoundIfClosing(err error) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if !m.closing || (!errors.Is(err, windows.ERROR_FILE_NOT_FOUND) && !errors.Is(err, windows.ERROR_PATH_NOT_FOUND)) {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func delValue(key registry.Key, name string) error {
|
||||
if err := key.DeleteValue(name); err != nil && err != registry.ErrNotExist {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setSplitDNS configures one or more NRPT (Name Resolution Policy Table) rules
|
||||
// to resolve queries for domains using resolvers, rather than the
|
||||
// system's "primary" resolver.
|
||||
//
|
||||
// If no resolvers are provided, the Tailscale NRPT rules are deleted.
|
||||
func (m *windowsManager) setSplitDNS(resolvers []netip.Addr, domains []dnsname.FQDN) error {
|
||||
if m.nrptDB == nil {
|
||||
if resolvers == nil {
|
||||
// Just a no-op in this case.
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("Split DNS unsupported on this Windows version")
|
||||
}
|
||||
|
||||
defer m.nrptDB.Refresh()
|
||||
if len(resolvers) == 0 {
|
||||
return m.nrptDB.DelAllRuleKeys()
|
||||
}
|
||||
|
||||
servers := make([]string, 0, len(resolvers))
|
||||
for _, resolver := range resolvers {
|
||||
servers = append(servers, resolver.String())
|
||||
}
|
||||
|
||||
return m.nrptDB.WriteSplitDNSConfig(servers, domains)
|
||||
}
|
||||
|
||||
func setTailscaleHosts(prevHostsFile []byte, hosts []*HostEntry) ([]byte, error) {
|
||||
b := bytes.ReplaceAll(prevHostsFile, []byte("\r\n"), []byte("\n"))
|
||||
sc := bufio.NewScanner(bytes.NewReader(b))
|
||||
const (
|
||||
header = "# TailscaleHostsSectionStart"
|
||||
footer = "# TailscaleHostsSectionEnd"
|
||||
)
|
||||
var comments = []string{
|
||||
"# This section contains MagicDNS entries for Tailscale.",
|
||||
"# Do not edit this section manually.",
|
||||
}
|
||||
var out bytes.Buffer
|
||||
var inSection bool
|
||||
for sc.Scan() {
|
||||
line := strings.TrimSpace(sc.Text())
|
||||
if line == header {
|
||||
inSection = true
|
||||
continue
|
||||
}
|
||||
if line == footer {
|
||||
inSection = false
|
||||
continue
|
||||
}
|
||||
if inSection {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintln(&out, line)
|
||||
}
|
||||
if err := sc.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(hosts) > 0 {
|
||||
fmt.Fprintln(&out, header)
|
||||
for _, c := range comments {
|
||||
fmt.Fprintln(&out, c)
|
||||
}
|
||||
fmt.Fprintln(&out)
|
||||
for _, he := range hosts {
|
||||
fmt.Fprintf(&out, "%s %s\n", he.Addr, strings.Join(he.Hosts, " "))
|
||||
}
|
||||
fmt.Fprintln(&out)
|
||||
fmt.Fprintln(&out, footer)
|
||||
}
|
||||
return bytes.ReplaceAll(out.Bytes(), []byte("\n"), []byte("\r\n")), nil
|
||||
}
|
||||
|
||||
// setHosts sets the hosts file to contain the given host entries.
|
||||
func (m *windowsManager) setHosts(hosts []*HostEntry) error {
|
||||
systemDir, err := windows.GetSystemDirectory()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hostsFile := filepath.Join(systemDir, "drivers", "etc", "hosts")
|
||||
b, err := os.ReadFile(hostsFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outB, err := setTailscaleHosts(b, hosts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
const fileMode = 0 // ignored on windows.
|
||||
|
||||
// This can fail spuriously with an access denied error, so retry it a
|
||||
// few times.
|
||||
for range 5 {
|
||||
if err = atomicfile.WriteFile(hostsFile, outB, fileMode); err == nil {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// setPrimaryDNS sets the given resolvers and domains as the Tailscale
|
||||
// interface's DNS configuration.
|
||||
// If resolvers is non-empty, those resolvers become the system's
|
||||
// "primary" resolvers.
|
||||
// domains can be set without resolvers, which just contributes new
|
||||
// paths to the global DNS search list.
|
||||
func (m *windowsManager) setPrimaryDNS(resolvers []netip.Addr, domains []dnsname.FQDN) error {
|
||||
var ipsv4 []string
|
||||
var ipsv6 []string
|
||||
|
||||
for _, ip := range resolvers {
|
||||
if ip.Is4() {
|
||||
ipsv4 = append(ipsv4, ip.String())
|
||||
} else {
|
||||
ipsv6 = append(ipsv6, ip.String())
|
||||
}
|
||||
}
|
||||
|
||||
domStrs := make([]string, 0, len(domains))
|
||||
for _, dom := range domains {
|
||||
domStrs = append(domStrs, dom.WithoutTrailingDot())
|
||||
}
|
||||
|
||||
key4, err := m.openInterfaceKey(winutil.IPv4TCPIPInterfacePrefix)
|
||||
if err != nil {
|
||||
return m.muteKeyNotFoundIfClosing(err)
|
||||
}
|
||||
defer key4.Close()
|
||||
|
||||
if len(ipsv4) == 0 {
|
||||
if err := delValue(key4, "NameServer"); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := key4.SetStringValue("NameServer", strings.Join(ipsv4, ",")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(domains) == 0 {
|
||||
if err := delValue(key4, "SearchList"); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := key4.SetStringValue("SearchList", strings.Join(domStrs, ",")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key6, err := m.openInterfaceKey(winutil.IPv6TCPIPInterfacePrefix)
|
||||
if err != nil {
|
||||
return m.muteKeyNotFoundIfClosing(err)
|
||||
}
|
||||
defer key6.Close()
|
||||
|
||||
if len(ipsv6) == 0 {
|
||||
if err := delValue(key6, "NameServer"); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := key6.SetStringValue("NameServer", strings.Join(ipsv6, ",")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(domains) == 0 {
|
||||
if err := delValue(key6, "SearchList"); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := key6.SetStringValue("SearchList", strings.Join(domStrs, ",")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Disable LLMNR on the Tailscale interface. We don't do multicast, and we
|
||||
// certainly don't do LLMNR, so it's pointless to make Windows try it. It is
|
||||
// being deprecated.
|
||||
if err := key4.SetDWordValue("EnableMulticast", 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := key6.SetDWordValue("EnableMulticast", 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *windowsManager) disableLocalDNSOverrideViaNRPT() bool {
|
||||
return m.knobs != nil && m.knobs.DisableLocalDNSOverrideViaNRPT.Load()
|
||||
}
|
||||
|
||||
func (m *windowsManager) SetDNS(cfg OSConfig) error {
|
||||
// We can configure Windows DNS in one of two ways:
|
||||
//
|
||||
// - In primary DNS mode, we set the NameServer and SearchList
|
||||
// registry keys on our interface. Because our interface metric
|
||||
// is very low, this turns us into the one and only "primary"
|
||||
// resolver for the OS, i.e. all queries flow to the
|
||||
// resolver(s) we specify.
|
||||
// - In split DNS mode, we set the Domain registry key on our
|
||||
// interface (which adds that domain to the global search list,
|
||||
// but does not contribute other DNS configuration from the
|
||||
// interface), and configure an NRPT (Name Resolution Policy
|
||||
// Table) rule to route queries for our suffixes to the
|
||||
// provided resolver.
|
||||
//
|
||||
// When switching modes, we delete all the configuration related
|
||||
// to the other mode, so these two are an XOR.
|
||||
//
|
||||
// Windows actually supports much more advanced configurations as
|
||||
// well, with arbitrary routing of hosts and suffixes to arbitrary
|
||||
// resolvers. However, we use it in a "simple" split domain
|
||||
// configuration only, routing one set of things to the "split"
|
||||
// resolver and the rest to the primary.
|
||||
|
||||
// Unconditionally disable dynamic DNS updates and NetBIOS on our
|
||||
// interfaces.
|
||||
if err := m.disableDynamicUpdates(); err != nil {
|
||||
m.logf("disableDynamicUpdates error: %v\n", err)
|
||||
}
|
||||
if err := m.disableNetBIOS(); err != nil {
|
||||
m.logf("disableNetBIOS error: %v\n", err)
|
||||
}
|
||||
|
||||
if len(cfg.MatchDomains) == 0 {
|
||||
var resolvers []netip.Addr
|
||||
var domains []dnsname.FQDN
|
||||
if !m.disableLocalDNSOverrideViaNRPT() {
|
||||
// Create a default catch-all rule to make ourselves the actual primary resolver.
|
||||
// Without this rule, Windows 8.1 and newer devices issue parallel DNS requests to DNS servers
|
||||
// associated with all network adapters, even when "Override local DNS" is enabled and/or
|
||||
// a Mullvad exit node is being used, resulting in DNS leaks.
|
||||
resolvers = cfg.Nameservers
|
||||
domains = []dnsname.FQDN{"."}
|
||||
}
|
||||
if err := m.setSplitDNS(resolvers, domains); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.setHosts(nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.setPrimaryDNS(cfg.Nameservers, cfg.SearchDomains); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := m.setSplitDNS(cfg.Nameservers, cfg.MatchDomains); err != nil {
|
||||
return err
|
||||
}
|
||||
// Unset the resolver on the interface to ensure that we do not become
|
||||
// the primary resolver. Although this is what we want, at the moment
|
||||
// (2022-08-13) it causes single label resolutions from the OS resolver
|
||||
// to wait for a MDNS response from the Tailscale interface.
|
||||
// See #1659 and #5366 for more details.
|
||||
//
|
||||
// Still set search domains on the interface, since NRPT only handles
|
||||
// query routing and not search domain expansion.
|
||||
if err := m.setPrimaryDNS(nil, cfg.SearchDomains); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// As we are not the primary resolver in this setup, we need to
|
||||
// explicitly set some single name hosts to ensure that we can resolve
|
||||
// them quickly and get around the 2.3s delay that otherwise occurs due
|
||||
// to multicast timeouts.
|
||||
if err := m.setHosts(cfg.Hosts); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Force DNS re-registration in Active Directory. What we actually
|
||||
// care about is that this command invokes the undocumented hidden
|
||||
// function that forces Windows to notice that adapter settings
|
||||
// have changed, which makes the DNS settings actually take
|
||||
// effect.
|
||||
//
|
||||
// This command can take a few seconds to run, so run it async, best effort.
|
||||
//
|
||||
// After re-registering DNS, also flush the DNS cache to clear out
|
||||
// any cached split-horizon queries that are no longer the correct
|
||||
// answer.
|
||||
go func() {
|
||||
t0 := time.Now()
|
||||
m.logf("running ipconfig /registerdns ...")
|
||||
cmd := exec.Command("ipconfig", "/registerdns")
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
CreationFlags: windows.DETACHED_PROCESS,
|
||||
}
|
||||
err := cmd.Run()
|
||||
d := time.Since(t0).Round(time.Millisecond)
|
||||
if err != nil {
|
||||
m.logf("error running ipconfig /registerdns after %v: %v", d, err)
|
||||
} else {
|
||||
m.logf("ran ipconfig /registerdns in %v", d)
|
||||
}
|
||||
|
||||
t0 = time.Now()
|
||||
m.logf("running ipconfig /flushdns ...")
|
||||
cmd = exec.Command("ipconfig", "/flushdns")
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
CreationFlags: windows.DETACHED_PROCESS,
|
||||
}
|
||||
err = cmd.Run()
|
||||
d = time.Since(t0).Round(time.Millisecond)
|
||||
if err != nil {
|
||||
m.logf("error running ipconfig /flushdns after %v: %v", d, err)
|
||||
} else {
|
||||
m.logf("ran ipconfig /flushdns in %v", d)
|
||||
}
|
||||
}()
|
||||
|
||||
// On initial setup of WSL, the restart caused by --shutdown is slow,
|
||||
// so we do it out-of-line.
|
||||
if configureWSL() {
|
||||
go func() {
|
||||
if err := m.wslManager.SetDNS(cfg); err != nil {
|
||||
m.logf("WSL SetDNS: %v", err) // continue
|
||||
} else {
|
||||
m.logf("WSL SetDNS: success")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *windowsManager) SupportsSplitDNS() bool {
|
||||
return m.nrptDB != nil
|
||||
}
|
||||
|
||||
func (m *windowsManager) Close() error {
|
||||
m.mu.Lock()
|
||||
if m.closing {
|
||||
m.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
m.closing = true
|
||||
m.mu.Unlock()
|
||||
|
||||
err := m.SetDNS(OSConfig{})
|
||||
if m.nrptDB != nil {
|
||||
m.nrptDB.Close()
|
||||
m.nrptDB = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// disableDynamicUpdates sets the appropriate registry values to prevent the
|
||||
// Windows DHCP client from sending dynamic DNS updates for our interface to
|
||||
// AD domain controllers.
|
||||
func (m *windowsManager) disableDynamicUpdates() error {
|
||||
prefixen := []winutil.RegistryPathPrefix{
|
||||
winutil.IPv4TCPIPInterfacePrefix,
|
||||
winutil.IPv6TCPIPInterfacePrefix,
|
||||
}
|
||||
|
||||
for _, prefix := range prefixen {
|
||||
k, err := m.openInterfaceKey(prefix)
|
||||
if err != nil {
|
||||
return m.muteKeyNotFoundIfClosing(err)
|
||||
}
|
||||
defer k.Close()
|
||||
|
||||
if err := k.SetDWordValue("RegistrationEnabled", 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := k.SetDWordValue("DisableDynamicUpdate", 1); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := k.SetDWordValue("MaxNumberOfAddressesToRegister", 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setSingleDWORD opens the Registry Key in HKLM for the interface associated
|
||||
// with the windowsManager and sets the "keyPrefix\value" to data.
|
||||
func (m *windowsManager) setSingleDWORD(prefix winutil.RegistryPathPrefix, value string, data uint32) error {
|
||||
k, err := m.openInterfaceKey(prefix)
|
||||
if err != nil {
|
||||
return m.muteKeyNotFoundIfClosing(err)
|
||||
}
|
||||
defer k.Close()
|
||||
return k.SetDWordValue(value, data)
|
||||
}
|
||||
|
||||
// disableNetBIOS sets the appropriate registry values to prevent Windows from
|
||||
// sending NetBIOS name resolution requests for our interface which we do not
|
||||
// handle nor want to. By leaving it enabled and not handling it we introduce
|
||||
// short-name resolution delays in certain conditions as Windows waits for
|
||||
// NetBIOS responses from our interface (#1659).
|
||||
//
|
||||
// Further, LLMNR and NetBIOS are being deprecated anyway in favor of MDNS.
|
||||
// https://techcommunity.microsoft.com/t5/networking-blog/aligning-on-mdns-ramping-down-netbios-name-resolution-and-llmnr/ba-p/3290816
|
||||
func (m *windowsManager) disableNetBIOS() error {
|
||||
return m.setSingleDWORD(winutil.NetBTInterfacePrefix, "NetbiosOptions", 2)
|
||||
}
|
||||
|
||||
func (m *windowsManager) GetBaseConfig() (OSConfig, error) {
|
||||
resolvers, err := m.getBasePrimaryResolver()
|
||||
if err != nil {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
return OSConfig{
|
||||
Nameservers: resolvers,
|
||||
// Don't return any search domains here, because even Windows
|
||||
// 7 correctly handles blending search domains from multiple
|
||||
// sources, and any search domains we add here will get tacked
|
||||
// onto the Tailscale config unnecessarily.
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getBasePrimaryResolver returns a guess of the non-Tailscale primary
|
||||
// resolver on the system.
|
||||
// It's used on Windows 7 to emulate split DNS by trying to figure out
|
||||
// what the "previous" primary resolver was. It might be wrong, or
|
||||
// incomplete.
|
||||
func (m *windowsManager) getBasePrimaryResolver() (resolvers []netip.Addr, err error) {
|
||||
tsGUID, err := windows.GUIDFromString(m.guid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tsLUID, err := winipcfg.LUIDFromGUID(&tsGUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ifrows, err := winipcfg.GetIPInterfaceTable(windows.AF_INET)
|
||||
if err == windows.ERROR_NOT_FOUND {
|
||||
// IPv4 seems disabled, try to get interface metrics from IPv6 instead.
|
||||
ifrows, err = winipcfg.GetIPInterfaceTable(windows.AF_INET6)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
type candidate struct {
|
||||
id winipcfg.LUID
|
||||
metric uint32
|
||||
}
|
||||
var candidates []candidate
|
||||
for _, row := range ifrows {
|
||||
if !row.Connected {
|
||||
continue
|
||||
}
|
||||
if row.InterfaceLUID == tsLUID {
|
||||
continue
|
||||
}
|
||||
candidates = append(candidates, candidate{row.InterfaceLUID, row.Metric})
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
// No resolvers set outside of Tailscale.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
sort.Slice(candidates, func(i, j int) bool { return candidates[i].metric < candidates[j].metric })
|
||||
|
||||
for _, candidate := range candidates {
|
||||
ips, err := candidate.id.DNS()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ipLoop:
|
||||
for _, ip := range ips {
|
||||
ip = ip.Unmap()
|
||||
// Skip IPv6 site-local resolvers. These are an ancient
|
||||
// and obsolete IPv6 RFC, which Windows still faithfully
|
||||
// implements. The net result is that some low-metric
|
||||
// interfaces can "have" DNS resolvers, but they're just
|
||||
// site-local resolver IPs that don't go anywhere. So, we
|
||||
// skip the site-local resolvers in order to find the
|
||||
// first interface that has real DNS servers configured.
|
||||
for _, sl := range siteLocalResolvers {
|
||||
if ip.WithZone("") == sl {
|
||||
continue ipLoop
|
||||
}
|
||||
}
|
||||
resolvers = append(resolvers, ip)
|
||||
}
|
||||
|
||||
if len(resolvers) > 0 {
|
||||
// Found some resolvers, we're done.
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return resolvers, nil
|
||||
}
|
||||
|
||||
var siteLocalResolvers = []netip.Addr{
|
||||
netip.MustParseAddr("fec0:0:0:ffff::1"),
|
||||
netip.MustParseAddr("fec0:0:0:ffff::2"),
|
||||
netip.MustParseAddr("fec0:0:0:ffff::3"),
|
||||
}
|
||||
|
||||
func isWindows10OrBetter() bool {
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE, versionKey, registry.READ)
|
||||
if err != nil {
|
||||
// Fail safe, assume old Windows.
|
||||
return false
|
||||
}
|
||||
// This key above only exists in Windows 10 and above. Its mere
|
||||
// presence is good enough.
|
||||
if _, _, err := key.GetIntegerValue("CurrentMajorVersionNumber"); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
391
vendor/tailscale.com/net/dns/nm.go
generated
vendored
Normal file
391
vendor/tailscale.com/net/dns/nm.go
generated
vendored
Normal file
@@ -0,0 +1,391 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"github.com/josharian/native"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
const (
|
||||
highestPriority = int32(-1 << 31)
|
||||
mediumPriority = int32(1) // Highest priority that doesn't hard-override
|
||||
lowerPriority = int32(200) // lower than all builtin auto priorities
|
||||
)
|
||||
|
||||
// reconfigTimeout is the time interval within which Manager.{Up,Down} should complete.
|
||||
//
|
||||
// This is particularly useful because certain conditions can cause indefinite hangs
|
||||
// (such as improper dbus auth followed by contextless dbus.Object.Call).
|
||||
// Such operations should be wrapped in a timeout context.
|
||||
const reconfigTimeout = time.Second
|
||||
|
||||
// nmManager uses the NetworkManager DBus API.
|
||||
type nmManager struct {
|
||||
interfaceName string
|
||||
manager dbus.BusObject
|
||||
dnsManager dbus.BusObject
|
||||
}
|
||||
|
||||
func newNMManager(interfaceName string) (*nmManager, error) {
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &nmManager{
|
||||
interfaceName: interfaceName,
|
||||
manager: conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager")),
|
||||
dnsManager: conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager/DnsManager")),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type nmConnectionSettings map[string]map[string]dbus.Variant
|
||||
|
||||
func (m *nmManager) SetDNS(config OSConfig) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout)
|
||||
defer cancel()
|
||||
|
||||
// NetworkManager only lets you set DNS settings on "active"
|
||||
// connections, which requires an assigned IP address. This got
|
||||
// configured before the DNS manager was invoked, but it might
|
||||
// take a little time for the netlink notifications to propagate
|
||||
// up. So, keep retrying for the duration of the reconfigTimeout.
|
||||
var err error
|
||||
for ctx.Err() == nil {
|
||||
err = m.trySet(ctx, config)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *nmManager) trySet(ctx context.Context, config OSConfig) error {
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to system bus: %w", err)
|
||||
}
|
||||
|
||||
// This is how we get at the DNS settings:
|
||||
//
|
||||
// org.freedesktop.NetworkManager
|
||||
// |
|
||||
// [GetDeviceByIpIface]
|
||||
// |
|
||||
// v
|
||||
// org.freedesktop.NetworkManager.Device <--------\
|
||||
// (describes a network interface) |
|
||||
// | |
|
||||
// [GetAppliedConnection] [Reapply]
|
||||
// | |
|
||||
// v |
|
||||
// org.freedesktop.NetworkManager.Connection |
|
||||
// (connection settings) ------/
|
||||
// contains {dns, dns-priority, dns-search}
|
||||
//
|
||||
// Ref: https://developer.gnome.org/NetworkManager/stable/settings-ipv4.html.
|
||||
|
||||
nm := conn.Object(
|
||||
"org.freedesktop.NetworkManager",
|
||||
dbus.ObjectPath("/org/freedesktop/NetworkManager"),
|
||||
)
|
||||
|
||||
var devicePath dbus.ObjectPath
|
||||
err = nm.CallWithContext(
|
||||
ctx, "org.freedesktop.NetworkManager.GetDeviceByIpIface", 0,
|
||||
m.interfaceName,
|
||||
).Store(&devicePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getDeviceByIpIface: %w", err)
|
||||
}
|
||||
device := conn.Object("org.freedesktop.NetworkManager", devicePath)
|
||||
|
||||
var (
|
||||
settings nmConnectionSettings
|
||||
version uint64
|
||||
)
|
||||
err = device.CallWithContext(
|
||||
ctx, "org.freedesktop.NetworkManager.Device.GetAppliedConnection", 0,
|
||||
uint32(0),
|
||||
).Store(&settings, &version)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getAppliedConnection: %w", err)
|
||||
}
|
||||
|
||||
// Frustratingly, NetworkManager represents IPv4 addresses as uint32s,
|
||||
// although IPv6 addresses are represented as byte arrays.
|
||||
// Perform the conversion here.
|
||||
var (
|
||||
dnsv4 []uint32
|
||||
dnsv6 [][]byte
|
||||
)
|
||||
for _, ip := range config.Nameservers {
|
||||
b := ip.As16()
|
||||
if ip.Is4() {
|
||||
dnsv4 = append(dnsv4, native.Endian.Uint32(b[12:]))
|
||||
} else {
|
||||
dnsv6 = append(dnsv6, b[:])
|
||||
}
|
||||
}
|
||||
|
||||
// NetworkManager wipes out IPv6 address configuration unless we
|
||||
// tell it explicitly to keep it. Read out the current interface
|
||||
// settings and mirror them out to NetworkManager.
|
||||
var addrs6 []map[string]any
|
||||
if tsIf, err := net.InterfaceByName(m.interfaceName); err == nil {
|
||||
addrs, _ := tsIf.Addrs()
|
||||
for _, a := range addrs {
|
||||
if ipnet, ok := a.(*net.IPNet); ok {
|
||||
nip, ok := netip.AddrFromSlice(ipnet.IP)
|
||||
nip = nip.Unmap()
|
||||
if ok && tsaddr.IsTailscaleIP(nip) && nip.Is6() {
|
||||
addrs6 = append(addrs6, map[string]any{
|
||||
"address": nip.String(),
|
||||
"prefix": uint32(128),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
seen := map[dnsname.FQDN]bool{}
|
||||
var search []string
|
||||
for _, dom := range config.SearchDomains {
|
||||
if seen[dom] {
|
||||
continue
|
||||
}
|
||||
seen[dom] = true
|
||||
search = append(search, dom.WithTrailingDot())
|
||||
}
|
||||
for _, dom := range config.MatchDomains {
|
||||
if seen[dom] {
|
||||
continue
|
||||
}
|
||||
seen[dom] = true
|
||||
search = append(search, "~"+dom.WithTrailingDot())
|
||||
}
|
||||
if len(config.MatchDomains) == 0 {
|
||||
// Non-split routing requested, add an all-domains match.
|
||||
search = append(search, "~.")
|
||||
}
|
||||
|
||||
// Ideally we would like to disable LLMNR and mdns on the
|
||||
// interface here, but older NetworkManagers don't understand
|
||||
// those settings and choke on them, so we don't. Both LLMNR and
|
||||
// mdns will fail since tailscale0 doesn't do multicast, so it's
|
||||
// effectively fine. We used to try and enforce LLMNR and mdns
|
||||
// settings here, but that led to #1870.
|
||||
|
||||
ipv4Map := settings["ipv4"]
|
||||
ipv4Map["dns"] = dbus.MakeVariant(dnsv4)
|
||||
ipv4Map["dns-search"] = dbus.MakeVariant(search)
|
||||
// We should only request priority if we have nameservers to set.
|
||||
if len(dnsv4) == 0 {
|
||||
ipv4Map["dns-priority"] = dbus.MakeVariant(lowerPriority)
|
||||
} else if len(config.MatchDomains) > 0 {
|
||||
// Set a fairly high priority, but don't override all other
|
||||
// configs when in split-DNS mode.
|
||||
ipv4Map["dns-priority"] = dbus.MakeVariant(mediumPriority)
|
||||
} else {
|
||||
// Negative priority means only the settings from the most
|
||||
// negative connection get used. The way this mixes with
|
||||
// per-domain routing is unclear, but it _seems_ that the
|
||||
// priority applies after routing has found possible
|
||||
// candidates for a resolution.
|
||||
ipv4Map["dns-priority"] = dbus.MakeVariant(highestPriority)
|
||||
}
|
||||
|
||||
ipv6Map := settings["ipv6"]
|
||||
// In IPv6 settings, you're only allowed to provide additional
|
||||
// static DNS settings in "auto" (SLAAC) or "manual" mode. In
|
||||
// "manual" mode you also have to specify IP addresses, so we use
|
||||
// "auto".
|
||||
//
|
||||
// NM actually documents that to set just DNS servers, you should
|
||||
// use "auto" mode and then set ignore auto routes and DNS, which
|
||||
// basically means "autoconfigure but ignore any autoconfiguration
|
||||
// results you might get". As a safety, we also say that
|
||||
// NetworkManager should never try to make us the default route
|
||||
// (none of its business anyway, we handle our own default
|
||||
// routing).
|
||||
ipv6Map["method"] = dbus.MakeVariant("auto")
|
||||
if len(addrs6) > 0 {
|
||||
ipv6Map["address-data"] = dbus.MakeVariant(addrs6)
|
||||
}
|
||||
ipv6Map["ignore-auto-routes"] = dbus.MakeVariant(true)
|
||||
ipv6Map["ignore-auto-dns"] = dbus.MakeVariant(true)
|
||||
ipv6Map["never-default"] = dbus.MakeVariant(true)
|
||||
|
||||
ipv6Map["dns"] = dbus.MakeVariant(dnsv6)
|
||||
ipv6Map["dns-search"] = dbus.MakeVariant(search)
|
||||
if len(dnsv6) == 0 {
|
||||
ipv6Map["dns-priority"] = dbus.MakeVariant(lowerPriority)
|
||||
} else if len(config.MatchDomains) > 0 {
|
||||
// Set a fairly high priority, but don't override all other
|
||||
// configs when in split-DNS mode.
|
||||
ipv6Map["dns-priority"] = dbus.MakeVariant(mediumPriority)
|
||||
} else {
|
||||
ipv6Map["dns-priority"] = dbus.MakeVariant(highestPriority)
|
||||
}
|
||||
|
||||
// deprecatedProperties are the properties in interface settings
|
||||
// that are deprecated by NetworkManager.
|
||||
//
|
||||
// In practice, this means that they are returned for reading,
|
||||
// but submitting a settings object with them present fails
|
||||
// with hard-to-diagnose errors. They must be removed.
|
||||
deprecatedProperties := []string{
|
||||
"addresses", "routes",
|
||||
}
|
||||
|
||||
for _, property := range deprecatedProperties {
|
||||
delete(ipv4Map, property)
|
||||
delete(ipv6Map, property)
|
||||
}
|
||||
|
||||
if call := device.CallWithContext(ctx, "org.freedesktop.NetworkManager.Device.Reapply", 0, settings, version, uint32(0)); call.Err != nil {
|
||||
return fmt.Errorf("reapply: %w", call.Err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *nmManager) SupportsSplitDNS() bool {
|
||||
var mode string
|
||||
v, err := m.dnsManager.GetProperty("org.freedesktop.NetworkManager.DnsManager.Mode")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
mode, ok := v.Value().(string)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// Per NM's documentation, it only does split-DNS when it's
|
||||
// programming dnsmasq or systemd-resolved. All other modes are
|
||||
// primary-only.
|
||||
return mode == "dnsmasq" || mode == "systemd-resolved"
|
||||
}
|
||||
|
||||
func (m *nmManager) GetBaseConfig() (OSConfig, error) {
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
|
||||
nm := conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager/DnsManager"))
|
||||
v, err := nm.GetProperty("org.freedesktop.NetworkManager.DnsManager.Configuration")
|
||||
if err != nil {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
cfgs, ok := v.Value().([]map[string]dbus.Variant)
|
||||
if !ok {
|
||||
return OSConfig{}, fmt.Errorf("unexpected NM config type %T", v.Value())
|
||||
}
|
||||
|
||||
if len(cfgs) == 0 {
|
||||
return OSConfig{}, nil
|
||||
}
|
||||
|
||||
type dnsPrio struct {
|
||||
resolvers []netip.Addr
|
||||
domains []string
|
||||
priority int32
|
||||
}
|
||||
order := make([]dnsPrio, 0, len(cfgs)-1)
|
||||
|
||||
for _, cfg := range cfgs {
|
||||
if name, ok := cfg["interface"]; ok {
|
||||
if s, ok := name.Value().(string); ok && s == m.interfaceName {
|
||||
// Config for the tailscale interface, skip.
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
var p dnsPrio
|
||||
|
||||
if v, ok := cfg["nameservers"]; ok {
|
||||
if ips, ok := v.Value().([]string); ok {
|
||||
for _, s := range ips {
|
||||
ip, err := netip.ParseAddr(s)
|
||||
if err != nil {
|
||||
// hmm, what do? Shouldn't really happen.
|
||||
continue
|
||||
}
|
||||
p.resolvers = append(p.resolvers, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
if v, ok := cfg["domains"]; ok {
|
||||
if domains, ok := v.Value().([]string); ok {
|
||||
p.domains = domains
|
||||
}
|
||||
}
|
||||
if v, ok := cfg["priority"]; ok {
|
||||
if prio, ok := v.Value().(int32); ok {
|
||||
p.priority = prio
|
||||
}
|
||||
}
|
||||
|
||||
order = append(order, p)
|
||||
}
|
||||
|
||||
sort.Slice(order, func(i, j int) bool {
|
||||
return order[i].priority < order[j].priority
|
||||
})
|
||||
|
||||
var (
|
||||
ret OSConfig
|
||||
seenResolvers = map[netip.Addr]bool{}
|
||||
seenSearch = map[string]bool{}
|
||||
)
|
||||
|
||||
for _, cfg := range order {
|
||||
for _, resolver := range cfg.resolvers {
|
||||
if seenResolvers[resolver] {
|
||||
continue
|
||||
}
|
||||
ret.Nameservers = append(ret.Nameservers, resolver)
|
||||
seenResolvers[resolver] = true
|
||||
}
|
||||
for _, dom := range cfg.domains {
|
||||
if seenSearch[dom] {
|
||||
continue
|
||||
}
|
||||
fqdn, err := dnsname.ToFQDN(dom)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ret.SearchDomains = append(ret.SearchDomains, fqdn)
|
||||
seenSearch[dom] = true
|
||||
}
|
||||
if cfg.priority < 0 {
|
||||
// exclusive configurations preempt all other
|
||||
// configurations, so we're done.
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (m *nmManager) Close() error {
|
||||
// No need to do anything on close, NetworkManager will delete our
|
||||
// settings when the tailscale interface goes away.
|
||||
return nil
|
||||
}
|
||||
17
vendor/tailscale.com/net/dns/noop.go
generated
vendored
Normal file
17
vendor/tailscale.com/net/dns/noop.go
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dns
|
||||
|
||||
type noopManager struct{}
|
||||
|
||||
func (m noopManager) SetDNS(OSConfig) error { return nil }
|
||||
func (m noopManager) SupportsSplitDNS() bool { return false }
|
||||
func (m noopManager) Close() error { return nil }
|
||||
func (m noopManager) GetBaseConfig() (OSConfig, error) {
|
||||
return OSConfig{}, ErrGetBaseConfigNotSupported
|
||||
}
|
||||
|
||||
func NewNoopManager() (noopManager, error) {
|
||||
return noopManager{}, nil
|
||||
}
|
||||
459
vendor/tailscale.com/net/dns/nrpt_windows.go
generated
vendored
Normal file
459
vendor/tailscale.com/net/dns/nrpt_windows.go
generated
vendored
Normal file
@@ -0,0 +1,459 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/winutil"
|
||||
"tailscale.com/util/winutil/gp"
|
||||
)
|
||||
|
||||
const (
|
||||
dnsBaseGP = `SOFTWARE\Policies\Microsoft\Windows NT\DNSClient`
|
||||
nrptBaseLocal = `SYSTEM\CurrentControlSet\Services\Dnscache\Parameters\DnsPolicyConfig`
|
||||
nrptBaseGP = `SOFTWARE\Policies\Microsoft\Windows NT\DNSClient\DnsPolicyConfig`
|
||||
|
||||
nrptOverrideDNS = 0x8 // bitmask value for "use the provided override DNS resolvers"
|
||||
|
||||
// Apparently NRPT rules cannot handle > 50 domains.
|
||||
nrptMaxDomainsPerRule = 50
|
||||
|
||||
// This is the legacy rule ID that previous versions used when we supported
|
||||
// only a single rule. Now that we support multiple rules are required, we
|
||||
// generate their GUIDs and store them under the Tailscale registry key.
|
||||
nrptSingleRuleID = `{5abe529b-675b-4486-8459-25a634dacc23}`
|
||||
|
||||
// This is the name of the registry value we use to save Rule IDs under
|
||||
// the Tailscale registry key.
|
||||
nrptRuleIDValueName = `NRPTRuleIDs`
|
||||
|
||||
// This is the name of the registry value the NRPT uses for storing a rule's version number.
|
||||
nrptRuleVersionName = `Version`
|
||||
|
||||
// This is the name of the registry value the NRPT uses for storing a rule's list of domains.
|
||||
nrptRuleDomsName = `Name`
|
||||
|
||||
// This is the name of the registry value the NRPT uses for storing a rule's list of DNS servers.
|
||||
nrptRuleServersName = `GenericDNSServers`
|
||||
|
||||
// This is the name of the registry value the NRPT uses for storing a rule's flags.
|
||||
nrptRuleFlagsName = `ConfigOptions`
|
||||
)
|
||||
|
||||
// nrptRuleDatabase encapsulates access to the Windows Name Resolution Policy
|
||||
// Table (NRPT).
|
||||
type nrptRuleDatabase struct {
|
||||
logf logger.Logf
|
||||
watcher *gp.ChangeWatcher
|
||||
isGPRefreshPending atomic.Bool
|
||||
mu sync.Mutex // protects the fields below
|
||||
ruleIDs []string
|
||||
isGPDirty bool
|
||||
writeAsGP bool
|
||||
}
|
||||
|
||||
func newNRPTRuleDatabase(logf logger.Logf) *nrptRuleDatabase {
|
||||
ret := &nrptRuleDatabase{logf: logf}
|
||||
ret.loadRuleSubkeyNames()
|
||||
ret.detectWriteAsGP()
|
||||
ret.watchForGPChanges()
|
||||
// Best-effort: if our NRPT rule exists, try to delete it. Unlike
|
||||
// per-interface configuration, NRPT rules survive the unclean
|
||||
// termination of the Tailscale process, and depending on the
|
||||
// rule, it may prevent us from reaching login.tailscale.com to
|
||||
// boot up. The bootstrap resolver logic will save us, but it
|
||||
// slows down start-up a bunch.
|
||||
ret.DelAllRuleKeys()
|
||||
return ret
|
||||
}
|
||||
|
||||
func (db *nrptRuleDatabase) loadRuleSubkeyNames() {
|
||||
// Use the legacy rule ID if none are specified in our registry key
|
||||
db.ruleIDs = winutil.GetRegStrings(nrptRuleIDValueName, []string{nrptSingleRuleID})
|
||||
}
|
||||
|
||||
// detectWriteAsGP determines which registry path should be used for writing
|
||||
// NRPT rules. If there are rules in the GP path that don't belong to us, then
|
||||
// we should use the GP path. When detectWriteAsGP determines that the desired
|
||||
// path has changed, it moves the NRPT policies as appropriate.
|
||||
func (db *nrptRuleDatabase) detectWriteAsGP() {
|
||||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
|
||||
writeAsGP := false
|
||||
var err error
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
prev := db.writeAsGP
|
||||
db.writeAsGP = writeAsGP
|
||||
db.logf("nrptRuleDatabase using group policy: %v, was %v\n", writeAsGP, prev)
|
||||
// When db.watcher == nil, prev != writeAsGP because we're initializing, not
|
||||
// because anything has changed. We do not invoke
|
||||
// db.updateGroupPoliciesLocked in that case.
|
||||
if db.watcher != nil && prev != writeAsGP {
|
||||
db.updateGroupPoliciesLocked(writeAsGP)
|
||||
}
|
||||
}()
|
||||
|
||||
// Get a list of all the NRPT rules under the GP subkey.
|
||||
nrptKey, err := registry.OpenKey(registry.LOCAL_MACHINE, nrptBaseGP, registry.READ)
|
||||
if err != nil {
|
||||
if err != registry.ErrNotExist {
|
||||
db.logf("Failed to open key %q with error: %v\n", nrptBaseGP, err)
|
||||
}
|
||||
// If this subkey does not exist then we definitely don't need to use the GP key.
|
||||
return
|
||||
}
|
||||
defer nrptKey.Close()
|
||||
|
||||
gpSubkeyNames, err := nrptKey.ReadSubKeyNames(0)
|
||||
if err != nil {
|
||||
db.logf("Failed to list subkeys under %q with error: %v\n", nrptBaseGP, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Add *all* rules from the GP subkey into a set.
|
||||
gpSubkeyMap := make(set.Set[string], len(gpSubkeyNames))
|
||||
for _, gpSubkey := range gpSubkeyNames {
|
||||
gpSubkeyMap.Add(strings.ToUpper(gpSubkey))
|
||||
}
|
||||
|
||||
// Remove *our* rules from the set.
|
||||
for _, ourRuleID := range db.ruleIDs {
|
||||
gpSubkeyMap.Delete(strings.ToUpper(ourRuleID))
|
||||
}
|
||||
|
||||
// Any leftover rules do not belong to us. When group policy is being used
|
||||
// by something else, we must also use the GP path.
|
||||
writeAsGP = len(gpSubkeyMap) > 0
|
||||
}
|
||||
|
||||
// DelAllRuleKeys removes any and all NRPT rules that are owned by Tailscale.
|
||||
func (db *nrptRuleDatabase) DelAllRuleKeys() error {
|
||||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
|
||||
if err := db.delRuleKeys(db.ruleIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := winutil.DeleteRegValue(nrptRuleIDValueName); err != nil {
|
||||
db.logf("Error deleting registry value %q: %v", nrptRuleIDValueName, err)
|
||||
return err
|
||||
}
|
||||
db.ruleIDs = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// delRuleKeys removes the NRPT rules specified by nrptRuleIDs from the
|
||||
// Windows registry. It attempts to remove the rules from both possible registry
|
||||
// keys: the local key and the group policy key.
|
||||
func (db *nrptRuleDatabase) delRuleKeys(nrptRuleIDs []string) error {
|
||||
for _, rid := range nrptRuleIDs {
|
||||
keyNameLocal := nrptBaseLocal + `\` + rid
|
||||
if err := registry.DeleteKey(registry.LOCAL_MACHINE, keyNameLocal); err != nil && err != registry.ErrNotExist {
|
||||
db.logf("Error deleting NRPT rule key %q: %v", keyNameLocal, err)
|
||||
return err
|
||||
}
|
||||
|
||||
keyNameGP := nrptBaseGP + `\` + rid
|
||||
err := registry.DeleteKey(registry.LOCAL_MACHINE, keyNameGP)
|
||||
if err == nil {
|
||||
// If this deleted subkey existed under the GP key, we will need to refresh.
|
||||
db.isGPDirty = true
|
||||
} else if err != registry.ErrNotExist {
|
||||
db.logf("Error deleting NRPT rule key %q: %v", keyNameGP, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !db.isGPDirty {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If we've removed keys from the Group Policy subkey, and the DNSPolicyConfig
|
||||
// subkey is now empty, we need to remove that subkey.
|
||||
isEmpty, err := isPolicyConfigSubkeyEmpty()
|
||||
if err != nil || !isEmpty {
|
||||
return err
|
||||
}
|
||||
|
||||
return registry.DeleteKey(registry.LOCAL_MACHINE, nrptBaseGP)
|
||||
}
|
||||
|
||||
// isPolicyConfigSubkeyEmpty returns true if and only if the nrptBaseGP exists
|
||||
// and does not contain any values or subkeys.
|
||||
func isPolicyConfigSubkeyEmpty() (bool, error) {
|
||||
subKey, err := registry.OpenKey(registry.LOCAL_MACHINE, nrptBaseGP, registry.READ)
|
||||
if err != nil {
|
||||
if err == registry.ErrNotExist {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
defer subKey.Close()
|
||||
|
||||
ki, err := subKey.Stat()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return (ki.ValueCount == 0 && ki.SubKeyCount == 0), nil
|
||||
}
|
||||
|
||||
func (db *nrptRuleDatabase) WriteSplitDNSConfig(servers []string, domains []dnsname.FQDN) error {
|
||||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
|
||||
// NRPT has an undocumented restriction that each rule may only be associated
|
||||
// with a maximum of 50 domains. If we are setting rules for more domains
|
||||
// than that, we need to split domains into chunks and write out a rule per chunk.
|
||||
domainRulesLen := (len(domains) + nrptMaxDomainsPerRule - 1) / nrptMaxDomainsPerRule
|
||||
db.loadRuleSubkeyNames()
|
||||
|
||||
for len(db.ruleIDs) < domainRulesLen {
|
||||
guid, err := windows.GenerateGUID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db.ruleIDs = append(db.ruleIDs, guid.String())
|
||||
}
|
||||
|
||||
// Remove any surplus rules that are no longer needed.
|
||||
ruleIDsToRemove := db.ruleIDs[domainRulesLen:]
|
||||
db.delRuleKeys(ruleIDsToRemove)
|
||||
|
||||
// We need to save the list of rule IDs to our Tailscale registry key so that
|
||||
// we know which rules are ours during subsequent modifications to NRPT rules.
|
||||
ruleIDsToWrite := db.ruleIDs[:domainRulesLen]
|
||||
if len(ruleIDsToWrite) == 0 {
|
||||
if err := winutil.DeleteRegValue(nrptRuleIDValueName); err != nil {
|
||||
return err
|
||||
}
|
||||
db.ruleIDs = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := winutil.SetRegStrings(nrptRuleIDValueName, ruleIDsToWrite); err != nil {
|
||||
return err
|
||||
}
|
||||
db.ruleIDs = ruleIDsToWrite
|
||||
|
||||
curRuleID := 0
|
||||
doms := make([]string, 0, nrptMaxDomainsPerRule)
|
||||
|
||||
for _, domain := range domains {
|
||||
if len(doms) == nrptMaxDomainsPerRule {
|
||||
if err := db.writeNRPTRule(db.ruleIDs[curRuleID], servers, doms); err != nil {
|
||||
return err
|
||||
}
|
||||
curRuleID++
|
||||
doms = doms[:0]
|
||||
}
|
||||
|
||||
// NRPT rules must have a leading dot, which is not usual for
|
||||
// DNS search paths.
|
||||
doms = append(doms, "."+domain.WithoutTrailingDot())
|
||||
}
|
||||
|
||||
if len(doms) > 0 {
|
||||
if err := db.writeNRPTRule(db.ruleIDs[curRuleID], servers, doms); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Refresh notifies the Windows group policy engine when policies have changed.
|
||||
func (db *nrptRuleDatabase) Refresh() {
|
||||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
|
||||
db.refreshLocked()
|
||||
}
|
||||
|
||||
func (db *nrptRuleDatabase) refreshLocked() {
|
||||
if !db.isGPDirty {
|
||||
return
|
||||
}
|
||||
|
||||
// Record that we are about to initiate a refresh.
|
||||
// (*nrptRuleDatabase).watchForGPChanges() checks this value to avoid false
|
||||
// positives.
|
||||
db.isGPRefreshPending.Store(true)
|
||||
|
||||
if err := gp.RefreshMachinePolicy(true); err != nil {
|
||||
db.logf("RefreshMachinePolicy failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
db.isGPDirty = false
|
||||
}
|
||||
|
||||
func (db *nrptRuleDatabase) writeNRPTRule(ruleID string, servers, doms []string) error {
|
||||
subKeys := []string{nrptBaseLocal, nrptBaseGP}
|
||||
if !db.writeAsGP {
|
||||
// We don't want to write to the GP key, so chop nrptBaseGP off of subKeys.
|
||||
subKeys = subKeys[:1]
|
||||
}
|
||||
|
||||
for _, subKeyBase := range subKeys {
|
||||
subKey := strings.Join([]string{subKeyBase, ruleID}, `\`)
|
||||
key, _, err := registry.CreateKey(registry.LOCAL_MACHINE, subKey, registry.SET_VALUE)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening %q: %w", subKey, err)
|
||||
}
|
||||
defer key.Close()
|
||||
|
||||
if err := writeNRPTValues(key, strings.Join(servers, "; "), doms); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
db.isGPDirty = db.writeAsGP
|
||||
return nil
|
||||
}
|
||||
|
||||
func readNRPTValues(key registry.Key) (servers string, doms []string, err error) {
|
||||
doms, _, err = key.GetStringsValue(nrptRuleDomsName)
|
||||
if err != nil {
|
||||
return servers, doms, err
|
||||
}
|
||||
|
||||
servers, _, err = key.GetStringValue(nrptRuleServersName)
|
||||
return servers, doms, err
|
||||
}
|
||||
|
||||
func writeNRPTValues(key registry.Key, servers string, doms []string) error {
|
||||
if err := key.SetDWordValue(nrptRuleVersionName, 1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := key.SetStringsValue(nrptRuleDomsName, doms); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := key.SetStringValue(nrptRuleServersName, servers); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return key.SetDWordValue(nrptRuleFlagsName, nrptOverrideDNS)
|
||||
}
|
||||
|
||||
func (db *nrptRuleDatabase) watchForGPChanges() {
|
||||
watchHandler := func() {
|
||||
// Do not invoke detectWriteAsGP when we ourselves were responsible for
|
||||
// initiating the group policy refresh.
|
||||
if db.isGPRefreshPending.CompareAndSwap(true, false) {
|
||||
return
|
||||
}
|
||||
db.logf("Computer group policies refreshed, reconfiguring NRPT rule database.")
|
||||
db.detectWriteAsGP()
|
||||
}
|
||||
|
||||
watcher, err := gp.NewChangeWatcher(gp.MachinePolicy, watchHandler)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
db.watcher = watcher
|
||||
}
|
||||
|
||||
// updateGroupPoliciesLocked updates the NRPT group policy table depending on
|
||||
// the value of writeAsGP. When writeAsGP is true, each NRPT rule is copied from
|
||||
// the local NRPT table to the group policy NRPT table. When writeAsGP is false,
|
||||
// we remove any Tailscale NRPT rules from the group policy table and, if no
|
||||
// non-Tailscale rules remain, we also delete the entire DnsPolicyConfig subkey.
|
||||
// db.mu must already be locked.
|
||||
func (db *nrptRuleDatabase) updateGroupPoliciesLocked(writeAsGP bool) {
|
||||
// Since we're updating the group policy NRPT table, we need
|
||||
// to refresh once this updateGroupPoliciesLocked is done.
|
||||
defer db.refreshLocked()
|
||||
|
||||
for _, id := range db.ruleIDs {
|
||||
if writeAsGP {
|
||||
if err := copyNRPTRule(id); err != nil {
|
||||
db.logf("updateGroupPoliciesLocked: copyNRPTRule(%q) failed with error %v", id, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
subKeyFrom := strings.Join([]string{nrptBaseGP, id}, `\`)
|
||||
if err := registry.DeleteKey(registry.LOCAL_MACHINE, subKeyFrom); err != nil && err != registry.ErrNotExist {
|
||||
db.logf("updateGroupPoliciesLocked: DeleteKey for rule %q failed with error %v", id, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
db.isGPDirty = true
|
||||
}
|
||||
|
||||
if writeAsGP {
|
||||
return
|
||||
}
|
||||
|
||||
// Now that we have removed our rules from group policy subkey, it should
|
||||
// now be empty. Let's verify that.
|
||||
isEmpty, err := isPolicyConfigSubkeyEmpty()
|
||||
if err != nil {
|
||||
db.logf("updateGroupPoliciesLocked: isPolicyConfigSubkeyEmpty error %v", err)
|
||||
return
|
||||
}
|
||||
if !isEmpty {
|
||||
db.logf("updateGroupPoliciesLocked: policy config subkey should be empty, but isn't!")
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the subkey itself. Group policy will continue to override local
|
||||
// settings unless we do so.
|
||||
if err := registry.DeleteKey(registry.LOCAL_MACHINE, nrptBaseGP); err != nil {
|
||||
db.logf("updateGroupPoliciesLocked DeleteKey error %v", err)
|
||||
}
|
||||
|
||||
db.isGPDirty = true
|
||||
}
|
||||
|
||||
func copyNRPTRule(ruleID string) error {
|
||||
subKeyFrom := strings.Join([]string{nrptBaseLocal, ruleID}, `\`)
|
||||
subKeyTo := strings.Join([]string{nrptBaseGP, ruleID}, `\`)
|
||||
|
||||
fromKey, err := registry.OpenKey(registry.LOCAL_MACHINE, subKeyFrom, registry.QUERY_VALUE)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fromKey.Close()
|
||||
|
||||
toKey, _, err := registry.CreateKey(registry.LOCAL_MACHINE, subKeyTo, registry.WRITE)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer toKey.Close()
|
||||
|
||||
servers, doms, err := readNRPTValues(fromKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return writeNRPTValues(toKey, servers, doms)
|
||||
}
|
||||
|
||||
func (db *nrptRuleDatabase) Close() error {
|
||||
if db.watcher == nil {
|
||||
return nil
|
||||
}
|
||||
err := db.watcher.Close()
|
||||
db.watcher = nil
|
||||
return err
|
||||
}
|
||||
115
vendor/tailscale.com/net/dns/openresolv.go
generated
vendored
Normal file
115
vendor/tailscale.com/net/dns/openresolv.go
generated
vendored
Normal file
@@ -0,0 +1,115 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux || freebsd || openbsd
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// openresolvManager manages DNS configuration using the openresolv
|
||||
// implementation of the `resolvconf` program.
|
||||
type openresolvManager struct {
|
||||
logf logger.Logf
|
||||
}
|
||||
|
||||
func newOpenresolvManager(logf logger.Logf) (openresolvManager, error) {
|
||||
return openresolvManager{logf}, nil
|
||||
}
|
||||
|
||||
func (m openresolvManager) logCmdErr(cmd *exec.Cmd, err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
commandStr := fmt.Sprintf("path=%q args=%q", cmd.Path, cmd.Args)
|
||||
exerr, ok := err.(*exec.ExitError)
|
||||
if !ok {
|
||||
m.logf("error running command %s: %v", commandStr, err)
|
||||
return
|
||||
}
|
||||
|
||||
m.logf("error running command %s stderr=%q exitCode=%d: %v", commandStr, exerr.Stderr, exerr.ExitCode(), err)
|
||||
}
|
||||
|
||||
func (m openresolvManager) deleteTailscaleConfig() error {
|
||||
cmd := exec.Command("resolvconf", "-f", "-d", "tailscale")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
m.logCmdErr(cmd, err)
|
||||
return fmt.Errorf("running %s: %s", cmd, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m openresolvManager) SetDNS(config OSConfig) error {
|
||||
if config.IsZero() {
|
||||
return m.deleteTailscaleConfig()
|
||||
}
|
||||
|
||||
var stdin bytes.Buffer
|
||||
writeResolvConf(&stdin, config.Nameservers, config.SearchDomains)
|
||||
|
||||
cmd := exec.Command("resolvconf", "-m", "0", "-x", "-a", "tailscale")
|
||||
cmd.Stdin = &stdin
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
m.logCmdErr(cmd, err)
|
||||
return fmt.Errorf("running %s: %s", cmd, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m openresolvManager) SupportsSplitDNS() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m openresolvManager) GetBaseConfig() (OSConfig, error) {
|
||||
// List the names of all config snippets openresolv is aware
|
||||
// of. Snippets get listed in priority order (most to least),
|
||||
// which we'll exploit later.
|
||||
bs, err := exec.Command("resolvconf", "-i").CombinedOutput()
|
||||
if err != nil {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
|
||||
// Remove the "tailscale" snippet from the list.
|
||||
args := []string{"-l"}
|
||||
for _, f := range strings.Split(strings.TrimSpace(string(bs)), " ") {
|
||||
if f == "tailscale" {
|
||||
continue
|
||||
}
|
||||
args = append(args, f)
|
||||
}
|
||||
|
||||
// List all resolvconf snippets except our own, and parse that as
|
||||
// a resolv.conf. This effectively generates a blended config of
|
||||
// "everyone except tailscale", which is what would be in use if
|
||||
// tailscale hadn't set exclusive mode.
|
||||
//
|
||||
// Note that this is not _entirely_ true. To be perfectly correct,
|
||||
// we should be looking for other interfaces marked exclusive that
|
||||
// predated tailscale, and stick to only those. However, in
|
||||
// practice, openresolv uses are generally quite limited, and boil
|
||||
// down to 1-2 DHCP leases, for which the correct outcome is a
|
||||
// blended config like the one we produce here.
|
||||
var buf bytes.Buffer
|
||||
cmd := exec.Command("resolvconf", args...)
|
||||
cmd.Stdout = &buf
|
||||
if err := cmd.Run(); err != nil {
|
||||
m.logCmdErr(cmd, err)
|
||||
return OSConfig{}, err
|
||||
}
|
||||
return readResolv(&buf)
|
||||
}
|
||||
|
||||
func (m openresolvManager) Close() error {
|
||||
return m.deleteTailscaleConfig()
|
||||
}
|
||||
196
vendor/tailscale.com/net/dns/osconfig.go
generated
vendored
Normal file
196
vendor/tailscale.com/net/dns/osconfig.go
generated
vendored
Normal file
@@ -0,0 +1,196 @@
|
||||
// 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")
|
||||
344
vendor/tailscale.com/net/dns/publicdns/publicdns.go
generated
vendored
Normal file
344
vendor/tailscale.com/net/dns/publicdns/publicdns.go
generated
vendored
Normal file
@@ -0,0 +1,344 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package publicdns contains mapping and helpers for working with
|
||||
// public DNS providers.
|
||||
package publicdns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/big"
|
||||
"net/netip"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// dohOfIP maps from public DNS IPs to their DoH base URL.
|
||||
//
|
||||
// This does not include NextDNS which is handled specially.
|
||||
var dohOfIP = map[netip.Addr]string{} // 8.8.8.8 => "https://..."
|
||||
|
||||
var dohIPsOfBase = map[string][]netip.Addr{}
|
||||
var populateOnce sync.Once
|
||||
|
||||
const (
|
||||
nextDNSBase = "https://dns.nextdns.io/"
|
||||
controlDBase = "https://dns.controld.com/"
|
||||
)
|
||||
|
||||
// DoHEndpointFromIP returns the DNS-over-HTTPS base URL for a given IP
|
||||
// and whether it's DoH-only (not speaking DNS on port 53).
|
||||
//
|
||||
// The ok result is whether the IP is a known DNS server.
|
||||
func DoHEndpointFromIP(ip netip.Addr) (dohBase string, dohOnly bool, ok bool) {
|
||||
populateOnce.Do(populate)
|
||||
if b, ok := dohOfIP[ip]; ok {
|
||||
return b, false, true
|
||||
}
|
||||
|
||||
// NextDNS DoH URLs are of the form "https://dns.nextdns.io/c3a884"
|
||||
// where the path component is the lower 12 bytes of the IPv6 address
|
||||
// in lowercase hex without any zero padding.
|
||||
if nextDNSv6RangeA.Contains(ip) || nextDNSv6RangeB.Contains(ip) {
|
||||
a := ip.As16()
|
||||
var sb strings.Builder
|
||||
sb.Grow(len(nextDNSBase) + 12)
|
||||
sb.WriteString(nextDNSBase)
|
||||
for _, b := range bytes.TrimLeft(a[4:], "\x00") {
|
||||
fmt.Fprintf(&sb, "%02x", b)
|
||||
}
|
||||
return sb.String(), true, true
|
||||
}
|
||||
|
||||
// Control D DoH URLs are of the form "https://dns.controld.com/8yezwenugs"
|
||||
// where the path component is represented by 8 bytes (7-14) of the IPv6 address in base36
|
||||
if controlDv6RangeA.Contains(ip) || controlDv6RangeB.Contains(ip) {
|
||||
path := big.NewInt(0).SetBytes(ip.AsSlice()[6:14]).Text(36)
|
||||
return controlDBase + path, true, true
|
||||
}
|
||||
|
||||
return "", false, false
|
||||
}
|
||||
|
||||
// KnownDoHPrefixes returns the list of DoH base URLs.
|
||||
//
|
||||
// It returns a new copy each time, sorted. It's meant for tests.
|
||||
//
|
||||
// It does not include providers that have customer-specific DoH URLs like
|
||||
// NextDNS.
|
||||
func KnownDoHPrefixes() []string {
|
||||
populateOnce.Do(populate)
|
||||
ret := make([]string, 0, len(dohIPsOfBase))
|
||||
for b := range dohIPsOfBase {
|
||||
ret = append(ret, b)
|
||||
}
|
||||
sort.Strings(ret)
|
||||
return ret
|
||||
}
|
||||
|
||||
func isSlashOrQuestionMark(r rune) bool {
|
||||
return r == '/' || r == '?'
|
||||
}
|
||||
|
||||
// DoHIPsOfBase returns the IP addresses to use to dial the provided DoH base
|
||||
// URL.
|
||||
//
|
||||
// It is basically the inverse of DoHEndpointFromIP with the exception that for
|
||||
// NextDNS it returns IPv4 addresses that DoHEndpointFromIP doesn't map back.
|
||||
func DoHIPsOfBase(dohBase string) []netip.Addr {
|
||||
populateOnce.Do(populate)
|
||||
if s := dohIPsOfBase[dohBase]; len(s) > 0 {
|
||||
return s
|
||||
}
|
||||
if hexStr, ok := strings.CutPrefix(dohBase, nextDNSBase); ok {
|
||||
// The path is of the form /<profile-hex>[/<hostname>/<model>/<device id>...]
|
||||
// or /<profile-hex>?<query params>
|
||||
// but only the <profile-hex> is required. Ignore the rest:
|
||||
if i := strings.IndexFunc(hexStr, isSlashOrQuestionMark); i != -1 {
|
||||
hexStr = hexStr[:i]
|
||||
}
|
||||
|
||||
// TODO(bradfitz): using the NextDNS anycast addresses works but is not
|
||||
// ideal. Some of their regions have better latency via a non-anycast IP
|
||||
// which we could get by first resolving A/AAAA "dns.nextdns.io" over
|
||||
// DoH using their anycast address. For now we only use the anycast
|
||||
// addresses. The IPv4 IPs we use are just the first one in their ranges.
|
||||
// For IPv6 we put the profile ID in the lower bytes, but that seems just
|
||||
// conventional for them and not required (it'll already be in the DoH path).
|
||||
// (Really we shouldn't use either IPv4 or IPv6 anycast for DoH once we
|
||||
// resolve "dns.nextdns.io".)
|
||||
if b, err := hex.DecodeString(hexStr); err == nil && len(b) <= 12 && len(b) > 0 {
|
||||
return []netip.Addr{
|
||||
nextDNSv4One,
|
||||
nextDNSv4Two,
|
||||
nextDNSv6Gen(nextDNSv6RangeA.Addr(), b),
|
||||
nextDNSv6Gen(nextDNSv6RangeB.Addr(), b),
|
||||
}
|
||||
}
|
||||
}
|
||||
if pathStr, ok := strings.CutPrefix(dohBase, controlDBase); ok {
|
||||
if i := strings.IndexFunc(pathStr, isSlashOrQuestionMark); i != -1 {
|
||||
pathStr = pathStr[:i]
|
||||
}
|
||||
return []netip.Addr{
|
||||
controlDv4One,
|
||||
controlDv4Two,
|
||||
controlDv6Gen(controlDv6RangeA.Addr(), pathStr),
|
||||
controlDv6Gen(controlDv6RangeB.Addr(), pathStr),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DoHV6 returns the first IPv6 DNS address from a given public DNS provider
|
||||
// if found, along with a boolean indicating success.
|
||||
func DoHV6(base string) (ip netip.Addr, ok bool) {
|
||||
populateOnce.Do(populate)
|
||||
for _, ip := range dohIPsOfBase[base] {
|
||||
if ip.Is6() {
|
||||
return ip, true
|
||||
}
|
||||
}
|
||||
return ip, false
|
||||
}
|
||||
|
||||
// addDoH parses a given well-formed ip string into a netip.Addr type and
|
||||
// adds it to both knownDoH and dohIPsOFBase maps.
|
||||
func addDoH(ipStr, base string) {
|
||||
ip := netip.MustParseAddr(ipStr)
|
||||
dohOfIP[ip] = base
|
||||
dohIPsOfBase[base] = append(dohIPsOfBase[base], ip)
|
||||
}
|
||||
|
||||
const (
|
||||
wikimediaDNSv4 = "185.71.138.138"
|
||||
wikimediaDNSv6 = "2001:67c:930::1"
|
||||
)
|
||||
|
||||
// populate is called once to initialize the knownDoH and dohIPsOfBase maps.
|
||||
func populate() {
|
||||
// Cloudflare
|
||||
// https://developers.cloudflare.com/1.1.1.1/ip-addresses/
|
||||
addDoH("1.1.1.1", "https://cloudflare-dns.com/dns-query")
|
||||
addDoH("1.0.0.1", "https://cloudflare-dns.com/dns-query")
|
||||
addDoH("2606:4700:4700::1111", "https://cloudflare-dns.com/dns-query")
|
||||
addDoH("2606:4700:4700::1001", "https://cloudflare-dns.com/dns-query")
|
||||
|
||||
// Cloudflare -Malware
|
||||
addDoH("1.1.1.2", "https://security.cloudflare-dns.com/dns-query")
|
||||
addDoH("1.0.0.2", "https://security.cloudflare-dns.com/dns-query")
|
||||
addDoH("2606:4700:4700::1112", "https://security.cloudflare-dns.com/dns-query")
|
||||
addDoH("2606:4700:4700::1002", "https://security.cloudflare-dns.com/dns-query")
|
||||
|
||||
// Cloudflare -Malware -Adult
|
||||
addDoH("1.1.1.3", "https://family.cloudflare-dns.com/dns-query")
|
||||
addDoH("1.0.0.3", "https://family.cloudflare-dns.com/dns-query")
|
||||
addDoH("2606:4700:4700::1113", "https://family.cloudflare-dns.com/dns-query")
|
||||
addDoH("2606:4700:4700::1003", "https://family.cloudflare-dns.com/dns-query")
|
||||
|
||||
// Google
|
||||
addDoH("8.8.8.8", "https://dns.google/dns-query")
|
||||
addDoH("8.8.4.4", "https://dns.google/dns-query")
|
||||
addDoH("2001:4860:4860::8888", "https://dns.google/dns-query")
|
||||
addDoH("2001:4860:4860::8844", "https://dns.google/dns-query")
|
||||
|
||||
// OpenDNS
|
||||
// TODO(bradfitz): OpenDNS is unique amongst this current set in that
|
||||
// its DoH DNS names resolve to different IPs than its normal DNS
|
||||
// IPs. Support that later. For now we assume that they're the same.
|
||||
// addDoH("208.67.222.222", "https://doh.opendns.com/dns-query")
|
||||
// addDoH("208.67.220.220", "https://doh.opendns.com/dns-query")
|
||||
// addDoH("208.67.222.123", "https://doh.familyshield.opendns.com/dns-query")
|
||||
// addDoH("208.67.220.123", "https://doh.familyshield.opendns.com/dns-query")
|
||||
|
||||
// Quad9
|
||||
// https://www.quad9.net/service/service-addresses-and-features
|
||||
addDoH("9.9.9.9", "https://dns.quad9.net/dns-query")
|
||||
addDoH("149.112.112.112", "https://dns.quad9.net/dns-query")
|
||||
addDoH("2620:fe::fe", "https://dns.quad9.net/dns-query")
|
||||
addDoH("2620:fe::9", "https://dns.quad9.net/dns-query")
|
||||
|
||||
// Quad9 +ECS +DNSSEC
|
||||
addDoH("9.9.9.11", "https://dns11.quad9.net/dns-query")
|
||||
addDoH("149.112.112.11", "https://dns11.quad9.net/dns-query")
|
||||
addDoH("2620:fe::11", "https://dns11.quad9.net/dns-query")
|
||||
addDoH("2620:fe::fe:11", "https://dns11.quad9.net/dns-query")
|
||||
|
||||
// Quad9 -DNSSEC
|
||||
addDoH("9.9.9.10", "https://dns10.quad9.net/dns-query")
|
||||
addDoH("149.112.112.10", "https://dns10.quad9.net/dns-query")
|
||||
addDoH("2620:fe::10", "https://dns10.quad9.net/dns-query")
|
||||
addDoH("2620:fe::fe:10", "https://dns10.quad9.net/dns-query")
|
||||
|
||||
// Mullvad
|
||||
// See https://mullvad.net/en/help/dns-over-https-and-dns-over-tls/
|
||||
// Mullvad (default)
|
||||
addDoH("194.242.2.2", "https://dns.mullvad.net/dns-query")
|
||||
addDoH("2a07:e340::2", "https://dns.mullvad.net/dns-query")
|
||||
// Mullvad (adblock)
|
||||
addDoH("194.242.2.3", "https://adblock.dns.mullvad.net/dns-query")
|
||||
addDoH("2a07:e340::3", "https://adblock.dns.mullvad.net/dns-query")
|
||||
// Mullvad (base)
|
||||
addDoH("194.242.2.4", "https://base.dns.mullvad.net/dns-query")
|
||||
addDoH("2a07:e340::4", "https://base.dns.mullvad.net/dns-query")
|
||||
// Mullvad (extended)
|
||||
addDoH("194.242.2.5", "https://extended.dns.mullvad.net/dns-query")
|
||||
addDoH("2a07:e340::5", "https://extended.dns.mullvad.net/dns-query")
|
||||
// Mullvad (family)
|
||||
addDoH("194.242.2.6", "https://family.dns.mullvad.net/dns-query")
|
||||
addDoH("2a07:e340::6", "https://family.dns.mullvad.net/dns-query")
|
||||
// Mullvad (all)
|
||||
addDoH("194.242.2.9", "https://all.dns.mullvad.net/dns-query")
|
||||
addDoH("2a07:e340::9", "https://all.dns.mullvad.net/dns-query")
|
||||
|
||||
// Wikimedia
|
||||
addDoH(wikimediaDNSv4, "https://wikimedia-dns.org/dns-query")
|
||||
addDoH(wikimediaDNSv6, "https://wikimedia-dns.org/dns-query")
|
||||
|
||||
// Control D
|
||||
addDoH("76.76.2.0", "https://freedns.controld.com/p0")
|
||||
addDoH("76.76.10.0", "https://freedns.controld.com/p0")
|
||||
addDoH("2606:1a40::", "https://freedns.controld.com/p0")
|
||||
addDoH("2606:1a40:1::", "https://freedns.controld.com/p0")
|
||||
|
||||
// Control D -Malware
|
||||
addDoH("76.76.2.1", "https://freedns.controld.com/p1")
|
||||
addDoH("76.76.10.1", "https://freedns.controld.com/p1")
|
||||
addDoH("2606:1a40::1", "https://freedns.controld.com/p1")
|
||||
addDoH("2606:1a40:1::1", "https://freedns.controld.com/p1")
|
||||
|
||||
// Control D -Malware + Ads
|
||||
addDoH("76.76.2.2", "https://freedns.controld.com/p2")
|
||||
addDoH("76.76.10.2", "https://freedns.controld.com/p2")
|
||||
addDoH("2606:1a40::2", "https://freedns.controld.com/p2")
|
||||
addDoH("2606:1a40:1::2", "https://freedns.controld.com/p2")
|
||||
|
||||
// Control D -Malware + Ads + Social
|
||||
addDoH("76.76.2.3", "https://freedns.controld.com/p3")
|
||||
addDoH("76.76.10.3", "https://freedns.controld.com/p3")
|
||||
addDoH("2606:1a40::3", "https://freedns.controld.com/p3")
|
||||
addDoH("2606:1a40:1::3", "https://freedns.controld.com/p3")
|
||||
|
||||
// Control D -Malware + Ads + Adult
|
||||
addDoH("76.76.2.4", "https://freedns.controld.com/family")
|
||||
addDoH("76.76.10.4", "https://freedns.controld.com/family")
|
||||
addDoH("2606:1a40::4", "https://freedns.controld.com/family")
|
||||
addDoH("2606:1a40:1::4", "https://freedns.controld.com/family")
|
||||
}
|
||||
|
||||
var (
|
||||
// The NextDNS IPv6 ranges (primary and secondary). The customer ID is
|
||||
// encoded in the lower bytes and is used (in hex form) as the DoH query
|
||||
// path.
|
||||
nextDNSv6RangeA = netip.MustParsePrefix("2a07:a8c0::/33")
|
||||
nextDNSv6RangeB = netip.MustParsePrefix("2a07:a8c1::/33")
|
||||
|
||||
// The first two IPs in the /24 v4 ranges can be used for DoH to NextDNS.
|
||||
//
|
||||
// They're Anycast and usually okay, but NextDNS has some locations that
|
||||
// don't do BGP and can get results for querying them over DoH to find the
|
||||
// IPv4 address of "dns.mynextdns.io" and find an even better result.
|
||||
//
|
||||
// Note that the Tailscale DNS client does not do any of the "IP address
|
||||
// linking" that NextDNS can do with its IPv4 addresses. These addresses
|
||||
// are only used for DoH.
|
||||
nextDNSv4RangeA = netip.MustParsePrefix("45.90.28.0/24")
|
||||
nextDNSv4RangeB = netip.MustParsePrefix("45.90.30.0/24")
|
||||
nextDNSv4One = nextDNSv4RangeA.Addr()
|
||||
nextDNSv4Two = nextDNSv4RangeB.Addr()
|
||||
|
||||
// Wikimedia DNS server IPs (anycast)
|
||||
wikimediaDNSv4Addr = netip.MustParseAddr(wikimediaDNSv4)
|
||||
wikimediaDNSv6Addr = netip.MustParseAddr(wikimediaDNSv6)
|
||||
|
||||
// The Control D IPv6 ranges (primary and secondary). The customer ID is
|
||||
// encoded in the ipv6 address is used (in base 36 form) as the DoH query
|
||||
controlDv6RangeA = netip.MustParsePrefix("2606:1a40::/48")
|
||||
controlDv6RangeB = netip.MustParsePrefix("2606:1a40:1::/48")
|
||||
controlDv4One = netip.MustParseAddr("76.76.2.22")
|
||||
controlDv4Two = netip.MustParseAddr("76.76.10.22")
|
||||
)
|
||||
|
||||
// nextDNSv6Gen generates a NextDNS IPv6 address from the upper 8 bytes in the
|
||||
// provided ip and using id as the lowest 0-8 bytes.
|
||||
func nextDNSv6Gen(ip netip.Addr, id []byte) netip.Addr {
|
||||
if len(id) > 12 {
|
||||
return netip.Addr{}
|
||||
}
|
||||
a := ip.As16()
|
||||
copy(a[16-len(id):], id)
|
||||
return netip.AddrFrom16(a)
|
||||
}
|
||||
|
||||
// controlDv6Gen generates a Control D IPv6 address from provided ip and id.
|
||||
//
|
||||
// The id is taken from the DoH query path component and represents a unique resolver configuration.
|
||||
// e.g. https://dns.controld.com/hyq3ipr2ct
|
||||
func controlDv6Gen(ip netip.Addr, id string) netip.Addr {
|
||||
b := make([]byte, 8)
|
||||
decoded, err := strconv.ParseUint(id, 36, 64)
|
||||
if err != nil {
|
||||
log.Printf("controlDv6Gen: failed to parse id %q: %v", id, err)
|
||||
}
|
||||
binary.BigEndian.PutUint64(b, decoded)
|
||||
a := ip.AsSlice()
|
||||
copy(a[6:14], b)
|
||||
addr, _ := netip.AddrFromSlice(a)
|
||||
return addr
|
||||
}
|
||||
|
||||
// IPIsDoHOnlyServer reports whether ip is a DNS server that should only use
|
||||
// DNS-over-HTTPS (not regular port 53 DNS).
|
||||
func IPIsDoHOnlyServer(ip netip.Addr) bool {
|
||||
return nextDNSv6RangeA.Contains(ip) || nextDNSv6RangeB.Contains(ip) ||
|
||||
nextDNSv4RangeA.Contains(ip) || nextDNSv4RangeB.Contains(ip) ||
|
||||
ip == wikimediaDNSv4Addr || ip == wikimediaDNSv6Addr ||
|
||||
controlDv6RangeA.Contains(ip) || controlDv6RangeB.Contains(ip) ||
|
||||
ip == controlDv4One || ip == controlDv4Two
|
||||
}
|
||||
621
vendor/tailscale.com/net/dns/recursive/recursive.go
generated
vendored
Normal file
621
vendor/tailscale.com/net/dns/recursive/recursive.go
generated
vendored
Normal file
@@ -0,0 +1,621 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package recursive implements a simple recursive DNS resolver.
|
||||
package recursive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/multierr"
|
||||
"tailscale.com/util/slicesx"
|
||||
)
|
||||
|
||||
const (
|
||||
// maxDepth is how deep from the root nameservers we'll recurse when
|
||||
// resolving; passing this limit will instead return an error.
|
||||
//
|
||||
// maxDepth must be at least 20 to resolve "console.aws.amazon.com",
|
||||
// which is a domain with a moderately complicated DNS setup. The
|
||||
// current value of 30 was chosen semi-arbitrarily to ensure that we
|
||||
// have about 50% headroom.
|
||||
maxDepth = 30
|
||||
// numStartingServers is the number of root nameservers that we use as
|
||||
// initial candidates for our recursion.
|
||||
numStartingServers = 3
|
||||
// udpQueryTimeout is the amount of time we wait for a UDP response
|
||||
// from a nameserver before falling back to a TCP connection.
|
||||
udpQueryTimeout = 5 * time.Second
|
||||
|
||||
// These constants aren't typed in the DNS package, so we create typed
|
||||
// versions here to avoid having to do repeated type casts.
|
||||
qtypeA dns.Type = dns.Type(dns.TypeA)
|
||||
qtypeAAAA dns.Type = dns.Type(dns.TypeAAAA)
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrMaxDepth is returned when recursive resolving exceeds the maximum
|
||||
// depth limit for this package.
|
||||
ErrMaxDepth = fmt.Errorf("exceeded max depth %d when resolving", maxDepth)
|
||||
|
||||
// ErrAuthoritativeNoResponses is the error returned when an
|
||||
// authoritative nameserver indicates that there are no responses to
|
||||
// the given query.
|
||||
ErrAuthoritativeNoResponses = errors.New("authoritative server returned no responses")
|
||||
|
||||
// ErrNoResponses is returned when our resolution process completes
|
||||
// with no valid responses from any nameserver, but no authoritative
|
||||
// server explicitly returned NXDOMAIN.
|
||||
ErrNoResponses = errors.New("no responses to query")
|
||||
)
|
||||
|
||||
var rootServersV4 = []netip.Addr{
|
||||
netip.MustParseAddr("198.41.0.4"), // a.root-servers.net
|
||||
netip.MustParseAddr("170.247.170.2"), // b.root-servers.net
|
||||
netip.MustParseAddr("192.33.4.12"), // c.root-servers.net
|
||||
netip.MustParseAddr("199.7.91.13"), // d.root-servers.net
|
||||
netip.MustParseAddr("192.203.230.10"), // e.root-servers.net
|
||||
netip.MustParseAddr("192.5.5.241"), // f.root-servers.net
|
||||
netip.MustParseAddr("192.112.36.4"), // g.root-servers.net
|
||||
netip.MustParseAddr("198.97.190.53"), // h.root-servers.net
|
||||
netip.MustParseAddr("192.36.148.17"), // i.root-servers.net
|
||||
netip.MustParseAddr("192.58.128.30"), // j.root-servers.net
|
||||
netip.MustParseAddr("193.0.14.129"), // k.root-servers.net
|
||||
netip.MustParseAddr("199.7.83.42"), // l.root-servers.net
|
||||
netip.MustParseAddr("202.12.27.33"), // m.root-servers.net
|
||||
}
|
||||
|
||||
var rootServersV6 = []netip.Addr{
|
||||
netip.MustParseAddr("2001:503:ba3e::2:30"), // a.root-servers.net
|
||||
netip.MustParseAddr("2801:1b8:10::b"), // b.root-servers.net
|
||||
netip.MustParseAddr("2001:500:2::c"), // c.root-servers.net
|
||||
netip.MustParseAddr("2001:500:2d::d"), // d.root-servers.net
|
||||
netip.MustParseAddr("2001:500:a8::e"), // e.root-servers.net
|
||||
netip.MustParseAddr("2001:500:2f::f"), // f.root-servers.net
|
||||
netip.MustParseAddr("2001:500:12::d0d"), // g.root-servers.net
|
||||
netip.MustParseAddr("2001:500:1::53"), // h.root-servers.net
|
||||
netip.MustParseAddr("2001:7fe::53"), // i.root-servers.net
|
||||
netip.MustParseAddr("2001:503:c27::2:30"), // j.root-servers.net
|
||||
netip.MustParseAddr("2001:7fd::1"), // k.root-servers.net
|
||||
netip.MustParseAddr("2001:500:9f::42"), // l.root-servers.net
|
||||
netip.MustParseAddr("2001:dc3::35"), // m.root-servers.net
|
||||
}
|
||||
|
||||
var debug = envknob.RegisterBool("TS_DEBUG_RECURSIVE_DNS")
|
||||
|
||||
// Resolver is a recursive DNS resolver that is designed for looking up A and AAAA records.
|
||||
type Resolver struct {
|
||||
// Dialer is used to create outbound connections. If nil, a zero
|
||||
// net.Dialer will be used instead.
|
||||
Dialer netns.Dialer
|
||||
|
||||
// Logf is the logging function to use; if none is specified, then logs
|
||||
// will be dropped.
|
||||
Logf logger.Logf
|
||||
|
||||
// NoIPv6, if set, will prevent this package from querying for AAAA
|
||||
// records and will avoid contacting nameservers over IPv6.
|
||||
NoIPv6 bool
|
||||
|
||||
// Test mocks
|
||||
testQueryHook func(name dnsname.FQDN, nameserver netip.Addr, protocol string, qtype dns.Type) (*dns.Msg, error)
|
||||
testExchangeHook func(nameserver netip.Addr, network string, msg *dns.Msg) (*dns.Msg, error)
|
||||
rootServers []netip.Addr
|
||||
timeNow func() time.Time
|
||||
|
||||
// Caching
|
||||
// NOTE(andrew): if we make resolution parallel, this needs a mutex
|
||||
queryCache map[dnsQuery]dnsMsgWithExpiry
|
||||
|
||||
// Possible future additions:
|
||||
// - Additional nameservers? From the system maybe?
|
||||
// - NoIPv4 for IPv4
|
||||
// - DNS-over-HTTPS or DNS-over-TLS support
|
||||
}
|
||||
|
||||
// queryState stores all state during the course of a single query
|
||||
type queryState struct {
|
||||
// rootServers are the root nameservers to start from
|
||||
rootServers []netip.Addr
|
||||
|
||||
// TODO: metrics?
|
||||
}
|
||||
|
||||
type dnsQuery struct {
|
||||
nameserver netip.Addr
|
||||
name dnsname.FQDN
|
||||
qtype dns.Type
|
||||
}
|
||||
|
||||
func (q dnsQuery) String() string {
|
||||
return fmt.Sprintf("dnsQuery{nameserver:%q,name:%q,qtype:%v}", q.nameserver.String(), q.name, q.qtype)
|
||||
}
|
||||
|
||||
type dnsMsgWithExpiry struct {
|
||||
*dns.Msg
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
func (r *Resolver) now() time.Time {
|
||||
if r.timeNow != nil {
|
||||
return r.timeNow()
|
||||
}
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
func (r *Resolver) logf(format string, args ...any) {
|
||||
if r.Logf == nil {
|
||||
return
|
||||
}
|
||||
r.Logf(format, args...)
|
||||
}
|
||||
|
||||
func (r *Resolver) depthlogf(depth int, format string, args ...any) {
|
||||
if r.Logf == nil || !debug() {
|
||||
return
|
||||
}
|
||||
prefix := fmt.Sprintf("[%d] %s", depth, strings.Repeat(" ", depth))
|
||||
r.Logf(prefix+format, args...)
|
||||
}
|
||||
|
||||
var defaultDialer net.Dialer
|
||||
|
||||
func (r *Resolver) dialer() netns.Dialer {
|
||||
if r.Dialer != nil {
|
||||
return r.Dialer
|
||||
}
|
||||
|
||||
return &defaultDialer
|
||||
}
|
||||
|
||||
func (r *Resolver) newState() *queryState {
|
||||
var rootServers []netip.Addr
|
||||
if len(r.rootServers) > 0 {
|
||||
rootServers = r.rootServers
|
||||
} else {
|
||||
// Select a random subset of root nameservers to start from, since if
|
||||
// we don't get responses from those, something else has probably gone
|
||||
// horribly wrong.
|
||||
roots4 := slices.Clone(rootServersV4)
|
||||
slicesx.Shuffle(roots4)
|
||||
roots4 = roots4[:numStartingServers]
|
||||
|
||||
var roots6 []netip.Addr
|
||||
if !r.NoIPv6 {
|
||||
roots6 = slices.Clone(rootServersV6)
|
||||
slicesx.Shuffle(roots6)
|
||||
roots6 = roots6[:numStartingServers]
|
||||
}
|
||||
|
||||
// Interleave the root servers so that we try to contact them over
|
||||
// IPv4, then IPv6, IPv4, IPv6, etc.
|
||||
rootServers = slicesx.Interleave(roots4, roots6)
|
||||
}
|
||||
|
||||
return &queryState{
|
||||
rootServers: rootServers,
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve will perform a recursive DNS resolution for the provided name,
|
||||
// starting at a randomly-chosen root DNS server, and return the A and AAAA
|
||||
// responses as a slice of netip.Addrs along with the minimum TTL for the
|
||||
// returned records.
|
||||
func (r *Resolver) Resolve(ctx context.Context, name string) (addrs []netip.Addr, minTTL time.Duration, err error) {
|
||||
dnsName, err := dnsname.ToFQDN(name)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
qstate := r.newState()
|
||||
|
||||
r.logf("querying IPv4 addresses for: %q", name)
|
||||
addrs4, minTTL4, err4 := r.resolveRecursiveFromRoot(ctx, qstate, 0, dnsName, qtypeA)
|
||||
|
||||
var (
|
||||
addrs6 []netip.Addr
|
||||
minTTL6 time.Duration
|
||||
err6 error
|
||||
)
|
||||
if !r.NoIPv6 {
|
||||
r.logf("querying IPv6 addresses for: %q", name)
|
||||
addrs6, minTTL6, err6 = r.resolveRecursiveFromRoot(ctx, qstate, 0, dnsName, qtypeAAAA)
|
||||
}
|
||||
|
||||
if err4 != nil && err6 != nil {
|
||||
if err4 == err6 {
|
||||
return nil, 0, err4
|
||||
}
|
||||
|
||||
return nil, 0, multierr.New(err4, err6)
|
||||
}
|
||||
if err4 != nil {
|
||||
return addrs6, minTTL6, nil
|
||||
} else if err6 != nil {
|
||||
return addrs4, minTTL4, nil
|
||||
}
|
||||
|
||||
minTTL = minTTL4
|
||||
if minTTL6 < minTTL {
|
||||
minTTL = minTTL6
|
||||
}
|
||||
|
||||
addrs = append(addrs4, addrs6...)
|
||||
if len(addrs) == 0 {
|
||||
return nil, 0, ErrNoResponses
|
||||
}
|
||||
|
||||
slicesx.Shuffle(addrs)
|
||||
return addrs, minTTL, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) resolveRecursiveFromRoot(
|
||||
ctx context.Context,
|
||||
qstate *queryState,
|
||||
depth int,
|
||||
name dnsname.FQDN, // what we're querying
|
||||
qtype dns.Type,
|
||||
) ([]netip.Addr, time.Duration, error) {
|
||||
r.depthlogf(depth, "resolving %q from root (type: %v)", name, qtype)
|
||||
|
||||
var depthError bool
|
||||
for _, server := range qstate.rootServers {
|
||||
addrs, minTTL, err := r.resolveRecursive(ctx, qstate, depth, name, server, qtype)
|
||||
if err == nil {
|
||||
return addrs, minTTL, err
|
||||
} else if errors.Is(err, ErrAuthoritativeNoResponses) {
|
||||
return nil, 0, ErrAuthoritativeNoResponses
|
||||
} else if errors.Is(err, ErrMaxDepth) {
|
||||
depthError = true
|
||||
}
|
||||
}
|
||||
|
||||
if depthError {
|
||||
return nil, 0, ErrMaxDepth
|
||||
}
|
||||
return nil, 0, ErrNoResponses
|
||||
}
|
||||
|
||||
func (r *Resolver) resolveRecursive(
|
||||
ctx context.Context,
|
||||
qstate *queryState,
|
||||
depth int,
|
||||
name dnsname.FQDN, // what we're querying
|
||||
nameserver netip.Addr,
|
||||
qtype dns.Type,
|
||||
) ([]netip.Addr, time.Duration, error) {
|
||||
if depth == maxDepth {
|
||||
r.depthlogf(depth, "not recursing past maximum depth")
|
||||
return nil, 0, ErrMaxDepth
|
||||
}
|
||||
|
||||
// Ask this nameserver for an answer.
|
||||
resp, err := r.queryNameserver(ctx, depth, name, nameserver, qtype)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// If we get an actual answer from the nameserver, then return it.
|
||||
var (
|
||||
answers []netip.Addr
|
||||
cnames []dnsname.FQDN
|
||||
minTTL = 24 * 60 * 60 // 24 hours in seconds
|
||||
)
|
||||
for _, answer := range resp.Answer {
|
||||
if crec, ok := answer.(*dns.CNAME); ok {
|
||||
cnameFQDN, err := dnsname.ToFQDN(crec.Target)
|
||||
if err != nil {
|
||||
r.logf("bad CNAME %q returned: %v", crec.Target, err)
|
||||
continue
|
||||
}
|
||||
|
||||
cnames = append(cnames, cnameFQDN)
|
||||
continue
|
||||
}
|
||||
|
||||
addr := addrFromRecord(answer)
|
||||
if !addr.IsValid() {
|
||||
r.logf("[unexpected] invalid record in %T answer", answer)
|
||||
} else if addr.Is4() && qtype != qtypeA {
|
||||
r.logf("[unexpected] got IPv4 answer but qtype=%v", qtype)
|
||||
} else if addr.Is6() && qtype != qtypeAAAA {
|
||||
r.logf("[unexpected] got IPv6 answer but qtype=%v", qtype)
|
||||
} else {
|
||||
answers = append(answers, addr)
|
||||
minTTL = min(minTTL, int(answer.Header().Ttl))
|
||||
}
|
||||
}
|
||||
|
||||
if len(answers) > 0 {
|
||||
r.depthlogf(depth, "got answers for %q: %v", name, answers)
|
||||
return answers, time.Duration(minTTL) * time.Second, nil
|
||||
}
|
||||
|
||||
r.depthlogf(depth, "no answers for %q", name)
|
||||
|
||||
// If we have a non-zero number of CNAMEs, then try resolving those
|
||||
// (from the root again) and return the first one that succeeds.
|
||||
//
|
||||
// TODO: return the union of all responses?
|
||||
// TODO: parallelism?
|
||||
if len(cnames) > 0 {
|
||||
r.depthlogf(depth, "got CNAME responses for %q: %v", name, cnames)
|
||||
}
|
||||
var cnameDepthError bool
|
||||
for _, cname := range cnames {
|
||||
answers, minTTL, err := r.resolveRecursiveFromRoot(ctx, qstate, depth+1, cname, qtype)
|
||||
if err == nil {
|
||||
return answers, minTTL, nil
|
||||
} else if errors.Is(err, ErrAuthoritativeNoResponses) {
|
||||
return nil, 0, ErrAuthoritativeNoResponses
|
||||
} else if errors.Is(err, ErrMaxDepth) {
|
||||
cnameDepthError = true
|
||||
}
|
||||
}
|
||||
|
||||
// If this is an authoritative response, then we know that continuing
|
||||
// to look further is not going to result in any answers and we should
|
||||
// bail out.
|
||||
if resp.MsgHdr.Authoritative {
|
||||
// If we failed to recurse into a CNAME due to a depth limit,
|
||||
// propagate that here.
|
||||
if cnameDepthError {
|
||||
return nil, 0, ErrMaxDepth
|
||||
}
|
||||
|
||||
r.depthlogf(depth, "got authoritative response with no answers; stopping")
|
||||
return nil, 0, ErrAuthoritativeNoResponses
|
||||
}
|
||||
|
||||
r.depthlogf(depth, "got %d NS responses and %d ADDITIONAL responses for %q", len(resp.Ns), len(resp.Extra), name)
|
||||
|
||||
// No CNAMEs and no answers; see if we got any AUTHORITY responses,
|
||||
// which indicate which nameservers to query next.
|
||||
var authorities []dnsname.FQDN
|
||||
for _, rr := range resp.Ns {
|
||||
ns, ok := rr.(*dns.NS)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
nsName, err := dnsname.ToFQDN(ns.Ns)
|
||||
if err != nil {
|
||||
r.logf("unexpected bad NS name %q: %v", ns.Ns, err)
|
||||
continue
|
||||
}
|
||||
|
||||
authorities = append(authorities, nsName)
|
||||
}
|
||||
|
||||
// Also check for "glue" records, which are IP addresses provided by
|
||||
// the DNS server for authority responses; these are required when the
|
||||
// authority server is a subdomain of what's being resolved.
|
||||
glueRecords := make(map[dnsname.FQDN][]netip.Addr)
|
||||
for _, rr := range resp.Extra {
|
||||
name, err := dnsname.ToFQDN(rr.Header().Name)
|
||||
if err != nil {
|
||||
r.logf("unexpected bad Name %q in Extra addr: %v", rr.Header().Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if addr := addrFromRecord(rr); addr.IsValid() {
|
||||
glueRecords[name] = append(glueRecords[name], addr)
|
||||
} else {
|
||||
r.logf("unexpected bad Extra %T addr", rr)
|
||||
}
|
||||
}
|
||||
|
||||
// Try authorities with glue records first, to minimize the number of
|
||||
// additional DNS queries that we need to make.
|
||||
authoritiesGlue, authoritiesNoGlue := slicesx.Partition(authorities, func(aa dnsname.FQDN) bool {
|
||||
return len(glueRecords[aa]) > 0
|
||||
})
|
||||
|
||||
authorityDepthError := false
|
||||
|
||||
r.depthlogf(depth, "authorities with glue records for recursion: %v", authoritiesGlue)
|
||||
for _, authority := range authoritiesGlue {
|
||||
for _, nameserver := range glueRecords[authority] {
|
||||
answers, minTTL, err := r.resolveRecursive(ctx, qstate, depth+1, name, nameserver, qtype)
|
||||
if err == nil {
|
||||
return answers, minTTL, nil
|
||||
} else if errors.Is(err, ErrAuthoritativeNoResponses) {
|
||||
return nil, 0, ErrAuthoritativeNoResponses
|
||||
} else if errors.Is(err, ErrMaxDepth) {
|
||||
authorityDepthError = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r.depthlogf(depth, "authorities with no glue records for recursion: %v", authoritiesNoGlue)
|
||||
for _, authority := range authoritiesNoGlue {
|
||||
// First, resolve the IP for the authority server from the
|
||||
// root, querying for both IPv4 and IPv6 addresses regardless
|
||||
// of what the current question type is.
|
||||
//
|
||||
// TODO: check for infinite recursion; it'll get caught by our
|
||||
// recursion depth, but we want to bail early.
|
||||
for _, authorityQtype := range []dns.Type{qtypeAAAA, qtypeA} {
|
||||
answers, _, err := r.resolveRecursiveFromRoot(ctx, qstate, depth+1, authority, authorityQtype)
|
||||
if err != nil {
|
||||
r.depthlogf(depth, "error querying authority %q: %v", authority, err)
|
||||
continue
|
||||
}
|
||||
r.depthlogf(depth, "resolved authority %q (type %v) to: %v", authority, authorityQtype, answers)
|
||||
|
||||
// Now, query this authority for the final address.
|
||||
for _, nameserver := range answers {
|
||||
answers, minTTL, err := r.resolveRecursive(ctx, qstate, depth+1, name, nameserver, qtype)
|
||||
if err == nil {
|
||||
return answers, minTTL, nil
|
||||
} else if errors.Is(err, ErrAuthoritativeNoResponses) {
|
||||
return nil, 0, ErrAuthoritativeNoResponses
|
||||
} else if errors.Is(err, ErrMaxDepth) {
|
||||
authorityDepthError = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if authorityDepthError {
|
||||
return nil, 0, ErrMaxDepth
|
||||
}
|
||||
return nil, 0, ErrNoResponses
|
||||
}
|
||||
|
||||
// queryNameserver sends a query for "name" to the nameserver "nameserver" for
|
||||
// records of type "qtype", trying both UDP and TCP connections as
|
||||
// appropriate.
|
||||
func (r *Resolver) queryNameserver(
|
||||
ctx context.Context,
|
||||
depth int,
|
||||
name dnsname.FQDN, // what we're querying
|
||||
nameserver netip.Addr, // destination of query
|
||||
qtype dns.Type,
|
||||
) (*dns.Msg, error) {
|
||||
// TODO(andrew): we should QNAME minimisation here to avoid sending the
|
||||
// full name to intermediate/root nameservers. See:
|
||||
// https://www.rfc-editor.org/rfc/rfc7816
|
||||
|
||||
// Handle the case where UDP is blocked by adding an explicit timeout
|
||||
// for the UDP portion of this query.
|
||||
udpCtx, udpCtxCancel := context.WithTimeout(ctx, udpQueryTimeout)
|
||||
defer udpCtxCancel()
|
||||
|
||||
msg, err := r.queryNameserverProto(udpCtx, depth, name, nameserver, "udp", qtype)
|
||||
if err == nil {
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
msg, err2 := r.queryNameserverProto(ctx, depth, name, nameserver, "tcp", qtype)
|
||||
if err2 == nil {
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
return nil, multierr.New(err, err2)
|
||||
}
|
||||
|
||||
// queryNameserverProto sends a query for "name" to the nameserver "nameserver"
|
||||
// for records of type "qtype" over the provided protocol (either "udp"
|
||||
// or "tcp"), and returns the DNS response or an error.
|
||||
func (r *Resolver) queryNameserverProto(
|
||||
ctx context.Context,
|
||||
depth int,
|
||||
name dnsname.FQDN, // what we're querying
|
||||
nameserver netip.Addr, // destination of query
|
||||
protocol string,
|
||||
qtype dns.Type,
|
||||
) (resp *dns.Msg, err error) {
|
||||
if r.testQueryHook != nil {
|
||||
return r.testQueryHook(name, nameserver, protocol, qtype)
|
||||
}
|
||||
|
||||
now := r.now()
|
||||
nameserverStr := nameserver.String()
|
||||
|
||||
cacheKey := dnsQuery{
|
||||
nameserver: nameserver,
|
||||
name: name,
|
||||
qtype: qtype,
|
||||
}
|
||||
cacheEntry, ok := r.queryCache[cacheKey]
|
||||
if ok && cacheEntry.expiresAt.Before(now) {
|
||||
r.depthlogf(depth, "using cached response from %s about %q (type: %v)", nameserverStr, name, qtype)
|
||||
return cacheEntry.Msg, nil
|
||||
}
|
||||
|
||||
var network string
|
||||
if nameserver.Is4() {
|
||||
network = protocol + "4"
|
||||
} else {
|
||||
network = protocol + "6"
|
||||
}
|
||||
|
||||
// Prepare a message asking for an appropriately-typed record
|
||||
// for the name we're querying.
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion(name.WithTrailingDot(), uint16(qtype))
|
||||
|
||||
// Allow mocking out the network components with our exchange hook.
|
||||
if r.testExchangeHook != nil {
|
||||
resp, err = r.testExchangeHook(nameserver, network, m)
|
||||
} else {
|
||||
// Dial the current nameserver using our dialer.
|
||||
var nconn net.Conn
|
||||
nconn, err = r.dialer().DialContext(ctx, network, net.JoinHostPort(nameserverStr, "53"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var c dns.Client // TODO: share?
|
||||
conn := &dns.Conn{
|
||||
Conn: nconn,
|
||||
UDPSize: c.UDPSize,
|
||||
}
|
||||
|
||||
// Send the DNS request to the current nameserver.
|
||||
r.depthlogf(depth, "asking %s over %s about %q (type: %v)", nameserverStr, protocol, name, qtype)
|
||||
resp, _, err = c.ExchangeWithConnContext(ctx, m, conn)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the message was truncated and we're using UDP, re-run with TCP.
|
||||
if resp.MsgHdr.Truncated && protocol == "udp" {
|
||||
r.depthlogf(depth, "response message truncated; re-running query with TCP")
|
||||
resp, err = r.queryNameserverProto(ctx, depth, name, nameserver, "tcp", qtype)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Find minimum expiry for all records in this message.
|
||||
var minTTL int
|
||||
for _, rr := range resp.Answer {
|
||||
minTTL = min(minTTL, int(rr.Header().Ttl))
|
||||
}
|
||||
for _, rr := range resp.Ns {
|
||||
minTTL = min(minTTL, int(rr.Header().Ttl))
|
||||
}
|
||||
for _, rr := range resp.Extra {
|
||||
minTTL = min(minTTL, int(rr.Header().Ttl))
|
||||
}
|
||||
|
||||
mak.Set(&r.queryCache, cacheKey, dnsMsgWithExpiry{
|
||||
Msg: resp,
|
||||
expiresAt: now.Add(time.Duration(minTTL) * time.Second),
|
||||
})
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func addrFromRecord(rr dns.RR) netip.Addr {
|
||||
switch v := rr.(type) {
|
||||
case *dns.A:
|
||||
ip, ok := netip.AddrFromSlice(v.A)
|
||||
if !ok || !ip.Is4() {
|
||||
return netip.Addr{}
|
||||
}
|
||||
return ip
|
||||
case *dns.AAAA:
|
||||
ip, ok := netip.AddrFromSlice(v.AAAA)
|
||||
if !ok || !ip.Is6() {
|
||||
return netip.Addr{}
|
||||
}
|
||||
return ip
|
||||
}
|
||||
return netip.Addr{}
|
||||
}
|
||||
62
vendor/tailscale.com/net/dns/resolvconf-workaround.sh
generated
vendored
Normal file
62
vendor/tailscale.com/net/dns/resolvconf-workaround.sh
generated
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
#!/bin/sh
|
||||
# Copyright (c) Tailscale Inc & AUTHORS
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
#
|
||||
# This script is a workaround for a vpn-unfriendly behavior of the
|
||||
# original resolvconf by Thomas Hood. Unlike the `openresolv`
|
||||
# implementation (whose binary is also called resolvconf,
|
||||
# confusingly), the original resolvconf lacks a way to specify
|
||||
# "exclusive mode" for a provider configuration. In practice, this
|
||||
# means that if Tailscale wants to install a DNS configuration, that
|
||||
# config will get "blended" with the configs from other sources,
|
||||
# rather than override those other sources.
|
||||
#
|
||||
# This script gets installed at /etc/resolvconf/update-libc.d, which
|
||||
# is a directory of hook scripts that get run after resolvconf's libc
|
||||
# helper has finished rewriting /etc/resolv.conf. It's meant to notify
|
||||
# consumers of resolv.conf of a new configuration.
|
||||
#
|
||||
# Instead, we use that hook mechanism to reach into resolvconf's
|
||||
# stuff, and rewrite the libc-generated resolv.conf to exclusively
|
||||
# contain Tailscale's configuration - effectively implementing
|
||||
# exclusive mode ourselves in post-production.
|
||||
|
||||
set -e
|
||||
|
||||
if [ -n "$TAILSCALE_RESOLVCONF_HOOK_LOOP" ]; then
|
||||
# Hook script being invoked by itself, skip.
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -f tun-tailscale.inet ]; then
|
||||
# Tailscale isn't trying to manage DNS, do nothing.
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! grep resolvconf /etc/resolv.conf >/dev/null; then
|
||||
# resolvconf isn't managing /etc/resolv.conf, do nothing.
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Write out a modified /etc/resolv.conf containing just our config.
|
||||
(
|
||||
if [ -f /etc/resolvconf/resolv.conf.d/head ]; then
|
||||
cat /etc/resolvconf/resolv.conf.d/head
|
||||
fi
|
||||
echo "# Tailscale workaround applied to set exclusive DNS configuration."
|
||||
cat tun-tailscale.inet
|
||||
if [ -f /etc/resolvconf/resolv.conf.d/base ]; then
|
||||
# Keep options and sortlist, discard other base things since
|
||||
# they're the things we're trying to override.
|
||||
grep -e 'sortlist ' -e 'options ' /etc/resolvconf/resolv.conf.d/base || true
|
||||
fi
|
||||
if [ -f /etc/resolvconf/resolv.conf.d/tail ]; then
|
||||
cat /etc/resolvconf/resolv.conf.d/tail
|
||||
fi
|
||||
) >/etc/resolv.conf
|
||||
|
||||
if [ -d /etc/resolvconf/update-libc.d ] ; then
|
||||
# Re-notify libc watchers that we've changed resolv.conf again.
|
||||
export TAILSCALE_RESOLVCONF_HOOK_LOOP=1
|
||||
exec run-parts /etc/resolvconf/update-libc.d
|
||||
fi
|
||||
30
vendor/tailscale.com/net/dns/resolvconf.go
generated
vendored
Normal file
30
vendor/tailscale.com/net/dns/resolvconf.go
generated
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux || freebsd || openbsd
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func resolvconfStyle() string {
|
||||
if _, err := exec.LookPath("resolvconf"); err != nil {
|
||||
return ""
|
||||
}
|
||||
output, err := exec.Command("resolvconf", "--version").CombinedOutput()
|
||||
if err != nil {
|
||||
// Debian resolvconf doesn't understand --version, and
|
||||
// exits with a specific error code.
|
||||
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 99 {
|
||||
return "debian"
|
||||
}
|
||||
}
|
||||
if bytes.HasPrefix(output, []byte("Debian resolvconf")) {
|
||||
return "debian"
|
||||
}
|
||||
// Treat everything else as openresolv, by far the more popular implementation.
|
||||
return "openresolv"
|
||||
}
|
||||
124
vendor/tailscale.com/net/dns/resolvconffile/resolvconffile.go
generated
vendored
Normal file
124
vendor/tailscale.com/net/dns/resolvconffile/resolvconffile.go
generated
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package resolvconffile parses & serializes /etc/resolv.conf-style files.
|
||||
//
|
||||
// It's a leaf package so both net/dns and net/dns/resolver can depend
|
||||
// on it and we can unify a handful of implementations.
|
||||
//
|
||||
// The package is verbosely named to disambiguate it from resolvconf
|
||||
// the daemon, which Tailscale also supports.
|
||||
package resolvconffile
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
// Path is the canonical location of resolv.conf.
|
||||
const Path = "/etc/resolv.conf"
|
||||
|
||||
// Config represents a resolv.conf(5) file.
|
||||
type Config struct {
|
||||
// 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
|
||||
}
|
||||
|
||||
// Write writes c to w. It does so in one Write call.
|
||||
func (c *Config) Write(w io.Writer) error {
|
||||
buf := new(bytes.Buffer)
|
||||
io.WriteString(buf, "# resolv.conf(5) file generated by tailscale\n")
|
||||
io.WriteString(buf, "# For more info, see https://tailscale.com/s/resolvconf-overwrite\n")
|
||||
io.WriteString(buf, "# DO NOT EDIT THIS FILE BY HAND -- CHANGES WILL BE OVERWRITTEN\n\n")
|
||||
for _, ns := range c.Nameservers {
|
||||
io.WriteString(buf, "nameserver ")
|
||||
io.WriteString(buf, ns.String())
|
||||
io.WriteString(buf, "\n")
|
||||
}
|
||||
if len(c.SearchDomains) > 0 {
|
||||
io.WriteString(buf, "search")
|
||||
for _, domain := range c.SearchDomains {
|
||||
io.WriteString(buf, " ")
|
||||
io.WriteString(buf, domain.WithoutTrailingDot())
|
||||
}
|
||||
io.WriteString(buf, "\n")
|
||||
}
|
||||
_, err := w.Write(buf.Bytes())
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse parses a resolv.conf file from r.
|
||||
func Parse(r io.Reader) (*Config, error) {
|
||||
config := new(Config)
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
line, _, _ = strings.Cut(line, "#") // remove any comments
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
if s, ok := strings.CutPrefix(line, "nameserver"); ok {
|
||||
nameserver := strings.TrimSpace(s)
|
||||
if len(nameserver) == len(s) {
|
||||
return nil, fmt.Errorf("missing space after \"nameserver\" in %q", line)
|
||||
}
|
||||
ip, err := netip.ParseAddr(nameserver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Nameservers = append(config.Nameservers, ip)
|
||||
continue
|
||||
}
|
||||
|
||||
if s, ok := strings.CutPrefix(line, "search"); ok {
|
||||
domains := strings.TrimSpace(s)
|
||||
if len(domains) == len(s) {
|
||||
// No leading space?!
|
||||
return nil, fmt.Errorf("missing space after \"search\" in %q", line)
|
||||
}
|
||||
for len(domains) > 0 {
|
||||
domain := domains
|
||||
i := strings.IndexAny(domain, " \t")
|
||||
if i != -1 {
|
||||
domain = domain[:i]
|
||||
domains = strings.TrimSpace(domains[i+1:])
|
||||
} else {
|
||||
domains = ""
|
||||
}
|
||||
fqdn, err := dnsname.ToFQDN(domain)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing search domain %q in %q: %w", domain, line, err)
|
||||
}
|
||||
config.SearchDomains = append(config.SearchDomains, fqdn)
|
||||
}
|
||||
}
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// ParseFile parses the named resolv.conf file.
|
||||
func ParseFile(name string) (*Config, error) {
|
||||
fi, err := os.Stat(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if n := fi.Size(); n > 10<<10 {
|
||||
return nil, fmt.Errorf("unexpectedly large %q file: %d bytes", name, n)
|
||||
}
|
||||
all, err := os.ReadFile(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return Parse(bytes.NewReader(all))
|
||||
}
|
||||
11
vendor/tailscale.com/net/dns/resolvconfpath_default.go
generated
vendored
Normal file
11
vendor/tailscale.com/net/dns/resolvconfpath_default.go
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !gokrazy
|
||||
|
||||
package dns
|
||||
|
||||
const (
|
||||
resolvConf = "/etc/resolv.conf"
|
||||
backupConf = "/etc/resolv.pre-tailscale-backup.conf"
|
||||
)
|
||||
11
vendor/tailscale.com/net/dns/resolvconfpath_gokrazy.go
generated
vendored
Normal file
11
vendor/tailscale.com/net/dns/resolvconfpath_gokrazy.go
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build gokrazy
|
||||
|
||||
package dns
|
||||
|
||||
const (
|
||||
resolvConf = "/tmp/resolv.conf"
|
||||
backupConf = "/tmp/resolv.pre-tailscale-backup.conf"
|
||||
)
|
||||
128
vendor/tailscale.com/net/dns/resolvd.go
generated
vendored
Normal file
128
vendor/tailscale.com/net/dns/resolvd.go
generated
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build openbsd
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/net/dns/resolvconffile"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func newResolvdManager(logf logger.Logf, interfaceName string) (*resolvdManager, error) {
|
||||
return &resolvdManager{
|
||||
logf: logf,
|
||||
ifName: interfaceName,
|
||||
fs: directFS{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// resolvdManager is an OSConfigurator which uses route(1) to teach OpenBSD's
|
||||
// resolvd(8) about DNS servers.
|
||||
type resolvdManager struct {
|
||||
logf logger.Logf
|
||||
ifName string
|
||||
fs directFS
|
||||
}
|
||||
|
||||
func (m *resolvdManager) SetDNS(config OSConfig) error {
|
||||
args := []string{
|
||||
"nameserver",
|
||||
m.ifName,
|
||||
}
|
||||
|
||||
origResolv, err := m.readAndCopy(resolvConf, backupConf, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newResolvConf := removeSearchLines(origResolv)
|
||||
|
||||
for _, ns := range config.Nameservers {
|
||||
args = append(args, ns.String())
|
||||
}
|
||||
|
||||
var newSearch = []string{
|
||||
"search",
|
||||
}
|
||||
for _, s := range config.SearchDomains {
|
||||
newSearch = append(newSearch, s.WithoutTrailingDot())
|
||||
}
|
||||
|
||||
if len(newSearch) > 1 {
|
||||
newResolvConf = append(newResolvConf, []byte(strings.Join(newSearch, " "))...)
|
||||
}
|
||||
|
||||
err = m.fs.WriteFile(resolvConf, newResolvConf, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command("/sbin/route", args...)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func (m *resolvdManager) SupportsSplitDNS() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *resolvdManager) GetBaseConfig() (OSConfig, error) {
|
||||
cfg, err := m.readResolvConf()
|
||||
if err != nil {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (m *resolvdManager) Close() error {
|
||||
// resolvd handles teardown of nameservers so we only need to write back the original
|
||||
// config and be done.
|
||||
|
||||
_, err := m.readAndCopy(backupConf, resolvConf, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.fs.Remove(backupConf)
|
||||
}
|
||||
|
||||
func (m *resolvdManager) readAndCopy(a, b string, mode os.FileMode) ([]byte, error) {
|
||||
orig, err := m.fs.ReadFile(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = m.fs.WriteFile(b, orig, mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return orig, nil
|
||||
}
|
||||
|
||||
func (m resolvdManager) readResolvConf() (config OSConfig, err error) {
|
||||
b, err := m.fs.ReadFile(resolvConf)
|
||||
if err != nil {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
|
||||
rconf, err := resolvconffile.Parse(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return config, err
|
||||
}
|
||||
return OSConfig{
|
||||
Nameservers: rconf.Nameservers,
|
||||
SearchDomains: rconf.SearchDomains,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func removeSearchLines(orig []byte) []byte {
|
||||
re := regexp.MustCompile(`(?m)^search\s+.+$`)
|
||||
return re.ReplaceAll(orig, []byte(""))
|
||||
}
|
||||
394
vendor/tailscale.com/net/dns/resolved.go
generated
vendored
Normal file
394
vendor/tailscale.com/net/dns/resolved.go
generated
vendored
Normal file
@@ -0,0 +1,394 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
// DBus entities we talk to.
|
||||
//
|
||||
// DBus is an RPC bus. In particular, the bus we're talking to is the
|
||||
// system-wide bus (there is also a per-user session bus for
|
||||
// user-specific applications).
|
||||
//
|
||||
// Daemons connect to the bus, and advertise themselves under a
|
||||
// well-known object name. That object exposes paths, and each path
|
||||
// implements one or more interfaces that contain methods, properties,
|
||||
// and signals.
|
||||
//
|
||||
// Clients connect to the bus and walk that same hierarchy to invoke
|
||||
// RPCs, get/set properties, or listen for signals.
|
||||
const (
|
||||
dbusResolvedObject = "org.freedesktop.resolve1"
|
||||
dbusResolvedPath dbus.ObjectPath = "/org/freedesktop/resolve1"
|
||||
dbusResolvedInterface = "org.freedesktop.resolve1.Manager"
|
||||
dbusPath dbus.ObjectPath = "/org/freedesktop/DBus"
|
||||
dbusInterface = "org.freedesktop.DBus"
|
||||
dbusOwnerSignal = "NameOwnerChanged" // broadcast when a well-known name's owning process changes.
|
||||
)
|
||||
|
||||
type resolvedLinkNameserver struct {
|
||||
Family int32
|
||||
Address []byte
|
||||
}
|
||||
|
||||
type resolvedLinkDomain struct {
|
||||
Domain string
|
||||
RoutingOnly bool
|
||||
}
|
||||
|
||||
// changeRequest tracks latest OSConfig and related error responses to update.
|
||||
type changeRequest struct {
|
||||
config OSConfig // configs OSConfigs, one per each SetDNS call
|
||||
res chan<- error // response channel
|
||||
}
|
||||
|
||||
// resolvedManager is an OSConfigurator which uses the systemd-resolved DBus API.
|
||||
type resolvedManager struct {
|
||||
ctx context.Context
|
||||
cancel func() // terminate the context, for close
|
||||
|
||||
logf logger.Logf
|
||||
health *health.Tracker
|
||||
ifidx int
|
||||
|
||||
configCR chan changeRequest // tracks OSConfigs changes and error responses
|
||||
}
|
||||
|
||||
func newResolvedManager(logf logger.Logf, health *health.Tracker, interfaceName string) (*resolvedManager, error) {
|
||||
iface, err := net.InterfaceByName(interfaceName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
logf = logger.WithPrefix(logf, "dns: ")
|
||||
|
||||
mgr := &resolvedManager{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
|
||||
logf: logf,
|
||||
health: health,
|
||||
ifidx: iface.Index,
|
||||
|
||||
configCR: make(chan changeRequest),
|
||||
}
|
||||
|
||||
go mgr.run(ctx)
|
||||
|
||||
return mgr, nil
|
||||
}
|
||||
|
||||
func (m *resolvedManager) SetDNS(config OSConfig) error {
|
||||
// NOTE: don't close this channel, since it's possible that the SetDNS
|
||||
// call will time out and return before the run loop answers, at which
|
||||
// point it will send on the now-closed channel.
|
||||
errc := make(chan error, 1)
|
||||
|
||||
select {
|
||||
case <-m.ctx.Done():
|
||||
return m.ctx.Err()
|
||||
case m.configCR <- changeRequest{config, errc}:
|
||||
}
|
||||
|
||||
select {
|
||||
case <-m.ctx.Done():
|
||||
return m.ctx.Err()
|
||||
case err := <-errc:
|
||||
if err != nil {
|
||||
m.logf("failed to configure resolved: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *resolvedManager) run(ctx context.Context) {
|
||||
var (
|
||||
conn *dbus.Conn
|
||||
signals chan *dbus.Signal
|
||||
rManager dbus.BusObject // rManager is the Resolved DBus connection
|
||||
)
|
||||
bo := backoff.NewBackoff("resolved-dbus", m.logf, 30*time.Second)
|
||||
needsReconnect := make(chan bool, 1)
|
||||
defer func() {
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
// Reconnect the systemBus if disconnected.
|
||||
reconnect := func() error {
|
||||
var err error
|
||||
signals = make(chan *dbus.Signal, 16)
|
||||
conn, err = dbus.SystemBus()
|
||||
if err != nil {
|
||||
m.logf("dbus connection error: %v", err)
|
||||
} else {
|
||||
m.logf("[v1] dbus connected")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// Backoff increases time between reconnect attempts.
|
||||
go func() {
|
||||
bo.BackOff(ctx, err)
|
||||
needsReconnect <- true
|
||||
}()
|
||||
return err
|
||||
}
|
||||
|
||||
rManager = conn.Object(dbusResolvedObject, dbus.ObjectPath(dbusResolvedPath))
|
||||
|
||||
// Only receive the DBus signals we need to resync our config on
|
||||
// resolved restart. Failure to set filters isn't a fatal error,
|
||||
// we'll just receive all broadcast signals and have to ignore
|
||||
// them on our end.
|
||||
if err = conn.AddMatchSignal(dbus.WithMatchObjectPath(dbusPath), dbus.WithMatchInterface(dbusInterface), dbus.WithMatchMember(dbusOwnerSignal), dbus.WithMatchArg(0, dbusResolvedObject)); err != nil {
|
||||
m.logf("[v1] Setting DBus signal filter failed: %v", err)
|
||||
}
|
||||
conn.Signal(signals)
|
||||
|
||||
// Reset backoff and SetNSOSHealth after successful on reconnect.
|
||||
bo.BackOff(ctx, nil)
|
||||
m.health.SetDNSOSHealth(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create initial systemBus connection.
|
||||
reconnect()
|
||||
|
||||
lastConfig := OSConfig{}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if rManager == nil {
|
||||
return
|
||||
}
|
||||
// RevertLink resets all per-interface settings on systemd-resolved to defaults.
|
||||
// When ctx goes away systemd-resolved auto reverts.
|
||||
// Keeping for potential use in future refactor.
|
||||
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".RevertLink", 0, m.ifidx); call.Err != nil {
|
||||
m.logf("[v1] RevertLink: %v", call.Err)
|
||||
return
|
||||
}
|
||||
return
|
||||
case configCR := <-m.configCR:
|
||||
// Track and update sync with latest config change.
|
||||
lastConfig = configCR.config
|
||||
|
||||
if rManager == nil {
|
||||
configCR.res <- fmt.Errorf("resolved DBus does not have a connection")
|
||||
continue
|
||||
}
|
||||
err := m.setConfigOverDBus(ctx, rManager, configCR.config)
|
||||
configCR.res <- err
|
||||
case <-needsReconnect:
|
||||
if err := reconnect(); err != nil {
|
||||
m.logf("[v1] SystemBus reconnect error %T", err)
|
||||
}
|
||||
continue
|
||||
case signal, ok := <-signals:
|
||||
// If signal ends and is nil then program tries to reconnect.
|
||||
if !ok {
|
||||
if err := reconnect(); err != nil {
|
||||
m.logf("[v1] SystemBus reconnect error %T", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// In theory the signal was filtered by DBus, but if
|
||||
// AddMatchSignal in the constructor failed, we may be
|
||||
// getting other spam.
|
||||
if signal.Path != dbusPath || signal.Name != dbusInterface+"."+dbusOwnerSignal {
|
||||
continue
|
||||
}
|
||||
if lastConfig.IsZero() {
|
||||
continue
|
||||
}
|
||||
// signal.Body is a []any of 3 strings: bus name, previous owner, new owner.
|
||||
if len(signal.Body) != 3 {
|
||||
m.logf("[unexpected] DBus NameOwnerChanged len(Body) = %d, want 3")
|
||||
}
|
||||
if name, ok := signal.Body[0].(string); !ok || name != dbusResolvedObject {
|
||||
continue
|
||||
}
|
||||
newOwner, ok := signal.Body[2].(string)
|
||||
if !ok {
|
||||
m.logf("[unexpected] DBus NameOwnerChanged.new_owner is a %T, not a string", signal.Body[2])
|
||||
}
|
||||
if newOwner == "" {
|
||||
// systemd-resolved left the bus, no current owner,
|
||||
// nothing to do.
|
||||
continue
|
||||
}
|
||||
// The resolved bus name has a new owner, meaning resolved
|
||||
// restarted. Reprogram current config.
|
||||
m.logf("systemd-resolved restarted, syncing DNS config")
|
||||
err := m.setConfigOverDBus(ctx, rManager, lastConfig)
|
||||
// Set health while holding the lock, because this will
|
||||
// graciously serialize the resync's health outcome with a
|
||||
// concurrent SetDNS call.
|
||||
m.health.SetDNSOSHealth(err)
|
||||
if err != nil {
|
||||
m.logf("failed to configure systemd-resolved: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setConfigOverDBus updates resolved DBus config and is only called from the run goroutine.
|
||||
func (m *resolvedManager) setConfigOverDBus(ctx context.Context, rManager dbus.BusObject, config OSConfig) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, reconfigTimeout)
|
||||
defer cancel()
|
||||
|
||||
var linkNameservers = make([]resolvedLinkNameserver, len(config.Nameservers))
|
||||
for i, server := range config.Nameservers {
|
||||
ip := server.As16()
|
||||
if server.Is4() {
|
||||
linkNameservers[i] = resolvedLinkNameserver{
|
||||
Family: unix.AF_INET,
|
||||
Address: ip[12:],
|
||||
}
|
||||
} else {
|
||||
linkNameservers[i] = resolvedLinkNameserver{
|
||||
Family: unix.AF_INET6,
|
||||
Address: ip[:],
|
||||
}
|
||||
}
|
||||
}
|
||||
err := rManager.CallWithContext(
|
||||
ctx, dbusResolvedInterface+".SetLinkDNS", 0,
|
||||
m.ifidx, linkNameservers,
|
||||
).Store()
|
||||
if err != nil {
|
||||
return fmt.Errorf("setLinkDNS: %w", err)
|
||||
}
|
||||
linkDomains := make([]resolvedLinkDomain, 0, len(config.SearchDomains)+len(config.MatchDomains))
|
||||
seenDomains := map[dnsname.FQDN]bool{}
|
||||
for _, domain := range config.SearchDomains {
|
||||
if seenDomains[domain] {
|
||||
continue
|
||||
}
|
||||
seenDomains[domain] = true
|
||||
linkDomains = append(linkDomains, resolvedLinkDomain{
|
||||
Domain: domain.WithTrailingDot(),
|
||||
RoutingOnly: false,
|
||||
})
|
||||
}
|
||||
for _, domain := range config.MatchDomains {
|
||||
if seenDomains[domain] {
|
||||
// Search domains act as both search and match in
|
||||
// resolved, so it's correct to skip.
|
||||
continue
|
||||
}
|
||||
seenDomains[domain] = true
|
||||
linkDomains = append(linkDomains, resolvedLinkDomain{
|
||||
Domain: domain.WithTrailingDot(),
|
||||
RoutingOnly: true,
|
||||
})
|
||||
}
|
||||
if len(config.MatchDomains) == 0 && len(config.Nameservers) > 0 {
|
||||
// Caller requested full DNS interception, install a
|
||||
// routing-only root domain.
|
||||
linkDomains = append(linkDomains, resolvedLinkDomain{
|
||||
Domain: ".",
|
||||
RoutingOnly: true,
|
||||
})
|
||||
}
|
||||
|
||||
err = rManager.CallWithContext(
|
||||
ctx, dbusResolvedInterface+".SetLinkDomains", 0,
|
||||
m.ifidx, linkDomains,
|
||||
).Store()
|
||||
if err != nil && err.Error() == "Argument list too long" { // TODO: better error match
|
||||
// Issue 3188: older systemd-resolved had argument length limits.
|
||||
// Trim out the *.arpa. entries and try again.
|
||||
err = rManager.CallWithContext(
|
||||
ctx, dbusResolvedInterface+".SetLinkDomains", 0,
|
||||
m.ifidx, linkDomainsWithoutReverseDNS(linkDomains),
|
||||
).Store()
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("setLinkDomains: %w", err)
|
||||
}
|
||||
|
||||
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkDefaultRoute", 0, m.ifidx, len(config.MatchDomains) == 0); call.Err != nil {
|
||||
if dbusErr, ok := call.Err.(dbus.Error); ok && dbusErr.Name == dbus.ErrMsgUnknownMethod.Name {
|
||||
// on some older systems like Kubuntu 18.04.6 with systemd 237 method SetLinkDefaultRoute is absent,
|
||||
// but otherwise it's working good
|
||||
m.logf("[v1] failed to set SetLinkDefaultRoute: %v", call.Err)
|
||||
} else {
|
||||
return fmt.Errorf("setLinkDefaultRoute: %w", call.Err)
|
||||
}
|
||||
}
|
||||
|
||||
// Some best-effort setting of things, but resolved should do the
|
||||
// right thing if these fail (e.g. a really old resolved version
|
||||
// or something).
|
||||
|
||||
// Disable LLMNR, we don't do multicast.
|
||||
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkLLMNR", 0, m.ifidx, "no"); call.Err != nil {
|
||||
m.logf("[v1] failed to disable LLMNR: %v", call.Err)
|
||||
}
|
||||
|
||||
// Disable mdns.
|
||||
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkMulticastDNS", 0, m.ifidx, "no"); call.Err != nil {
|
||||
m.logf("[v1] failed to disable mdns: %v", call.Err)
|
||||
}
|
||||
|
||||
// We don't support dnssec consistently right now, force it off to
|
||||
// avoid partial failures when we split DNS internally.
|
||||
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkDNSSEC", 0, m.ifidx, "no"); call.Err != nil {
|
||||
m.logf("[v1] failed to disable DNSSEC: %v", call.Err)
|
||||
}
|
||||
|
||||
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkDNSOverTLS", 0, m.ifidx, "no"); call.Err != nil {
|
||||
m.logf("[v1] failed to disable DoT: %v", call.Err)
|
||||
}
|
||||
|
||||
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".FlushCaches", 0); call.Err != nil {
|
||||
m.logf("failed to flush resolved DNS cache: %v", call.Err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *resolvedManager) SupportsSplitDNS() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *resolvedManager) GetBaseConfig() (OSConfig, error) {
|
||||
return OSConfig{}, ErrGetBaseConfigNotSupported
|
||||
}
|
||||
|
||||
func (m *resolvedManager) Close() error {
|
||||
m.cancel() // stops the 'run' method goroutine
|
||||
return nil
|
||||
}
|
||||
|
||||
// linkDomainsWithoutReverseDNS returns a copy of v without
|
||||
// *.arpa. entries.
|
||||
func linkDomainsWithoutReverseDNS(v []resolvedLinkDomain) (ret []resolvedLinkDomain) {
|
||||
for _, d := range v {
|
||||
if strings.HasSuffix(d.Domain, ".arpa.") {
|
||||
// Oh well. At least the rest will work.
|
||||
continue
|
||||
}
|
||||
ret = append(ret, d)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
77
vendor/tailscale.com/net/dns/resolver/debug.go
generated
vendored
Normal file
77
vendor/tailscale.com/net/dns/resolver/debug.go
generated
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/health"
|
||||
)
|
||||
|
||||
func init() {
|
||||
health.RegisterDebugHandler("dnsfwd", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
n, _ := strconv.Atoi(r.FormValue("n"))
|
||||
if n <= 0 {
|
||||
n = 100
|
||||
} else if n > 10000 {
|
||||
n = 10000
|
||||
}
|
||||
fl := fwdLogAtomic.Load()
|
||||
if fl == nil || n != len(fl.ent) {
|
||||
fl = &fwdLog{ent: make([]fwdLogEntry, n)}
|
||||
fwdLogAtomic.Store(fl)
|
||||
}
|
||||
fl.ServeHTTP(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
var fwdLogAtomic atomic.Pointer[fwdLog]
|
||||
|
||||
type fwdLog struct {
|
||||
mu sync.Mutex
|
||||
pos int // ent[pos] is next entry
|
||||
ent []fwdLogEntry
|
||||
}
|
||||
|
||||
type fwdLogEntry struct {
|
||||
Domain string
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
func (fl *fwdLog) addName(name string) {
|
||||
if fl == nil {
|
||||
return
|
||||
}
|
||||
fl.mu.Lock()
|
||||
defer fl.mu.Unlock()
|
||||
if len(fl.ent) == 0 {
|
||||
return
|
||||
}
|
||||
fl.ent[fl.pos] = fwdLogEntry{Domain: name, Time: time.Now()}
|
||||
fl.pos++
|
||||
if fl.pos == len(fl.ent) {
|
||||
fl.pos = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (fl *fwdLog) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
fl.mu.Lock()
|
||||
defer fl.mu.Unlock()
|
||||
|
||||
fmt.Fprintf(w, "<html><h1>DNS forwards</h1>")
|
||||
now := time.Now()
|
||||
for i := range len(fl.ent) {
|
||||
ent := fl.ent[(i+fl.pos)%len(fl.ent)]
|
||||
if ent.Domain == "" {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(w, "%v ago: %v<br>\n", now.Sub(ent.Time).Round(time.Second), html.EscapeString(ent.Domain))
|
||||
}
|
||||
}
|
||||
1195
vendor/tailscale.com/net/dns/resolver/forwarder.go
generated
vendored
Normal file
1195
vendor/tailscale.com/net/dns/resolver/forwarder.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
26
vendor/tailscale.com/net/dns/resolver/macios_ext.go
generated
vendored
Normal file
26
vendor/tailscale.com/net/dns/resolver/macios_ext.go
generated
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ts_macext && (darwin || ios)
|
||||
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/netns"
|
||||
)
|
||||
|
||||
func init() {
|
||||
initListenConfig = initListenConfigNetworkExtension
|
||||
}
|
||||
|
||||
func initListenConfigNetworkExtension(nc *net.ListenConfig, netMon *netmon.Monitor, tunName string) error {
|
||||
nif, ok := netMon.InterfaceState().Interface[tunName]
|
||||
if !ok {
|
||||
return errors.New("utun not found")
|
||||
}
|
||||
return netns.SetListenConfigInterfaceIndex(nc, nif.Interface.Index)
|
||||
}
|
||||
1402
vendor/tailscale.com/net/dns/resolver/tsdns.go
generated
vendored
Normal file
1402
vendor/tailscale.com/net/dns/resolver/tsdns.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
55
vendor/tailscale.com/net/dns/utf.go
generated
vendored
Normal file
55
vendor/tailscale.com/net/dns/utf.go
generated
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dns
|
||||
|
||||
// This code is only used in Windows builds, but is in an
|
||||
// OS-independent file so tests can run all the time.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
// maybeUnUTF16 tries to detect whether bs contains UTF-16, and if so
|
||||
// translates it to regular UTF-8.
|
||||
//
|
||||
// Some of wsl.exe's output get printed as UTF-16, which breaks a
|
||||
// bunch of things. Try to detect this by looking for a zero byte in
|
||||
// the first few bytes of output (which will appear if any of those
|
||||
// codepoints are basic ASCII - very likely). From that we can infer
|
||||
// that UTF-16 is being printed, and the byte order in use, and we
|
||||
// decode that back to UTF-8.
|
||||
//
|
||||
// https://github.com/microsoft/WSL/issues/4607
|
||||
func maybeUnUTF16(bs []byte) []byte {
|
||||
if len(bs)%2 != 0 {
|
||||
// Can't be complete UTF-16.
|
||||
return bs
|
||||
}
|
||||
checkLen := 20
|
||||
if len(bs) < checkLen {
|
||||
checkLen = len(bs)
|
||||
}
|
||||
zeroOff := bytes.IndexByte(bs[:checkLen], 0)
|
||||
if zeroOff == -1 {
|
||||
return bs
|
||||
}
|
||||
|
||||
// We assume wsl.exe is trying to print an ASCII codepoint,
|
||||
// meaning the zero byte is in the upper 8 bits of the
|
||||
// codepoint. That means we can use the zero's byte offset to
|
||||
// work out if we're seeing little-endian or big-endian
|
||||
// UTF-16.
|
||||
var endian binary.ByteOrder = binary.LittleEndian
|
||||
if zeroOff%2 == 0 {
|
||||
endian = binary.BigEndian
|
||||
}
|
||||
|
||||
var u16 []uint16
|
||||
for i := 0; i < len(bs); i += 2 {
|
||||
u16 = append(u16, endian.Uint16(bs[i:]))
|
||||
}
|
||||
return []byte(string(utf16.Decode(u16)))
|
||||
}
|
||||
239
vendor/tailscale.com/net/dns/wsl_windows.go
generated
vendored
Normal file
239
vendor/tailscale.com/net/dns/wsl_windows.go
generated
vendored
Normal file
@@ -0,0 +1,239 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/winutil"
|
||||
)
|
||||
|
||||
// wslDistros reports the names of the installed WSL2 linux distributions.
|
||||
func wslDistros() ([]string, error) {
|
||||
// There is a bug in some builds of wsl.exe that causes it to block
|
||||
// indefinitely while executing this operation. Set a timeout so that we don't
|
||||
// get wedged! (Issue #7476)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
b, err := wslCombinedOutput(exec.CommandContext(ctx, "wsl.exe", "-l"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%v: %q", err, string(b))
|
||||
}
|
||||
|
||||
lines := strings.Split(string(b), "\n")
|
||||
if len(lines) < 1 {
|
||||
return nil, nil
|
||||
}
|
||||
lines = lines[1:] // drop "Windows Subsystem For Linux" header
|
||||
|
||||
var distros []string
|
||||
for _, name := range lines {
|
||||
name = strings.TrimSpace(name)
|
||||
name = strings.TrimSuffix(name, " (Default)")
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
distros = append(distros, name)
|
||||
}
|
||||
return distros, nil
|
||||
}
|
||||
|
||||
// wslManager is a DNS manager for WSL2 linux distributions.
|
||||
// It configures /etc/wsl.conf and /etc/resolv.conf.
|
||||
type wslManager struct {
|
||||
logf logger.Logf
|
||||
health *health.Tracker
|
||||
}
|
||||
|
||||
func newWSLManager(logf logger.Logf, health *health.Tracker) *wslManager {
|
||||
m := &wslManager{
|
||||
logf: logf,
|
||||
health: health,
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (wm *wslManager) SetDNS(cfg OSConfig) error {
|
||||
distros, err := wslDistros()
|
||||
if err != nil {
|
||||
return err
|
||||
} else if len(distros) == 0 {
|
||||
return nil
|
||||
}
|
||||
managers := make(map[string]*directManager)
|
||||
for _, distro := range distros {
|
||||
managers[distro] = newDirectManagerOnFS(wm.logf, wm.health, wslFS{
|
||||
user: "root",
|
||||
distro: distro,
|
||||
})
|
||||
}
|
||||
|
||||
if !cfg.IsZero() {
|
||||
if wm.setWSLConf(managers) {
|
||||
// What's this? So glad you asked.
|
||||
//
|
||||
// WSL2 writes the /etc/resolv.conf.
|
||||
// It is aggressive about it. Every time you execute wsl.exe,
|
||||
// it writes it. (Opening a terminal is done by running wsl.exe.)
|
||||
// You can turn this off using /etc/wsl.conf! But: this wsl.conf
|
||||
// file is only parsed when the VM boots up. To do that, we
|
||||
// have to shut down WSL2.
|
||||
//
|
||||
// So we do it here, before we call wsl.exe to write resolv.conf.
|
||||
if b, err := wslCombinedOutput(wslCommand("--shutdown")); err != nil {
|
||||
wm.logf("WSL SetDNS shutdown: %v: %s", err, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for distro, m := range managers {
|
||||
if err := m.SetDNS(cfg); err != nil {
|
||||
wm.logf("WSL(%q) SetDNS: %v", distro, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const wslConf = "/etc/wsl.conf"
|
||||
const wslConfSection = `# added by tailscale
|
||||
[network]
|
||||
generateResolvConf = false
|
||||
`
|
||||
|
||||
// setWSLConf attempts to disable generateResolvConf in each WSL2 linux.
|
||||
// If any are changed, it reports true.
|
||||
func (wm *wslManager) setWSLConf(managers map[string]*directManager) (changed bool) {
|
||||
for distro, m := range managers {
|
||||
b, err := m.fs.ReadFile(wslConf)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
wm.logf("WSL(%q) wsl.conf: read: %v", distro, err)
|
||||
continue
|
||||
}
|
||||
ini := parseIni(string(b))
|
||||
if v := ini["network"]["generateResolvConf"]; v == "" {
|
||||
b = append(b, wslConfSection...)
|
||||
if err := m.fs.WriteFile(wslConf, b, 0644); err != nil {
|
||||
wm.logf("WSL(%q) wsl.conf: write: %v", distro, err)
|
||||
continue
|
||||
}
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
func (m *wslManager) SupportsSplitDNS() bool { return false }
|
||||
func (m *wslManager) Close() error { return m.SetDNS(OSConfig{}) }
|
||||
|
||||
// wslFS is a pinholeFS implemented on top of wsl.exe.
|
||||
//
|
||||
// We access WSL2 file systems via wsl.exe instead of \\wsl$\ because
|
||||
// the netpath appears to operate as the standard user, not root.
|
||||
type wslFS struct {
|
||||
user string
|
||||
distro string
|
||||
}
|
||||
|
||||
func (fs wslFS) Stat(name string) (isRegular bool, err error) {
|
||||
err = wslRun(fs.cmd("test", "-f", name))
|
||||
if ee, _ := err.(*exec.ExitError); ee != nil {
|
||||
if ee.ExitCode() == 1 {
|
||||
return false, os.ErrNotExist
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (fs wslFS) Chmod(name string, perm os.FileMode) error {
|
||||
return wslRun(fs.cmd("chmod", "--", fmt.Sprintf("%04o", perm), name))
|
||||
}
|
||||
|
||||
func (fs wslFS) Rename(oldName, newName string) error {
|
||||
return wslRun(fs.cmd("mv", "--", oldName, newName))
|
||||
}
|
||||
func (fs wslFS) Remove(name string) error { return wslRun(fs.cmd("rm", "--", name)) }
|
||||
|
||||
func (fs wslFS) Truncate(name string) error { return fs.WriteFile(name, nil, 0644) }
|
||||
|
||||
func (fs wslFS) ReadFile(name string) ([]byte, error) {
|
||||
b, err := wslCombinedOutput(fs.cmd("cat", "--", name))
|
||||
var ee *exec.ExitError
|
||||
if errors.As(err, &ee) && ee.ExitCode() == 1 {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
return b, err
|
||||
}
|
||||
|
||||
func (fs wslFS) WriteFile(name string, contents []byte, perm os.FileMode) error {
|
||||
cmd := fs.cmd("tee", "--", name)
|
||||
cmd.Stdin = bytes.NewReader(contents)
|
||||
cmd.Stdout = nil
|
||||
if err := wslRun(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
return wslRun(fs.cmd("chmod", "--", fmt.Sprintf("%04o", perm), name))
|
||||
}
|
||||
|
||||
func (fs wslFS) cmd(args ...string) *exec.Cmd {
|
||||
cmd := wslCommand("-u", fs.user, "-d", fs.distro, "-e")
|
||||
cmd.Args = append(cmd.Args, args...)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func wslCommand(args ...string) *exec.Cmd {
|
||||
cmd := exec.Command("wsl.exe", args...)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func wslCombinedOutput(cmd *exec.Cmd) ([]byte, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd.Stdout = buf
|
||||
cmd.Stderr = buf
|
||||
err := wslRun(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return maybeUnUTF16(buf.Bytes()), nil
|
||||
}
|
||||
|
||||
func wslRun(cmd *exec.Cmd) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = fmt.Errorf("wslRun(%v): %w", cmd.Args, err)
|
||||
}
|
||||
}()
|
||||
|
||||
var token windows.Token
|
||||
if u, err := user.Current(); err == nil && u.Name == "SYSTEM" {
|
||||
// We need to switch user to run wsl.exe.
|
||||
// https://github.com/microsoft/WSL/issues/4803
|
||||
sessionID := winutil.WTSGetActiveConsoleSessionId()
|
||||
if sessionID != 0xFFFFFFFF {
|
||||
if err := windows.WTSQueryUserToken(sessionID, &token); err != nil {
|
||||
return err
|
||||
}
|
||||
defer token.Close()
|
||||
}
|
||||
}
|
||||
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
CreationFlags: windows.CREATE_NO_WINDOW,
|
||||
Token: syscall.Token(token),
|
||||
}
|
||||
return cmd.Run()
|
||||
}
|
||||
Reference in New Issue
Block a user