2021-08-30 21:16:12 +00:00
// 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.
package dns
import (
"bytes"
2021-09-05 06:40:48 +00:00
"errors"
2021-08-30 21:16:12 +00:00
"fmt"
"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
"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" ) ) ,
wantLog : "dns: [rc=nm ret=direct]" ,
want : "direct" ,
} ,
{
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" ,
} ,
{
name : "resolved_not_running" ,
env : env ( resolvDotConf ( "# Managed by systemd-resolved" , "nameserver 127.0.0.53" ) ) ,
wantLog : "dns: [rc=resolved resolved=no ret=direct]" ,
want : "direct" ,
} ,
{
name : "resolved_alone" ,
env : env (
resolvDotConf ( "# Managed by systemd-resolved" , "nameserver 127.0.0.53" ) ,
resolvedRunning ( ) ) ,
wantLog : "dns: [rc=resolved nm=no ret=systemd-resolved]" ,
want : "systemd-resolved" ,
} ,
{
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: [rc=resolved nm=yes nm-resolved=no ret=systemd-resolved]" ,
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 ) ) ,
wantLog : "dns: [rc=resolved nm=yes nm-resolved=yes nm-safe=yes ret=network-manager]" ,
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 ) ) ,
wantLog : "dns: [rc=resolved nm=yes nm-resolved=yes nm-safe=no ret=systemd-resolved]" ,
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 ) ) ,
wantLog : "dns: [rc=resolved nm=yes nm-resolved=yes nm-safe=no ret=systemd-resolved]" ,
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.
name : "allegedly_resolved_but_not_in_resolv.conf" ,
env : env ( resolvDotConf ( "# Managed by systemd-resolved" , "nameserver 10.0.0.1" ) ) ,
wantLog : "dns: [rc=resolved resolved=not-in-use ret=direct]" ,
want : "direct" ,
} ,
{
// 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 ( ) ) ,
wantLog : "dns: [rc=resolved nm=no ret=systemd-resolved]" ,
want : "systemd-resolved" ,
2021-08-30 21:16:12 +00:00
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
var logBuf bytes . Buffer
logf := func ( format string , a ... interface { } ) {
fmt . Fprintf ( & logBuf , format , a ... )
logBuf . WriteByte ( '\n' )
}
2021-09-05 06:40:48 +00:00
got , err := dnsMode ( logf , 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
}
} )
}
}
type memFS map [ string ] interface { } // full path => string for regular files
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
}
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 {
return nil , fs . ErrNotExist
}
if s , ok := v . ( string ) ; ok {
return [ ] byte ( s ) , nil
}
panic ( "TODO" )
}
func ( fs memFS ) WriteFile ( name string , contents [ ] byte , perm os . FileMode ) error {
fs [ name ] = string ( contents )
return nil
}
2021-09-05 06:40:48 +00:00
type envBuilder struct {
fs memFS
dbus [ ] struct { name , path string }
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 {
return nil
}
}
return errors . New ( "dbus service not found" )
} ,
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" )
} )
}
func resolvedRunning ( ) envOption {
return envOpt ( func ( b * envBuilder ) {
b . dbus = append ( b . dbus , struct { name , path string } { "org.freedesktop.resolve1" , "/org/freedesktop/resolve1" } )
} )
}
func nmRunning ( version string , usingResolved bool ) envOption {
return envOpt ( func ( b * envBuilder ) {
b . nmUsingResolved = usingResolved
b . nmVersion = version
b . dbus = append ( b . dbus , struct { name , path string } { "org.freedesktop.NetworkManager" , "/org/freedesktop/NetworkManager/DnsManager" } )
} )
}
func resolvconf ( s string ) envOption {
return envOpt ( func ( b * envBuilder ) {
b . resolvconfStyle = s
} )
}