2023-01-27 21:37:20 +00:00
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
2020-07-31 20:27:09 +00:00
package dns
2021-04-14 00:10:30 +00:00
import (
2021-09-05 06:40:48 +00:00
"bytes"
2021-04-14 00:10:30 +00:00
"context"
"errors"
"fmt"
"os"
2022-12-01 11:07:44 +00:00
"strings"
"sync"
2021-04-14 00:10:30 +00:00
"time"
"github.com/godbus/dbus/v5"
2022-02-15 14:59:15 +00:00
"tailscale.com/health"
2022-07-25 03:08:42 +00:00
"tailscale.com/net/netaddr"
2021-04-14 00:10:30 +00:00
"tailscale.com/types/logger"
2022-12-01 11:07:44 +00:00
"tailscale.com/util/clientmetric"
2021-04-24 03:57:35 +00:00
"tailscale.com/util/cmpver"
2021-04-14 00:10:30 +00:00
)
2021-04-02 06:26:52 +00:00
2021-04-14 22:52:41 +00:00
type kv struct {
k , v string
}
func ( kv kv ) String ( ) string {
return fmt . Sprintf ( "%s=%s" , kv . k , kv . v )
}
2022-12-01 11:07:44 +00:00
var publishOnce sync . Once
2021-04-14 22:52:41 +00:00
func NewOSConfigurator ( logf logger . Logf , interfaceName string ) ( ret OSConfigurator , err error ) {
2021-09-05 06:40:48 +00:00
env := newOSConfigEnv {
fs : directFS { } ,
dbusPing : dbusPing ,
2022-10-14 18:25:22 +00:00
dbusReadString : dbusReadString ,
2021-09-05 06:40:48 +00:00
nmIsUsingResolved : nmIsUsingResolved ,
nmVersionBetween : nmVersionBetween ,
resolvconfStyle : resolvconfStyle ,
}
mode , err := dnsMode ( logf , env )
if err != nil {
return nil , err
}
2022-12-01 11:07:44 +00: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-05 06:40:48 +00:00
switch mode {
case "direct" :
2021-10-07 00:49:32 +00:00
return newDirectManagerOnFS ( logf , env . fs ) , nil
2021-09-05 06:40:48 +00:00
case "systemd-resolved" :
return newResolvedManager ( logf , interfaceName )
case "network-manager" :
return newNMManager ( interfaceName )
case "debian-resolvconf" :
return newDebianResolvconfManager ( logf )
case "openresolv" :
2024-02-14 01:19:24 +00:00
return newOpenresolvManager ( logf )
2021-09-05 06:40:48 +00:00
default :
logf ( "[unexpected] detected unknown DNS mode %q, using direct manager as last resort" , mode )
2021-10-07 00:49:32 +00:00
return newDirectManagerOnFS ( logf , env . fs ) , nil
2021-09-05 06:40:48 +00:00
}
2021-08-30 20:48:35 +00:00
}
// newOSConfigEnv are the funcs newOSConfigurator needs, pulled out for testing.
type newOSConfigEnv struct {
2023-12-22 01:40:03 +00: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 20:48:35 +00:00
}
2021-09-05 06:40:48 +00:00
func dnsMode ( logf logger . Logf , env newOSConfigEnv ) ( ret string , err error ) {
2021-04-14 22:52:41 +00:00
var debug [ ] kv
dbg := func ( k , v string ) {
debug = append ( debug , kv { k , v } )
}
defer func ( ) {
2021-09-05 06:40:48 +00:00
if ret != "" {
dbg ( "ret" , ret )
2021-04-14 22:52:41 +00:00
}
logf ( "dns: %v" , debug )
} ( )
2022-10-14 18:25:22 +00: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-11 05:11:18 +00: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 21:16:12 +00:00
bs , err := env . fs . ReadFile ( resolvConf )
2021-04-14 00:10:30 +00:00
if os . IsNotExist ( err ) {
2021-04-14 22:52:41 +00:00
dbg ( "rc" , "missing" )
2021-09-05 06:40:48 +00:00
return "direct" , nil
2021-04-14 00:10:30 +00:00
}
2021-04-14 22:35:32 +00:00
if err != nil {
2021-09-05 06:40:48 +00:00
return "" , fmt . Errorf ( "reading /etc/resolv.conf: %w" , err )
2021-04-14 22:35:32 +00:00
}
2021-04-14 00:10:30 +00:00
2021-09-05 06:40:48 +00:00
switch resolvOwner ( bs ) {
2021-04-14 00:10:30 +00:00
case "systemd-resolved" :
2021-04-14 22:52:41 +00:00
dbg ( "rc" , "resolved" )
2022-10-14 18:25:22 +00:00
2021-06-15 23:39:21 +00: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 10:43:42 +00:00
if err := resolvedIsActuallyResolver ( logf , env , dbg , bs ) ; err != nil {
2022-01-24 16:19:24 +00:00
logf ( "dns: resolvedIsActuallyResolver error: %v" , err )
2021-06-15 23:39:21 +00:00
dbg ( "resolved" , "not-in-use" )
2021-09-05 06:40:48 +00:00
return "direct" , nil
2021-06-15 23:39:21 +00:00
}
2021-08-30 20:48:35 +00:00
if err := env . dbusPing ( "org.freedesktop.NetworkManager" , "/org/freedesktop/NetworkManager/DnsManager" ) ; err != nil {
2021-04-14 22:52:41 +00:00
dbg ( "nm" , "no" )
2021-09-05 06:40:48 +00:00
return "systemd-resolved" , nil
2021-04-14 00:10:30 +00:00
}
2021-04-14 22:52:41 +00:00
dbg ( "nm" , "yes" )
2021-08-30 20:48:35 +00:00
if err := env . nmIsUsingResolved ( ) ; err != nil {
2021-04-14 22:52:41 +00:00
dbg ( "nm-resolved" , "no" )
2021-09-05 06:40:48 +00:00
return "systemd-resolved" , nil
2021-04-14 00:10:30 +00:00
}
2021-04-14 22:52:41 +00:00
dbg ( "nm-resolved" , "yes" )
2021-04-24 03:57:35 +00: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 14:46:08 +00:00
//
2021-06-15 22:34:35 +00: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 14:46:08 +00: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 22:34:35 +00:00
// https://github.com/tailscale/tailscale/issues/1699,
// https://github.com/tailscale/tailscale/pull/1945)
2021-09-05 06:40:48 +00:00
safe , err := env . nmVersionBetween ( "1.26.0" , "1.26.5" )
2021-04-24 03:57:35 +00:00
if err != nil {
// Failed to figure out NM's version, can't make a correct
// decision.
2021-09-05 06:40:48 +00:00
return "" , fmt . Errorf ( "checking NetworkManager version: %v" , err )
2021-04-24 03:57:35 +00:00
}
2021-06-15 22:34:35 +00:00
if safe {
dbg ( "nm-safe" , "yes" )
2021-09-05 06:40:48 +00:00
return "network-manager" , nil
2021-04-24 03:57:35 +00:00
}
2021-06-15 22:34:35 +00:00
dbg ( "nm-safe" , "no" )
2021-09-05 06:40:48 +00:00
return "systemd-resolved" , nil
2021-04-14 00:10:30 +00:00
case "resolvconf" :
2021-04-14 22:52:41 +00:00
dbg ( "rc" , "resolvconf" )
2021-09-05 06:40:48 +00:00
style := env . resolvconfStyle ( )
switch style {
case "" :
2021-04-14 22:52:41 +00:00
dbg ( "resolvconf" , "no" )
2021-09-05 06:40:48 +00: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-14 00:10:30 +00:00
}
case "NetworkManager" :
2021-04-14 22:52:41 +00:00
dbg ( "rc" , "nm" )
2021-11-15 18:33:27 +00:00
// Sometimes, NetworkManager owns the configuration but points
// it at systemd-resolved.
2023-07-18 10:43:42 +00:00
if err := resolvedIsActuallyResolver ( logf , env , dbg , bs ) ; err != nil {
2022-01-24 16:19:24 +00:00
logf ( "dns: resolvedIsActuallyResolver error: %v" , err )
2021-11-15 18:33:27 +00: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 22:32:37 +00: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-25 20:24:49 +00:00
health . Global . 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 18:33:27 +00:00
dbg ( "nm-safe" , "no" )
return "systemd-resolved" , nil
2020-07-31 20:27:09 +00:00
default :
2021-04-14 22:52:41 +00:00
dbg ( "rc" , "unknown" )
2021-09-05 06:40:48 +00:00
return "direct" , nil
2020-07-31 20:27:09 +00:00
}
}
2021-04-14 00:10:30 +00:00
2021-06-15 22:34:35 +00:00
func nmVersionBetween ( first , last string ) ( bool , error ) {
2021-04-24 03:57:35 +00:00
conn , err := dbus . SystemBus ( )
if err != nil {
// DBus probably not running.
return false , err
}
nm := conn . Object ( "org.freedesktop.NetworkManager" , dbus . ObjectPath ( "/org/freedesktop/NetworkManager" ) )
v , err := nm . GetProperty ( "org.freedesktop.NetworkManager.Version" )
if err != nil {
return false , err
}
version , ok := v . Value ( ) . ( string )
if ! ok {
return false , fmt . Errorf ( "unexpected type %T for NM version" , v . Value ( ) )
}
2021-06-16 03:53:03 +00:00
outside := cmpver . Compare ( version , first ) < 0 || cmpver . Compare ( version , last ) > 0
return ! outside , nil
2021-04-24 03:57:35 +00:00
}
2021-04-14 00:10:30 +00:00
func nmIsUsingResolved ( ) error {
conn , err := dbus . SystemBus ( )
if err != nil {
// DBus probably not running.
return err
}
nm := conn . Object ( "org.freedesktop.NetworkManager" , dbus . ObjectPath ( "/org/freedesktop/NetworkManager/DnsManager" ) )
v , err := nm . GetProperty ( "org.freedesktop.NetworkManager.DnsManager.Mode" )
if err != nil {
return fmt . Errorf ( "getting NM mode: %w" , err )
}
mode , ok := v . Value ( ) . ( string )
if ! ok {
return fmt . Errorf ( "unexpected type %T for NM DNS mode" , v . Value ( ) )
}
if mode != "systemd-resolved" {
return errors . New ( "NetworkManager is not using systemd-resolved for DNS" )
}
return nil
}
2023-07-18 10:43:42 +00: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 18:33:27 +00: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 10:43:42 +00: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-05 06:40:48 +00:00
cfg , err := readResolv ( bytes . NewBuffer ( bs ) )
2021-06-15 23:39:21 +00:00
if err != nil {
return err
}
2021-09-05 05:32:28 +00: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 16:19:24 +00:00
return fmt . Errorf ( "resolv.conf doesn't point to systemd-resolved; points to %v" , cfg . Nameservers )
2021-09-05 05:32:28 +00:00
}
2021-06-15 23:39:21 +00:00
}
2023-07-18 10:43:42 +00:00
dbg ( "resolved" , "file" )
2021-06-15 23:39:21 +00:00
return nil
}
2023-07-18 10:43:42 +00: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" )
}
2021-04-14 00:10:30 +00:00
func dbusPing ( name , objectPath string ) error {
conn , err := dbus . SystemBus ( )
if err != nil {
// DBus probably not running.
return err
}
2022-02-11 15:48:03 +00:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , time . Second )
defer cancel ( )
2021-04-14 00:10:30 +00:00
obj := conn . Object ( name , dbus . ObjectPath ( objectPath ) )
call := obj . CallWithContext ( ctx , "org.freedesktop.DBus.Peer.Ping" , 0 )
return call . Err
}
2022-10-14 18:25:22 +00:00
// dbusReadString reads a string property from the provided name and object
// path. property must be in "interface.member" notation.
func dbusReadString ( name , objectPath , iface , member string ) ( string , error ) {
conn , err := dbus . SystemBus ( )
if err != nil {
// DBus probably not running.
return "" , err
}
ctx , cancel := context . WithTimeout ( context . Background ( ) , time . Second )
defer cancel ( )
obj := conn . Object ( name , dbus . ObjectPath ( objectPath ) )
var result dbus . Variant
err = obj . CallWithContext ( ctx , "org.freedesktop.DBus.Properties.Get" , 0 , iface , member ) . Store ( & result )
if err != nil {
return "" , err
}
if s , ok := result . Value ( ) . ( string ) ; ok {
return s , nil
}
return result . String ( ) , nil
}