diff options
Diffstat (limited to 'net/dns/resolved.go')
| -rw-r--r-- | net/dns/resolved.go | 188 |
1 files changed, 188 insertions, 0 deletions
diff --git a/net/dns/resolved.go b/net/dns/resolved.go new file mode 100644 index 000000000..9d8c40d90 --- /dev/null +++ b/net/dns/resolved.go @@ -0,0 +1,188 @@ +// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build linux + +package dns + +import ( + "context" + "errors" + "fmt" + "os/exec" + + "github.com/godbus/dbus/v5" + "golang.org/x/sys/unix" + "inet.af/netaddr" + "tailscale.com/net/interfaces" +) + +// resolvedListenAddr is the listen address of the resolved stub resolver. +// +// We only consider resolved to be the system resolver if the stub resolver is; +// that is, if this address is the sole nameserver in /etc/resolved.conf. +// In other cases, resolved may be managing the system DNS configuration directly. +// Then the nameserver list will be a concatenation of those for all +// the interfaces that register their interest in being a default resolver with +// SetLinkDomains([]{{"~.", true}, ...}) +// which includes at least the interface with the default route, i.e. not us. +// This does not work for us: there is a possibility of getting NXDOMAIN +// from the other nameservers before we are asked or get a chance to respond. +// We consider this case as lacking resolved support and fall through to dnsDirect. +// +// While it may seem that we need to read a config option to get at this, +// this address is, in fact, hard-coded into resolved. +var resolvedListenAddr = netaddr.IPv4(127, 0, 0, 53) + +var errNotReady = errors.New("interface not ready") + +type resolvedLinkNameserver struct { + Family int32 + Address []byte +} + +type resolvedLinkDomain struct { + Domain string + RoutingOnly bool +} + +// isResolvedActive determines if resolved is currently managing system DNS settings. +func isResolvedActive() bool { + // systemd-resolved is never installed without systemd. + _, err := exec.LookPath("systemctl") + if err != nil { + return false + } + + // is-active exits with code 3 if the service is not active. + err = exec.Command("systemctl", "is-active", "systemd-resolved").Run() + if err != nil { + return false + } + + config, err := readResolvConf() + if err != nil { + return false + } + + // The sole nameserver must be the systemd-resolved stub. + if len(config.Nameservers) == 1 && config.Nameservers[0] == resolvedListenAddr { + return true + } + + return false +} + +// resolvedManager uses the systemd-resolved DBus API. +type resolvedManager struct{} + +func newResolvedManager(mconfig ManagerConfig) managerImpl { + return resolvedManager{} +} + +// Up implements managerImpl. +func (m resolvedManager) Up(config Config) error { + ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout) + defer cancel() + + // conn is a shared connection whose lifecycle is managed by the dbus package. + // We should not interfere with that by closing it. + conn, err := dbus.SystemBus() + if err != nil { + return fmt.Errorf("connecting to system bus: %w", err) + } + + resolved := conn.Object( + "org.freedesktop.resolve1", + dbus.ObjectPath("/org/freedesktop/resolve1"), + ) + + // In principle, we could persist this in the manager struct + // if we knew that interface indices are persistent. This does not seem to be the case. + _, iface, err := interfaces.Tailscale() + if err != nil { + return fmt.Errorf("getting interface index: %w", err) + } + if iface == nil { + return errNotReady + } + + 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 = resolved.CallWithContext( + ctx, "org.freedesktop.resolve1.Manager.SetLinkDNS", 0, + iface.Index, linkNameservers, + ).Store() + if err != nil { + return fmt.Errorf("setLinkDNS: %w", err) + } + + var linkDomains = make([]resolvedLinkDomain, len(config.Domains)) + for i, domain := range config.Domains { + linkDomains[i] = resolvedLinkDomain{ + Domain: domain, + RoutingOnly: false, + } + } + + err = resolved.CallWithContext( + ctx, "org.freedesktop.resolve1.Manager.SetLinkDomains", 0, + iface.Index, linkDomains, + ).Store() + if err != nil { + return fmt.Errorf("setLinkDomains: %w", err) + } + + return nil +} + +// Down implements managerImpl. +func (m resolvedManager) Down() error { + ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout) + defer cancel() + + // conn is a shared connection whose lifecycle is managed by the dbus package. + // We should not interfere with that by closing it. + conn, err := dbus.SystemBus() + if err != nil { + return fmt.Errorf("connecting to system bus: %w", err) + } + + resolved := conn.Object( + "org.freedesktop.resolve1", + dbus.ObjectPath("/org/freedesktop/resolve1"), + ) + + _, iface, err := interfaces.Tailscale() + if err != nil { + return fmt.Errorf("getting interface index: %w", err) + } + if iface == nil { + return errNotReady + } + + err = resolved.CallWithContext( + ctx, "org.freedesktop.resolve1.Manager.RevertLink", 0, + iface.Index, + ).Store() + if err != nil { + return fmt.Errorf("RevertLink: %w", err) + } + + return nil +} |
