mirror of
https://github.com/tailscale/tailscale.git
synced 2026-02-10 10:12:27 +01:00
This file was never truly necessary and has never actually been used in the history of Tailscale's open source releases. A Brief History of AUTHORS files --- The AUTHORS file was a pattern developed at Google, originally for Chromium, then adopted by Go and a bunch of other projects. The problem was that Chromium originally had a copyright line only recognizing Google as the copyright holder. Because Google (and most open source projects) do not require copyright assignemnt for contributions, each contributor maintains their copyright. Some large corporate contributors then tried to add their own name to the copyright line in the LICENSE file or in file headers. This quickly becomes unwieldy, and puts a tremendous burden on anyone building on top of Chromium, since the license requires that they keep all copyright lines intact. The compromise was to create an AUTHORS file that would list all of the copyright holders. The LICENSE file and source file headers would then include that list by reference, listing the copyright holder as "The Chromium Authors". This also become cumbersome to simply keep the file up to date with a high rate of new contributors. Plus it's not always obvious who the copyright holder is. Sometimes it is the individual making the contribution, but many times it may be their employer. There is no way for the proejct maintainer to know. Eventually, Google changed their policy to no longer recommend trying to keep the AUTHORS file up to date proactively, and instead to only add to it when requested: https://opensource.google/docs/releasing/authors. They are also clear that: > Adding contributors to the AUTHORS file is entirely within the > project's discretion and has no implications for copyright ownership. It was primarily added to appease a small number of large contributors that insisted that they be recognized as copyright holders (which was entirely their right to do). But it's not truly necessary, and not even the most accurate way of identifying contributors and/or copyright holders. In practice, we've never added anyone to our AUTHORS file. It only lists Tailscale, so it's not really serving any purpose. It also causes confusion because Tailscalars put the "Tailscale Inc & AUTHORS" header in other open source repos which don't actually have an AUTHORS file, so it's ambiguous what that means. Instead, we just acknowledge that the contributors to Tailscale (whoever they are) are copyright holders for their individual contributions. We also have the benefit of using the DCO (developercertificate.org) which provides some additional certification of their right to make the contribution. The source file changes were purely mechanical with: git ls-files | xargs sed -i -e 's/\(Tailscale Inc &\) AUTHORS/\1 contributors/g' Updates #cleanup Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d Signed-off-by: Will Norris <will@tailscale.com>
399 lines
14 KiB
Go
399 lines
14 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build linux && !android
|
|
|
|
package dns
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"tailscale.com/control/controlknobs"
|
|
"tailscale.com/feature"
|
|
"tailscale.com/feature/buildfeatures"
|
|
"tailscale.com/health"
|
|
"tailscale.com/net/netaddr"
|
|
"tailscale.com/types/logger"
|
|
"tailscale.com/util/clientmetric"
|
|
"tailscale.com/util/eventbus"
|
|
"tailscale.com/util/syspolicy/policyclient"
|
|
"tailscale.com/version/distro"
|
|
)
|
|
|
|
type kv struct {
|
|
k, v string
|
|
}
|
|
|
|
func (kv kv) String() string {
|
|
return fmt.Sprintf("%s=%s", kv.k, kv.v)
|
|
}
|
|
|
|
var publishOnce sync.Once
|
|
|
|
// 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
|
|
|
|
// Set unless ts_omit_networkmanager
|
|
var (
|
|
optNewNMManager feature.Hook[func(ifName string) (OSConfigurator, error)]
|
|
optNMIsUsingResolved feature.Hook[func() error]
|
|
optNMVersionBetween feature.Hook[func(v1, v2 string) (bool, error)]
|
|
)
|
|
|
|
// Set unless ts_omit_resolved
|
|
var (
|
|
optNewResolvedManager feature.Hook[func(logf logger.Logf, health *health.Tracker, interfaceName string) (OSConfigurator, error)]
|
|
)
|
|
|
|
// Set unless ts_omit_dbus
|
|
var (
|
|
optDBusPing feature.Hook[func(name, objectPath string) error]
|
|
optDBusReadString feature.Hook[func(name, objectPath, iface, member string) (string, 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, bus *eventbus.Bus, _ policyclient.Client, _ *controlknobs.Knobs, interfaceName string) (ret OSConfigurator, err error) {
|
|
if !buildfeatures.HasDNS || distro.Get() == distro.JetKVM {
|
|
return NewNoopManager()
|
|
}
|
|
|
|
env := newOSConfigEnv{
|
|
fs: directFS{},
|
|
resolvconfStyle: resolvconfStyle,
|
|
}
|
|
if f, ok := optDBusPing.GetOk(); ok {
|
|
env.dbusPing = f
|
|
} else {
|
|
env.dbusPing = func(_, _ string) error { return errors.ErrUnsupported }
|
|
}
|
|
if f, ok := optDBusReadString.GetOk(); ok {
|
|
env.dbusReadString = f
|
|
} else {
|
|
env.dbusReadString = func(_, _, _, _ string) (string, error) { return "", errors.ErrUnsupported }
|
|
}
|
|
if f, ok := optNMIsUsingResolved.GetOk(); ok {
|
|
env.nmIsUsingResolved = f
|
|
} else {
|
|
env.nmIsUsingResolved = func() error { return errors.ErrUnsupported }
|
|
}
|
|
env.nmVersionBetween, _ = optNMVersionBetween.GetOk() // GetOk to not panic if nil; unused if optNMIsUsingResolved returns an error
|
|
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, bus, env.fs), nil
|
|
case "systemd-resolved":
|
|
if f, ok := optNewResolvedManager.GetOk(); ok {
|
|
return f(logf, health, interfaceName)
|
|
}
|
|
return nil, fmt.Errorf("tailscaled was built without DNS %q support", mode)
|
|
case "network-manager":
|
|
if f, ok := optNewNMManager.GetOk(); ok {
|
|
return f(interfaceName)
|
|
}
|
|
return nil, fmt.Errorf("tailscaled was built without DNS %q support", mode)
|
|
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, bus, 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
|
|
}
|
|
}
|
|
|
|
// 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")
|
|
}
|