diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index e8a5c024b..e300e9af9 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -216,6 +216,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de debug/elf from rsc.io/goversion/version debug/macho from rsc.io/goversion/version debug/pe from rsc.io/goversion/version + embed from tailscale.com/net/dns encoding from encoding/json+ encoding/asn1 from crypto/x509+ encoding/base64 from encoding/json+ diff --git a/net/dns/direct.go b/net/dns/direct.go index 9bb6ef34b..c58a1907c 100644 --- a/net/dns/direct.go +++ b/net/dns/direct.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build linux freebsd openbsd - package dns import ( diff --git a/net/dns/resolvconf-workaround.sh b/net/dns/resolvconf-workaround.sh new file mode 100644 index 000000000..5f8ef223d --- /dev/null +++ b/net/dns/resolvconf-workaround.sh @@ -0,0 +1,55 @@ +#!/bin/sh +# Copyright (c) 2021 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. +# +# 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 +) >/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 diff --git a/net/dns/resolvconf.go b/net/dns/resolvconf.go index 10f40f0c8..750c10c22 100644 --- a/net/dns/resolvconf.go +++ b/net/dns/resolvconf.go @@ -2,20 +2,47 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build linux freebsd - 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 legacyResolvconfScript []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") + // isResolvconfActive indicates whether the system appears to be using resolvconf. // If this is true, then directManager should be avoided: // resolvconf has exclusive ownership of /etc/resolv.conf. @@ -98,24 +125,33 @@ func getResolvconfImpl() resolvconfImpl { } type resolvconfManager struct { - impl resolvconfImpl + logf logger.Logf + impl resolvconfImpl + workaroundApplied bool // libc update script has been installed. } -func newResolvconfManager(logf logger.Logf) resolvconfManager { +func newResolvconfManager(logf logger.Logf) *resolvconfManager { impl := getResolvconfImpl() logf("resolvconf implementation is %s", impl) - return resolvconfManager{ + return &resolvconfManager{ + logf: logf, impl: impl, } } -// resolvconfConfigName is the name of the config submitted to resolvconf. -// It has this form to match the "tun*" rule in interface-order -// when running resolvconfLegacy, hopefully placing our config first. -const resolvconfConfigName = "tun-tailscale.inet" +func (m *resolvconfManager) SetDNS(config OSConfig) error { + if m.impl == resolvconfLegacy && !m.workaroundApplied { + m.logf("injecting resolvconf workaround script") + if err := os.MkdirAll(resolvconfLibcHookPath, 0755); err != nil { + return err + } + if err := atomicfile.WriteFile(resolvconfHookPath, legacyResolvconfScript, 0755); err != nil { + return err + } + m.workaroundApplied = true + } -func (m resolvconfManager) SetDNS(config OSConfig) error { stdin := new(bytes.Buffer) writeResolvConf(stdin, config.Nameservers, config.SearchDomains) // dns_direct.go @@ -138,15 +174,15 @@ func (m resolvconfManager) SetDNS(config OSConfig) error { return nil } -func (m resolvconfManager) SupportsSplitDNS() bool { +func (m *resolvconfManager) SupportsSplitDNS() bool { return false } -func (m resolvconfManager) GetBaseConfig() (OSConfig, error) { +func (m *resolvconfManager) GetBaseConfig() (OSConfig, error) { return OSConfig{}, ErrGetBaseConfigNotSupported } -func (m resolvconfManager) Close() error { +func (m *resolvconfManager) Close() error { var cmd *exec.Cmd switch m.impl { case resolvconfOpenresolv: @@ -161,5 +197,10 @@ func (m resolvconfManager) Close() error { return fmt.Errorf("running %s: %s", cmd, out) } + if m.workaroundApplied { + m.logf("removing resolvconf workaround script") + os.Remove(resolvconfHookPath) // Best-effort + } + return nil }