2023-01-27 13:37:20 -08:00
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
2020-07-31 16:27:09 -04:00
2025-05-07 15:56:57 -07:00
//go:build linux && !android
2020-07-31 16:27:09 -04:00
package dns
2021-04-13 17:10:30 -07:00
import (
2021-09-04 23:40:48 -07:00
"bytes"
2021-04-13 17:10:30 -07:00
"errors"
"fmt"
"os"
2022-12-01 16:07:44 +05:00
"strings"
"sync"
2021-04-13 17:10:30 -07:00
"time"
2024-06-10 22:05:15 -05:00
"tailscale.com/control/controlknobs"
2025-09-23 14:11:04 -07:00
"tailscale.com/feature"
2022-02-15 06:59:15 -08:00
"tailscale.com/health"
2022-07-24 20:08:42 -07:00
"tailscale.com/net/netaddr"
2021-04-13 17:10:30 -07:00
"tailscale.com/types/logger"
2022-12-01 16:07:44 +05:00
"tailscale.com/util/clientmetric"
2025-09-02 12:49:37 -07:00
"tailscale.com/util/syspolicy/policyclient"
2025-07-10 11:14:08 -07:00
"tailscale.com/version/distro"
2021-04-13 17:10:30 -07:00
)
2021-04-01 23:26:52 -07:00
2021-04-14 15:52:41 -07:00
type kv struct {
k , v string
}
func ( kv kv ) String ( ) string {
return fmt . Sprintf ( "%s=%s" , kv . k , kv . v )
}
2022-12-01 16:07:44 +05:00
var publishOnce sync . Once
2025-09-23 14:11:04 -07:00
// 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 ) ]
)
2024-06-10 22:05:15 -05:00
// NewOSConfigurator created a new OS configurator.
//
// The health tracker may be nil; the knobs may be nil and are ignored on this platform.
2025-09-02 12:49:37 -07:00
func NewOSConfigurator ( logf logger . Logf , health * health . Tracker , _ policyclient . Client , _ * controlknobs . Knobs , interfaceName string ) ( ret OSConfigurator , err error ) {
2025-07-10 11:14:08 -07:00
if distro . Get ( ) == distro . JetKVM {
return NewNoopManager ( )
}
2021-09-04 23:40:48 -07:00
env := newOSConfigEnv {
2025-09-23 14:11:04 -07:00
fs : directFS { } ,
resolvconfStyle : resolvconfStyle ,
2021-09-04 23:40:48 -07:00
}
2025-09-23 14:11:04 -07:00
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
2024-04-26 10:12:46 -07:00
mode , err := dnsMode ( logf , health , env )
2021-09-04 23:40:48 -07:00
if err != nil {
return nil , err
}
2022-12-01 16:07:44 +05:00
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 )
2021-09-04 23:40:48 -07:00
switch mode {
case "direct" :
2024-04-26 10:12:46 -07:00
return newDirectManagerOnFS ( logf , health , env . fs ) , nil
2021-09-04 23:40:48 -07:00
case "systemd-resolved" :
2025-09-23 14:11:04 -07:00
if f , ok := optNewResolvedManager . GetOk ( ) ; ok {
return f ( logf , health , interfaceName )
}
return nil , fmt . Errorf ( "tailscaled was built without DNS %q support" , mode )
2021-09-04 23:40:48 -07:00
case "network-manager" :
2025-09-23 14:11:04 -07:00
if f , ok := optNewNMManager . GetOk ( ) ; ok {
return f ( interfaceName )
}
return nil , fmt . Errorf ( "tailscaled was built without DNS %q support" , mode )
2021-09-04 23:40:48 -07:00
case "debian-resolvconf" :
return newDebianResolvconfManager ( logf )
case "openresolv" :
2024-02-13 20:19:24 -05:00
return newOpenresolvManager ( logf )
2021-09-04 23:40:48 -07:00
default :
logf ( "[unexpected] detected unknown DNS mode %q, using direct manager as last resort" , mode )
}
2025-09-23 14:11:04 -07:00
return newDirectManagerOnFS ( logf , health , env . fs ) , nil
2021-08-30 13:48:35 -07:00
}
// newOSConfigEnv are the funcs newOSConfigurator needs, pulled out for testing.
type newOSConfigEnv struct {
2023-12-21 19:40:03 -06:00
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
2021-08-30 13:48:35 -07:00
}
2024-04-26 10:12:46 -07:00
func dnsMode ( logf logger . Logf , health * health . Tracker , env newOSConfigEnv ) ( ret string , err error ) {
2021-04-14 15:52:41 -07:00
var debug [ ] kv
dbg := func ( k , v string ) {
debug = append ( debug , kv { k , v } )
}
defer func ( ) {
2021-09-04 23:40:48 -07:00
if ret != "" {
dbg ( "ret" , ret )
2021-04-14 15:52:41 -07:00
}
logf ( "dns: %v" , debug )
} ( )
2022-10-14 20:25:22 +02:00
// 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 )
}
} ( )
2022-02-10 21:11:18 -08:00
// 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" )
}
2021-08-30 14:16:12 -07:00
bs , err := env . fs . ReadFile ( resolvConf )
2021-04-13 17:10:30 -07:00
if os . IsNotExist ( err ) {
2021-04-14 15:52:41 -07:00
dbg ( "rc" , "missing" )
2021-09-04 23:40:48 -07:00
return "direct" , nil
2021-04-13 17:10:30 -07:00
}
2021-04-14 15:35:32 -07:00
if err != nil {
2021-09-04 23:40:48 -07:00
return "" , fmt . Errorf ( "reading /etc/resolv.conf: %w" , err )
2021-04-14 15:35:32 -07:00
}
2021-04-13 17:10:30 -07:00
2021-09-04 23:40:48 -07:00
switch resolvOwner ( bs ) {
2021-04-13 17:10:30 -07:00
case "systemd-resolved" :
2021-04-14 15:52:41 -07:00
dbg ( "rc" , "resolved" )
2022-10-14 20:25:22 +02:00
2021-06-15 16:39:21 -07:00
// 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
2023-07-18 12:43:42 +02:00
if err := resolvedIsActuallyResolver ( logf , env , dbg , bs ) ; err != nil {
2022-01-24 08:19:24 -08:00
logf ( "dns: resolvedIsActuallyResolver error: %v" , err )
2021-06-15 16:39:21 -07:00
dbg ( "resolved" , "not-in-use" )
2021-09-04 23:40:48 -07:00
return "direct" , nil
2021-06-15 16:39:21 -07:00
}
2021-08-30 13:48:35 -07:00
if err := env . dbusPing ( "org.freedesktop.NetworkManager" , "/org/freedesktop/NetworkManager/DnsManager" ) ; err != nil {
2021-04-14 15:52:41 -07:00
dbg ( "nm" , "no" )
2021-09-04 23:40:48 -07:00
return "systemd-resolved" , nil
2021-04-13 17:10:30 -07:00
}
2021-04-14 15:52:41 -07:00
dbg ( "nm" , "yes" )
2021-08-30 13:48:35 -07:00
if err := env . nmIsUsingResolved ( ) ; err != nil {
2021-04-14 15:52:41 -07:00
dbg ( "nm-resolved" , "no" )
2021-09-04 23:40:48 -07:00
return "systemd-resolved" , nil
2021-04-13 17:10:30 -07:00
}
2021-04-14 15:52:41 -07:00
dbg ( "nm-resolved" , "yes" )
2021-04-23 20:57:35 -07:00
// 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.
2021-06-10 07:46:08 -07:00
//
2021-06-15 15:34:35 -07:00
// 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.
//
2021-06-10 07:46:08 -07:00
// 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
2021-06-15 15:34:35 -07:00
// https://github.com/tailscale/tailscale/issues/1699,
// https://github.com/tailscale/tailscale/pull/1945)
2021-09-04 23:40:48 -07:00
safe , err := env . nmVersionBetween ( "1.26.0" , "1.26.5" )
2021-04-23 20:57:35 -07:00
if err != nil {
// Failed to figure out NM's version, can't make a correct
// decision.
2021-09-04 23:40:48 -07:00
return "" , fmt . Errorf ( "checking NetworkManager version: %v" , err )
2021-04-23 20:57:35 -07:00
}
2021-06-15 15:34:35 -07:00
if safe {
dbg ( "nm-safe" , "yes" )
2021-09-04 23:40:48 -07:00
return "network-manager" , nil
2021-04-23 20:57:35 -07:00
}
2021-06-15 15:34:35 -07:00
dbg ( "nm-safe" , "no" )
2021-09-04 23:40:48 -07:00
return "systemd-resolved" , nil
2021-04-13 17:10:30 -07:00
case "resolvconf" :
2021-04-14 15:52:41 -07:00
dbg ( "rc" , "resolvconf" )
2021-09-04 23:40:48 -07:00
style := env . resolvconfStyle ( )
switch style {
case "" :
2021-04-14 15:52:41 -07:00
dbg ( "resolvconf" , "no" )
2021-09-04 23:40:48 -07:00
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
2021-04-13 17:10:30 -07:00
}
case "NetworkManager" :
2021-04-14 15:52:41 -07:00
dbg ( "rc" , "nm" )
2021-11-15 10:33:27 -08:00
// Sometimes, NetworkManager owns the configuration but points
// it at systemd-resolved.
2023-07-18 12:43:42 +02:00
if err := resolvedIsActuallyResolver ( logf , env , dbg , bs ) ; err != nil {
2022-01-24 08:19:24 -08:00
logf ( "dns: resolvedIsActuallyResolver error: %v" , err )
2021-11-15 10:33:27 -08:00
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
}
2023-10-15 15:32:37 -07:00
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
}
2024-04-26 10:12:46 -07:00
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" ) )
2021-11-15 10:33:27 -08:00
dbg ( "nm-safe" , "no" )
return "systemd-resolved" , nil
2020-07-31 16:27:09 -04:00
default :
2021-04-14 15:52:41 -07:00
dbg ( "rc" , "unknown" )
2021-09-04 23:40:48 -07:00
return "direct" , nil
2020-07-31 16:27:09 -04:00
}
}
2021-04-13 17:10:30 -07:00
2023-07-18 12:43:42 +02:00
// 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)
2021-11-15 10:33:27 -08:00
//
// Returns an error if the configuration is something other than
// exclusively systemd-resolved, or nil if the config is only
// systemd-resolved.
2023-07-18 12:43:42 +02:00
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
}
2021-09-04 23:40:48 -07:00
cfg , err := readResolv ( bytes . NewBuffer ( bs ) )
2021-06-15 16:39:21 -07:00
if err != nil {
return err
}
2021-09-04 22:32:28 -07:00
// 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 ) {
2022-01-24 08:19:24 -08:00
return fmt . Errorf ( "resolv.conf doesn't point to systemd-resolved; points to %v" , cfg . Nameservers )
2021-09-04 22:32:28 -07:00
}
2021-06-15 16:39:21 -07:00
}
2023-07-18 12:43:42 +02:00
dbg ( "resolved" , "file" )
2021-06-15 16:39:21 -07:00
return nil
}
2023-07-18 12:43:42 +02:00
// 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" )
}