2023-01-27 21:37:20 +00:00
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
2021-08-30 21:16:12 +00:00
package dns
import (
2021-09-05 06:40:48 +00:00
"errors"
2021-08-30 21:16:12 +00:00
"io/fs"
"os"
2021-09-05 06:40:48 +00:00
"strings"
2021-08-30 21:16:12 +00:00
"testing"
2021-09-05 06:40:48 +00:00
2021-09-08 02:27:19 +00:00
"tailscale.com/tstest"
2021-09-05 06:40:48 +00:00
"tailscale.com/util/cmpver"
2021-08-30 21:16:12 +00:00
)
2021-09-05 06:40:48 +00:00
func TestLinuxDNSMode ( t * testing . T ) {
2021-08-30 21:16:12 +00:00
tests := [ ] struct {
name string
env newOSConfigEnv
wantLog string
2021-09-05 06:40:48 +00:00
want string
2021-08-30 21:16:12 +00:00
} {
{
2021-09-05 06:40:48 +00:00
name : "no_obvious_resolv.conf_owner" ,
env : env ( resolvDotConf ( "nameserver 10.0.0.1" ) ) ,
wantLog : "dns: [rc=unknown ret=direct]" ,
want : "direct" ,
} ,
{
name : "network_manager" ,
env : env (
resolvDotConf (
"# Managed by NetworkManager" ,
"nameserver 10.0.0.1" ) ) ,
2022-01-24 16:19:24 +00:00
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" ,
2021-09-05 06:40:48 +00:00
} ,
{
name : "resolvconf_but_no_resolvconf_binary" ,
env : env ( resolvDotConf ( "# Managed by resolvconf" , "nameserver 10.0.0.1" ) ) ,
wantLog : "dns: [rc=resolvconf resolvconf=no ret=direct]" ,
want : "direct" ,
} ,
{
name : "debian_resolvconf" ,
env : env (
resolvDotConf ( "# Managed by resolvconf" , "nameserver 10.0.0.1" ) ,
resolvconf ( "debian" ) ) ,
wantLog : "dns: [rc=resolvconf resolvconf=debian ret=debian-resolvconf]" ,
want : "debian-resolvconf" ,
} ,
{
name : "openresolv" ,
env : env (
resolvDotConf ( "# Managed by resolvconf" , "nameserver 10.0.0.1" ) ,
resolvconf ( "openresolv" ) ) ,
wantLog : "dns: [rc=resolvconf resolvconf=openresolv ret=openresolv]" ,
want : "openresolv" ,
} ,
{
name : "unknown_resolvconf_flavor" ,
env : env (
resolvDotConf ( "# Managed by resolvconf" , "nameserver 10.0.0.1" ) ,
resolvconf ( "daves-discount-resolvconf" ) ) ,
wantLog : "[unexpected] got unknown flavor of resolvconf \"daves-discount-resolvconf\", falling back to direct manager\ndns: [rc=resolvconf resolvconf=daves-discount-resolvconf ret=direct]" ,
want : "direct" ,
} ,
{
2022-07-25 20:20:48 +00:00
name : "resolved_alone_without_ping" ,
2021-09-05 06:40:48 +00:00
env : env ( resolvDotConf ( "# Managed by systemd-resolved" , "nameserver 127.0.0.53" ) ) ,
2023-07-18 10:43:42 +00:00
wantLog : "dns: ResolvConfMode error: dbus property not found\ndns: [rc=resolved resolved=file nm=no resolv-conf-mode=error ret=systemd-resolved]" ,
2022-07-25 20:20:48 +00:00
want : "systemd-resolved" ,
2021-09-05 06:40:48 +00:00
} ,
{
2022-07-25 20:20:48 +00:00
name : "resolved_alone_with_ping" ,
2021-09-05 06:40:48 +00:00
env : env (
resolvDotConf ( "# Managed by systemd-resolved" , "nameserver 127.0.0.53" ) ,
resolvedRunning ( ) ) ,
2023-07-18 10:43:42 +00:00
wantLog : "dns: [resolved-ping=yes rc=resolved resolved=file nm=no resolv-conf-mode=fortests ret=systemd-resolved]" ,
2021-09-05 06:40:48 +00:00
want : "systemd-resolved" ,
} ,
2023-07-18 10:43:42 +00:00
{
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" ,
} ,
2021-09-05 06:40:48 +00:00
{
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 ) ) ,
2023-07-18 10:43:42 +00:00
wantLog : "dns: [resolved-ping=yes rc=resolved resolved=file nm=yes nm-resolved=no resolv-conf-mode=fortests ret=systemd-resolved]" ,
2021-09-05 06:40:48 +00:00
want : "systemd-resolved" ,
} ,
{
name : "resolved_and_mid_2020_networkmanager" ,
env : env (
resolvDotConf ( "# Managed by systemd-resolved" , "nameserver 127.0.0.53" ) ,
resolvedRunning ( ) ,
nmRunning ( "1.26.2" , true ) ) ,
2023-07-18 10:43:42 +00:00
wantLog : "dns: [resolved-ping=yes rc=resolved resolved=file nm=yes nm-resolved=yes nm-safe=yes ret=network-manager]" ,
2021-09-05 06:40:48 +00:00
want : "network-manager" ,
} ,
{
name : "resolved_and_2021_networkmanager" ,
env : env (
resolvDotConf ( "# Managed by systemd-resolved" , "nameserver 127.0.0.53" ) ,
resolvedRunning ( ) ,
nmRunning ( "1.27.0" , true ) ) ,
2023-07-18 10:43:42 +00:00
wantLog : "dns: [resolved-ping=yes rc=resolved resolved=file nm=yes nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]" ,
2021-09-05 06:40:48 +00:00
want : "systemd-resolved" ,
} ,
{
name : "resolved_and_ancient_networkmanager" ,
env : env (
resolvDotConf ( "# Managed by systemd-resolved" , "nameserver 127.0.0.53" ) ,
resolvedRunning ( ) ,
nmRunning ( "1.22.0" , true ) ) ,
2023-07-18 10:43:42 +00:00
wantLog : "dns: [resolved-ping=yes rc=resolved resolved=file nm=yes nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]" ,
2021-09-05 06:40:48 +00:00
want : "systemd-resolved" ,
} ,
// Regression tests for extreme corner cases below.
{
// One user reported a configuration whose comment string
// alleged that it was managed by systemd-resolved, but it
// was actually a completely static config file pointing
// elsewhere.
2022-01-24 16:19:24 +00:00
name : "allegedly_resolved_but_not_in_resolv.conf" ,
env : env ( resolvDotConf ( "# Managed by systemd-resolved" , "nameserver 10.0.0.1" ) ) ,
wantLog : "dns: resolvedIsActuallyResolver error: resolv.conf doesn't point to systemd-resolved; points to [10.0.0.1]\n" +
"dns: [rc=resolved resolved=not-in-use ret=direct]" ,
want : "direct" ,
2021-09-05 06:40:48 +00:00
} ,
{
// We used to incorrectly decide that resolved wasn't in
// charge when handed this (admittedly weird and bugged)
// resolv.conf.
name : "resolved_with_duplicates_in_resolv.conf" ,
env : env (
resolvDotConf (
"# Managed by systemd-resolved" ,
"nameserver 127.0.0.53" ,
"nameserver 127.0.0.53" ) ,
resolvedRunning ( ) ) ,
2023-07-18 10:43:42 +00:00
wantLog : "dns: [resolved-ping=yes rc=resolved resolved=file nm=no resolv-conf-mode=fortests ret=systemd-resolved]" ,
2021-09-05 06:40:48 +00:00
want : "systemd-resolved" ,
2021-08-30 21:16:12 +00:00
} ,
2021-10-08 13:51:48 +00:00
{
// More than one user has had resolvconf write a config that points to
// systemd-resolved. We're better off using systemd-resolved.
// regression test for https://github.com/tailscale/tailscale/issues/3026
name : "allegedly_resolvconf_but_actually_systemd-resolved" ,
env : env ( resolvDotConf (
"# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)" ,
"# DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN" ,
"# 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" ) ,
resolvedRunning ( ) ) ,
2023-07-18 10:43:42 +00:00
wantLog : "dns: [resolved-ping=yes rc=resolved resolved=file nm=no resolv-conf-mode=fortests ret=systemd-resolved]" ,
2021-10-08 13:51:48 +00:00
want : "systemd-resolved" ,
} ,
{
// More than one user has had resolvconf write a config that points to
// systemd-resolved. We're better off using systemd-resolved.
2022-07-25 20:20:48 +00:00
// and assuming that even if the ping doesn't show that env is correct
2021-10-08 13:51:48 +00:00
// regression test for https://github.com/tailscale/tailscale/issues/3026
2022-07-25 20:20:48 +00:00
name : "allegedly_resolvconf_but_actually_systemd-resolved_but_no_ping" ,
2021-10-08 13:51:48 +00:00
env : env ( resolvDotConf (
"# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)" ,
"# DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN" ,
"# 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" ) ) ,
2023-07-18 10:43:42 +00:00
wantLog : "dns: ResolvConfMode error: dbus property not found\ndns: [rc=resolved resolved=file nm=no resolv-conf-mode=error ret=systemd-resolved]" ,
2022-07-25 20:20:48 +00:00
want : "systemd-resolved" ,
2021-10-08 13:51:48 +00:00
} ,
2021-11-15 18:33:27 +00:00
{
// regression test for https://github.com/tailscale/tailscale/issues/3304
name : "networkmanager_but_pointing_at_systemd-resolved" ,
env : env ( resolvDotConf (
"# Generated by NetworkManager" ,
"nameserver 127.0.0.53" ,
"options edns0 trust-ad" ) ,
resolvedRunning ( ) ,
nmRunning ( "1.32.12" , true ) ) ,
2023-07-18 10:43:42 +00:00
wantLog : "dns: [resolved-ping=yes rc=nm resolved=file nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]" ,
2021-11-15 18:33:27 +00:00
want : "systemd-resolved" ,
} ,
{
// regression test for https://github.com/tailscale/tailscale/issues/3304
2022-07-25 20:20:48 +00:00
name : "networkmanager_but_pointing_at_systemd-resolved_but_no_resolved_ping" ,
2021-11-15 18:33:27 +00:00
env : env ( resolvDotConf (
"# Generated by NetworkManager" ,
"nameserver 127.0.0.53" ,
"options edns0 trust-ad" ) ,
nmRunning ( "1.32.12" , true ) ) ,
2023-07-18 10:43:42 +00:00
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]" ,
2022-07-25 20:20:48 +00:00
want : "systemd-resolved" ,
2021-11-15 18:33:27 +00:00
} ,
{
// regression test for https://github.com/tailscale/tailscale/issues/3304
name : "networkmanager_but_pointing_at_systemd-resolved_and_safe_nm" ,
env : env ( resolvDotConf (
"# Generated by NetworkManager" ,
"nameserver 127.0.0.53" ,
"options edns0 trust-ad" ) ,
resolvedRunning ( ) ,
nmRunning ( "1.26.3" , true ) ) ,
2023-07-18 10:43:42 +00:00
wantLog : "dns: [resolved-ping=yes rc=nm resolved=file nm-resolved=yes nm-safe=yes ret=network-manager]" ,
2021-11-15 18:33:27 +00:00
want : "network-manager" ,
} ,
{
// regression test for https://github.com/tailscale/tailscale/issues/3304
name : "networkmanager_but_pointing_at_systemd-resolved_and_no_networkmanager" ,
env : env ( resolvDotConf (
"# Generated by NetworkManager" ,
"nameserver 127.0.0.53" ,
"options edns0 trust-ad" ) ,
resolvedRunning ( ) ) ,
2023-07-18 10:43:42 +00:00
wantLog : "dns: [resolved-ping=yes rc=nm resolved=file nm-resolved=yes nm=no resolv-conf-mode=fortests ret=systemd-resolved]" ,
2021-11-15 18:33:27 +00:00
want : "systemd-resolved" ,
} ,
2022-02-10 21:41:04 +00:00
{
// regression test for https://github.com/tailscale/tailscale/issues/3531
name : "networkmanager_but_systemd-resolved_with_search_domain" ,
env : env ( resolvDotConf (
"# Generated by NetworkManager" ,
"search lan" ,
"nameserver 127.0.0.53" ) ,
resolvedRunning ( ) ) ,
2023-07-18 10:43:42 +00:00
wantLog : "dns: [resolved-ping=yes rc=nm resolved=file nm-resolved=yes nm=no resolv-conf-mode=fortests ret=systemd-resolved]" ,
2022-02-11 05:11:18 +00:00
want : "systemd-resolved" ,
} ,
{
// Make sure that we ping systemd-resolved to let it start up and write its resolv.conf
// before we read its file.
env : env ( resolvedStartOnPingAndThen (
resolvDotConf ( "# Managed by systemd-resolved" , "nameserver 127.0.0.53" ) ,
2022-10-14 18:25:22 +00:00
resolvedDbusProperty ( ) ,
2022-02-11 05:11:18 +00:00
) ) ,
2023-07-18 10:43:42 +00:00
wantLog : "dns: [resolved-ping=yes rc=resolved resolved=file nm=no resolv-conf-mode=fortests ret=systemd-resolved]" ,
2022-02-10 21:41:04 +00:00
want : "systemd-resolved" ,
} ,
2023-10-15 22:32:37 +00:00
{
// 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" ,
} ,
2021-08-30 21:16:12 +00:00
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
2021-09-08 02:27:19 +00:00
var logBuf tstest . MemLogger
2024-04-26 17:12:46 +00:00
got , err := dnsMode ( logBuf . Logf , nil , tt . env )
2021-08-30 21:16:12 +00:00
if err != nil {
t . Fatal ( err )
}
2021-09-05 06:40:48 +00:00
if got != tt . want {
2021-08-30 21:16:12 +00:00
t . Errorf ( "got %s; want %s" , got , tt . want )
}
2021-09-05 06:40:48 +00:00
if got := strings . TrimSpace ( logBuf . String ( ) ) ; got != tt . wantLog {
t . Errorf ( "log output mismatch:\n got: %q\nwant: %q\n" , got , tt . wantLog )
2021-08-30 21:16:12 +00:00
}
} )
}
}
2022-03-16 23:27:57 +00:00
type memFS map [ string ] any // full path => string for regular files
2021-08-30 21:16:12 +00:00
func ( m memFS ) Stat ( name string ) ( isRegular bool , err error ) {
v , ok := m [ name ]
if ! ok {
return false , fs . ErrNotExist
}
if _ , ok := v . ( string ) ; ok {
return true , nil
}
return false , nil
}
2024-06-26 03:25:44 +00:00
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" ) }
2021-08-30 21:16:12 +00:00
func ( m memFS ) ReadFile ( name string ) ( [ ] byte , error ) {
v , ok := m [ name ]
if ! ok {
return nil , fs . ErrNotExist
}
if s , ok := v . ( string ) ; ok {
return [ ] byte ( s ) , nil
}
panic ( "TODO" )
}
2021-10-07 00:49:32 +00:00
func ( m memFS ) Truncate ( name string ) error {
v , ok := m [ name ]
if ! ok {
return fs . ErrNotExist
}
if s , ok := v . ( string ) ; ok {
m [ name ] = s [ : 0 ]
}
return nil
}
func ( m memFS ) WriteFile ( name string , contents [ ] byte , perm os . FileMode ) error {
m [ name ] = string ( contents )
2021-08-30 21:16:12 +00:00
return nil
}
2021-09-05 06:40:48 +00:00
2022-02-11 05:11:18 +00:00
type dbusService struct {
name , path string
hook func ( ) // if non-nil, run on ping
}
2022-10-14 18:25:22 +00:00
type dbusProperty struct {
name , path string
iface , member string
hook func ( ) ( string , error ) // what to return
}
2021-09-05 06:40:48 +00:00
type envBuilder struct {
fs memFS
2022-02-11 05:11:18 +00:00
dbus [ ] dbusService
2022-10-14 18:25:22 +00:00
dbusProperties [ ] dbusProperty
2021-09-05 06:40:48 +00:00
nmUsingResolved bool
nmVersion string
resolvconfStyle string
}
type envOption interface {
apply ( * envBuilder )
}
type envOpt func ( * envBuilder )
func ( e envOpt ) apply ( b * envBuilder ) {
e ( b )
}
func env ( opts ... envOption ) newOSConfigEnv {
b := & envBuilder {
fs : memFS { } ,
}
for _ , opt := range opts {
opt . apply ( b )
}
return newOSConfigEnv {
fs : b . fs ,
dbusPing : func ( name , path string ) error {
for _ , svc := range b . dbus {
if svc . name == name && svc . path == path {
2022-02-11 05:11:18 +00:00
if svc . hook != nil {
svc . hook ( )
}
2021-09-05 06:40:48 +00:00
return nil
}
}
return errors . New ( "dbus service not found" )
} ,
2022-10-14 18:25:22 +00:00
dbusReadString : func ( name , path , iface , member string ) ( string , error ) {
for _ , svc := range b . dbusProperties {
if svc . name == name && svc . path == path && svc . iface == iface && svc . member == member {
return svc . hook ( )
}
}
return "" , errors . New ( "dbus property not found" )
} ,
2021-09-05 06:40:48 +00:00
nmIsUsingResolved : func ( ) error {
if ! b . nmUsingResolved {
return errors . New ( "networkmanager not using resolved" )
}
return nil
} ,
nmVersionBetween : func ( first , last string ) ( bool , error ) {
outside := cmpver . Compare ( b . nmVersion , first ) < 0 || cmpver . Compare ( b . nmVersion , last ) > 0
return ! outside , nil
} ,
resolvconfStyle : func ( ) string { return b . resolvconfStyle } ,
}
}
func resolvDotConf ( ss ... string ) envOption {
return envOpt ( func ( b * envBuilder ) {
b . fs [ "/etc/resolv.conf" ] = strings . Join ( ss , "\n" )
} )
}
2023-07-18 10:43:42 +00:00
func nsswitchDotConf ( ss ... string ) envOption {
return envOpt ( func ( b * envBuilder ) {
b . fs [ "/etc/nsswitch.conf" ] = strings . Join ( ss , "\n" )
} )
}
2022-10-14 18:25:22 +00:00
// resolvedRunning returns an option that makes resolved reply to a dbusPing
// and the ResolvConfMode property.
2021-09-05 06:40:48 +00:00
func resolvedRunning ( ) envOption {
2022-10-14 18:25:22 +00:00
return resolvedStartOnPingAndThen ( resolvedDbusProperty ( ) )
}
// resolvedDbusProperty returns an option that responds to the ResolvConfMode
// property that resolved exposes.
func resolvedDbusProperty ( ) envOption {
return setDbusProperty ( "org.freedesktop.resolve1" , "/org/freedesktop/resolve1" , "org.freedesktop.resolve1.Manager" , "ResolvConfMode" , "fortests" )
2022-02-11 05:11:18 +00:00
}
// resolvedStartOnPingAndThen returns an option that makes resolved be
// active but not yet running. On a dbus ping, it then applies the
// provided options.
func resolvedStartOnPingAndThen ( opts ... envOption ) envOption {
2021-09-05 06:40:48 +00:00
return envOpt ( func ( b * envBuilder ) {
2022-02-11 05:11:18 +00:00
b . dbus = append ( b . dbus , dbusService {
name : "org.freedesktop.resolve1" ,
path : "/org/freedesktop/resolve1" ,
hook : func ( ) {
for _ , opt := range opts {
opt . apply ( b )
}
} ,
} )
2021-09-05 06:40:48 +00:00
} )
}
func nmRunning ( version string , usingResolved bool ) envOption {
return envOpt ( func ( b * envBuilder ) {
b . nmUsingResolved = usingResolved
b . nmVersion = version
2022-02-11 05:11:18 +00:00
b . dbus = append ( b . dbus , dbusService { name : "org.freedesktop.NetworkManager" , path : "/org/freedesktop/NetworkManager/DnsManager" } )
2021-09-05 06:40:48 +00:00
} )
}
func resolvconf ( s string ) envOption {
return envOpt ( func ( b * envBuilder ) {
b . resolvconfStyle = s
} )
}
2022-10-14 18:25:22 +00:00
func setDbusProperty ( name , path , iface , member , value string ) envOption {
return envOpt ( func ( b * envBuilder ) {
b . dbusProperties = append ( b . dbusProperties , dbusProperty {
name : name ,
path : path ,
iface : iface ,
member : member ,
hook : func ( ) ( string , error ) {
return value , nil
} ,
} )
} )
}