@ -6,29 +6,142 @@ package dns
import (
"bytes"
"errors"
"fmt"
"io/fs"
"os"
"strings"
"testing"
"tailscale.com/util/cmpver"
)
func TestLinux NewOSConfigurator ( t * testing . T ) {
func TestLinux DNSMode ( t * testing . T ) {
tests := [ ] struct {
name string
env newOSConfigEnv
wantLog string
want string // reflect type string
want string
} {
{
name : "no_obvious_resolv.conf_owner" ,
env : newOSConfigEnv {
fs : memFS {
"/etc/resolv.conf" : "nameserver 10.0.0.1\n" ,
} ,
resolvOwner : resolvOwner ,
} ,
wantLog : "dns: [rc=unknown ret=dns.directManager]\n" ,
want : "dns.directManager" ,
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" ,
} ,
}
for _ , tt := range tests {
@ -38,15 +151,15 @@ func TestLinuxNewOSConfigurator(t *testing.T) {
fmt . Fprintf ( & logBuf , format , a ... )
logBuf . WriteByte ( '\n' )
}
osc, err := newOSConfigurator ( logf , "unused_if_name0" , tt . env )
got, err := dnsMode ( logf , tt . env )
if err != nil {
t . Fatal ( err )
}
if got := fmt . Sprintf ( "%T" , osc ) ; got != tt . want {
if got != tt . want {
t . Errorf ( "got %s; want %s" , got , tt . want )
}
if tt. wantLog != string ( logBuf . Bytes ( ) ) {
t . Errorf ( "log output mismatch:\n got: %q\nwant: %q\n" , logBuf. Bytes ( ) , tt . wantLog )
if got := strings . TrimSpace ( logBuf . String ( ) ) ; got != tt . wantLog {
t . Errorf ( "log output mismatch:\n got: %q\nwant: %q\n" , got , tt . wantLog )
}
} )
}
@ -82,3 +195,79 @@ func (fs memFS) WriteFile(name string, contents []byte, perm os.FileMode) error
fs [ name ] = string ( contents )
return nil
}
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
} )
}