mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-07-04 01:07:49 +02:00
all: update tailscale.com to v1.74.0
This commit is contained in:
committed by
Cuong Manh Le
parent
7088df58dd
commit
617674ce43
@@ -13,8 +13,9 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"tailscale.com/net/netmon"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/util/lineread"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
@@ -356,7 +357,7 @@ func (d *dhcp) addSelf() {
|
||||
d.ip2name.Store(ipV4Loopback, hostname)
|
||||
d.ip2name.Store(ipv6Loopback, hostname)
|
||||
found := false
|
||||
interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) {
|
||||
netmon.ForeachInterface(func(i netmon.Interface, prefixes []netip.Prefix) {
|
||||
mac := i.HardwareAddr.String()
|
||||
// Skip loopback interfaces, info was stored above.
|
||||
if mac == "" {
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
This is a fork of https://pkg.go.dev/tailscale.com@v1.34.2/net/dns with modification
|
||||
This is a fork of https://pkg.go.dev/tailscale.com@v1.74.0/net/dns with modification
|
||||
to fit ctrld use case.
|
||||
@@ -1,12 +1,12 @@
|
||||
// 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.
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux || freebsd || openbsd
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
@@ -33,7 +33,7 @@ var workaroundScript []byte
|
||||
// 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 = "ctrld.inet"
|
||||
const resolvconfConfigName = "tun-ctrld.inet"
|
||||
|
||||
// resolvconfLibcHookPath is the directory containing libc update
|
||||
// scripts, which are run by Debian resolvconf when /etc/resolv.conf
|
||||
@@ -53,8 +53,6 @@ type resolvconfManager struct {
|
||||
scriptInstalled bool // libc update script has been installed
|
||||
}
|
||||
|
||||
var _ OSConfigurator = (*resolvconfManager)(nil)
|
||||
|
||||
func newDebianResolvconfManager(logf logger.Logf) (*resolvconfManager, error) {
|
||||
ret := &resolvconfManager{
|
||||
logf: logf,
|
||||
@@ -135,6 +133,43 @@ func (m *resolvconfManager) SetDNS(config OSConfig) error {
|
||||
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.deleteCtrldConfig(); err != nil {
|
||||
return err
|
||||
|
||||
+94
-76
@@ -1,9 +1,5 @@
|
||||
// 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.
|
||||
|
||||
//lint:file-ignore U1000 Ignore, this file is forked from upstream code.
|
||||
//lint:file-ignore ST1005 Ignore, this file is forked from upstream code.
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dns
|
||||
|
||||
@@ -20,11 +16,13 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/version/distro"
|
||||
@@ -32,11 +30,6 @@ import (
|
||||
"github.com/Control-D-Inc/ctrld/internal/dns/resolvconffile"
|
||||
)
|
||||
|
||||
const (
|
||||
backupConf = "/etc/resolv.pre-ctrld-backup.conf"
|
||||
resolvConf = "/etc/resolv.conf"
|
||||
)
|
||||
|
||||
// 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{
|
||||
@@ -60,6 +53,8 @@ func readResolv(r io.Reader) (OSConfig, error) {
|
||||
// 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)
|
||||
@@ -123,8 +118,9 @@ func restartResolved() error {
|
||||
// The caller must call Down before program shutdown
|
||||
// or as cleanup if the program terminates unexpectedly.
|
||||
type directManager struct {
|
||||
logf logger.Logf
|
||||
fs wholeFileFS
|
||||
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,
|
||||
@@ -140,19 +136,22 @@ type directManager struct {
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
func newDirectManager(logf logger.Logf) *directManager {
|
||||
return newDirectManagerOnFS(logf, directFS{})
|
||||
//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, fs wholeFileFS) *directManager {
|
||||
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,
|
||||
@@ -193,13 +192,13 @@ func (m *directManager) ownedByCtrld() (bool, error) {
|
||||
}
|
||||
|
||||
// backupConfig creates or updates a backup of /etc/resolv.conf, if
|
||||
// resolv.conf does not currently contain a Tailscale-managed config.
|
||||
// resolv.conf does not currently contain a ctrld-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)
|
||||
m.fs.Remove(backupConf)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
@@ -237,7 +236,7 @@ func (m *directManager) restoreBackup() (restored bool, err error) {
|
||||
if resolvConfExists && !owned {
|
||||
// There's already a non-ctrld config in place, get rid of
|
||||
// our backup.
|
||||
_ = m.fs.Remove(backupConf)
|
||||
m.fs.Remove(backupConf)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -278,6 +277,14 @@ func (m *directManager) rename(old, new string) error {
|
||||
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 {
|
||||
@@ -298,53 +305,6 @@ func (m *directManager) setWant(want []byte) {
|
||||
m.wantResolvConf = want
|
||||
}
|
||||
|
||||
var warnTrample = health.NewWarnable()
|
||||
|
||||
// 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) {
|
||||
warnTrample.Set(nil)
|
||||
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)
|
||||
//lint:ignore ST1005 This error is for human.
|
||||
warnTrample.Set(errors.New("Linux DNS config not ideal. /etc/resolv.conf overwritten. See https://tailscale.com/s/dns-fight"))
|
||||
}
|
||||
|
||||
func (m *directManager) SetDNS(config OSConfig) (err error) {
|
||||
defer func() {
|
||||
if err != nil && errors.Is(err, fs.ErrPermission) && runtime.GOOS == "linux" &&
|
||||
@@ -370,7 +330,7 @@ func (m *directManager) SetDNS(config OSConfig) (err error) {
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
_ = writeResolvConf(buf, config.Nameservers, config.SearchDomains)
|
||||
writeResolvConf(buf, config.Nameservers, config.SearchDomains)
|
||||
if err := m.atomicWriteFile(m.fs, resolvConf, buf.Bytes(), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -411,12 +371,57 @@ func (m *directManager) SetDNS(config OSConfig) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *directManager) SupportsSplitDNS() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *directManager) GetBaseConfig() (OSConfig, error) {
|
||||
owned, err := m.ownedByCtrld()
|
||||
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 {
|
||||
// We used to keep a file for the ctrld config and symlinked
|
||||
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.ctrld.conf")
|
||||
m.fs.Remove("/etc/resolv.ctrld.conf")
|
||||
|
||||
if _, err := m.fs.Stat(backupConf); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
@@ -436,9 +441,9 @@ func (m *directManager) Close() error {
|
||||
resolvConfExists := !os.IsNotExist(err)
|
||||
|
||||
if resolvConfExists && !owned {
|
||||
// There's already a non-ctrld config in place, get rid of
|
||||
// There's already a non-tailscale config in place, get rid of
|
||||
// our backup.
|
||||
_ = m.fs.Remove(backupConf)
|
||||
m.fs.Remove(backupConf)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -475,6 +480,14 @@ func (m *directManager) atomicWriteFile(fs wholeFileFS, filename string, data []
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -483,10 +496,11 @@ func (m *directManager) atomicWriteFile(fs wholeFileFS, filename string, data []
|
||||
//
|
||||
// All name parameters are absolute paths.
|
||||
type wholeFileFS interface {
|
||||
Stat(name string) (isRegular bool, err error)
|
||||
Rename(oldName, newName string) error
|
||||
Remove(name string) error
|
||||
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
|
||||
}
|
||||
@@ -510,6 +524,10 @@ func (fs directFS) Stat(name string) (isRegular bool, err error) {
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
// Copyright (c) 2022 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.
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"github.com/illarion/gonotify"
|
||||
"github.com/illarion/gonotify/v2"
|
||||
"tailscale.com/health"
|
||||
)
|
||||
|
||||
func (m *directManager) runFileWatcher() {
|
||||
in, err := gonotify.NewInotify()
|
||||
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
|
||||
}
|
||||
ctx, cancel := context.WithCancel(m.ctx)
|
||||
defer cancel()
|
||||
go m.closeInotifyOnDone(ctx, in)
|
||||
|
||||
const events = gonotify.IN_ATTRIB |
|
||||
gonotify.IN_CLOSE_WRITE |
|
||||
@@ -56,7 +56,53 @@ func (m *directManager) runFileWatcher() {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *directManager) closeInotifyOnDone(ctx context.Context, in *gonotify.Inotify) {
|
||||
<-ctx.Done()
|
||||
_ = in.Close()
|
||||
var resolvTrampleWarnable = health.Register(&health.Warnable{
|
||||
Code: "ctrld-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)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Copyright (c) 2022 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.
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !linux
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// 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.
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
@@ -79,7 +79,10 @@ func testDirect(t *testing.T, fs wholeFileFS) {
|
||||
}
|
||||
}
|
||||
|
||||
m := directManager{logf: t.Logf, fs: fs}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
m := directManager{logf: t.Logf, fs: fs, ctx: ctx, ctxClose: cancel}
|
||||
if err := m.SetDNS(OSConfig{
|
||||
Nameservers: []netip.Addr{netip.MustParseAddr("8.8.8.8"), netip.MustParseAddr("8.8.4.4")},
|
||||
SearchDomains: []dnsname.FQDN{"controld.com."},
|
||||
@@ -121,7 +124,7 @@ type brokenRemoveFS struct {
|
||||
directFS
|
||||
}
|
||||
|
||||
func (b brokenRemoveFS) Rename(_, _ string) error {
|
||||
func (b brokenRemoveFS) Rename(old, new string) error {
|
||||
return errors.New("nyaaah I'm a silly container!")
|
||||
}
|
||||
|
||||
@@ -178,12 +181,12 @@ func TestReadResolve(t *testing.T) {
|
||||
SearchDomains: []dnsname.FQDN{"controld.com."},
|
||||
},
|
||||
},
|
||||
{in: `search controld.com # typo`,
|
||||
{in: `search controld.com # comment`,
|
||||
want: OSConfig{
|
||||
SearchDomains: []dnsname.FQDN{"controld.com."},
|
||||
},
|
||||
},
|
||||
{in: `searchcontrold.com`, wantErr: true},
|
||||
{in: `searchctrld.com`, wantErr: true},
|
||||
{in: `search`, wantErr: true},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// 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.
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dns
|
||||
|
||||
@@ -8,13 +7,18 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func NewOSConfigurator(logf logger.Logf, _ string) (OSConfigurator, error) {
|
||||
// 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), nil
|
||||
return newDirectManager(logf, health), nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading /etc/resolv.conf: %w", err)
|
||||
@@ -24,16 +28,16 @@ func NewOSConfigurator(logf logger.Logf, _ string) (OSConfigurator, error) {
|
||||
case "resolvconf":
|
||||
switch resolvconfStyle() {
|
||||
case "":
|
||||
return newDirectManager(logf), nil
|
||||
return newDirectManager(logf, health), nil
|
||||
case "debian":
|
||||
return newDebianResolvconfManager(logf)
|
||||
case "openresolv":
|
||||
return newOpenresolvManager()
|
||||
return newOpenresolvManager(logf)
|
||||
default:
|
||||
logf("[unexpected] got unknown flavor of resolvconf %q, falling back to direct manager", resolvconfStyle())
|
||||
return newDirectManager(logf), nil
|
||||
return newDirectManager(logf, health), nil
|
||||
}
|
||||
default:
|
||||
return newDirectManager(logf), nil
|
||||
return newDirectManager(logf, health), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
// 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.
|
||||
|
||||
//lint:file-ignore U1000 Ignore this file, it's a copy.
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dns
|
||||
|
||||
@@ -17,6 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -38,7 +36,10 @@ func (kv kv) String() string {
|
||||
|
||||
var publishOnce sync.Once
|
||||
|
||||
func NewOSConfigurator(logf logger.Logf, interfaceName string) (ret OSConfigurator, err error) {
|
||||
// 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,
|
||||
@@ -47,7 +48,7 @@ func NewOSConfigurator(logf logger.Logf, interfaceName string) (ret OSConfigurat
|
||||
nmVersionBetween: nmVersionBetween,
|
||||
resolvconfStyle: resolvconfStyle,
|
||||
}
|
||||
mode, err := dnsMode(logf, env)
|
||||
mode, err := dnsMode(logf, health, env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -59,18 +60,18 @@ func NewOSConfigurator(logf logger.Logf, interfaceName string) (ret OSConfigurat
|
||||
logf("dns: using %q mode", mode)
|
||||
switch mode {
|
||||
case "direct":
|
||||
return newDirectManagerOnFS(logf, env.fs), nil
|
||||
return newDirectManagerOnFS(logf, health, env.fs), nil
|
||||
case "systemd-resolved":
|
||||
return newResolvedManager(logf, interfaceName)
|
||||
return newResolvedManager(logf, health, interfaceName)
|
||||
case "network-manager":
|
||||
return newNMManager(interfaceName)
|
||||
case "debian-resolvconf":
|
||||
return newDebianResolvconfManager(logf)
|
||||
case "openresolv":
|
||||
return newOpenresolvManager()
|
||||
return newOpenresolvManager(logf)
|
||||
default:
|
||||
logf("[unexpected] detected unknown DNS mode %q, using direct manager as last resort", mode)
|
||||
return newDirectManagerOnFS(logf, env.fs), nil
|
||||
return newDirectManagerOnFS(logf, health, env.fs), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +85,7 @@ type newOSConfigEnv struct {
|
||||
resolvconfStyle func() string
|
||||
}
|
||||
|
||||
func dnsMode(logf logger.Logf, env newOSConfigEnv) (ret string, err error) {
|
||||
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})
|
||||
@@ -145,7 +146,7 @@ func dnsMode(logf logger.Logf, env newOSConfigEnv) (ret string, err error) {
|
||||
// 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(bs); err != nil {
|
||||
if err := resolvedIsActuallyResolver(logf, env, dbg, bs); err != nil {
|
||||
logf("dns: resolvedIsActuallyResolver error: %v", err)
|
||||
dbg("resolved", "not-in-use")
|
||||
return "direct", nil
|
||||
@@ -231,7 +232,7 @@ func dnsMode(logf logger.Logf, env newOSConfigEnv) (ret string, err error) {
|
||||
dbg("rc", "nm")
|
||||
// Sometimes, NetworkManager owns the configuration but points
|
||||
// it at systemd-resolved.
|
||||
if err := resolvedIsActuallyResolver(bs); err != nil {
|
||||
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
|
||||
@@ -271,6 +272,14 @@ func dnsMode(logf logger.Logf, env newOSConfigEnv) (ret string, err error) {
|
||||
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
|
||||
}
|
||||
|
||||
//lint:ignore SA1019 upstream code still use it.
|
||||
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
|
||||
@@ -324,14 +333,23 @@ func nmIsUsingResolved() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolvedIsActuallyResolver reports whether the given resolv.conf
|
||||
// bytes describe a configuration where systemd-resolved (127.0.0.53)
|
||||
// is the only configured nameserver.
|
||||
// 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(bs []byte) error {
|
||||
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
|
||||
@@ -348,9 +366,34 @@ func resolvedIsActuallyResolver(bs []byte) error {
|
||||
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 {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// 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.
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dns
|
||||
|
||||
@@ -71,7 +70,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
{
|
||||
name: "resolved_alone_without_ping",
|
||||
env: env(resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53")),
|
||||
wantLog: "dns: ResolvConfMode error: dbus property not found\ndns: [rc=resolved nm=no resolv-conf-mode=error ret=systemd-resolved]",
|
||||
wantLog: "dns: ResolvConfMode error: dbus property not found\ndns: [rc=resolved resolved=file nm=no resolv-conf-mode=error ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
@@ -79,16 +78,46 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
env: env(
|
||||
resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"),
|
||||
resolvedRunning()),
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
name: "resolved_and_nsswitch_resolve",
|
||||
env: env(
|
||||
resolvDotConf("# Managed by systemd-resolved", "nameserver 1.1.1.1"),
|
||||
resolvedRunning(),
|
||||
nsswitchDotConf("hosts: files resolve [!UNAVAIL=return] dns"),
|
||||
),
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved resolved=nss nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
name: "resolved_and_nsswitch_dns",
|
||||
env: env(
|
||||
resolvDotConf("# Managed by systemd-resolved", "nameserver 1.1.1.1"),
|
||||
resolvedRunning(),
|
||||
nsswitchDotConf("hosts: files dns resolve [!UNAVAIL=return]"),
|
||||
),
|
||||
wantLog: "dns: resolvedIsActuallyResolver error: resolv.conf doesn't point to systemd-resolved; points to [1.1.1.1]\ndns: [resolved-ping=yes rc=resolved resolved=not-in-use ret=direct]",
|
||||
want: "direct",
|
||||
},
|
||||
{
|
||||
name: "resolved_and_nsswitch_none",
|
||||
env: env(
|
||||
resolvDotConf("# Managed by systemd-resolved", "nameserver 1.1.1.1"),
|
||||
resolvedRunning(),
|
||||
nsswitchDotConf("hosts:"),
|
||||
),
|
||||
wantLog: "dns: resolvedIsActuallyResolver error: resolv.conf doesn't point to systemd-resolved; points to [1.1.1.1]\ndns: [resolved-ping=yes rc=resolved resolved=not-in-use ret=direct]",
|
||||
want: "direct",
|
||||
},
|
||||
{
|
||||
name: "resolved_and_networkmanager_not_using_resolved",
|
||||
env: env(
|
||||
resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"),
|
||||
resolvedRunning(),
|
||||
nmRunning("1.2.3", false)),
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved nm=yes nm-resolved=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=yes nm-resolved=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
@@ -97,7 +126,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"),
|
||||
resolvedRunning(),
|
||||
nmRunning("1.26.2", true)),
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved nm=yes nm-resolved=yes nm-safe=yes ret=network-manager]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=yes nm-resolved=yes nm-safe=yes ret=network-manager]",
|
||||
want: "network-manager",
|
||||
},
|
||||
{
|
||||
@@ -106,7 +135,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"),
|
||||
resolvedRunning(),
|
||||
nmRunning("1.27.0", true)),
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved nm=yes nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=yes nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
@@ -115,7 +144,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"),
|
||||
resolvedRunning(),
|
||||
nmRunning("1.22.0", true)),
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved nm=yes nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=yes nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
// Regression tests for extreme corner cases below.
|
||||
@@ -141,7 +170,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
"nameserver 127.0.0.53",
|
||||
"nameserver 127.0.0.53"),
|
||||
resolvedRunning()),
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
@@ -156,7 +185,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
"# run \"systemd-resolve --status\" to see details about the actual nameservers.",
|
||||
"nameserver 127.0.0.53"),
|
||||
resolvedRunning()),
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
@@ -171,7 +200,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
"# 127.0.0.53 is the systemd-resolved stub resolver.",
|
||||
"# run \"systemd-resolve --status\" to see details about the actual nameservers.",
|
||||
"nameserver 127.0.0.53")),
|
||||
wantLog: "dns: ResolvConfMode error: dbus property not found\ndns: [rc=resolved nm=no resolv-conf-mode=error ret=systemd-resolved]",
|
||||
wantLog: "dns: ResolvConfMode error: dbus property not found\ndns: [rc=resolved resolved=file nm=no resolv-conf-mode=error ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
@@ -183,7 +212,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
"options edns0 trust-ad"),
|
||||
resolvedRunning(),
|
||||
nmRunning("1.32.12", true)),
|
||||
wantLog: "dns: [resolved-ping=yes rc=nm nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=nm resolved=file nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
@@ -194,7 +223,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
"nameserver 127.0.0.53",
|
||||
"options edns0 trust-ad"),
|
||||
nmRunning("1.32.12", true)),
|
||||
wantLog: "dns: ResolvConfMode error: dbus property not found\ndns: [rc=nm nm-resolved=yes nm-safe=no resolv-conf-mode=error ret=systemd-resolved]",
|
||||
wantLog: "dns: ResolvConfMode error: dbus property not found\ndns: [rc=nm resolved=file nm-resolved=yes nm-safe=no resolv-conf-mode=error ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
@@ -206,7 +235,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
"options edns0 trust-ad"),
|
||||
resolvedRunning(),
|
||||
nmRunning("1.26.3", true)),
|
||||
wantLog: "dns: [resolved-ping=yes rc=nm nm-resolved=yes nm-safe=yes ret=network-manager]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=nm resolved=file nm-resolved=yes nm-safe=yes ret=network-manager]",
|
||||
want: "network-manager",
|
||||
},
|
||||
{
|
||||
@@ -217,7 +246,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
"nameserver 127.0.0.53",
|
||||
"options edns0 trust-ad"),
|
||||
resolvedRunning()),
|
||||
wantLog: "dns: [resolved-ping=yes rc=nm nm-resolved=yes nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=nm resolved=file nm-resolved=yes nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
@@ -228,7 +257,7 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
"search lan",
|
||||
"nameserver 127.0.0.53"),
|
||||
resolvedRunning()),
|
||||
wantLog: "dns: [resolved-ping=yes rc=nm nm-resolved=yes nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=nm resolved=file nm-resolved=yes nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
@@ -238,14 +267,26 @@ func TestLinuxDNSMode(t *testing.T) {
|
||||
resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"),
|
||||
resolvedDbusProperty(),
|
||||
)),
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
|
||||
want: "systemd-resolved",
|
||||
},
|
||||
{
|
||||
// regression test for https://github.com/tailscale/tailscale/issues/9687
|
||||
name: "networkmanager_endeavouros",
|
||||
env: env(resolvDotConf(
|
||||
"# Generated by NetworkManager",
|
||||
"search example.com localdomain",
|
||||
"nameserver 10.0.0.1"),
|
||||
nmRunning("1.44.2", false)),
|
||||
wantLog: "dns: resolvedIsActuallyResolver error: resolv.conf doesn't point to systemd-resolved; points to [10.0.0.1]\n" +
|
||||
"dns: [rc=nm resolved=not-in-use ret=direct]",
|
||||
want: "direct",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var logBuf tstest.MemLogger
|
||||
got, err := dnsMode(logBuf.Logf, tt.env)
|
||||
got, err := dnsMode(logBuf.Logf, nil, tt.env)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -272,8 +313,9 @@ func (m memFS) Stat(name string) (isRegular bool, err error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (m memFS) Rename(_, _ string) error { panic("TODO") }
|
||||
func (m memFS) Remove(_ string) error { panic("TODO") }
|
||||
func (m memFS) Chmod(name string, mode os.FileMode) error { panic("TODO") }
|
||||
func (m memFS) Rename(oldName, newName string) error { panic("TODO") }
|
||||
func (m memFS) Remove(name string) error { panic("TODO") }
|
||||
func (m memFS) ReadFile(name string) ([]byte, error) {
|
||||
v, ok := m[name]
|
||||
if !ok {
|
||||
@@ -297,7 +339,7 @@ func (m memFS) Truncate(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m memFS) WriteFile(name string, contents []byte, _ os.FileMode) error {
|
||||
func (m memFS) WriteFile(name string, contents []byte, perm os.FileMode) error {
|
||||
m[name] = string(contents)
|
||||
return nil
|
||||
}
|
||||
@@ -381,6 +423,12 @@ func resolvDotConf(ss ...string) envOption {
|
||||
})
|
||||
}
|
||||
|
||||
func nsswitchDotConf(ss ...string) envOption {
|
||||
return envOpt(func(b *envBuilder) {
|
||||
b.fs["/etc/nsswitch.conf"] = strings.Join(ss, "\n")
|
||||
})
|
||||
}
|
||||
|
||||
// resolvedRunning returns an option that makes resolved reply to a dbusPing
|
||||
// and the ResolvConfMode property.
|
||||
func resolvedRunning() envOption {
|
||||
|
||||
+140
-17
@@ -1,6 +1,5 @@
|
||||
// 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.
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
@@ -11,6 +10,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
@@ -24,6 +24,13 @@ const (
|
||||
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
|
||||
@@ -31,8 +38,6 @@ type nmManager struct {
|
||||
dnsManager dbus.BusObject
|
||||
}
|
||||
|
||||
var _ OSConfigurator = (*nmManager)(nil)
|
||||
|
||||
func newNMManager(interfaceName string) (*nmManager, error) {
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
@@ -141,18 +146,17 @@ func (m *nmManager) trySet(ctx context.Context, config OSConfig) error {
|
||||
// tell it explicitly to keep it. Read out the current interface
|
||||
// settings and mirror them out to NetworkManager.
|
||||
var addrs6 []map[string]any
|
||||
if netIface, err := net.InterfaceByName(m.interfaceName); err == nil {
|
||||
if addrs, err := netIface.Addrs(); err == nil {
|
||||
for _, a := range addrs {
|
||||
if ipnet, ok := a.(*net.IPNet); ok {
|
||||
nip, ok := netip.AddrFromSlice(ipnet.IP)
|
||||
nip = nip.Unmap()
|
||||
if ok && nip.Is6() {
|
||||
addrs6 = append(addrs6, map[string]any{
|
||||
"address": nip.String(),
|
||||
"prefix": uint32(128),
|
||||
})
|
||||
}
|
||||
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 && nip.Is6() {
|
||||
addrs6 = append(addrs6, map[string]any{
|
||||
"address": nip.String(),
|
||||
"prefix": uint32(128),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -260,6 +264,125 @@ func (m *nmManager) trySet(ctx context.Context, config OSConfig) error {
|
||||
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.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// 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.
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux || freebsd || openbsd
|
||||
|
||||
@@ -10,22 +9,41 @@ 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{}
|
||||
type openresolvManager struct {
|
||||
logf logger.Logf
|
||||
}
|
||||
|
||||
var _ OSConfigurator = (*openresolvManager)(nil)
|
||||
func newOpenresolvManager(logf logger.Logf) (openresolvManager, error) {
|
||||
return openresolvManager{logf}, nil
|
||||
}
|
||||
|
||||
func newOpenresolvManager() (openresolvManager, error) {
|
||||
return openresolvManager{}, 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", "ctrld")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
m.logCmdErr(cmd, err)
|
||||
return fmt.Errorf("running %s: %s", cmd, out)
|
||||
}
|
||||
return nil
|
||||
@@ -43,11 +61,55 @@ func (m openresolvManager) SetDNS(config OSConfig) error {
|
||||
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()
|
||||
}
|
||||
|
||||
+92
-20
@@ -1,20 +1,20 @@
|
||||
// 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.
|
||||
// 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"
|
||||
)
|
||||
|
||||
var _ OSConfigurator = (*directManager)(nil)
|
||||
|
||||
// An OSConfigurator applies DNS settings to the operating system.
|
||||
type OSConfigurator interface {
|
||||
// SetDNS updates the OS's DNS configuration to match cfg.
|
||||
@@ -23,8 +23,21 @@ type OSConfigurator interface {
|
||||
// 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.
|
||||
|
||||
// Close removes ctrld-related DNS configuration from the OS.
|
||||
// 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
|
||||
|
||||
Mode() string
|
||||
@@ -50,14 +63,59 @@ type OSConfig struct {
|
||||
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.Nameservers) == 0 && len(o.SearchDomains) == 0 && len(o.MatchDomains) == 0
|
||||
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
|
||||
}
|
||||
@@ -68,6 +126,15 @@ func (a OSConfig) Equal(b OSConfig) bool {
|
||||
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
|
||||
@@ -93,34 +160,39 @@ func (a OSConfig) Equal(b OSConfig) bool {
|
||||
// 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:[`)
|
||||
w.WriteString(`{Nameservers:[`)
|
||||
for i, ns := range a.Nameservers {
|
||||
if i != 0 {
|
||||
_, _ = w.WriteString(" ")
|
||||
w.WriteString(" ")
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "%+v", ns)
|
||||
fmt.Fprintf(w, "%+v", ns)
|
||||
}
|
||||
_, _ = w.WriteString(`] SearchDomains:[`)
|
||||
w.WriteString(`] SearchDomains:[`)
|
||||
for i, domain := range a.SearchDomains {
|
||||
if i != 0 {
|
||||
_, _ = w.WriteString(" ")
|
||||
w.WriteString(" ")
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "%+v", domain)
|
||||
fmt.Fprintf(w, "%+v", domain)
|
||||
}
|
||||
_, _ = w.WriteString(`] MatchDomains:[`)
|
||||
w.WriteString(`] MatchDomains:[`)
|
||||
for i, domain := range a.MatchDomains {
|
||||
if i != 0 {
|
||||
_, _ = w.WriteString(" ")
|
||||
w.WriteString(" ")
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "%+v", domain)
|
||||
fmt.Fprintf(w, "%+v", domain)
|
||||
}
|
||||
_, _ = w.WriteString(`] Hosts:[`)
|
||||
w.WriteString(`] Hosts:[`)
|
||||
for i, host := range a.Hosts {
|
||||
if i != 0 {
|
||||
_, _ = w.WriteString(" ")
|
||||
w.WriteString(" ")
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "%+v", host)
|
||||
fmt.Fprintf(w, "%+v", host)
|
||||
}
|
||||
_, _ = w.WriteString(`]}`)
|
||||
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")
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
// Copyright (c) 2022 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.
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
@@ -42,3 +43,13 @@ func TestOSConfigPrintable(t *testing.T) {
|
||||
t.Errorf("format mismatch:\n got: %s\n want: %s", s, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsZero(t *testing.T) {
|
||||
tstest.CheckIsZero[OSConfig](t, map[reflect.Type]any{
|
||||
reflect.TypeFor[dnsname.FQDN](): dnsname.FQDN("foo.bar."),
|
||||
reflect.TypeFor[*HostEntry](): &HostEntry{
|
||||
Addr: netip.AddrFrom4([4]byte{100, 1, 2, 3}),
|
||||
Hosts: []string{"foo", "bar"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/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.
|
||||
# Copyright (c) Ctrld 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`
|
||||
@@ -29,7 +28,7 @@ if [ -n "$CTRLD_RESOLVCONF_HOOK_LOOP" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -f ctrld.inet ]; then
|
||||
if [ ! -f tun-ctrld.inet ]; then
|
||||
# Ctrld isn't trying to manage DNS, do nothing.
|
||||
exit 0
|
||||
fi
|
||||
@@ -60,4 +59,4 @@ if [ -d /etc/resolvconf/update-libc.d ] ; then
|
||||
# Re-notify libc watchers that we've changed resolv.conf again.
|
||||
export CTRLD_RESOLVCONF_HOOK_LOOP=1
|
||||
exec run-parts /etc/resolvconf/update-libc.d
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// 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.
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux || freebsd || openbsd
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
@@ -14,13 +14,17 @@ func resolvconfStyle() string {
|
||||
if _, err := exec.LookPath("resolvconf"); err != nil {
|
||||
return ""
|
||||
}
|
||||
if _, err := exec.Command("resolvconf", "--version").CombinedOutput(); err != nil {
|
||||
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"
|
||||
}
|
||||
|
||||
@@ -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-ctrld-backup.conf"
|
||||
)
|
||||
@@ -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-ctrld-backup.conf"
|
||||
)
|
||||
+27
-29
@@ -1,6 +1,5 @@
|
||||
// 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.
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
@@ -21,8 +20,6 @@ import (
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
const reconfigTimeout = time.Second
|
||||
|
||||
// DBus entities we talk to.
|
||||
//
|
||||
// DBus is an RPC bus. In particular, the bus we're talking to is the
|
||||
@@ -97,16 +94,14 @@ type resolvedManager struct {
|
||||
ctx context.Context
|
||||
cancel func() // terminate the context, for close
|
||||
|
||||
logf logger.Logf
|
||||
ifidx int
|
||||
logf logger.Logf
|
||||
health *health.Tracker
|
||||
ifidx int
|
||||
|
||||
configCR chan changeRequest // tracks OSConfigs changes and error responses
|
||||
revertCh chan struct{}
|
||||
}
|
||||
|
||||
var _ OSConfigurator = (*resolvedManager)(nil)
|
||||
|
||||
func newResolvedManager(logf logger.Logf, interfaceName string) (*resolvedManager, error) {
|
||||
func newResolvedManager(logf logger.Logf, health *health.Tracker, interfaceName string) (*resolvedManager, error) {
|
||||
iface, err := net.InterfaceByName(interfaceName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -119,11 +114,11 @@ func newResolvedManager(logf logger.Logf, interfaceName string) (*resolvedManage
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
|
||||
logf: logf,
|
||||
ifidx: iface.Index,
|
||||
logf: logf,
|
||||
health: health,
|
||||
ifidx: iface.Index,
|
||||
|
||||
configCR: make(chan changeRequest),
|
||||
revertCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
go mgr.run(ctx)
|
||||
@@ -132,8 +127,10 @@ func newResolvedManager(logf logger.Logf, interfaceName string) (*resolvedManage
|
||||
}
|
||||
|
||||
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)
|
||||
defer close(errc)
|
||||
|
||||
select {
|
||||
case <-m.ctx.Done():
|
||||
@@ -221,14 +218,12 @@ func (m *resolvedManager) run(ctx context.Context) {
|
||||
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)
|
||||
}
|
||||
if err = conn.AddMatchSignal(dbus.WithMatchObjectPath(dbusPath), dbus.WithMatchInterface(dbusInterface), dbus.WithMatchMember(dbusOwnerSignal), dbus.WithMatchArg(0, dbusNetworkdObject)); 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)
|
||||
health.SetDNSOSHealth(nil)
|
||||
//lint:ignore SA1019 upstream code still use it.
|
||||
m.health.SetDNSOSHealth(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -243,15 +238,13 @@ func (m *resolvedManager) run(ctx context.Context) {
|
||||
if rManager == nil {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
// 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, dbusRevertLink, 0, m.ifidx); call.Err != nil {
|
||||
m.logf("[v1] RevertLink: %v", call.Err)
|
||||
return
|
||||
}
|
||||
cancel()
|
||||
close(m.revertCh)
|
||||
return
|
||||
case configCR := <-m.configCR:
|
||||
// Track and update sync with latest config change.
|
||||
@@ -308,7 +301,8 @@ func (m *resolvedManager) run(ctx context.Context) {
|
||||
// Set health while holding the lock, because this will
|
||||
// graciously serialize the resync's health outcome with a
|
||||
// concurrent SetDNS call.
|
||||
health.SetDNSOSHealth(err)
|
||||
//lint:ignore SA1019 upstream code still use it.
|
||||
m.health.SetDNSOSHealth(err)
|
||||
if err != nil {
|
||||
m.logf("failed to configure systemd-resolved: %v", err)
|
||||
}
|
||||
@@ -426,18 +420,22 @@ func (m *resolvedManager) setConfigOverDBus(ctx context.Context, rManager dbus.B
|
||||
m.logf("[v1] failed to disable DoT: %v", call.Err)
|
||||
}
|
||||
|
||||
if rManager.Path() == dbusResolvedPath {
|
||||
if call := rManager.CallWithContext(ctx, dbusFlushCaches, 0); call.Err != nil {
|
||||
m.logf("failed to flush resolved DNS cache: %v", call.Err)
|
||||
}
|
||||
if call := rManager.CallWithContext(ctx, dbusFlushCaches, 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
|
||||
<-m.revertCh
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user