github.com/tonistiigi/docker@v0.10.1-0.20240229224939-974013b0dc6a/libnetwork/internal/resolvconf/resolvconf_test.go (about)

     1  package resolvconf
     2  
     3  import (
     4  	"bytes"
     5  	"io/fs"
     6  	"net/netip"
     7  	"os"
     8  	"path/filepath"
     9  	"runtime"
    10  	"strings"
    11  	"testing"
    12  
    13  	"github.com/docker/docker/internal/sliceutil"
    14  	"github.com/google/go-cmp/cmp/cmpopts"
    15  	"gotest.tools/v3/assert"
    16  	is "gotest.tools/v3/assert/cmp"
    17  	"gotest.tools/v3/golden"
    18  )
    19  
    20  func TestRCOption(t *testing.T) {
    21  	testcases := []struct {
    22  		name     string
    23  		options  string
    24  		search   string
    25  		expFound bool
    26  		expValue string
    27  	}{
    28  		{
    29  			name:    "Empty options",
    30  			options: "",
    31  			search:  "ndots",
    32  		},
    33  		{
    34  			name:    "Not found",
    35  			options: "ndots:0 edns0",
    36  			search:  "trust-ad",
    37  		},
    38  		{
    39  			name:     "Found with value",
    40  			options:  "ndots:0 edns0",
    41  			search:   "ndots",
    42  			expFound: true,
    43  			expValue: "0",
    44  		},
    45  		{
    46  			name:     "Found without value",
    47  			options:  "ndots:0 edns0",
    48  			search:   "edns0",
    49  			expFound: true,
    50  			expValue: "",
    51  		},
    52  		{
    53  			name:     "Found last value",
    54  			options:  "ndots:0 edns0 ndots:1",
    55  			search:   "ndots",
    56  			expFound: true,
    57  			expValue: "1",
    58  		},
    59  	}
    60  
    61  	for _, tc := range testcases {
    62  		t.Run(tc.name, func(t *testing.T) {
    63  			rc, err := Parse(bytes.NewBuffer([]byte("options "+tc.options)), "")
    64  			assert.NilError(t, err)
    65  			value, found := rc.Option(tc.search)
    66  			assert.Check(t, is.Equal(found, tc.expFound))
    67  			assert.Check(t, is.Equal(value, tc.expValue))
    68  		})
    69  	}
    70  }
    71  
    72  func TestRCWrite(t *testing.T) {
    73  	testcases := []struct {
    74  		name            string
    75  		fileName        string
    76  		perm            os.FileMode
    77  		hashFileName    string
    78  		modify          bool
    79  		expUserModified bool
    80  	}{
    81  		{
    82  			name:         "Write with hash",
    83  			fileName:     "testfile",
    84  			hashFileName: "testfile.hash",
    85  		},
    86  		{
    87  			name:            "Write with hash and modify",
    88  			fileName:        "testfile",
    89  			hashFileName:    "testfile.hash",
    90  			modify:          true,
    91  			expUserModified: true,
    92  		},
    93  		{
    94  			name:            "Write without hash and modify",
    95  			fileName:        "testfile",
    96  			modify:          true,
    97  			expUserModified: false,
    98  		},
    99  		{
   100  			name:     "Write perm",
   101  			fileName: "testfile",
   102  			perm:     0640,
   103  		},
   104  	}
   105  
   106  	rc, err := Parse(bytes.NewBuffer([]byte("nameserver 1.2.3.4")), "")
   107  	assert.NilError(t, err)
   108  
   109  	for _, tc := range testcases {
   110  		t.Run(tc.name, func(t *testing.T) {
   111  			tc := tc
   112  			d := t.TempDir()
   113  			path := filepath.Join(d, tc.fileName)
   114  			var hashPath string
   115  			if tc.hashFileName != "" {
   116  				hashPath = filepath.Join(d, tc.hashFileName)
   117  			}
   118  			if tc.perm == 0 {
   119  				tc.perm = 0644
   120  			}
   121  			err := rc.WriteFile(path, hashPath, tc.perm)
   122  			assert.NilError(t, err)
   123  
   124  			fi, err := os.Stat(path)
   125  			assert.NilError(t, err)
   126  			// Windows files won't have the expected perms.
   127  			if runtime.GOOS != "windows" {
   128  				assert.Check(t, is.Equal(fi.Mode(), tc.perm))
   129  			}
   130  
   131  			if tc.modify {
   132  				err := os.WriteFile(path, []byte("modified"), 0644)
   133  				assert.NilError(t, err)
   134  			}
   135  
   136  			um, err := UserModified(path, hashPath)
   137  			assert.NilError(t, err)
   138  			assert.Check(t, is.Equal(um, tc.expUserModified))
   139  		})
   140  	}
   141  }
   142  
   143  var a2s = sliceutil.Mapper(netip.Addr.String)
   144  var s2a = sliceutil.Mapper(netip.MustParseAddr)
   145  
   146  // Test that a resolv.conf file can be modified using OverrideXXX() methods
   147  // to modify nameservers/search/options directives, and tha options can be
   148  // added via AddOption().
   149  func TestRCModify(t *testing.T) {
   150  	testcases := []struct {
   151  		name            string
   152  		inputNS         []string
   153  		inputSearch     []string
   154  		inputOptions    []string
   155  		noOverrides     bool // Whether to apply overrides (empty lists are valid overrides).
   156  		overrideNS      []string
   157  		overrideSearch  []string
   158  		overrideOptions []string
   159  		addOption       string
   160  	}{
   161  		{
   162  			name:    "No content no overrides",
   163  			inputNS: []string{},
   164  		},
   165  		{
   166  			name:         "No overrides",
   167  			noOverrides:  true,
   168  			inputNS:      []string{"1.2.3.4"},
   169  			inputSearch:  []string{"invalid"},
   170  			inputOptions: []string{"ndots:0"},
   171  		},
   172  		{
   173  			name:         "Empty overrides",
   174  			inputNS:      []string{"1.2.3.4"},
   175  			inputSearch:  []string{"invalid"},
   176  			inputOptions: []string{"ndots:0"},
   177  		},
   178  		{
   179  			name:            "Overrides",
   180  			inputNS:         []string{"1.2.3.4"},
   181  			inputSearch:     []string{"invalid"},
   182  			inputOptions:    []string{"ndots:0"},
   183  			overrideNS:      []string{"2.3.4.5", "fdba:acdd:587c::53"},
   184  			overrideSearch:  []string{"com", "invalid", "example"},
   185  			overrideOptions: []string{"ndots:1", "edns0", "trust-ad"},
   186  		},
   187  		{
   188  			name:         "Add option no overrides",
   189  			noOverrides:  true,
   190  			inputNS:      []string{"1.2.3.4"},
   191  			inputSearch:  []string{"invalid"},
   192  			inputOptions: []string{"ndots:0"},
   193  			addOption:    "attempts:3",
   194  		},
   195  	}
   196  
   197  	for _, tc := range testcases {
   198  		t.Run(tc.name, func(t *testing.T) {
   199  			tc := tc
   200  			var input string
   201  			if len(tc.inputNS) != 0 {
   202  				for _, ns := range tc.inputNS {
   203  					input += "nameserver " + ns + "\n"
   204  				}
   205  			}
   206  			if len(tc.inputSearch) != 0 {
   207  				input += "search " + strings.Join(tc.inputSearch, " ") + "\n"
   208  			}
   209  			if len(tc.inputOptions) != 0 {
   210  				input += "options " + strings.Join(tc.inputOptions, " ") + "\n"
   211  			}
   212  			rc, err := Parse(bytes.NewBuffer([]byte(input)), "")
   213  			assert.NilError(t, err)
   214  			assert.Check(t, is.DeepEqual(a2s(rc.NameServers()), tc.inputNS))
   215  			assert.Check(t, is.DeepEqual(rc.Search(), tc.inputSearch))
   216  			assert.Check(t, is.DeepEqual(rc.Options(), tc.inputOptions))
   217  
   218  			if !tc.noOverrides {
   219  				overrideNS := s2a(tc.overrideNS)
   220  				rc.OverrideNameServers(overrideNS)
   221  				rc.OverrideSearch(tc.overrideSearch)
   222  				rc.OverrideOptions(tc.overrideOptions)
   223  
   224  				assert.Check(t, is.DeepEqual(rc.NameServers(), overrideNS, cmpopts.EquateComparable(netip.Addr{})))
   225  				assert.Check(t, is.DeepEqual(rc.Search(), tc.overrideSearch))
   226  				assert.Check(t, is.DeepEqual(rc.Options(), tc.overrideOptions))
   227  			}
   228  
   229  			if tc.addOption != "" {
   230  				options := rc.Options()
   231  				rc.AddOption(tc.addOption)
   232  				assert.Check(t, is.DeepEqual(rc.Options(), append(options, tc.addOption)))
   233  			}
   234  
   235  			d := t.TempDir()
   236  			path := filepath.Join(d, "resolv.conf")
   237  			err = rc.WriteFile(path, "", 0644)
   238  			assert.NilError(t, err)
   239  
   240  			content, err := os.ReadFile(path)
   241  			assert.NilError(t, err)
   242  			assert.Check(t, golden.String(string(content), t.Name()+".golden"))
   243  		})
   244  	}
   245  }
   246  
   247  func TestRCTransformForLegacyNw(t *testing.T) {
   248  	testcases := []struct {
   249  		name       string
   250  		input      string
   251  		ipv6       bool
   252  		overrideNS []string
   253  	}{
   254  		{
   255  			name:  "Routable IPv4 only",
   256  			input: "nameserver 10.0.0.1",
   257  		},
   258  		{
   259  			name:  "Routable IPv4 and IPv6, ipv6 enabled",
   260  			input: "nameserver 10.0.0.1\nnameserver fdb6:b8fe:b528::1",
   261  			ipv6:  true,
   262  		},
   263  		{
   264  			name:  "Routable IPv4 and IPv6, ipv6 disabled",
   265  			input: "nameserver 10.0.0.1\nnameserver fdb6:b8fe:b528::1",
   266  			ipv6:  false,
   267  		},
   268  		{
   269  			name:  "IPv4 localhost, ipv6 disabled",
   270  			input: "nameserver 127.0.0.53",
   271  			ipv6:  false,
   272  		},
   273  		{
   274  			name:  "IPv4 localhost, ipv6 enabled",
   275  			input: "nameserver 127.0.0.53",
   276  			ipv6:  true,
   277  		},
   278  		{
   279  			name:  "IPv4 and IPv6 localhost, ipv6 disabled",
   280  			input: "nameserver 127.0.0.53\nnameserver ::1",
   281  			ipv6:  false,
   282  		},
   283  		{
   284  			name:  "IPv4 and IPv6 localhost, ipv6 enabled",
   285  			input: "nameserver 127.0.0.53\nnameserver ::1",
   286  			ipv6:  true,
   287  		},
   288  		{
   289  			name:  "IPv4 localhost, IPv6 routeable, ipv6 enabled",
   290  			input: "nameserver 127.0.0.53\nnameserver fd3e:2d1a:1f5a::1",
   291  			ipv6:  true,
   292  		},
   293  		{
   294  			name:  "IPv4 localhost, IPv6 routeable, ipv6 disabled",
   295  			input: "nameserver 127.0.0.53\nnameserver fd3e:2d1a:1f5a::1",
   296  			ipv6:  false,
   297  		},
   298  		{
   299  			name:       "Override nameservers",
   300  			input:      "nameserver 127.0.0.53",
   301  			overrideNS: []string{"127.0.0.1", "::1"},
   302  			ipv6:       false,
   303  		},
   304  	}
   305  
   306  	for _, tc := range testcases {
   307  		t.Run(tc.name, func(t *testing.T) {
   308  			tc := tc
   309  			rc, err := Parse(bytes.NewBuffer([]byte(tc.input)), "/etc/resolv.conf")
   310  			assert.NilError(t, err)
   311  			if tc.overrideNS != nil {
   312  				rc.OverrideNameServers(s2a(tc.overrideNS))
   313  			}
   314  
   315  			rc.TransformForLegacyNw(tc.ipv6)
   316  
   317  			d := t.TempDir()
   318  			path := filepath.Join(d, "resolv.conf")
   319  			err = rc.WriteFile(path, "", 0644)
   320  			assert.NilError(t, err)
   321  
   322  			content, err := os.ReadFile(path)
   323  			assert.NilError(t, err)
   324  			assert.Check(t, golden.String(string(content), t.Name()+".golden"))
   325  		})
   326  	}
   327  }
   328  
   329  func TestRCTransformForIntNS(t *testing.T) {
   330  	mke := func(addr string, hostLoopback bool) ExtDNSEntry {
   331  		return ExtDNSEntry{
   332  			Addr:         netip.MustParseAddr(addr),
   333  			HostLoopback: hostLoopback,
   334  		}
   335  	}
   336  
   337  	testcases := []struct {
   338  		name            string
   339  		input           string
   340  		intNameServer   string
   341  		ipv6            bool
   342  		overrideNS      []string
   343  		overrideOptions []string
   344  		reqdOptions     []string
   345  		expExtServers   []ExtDNSEntry
   346  		expErr          string
   347  	}{
   348  		{
   349  			name:          "IPv4 only",
   350  			input:         "nameserver 10.0.0.1",
   351  			expExtServers: []ExtDNSEntry{mke("10.0.0.1", false)},
   352  		},
   353  		{
   354  			name:          "IPv4 and IPv6, ipv6 enabled",
   355  			input:         "nameserver 10.0.0.1\nnameserver fdb6:b8fe:b528::1",
   356  			ipv6:          true,
   357  			expExtServers: []ExtDNSEntry{mke("10.0.0.1", false)},
   358  		},
   359  		{
   360  			name:          "IPv4 and IPv6, ipv6 disabled",
   361  			input:         "nameserver 10.0.0.1\nnameserver fdb6:b8fe:b528::1",
   362  			ipv6:          false,
   363  			expExtServers: []ExtDNSEntry{mke("10.0.0.1", false)},
   364  		},
   365  		{
   366  			name:          "IPv4 localhost",
   367  			input:         "nameserver 127.0.0.53",
   368  			ipv6:          false,
   369  			expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
   370  		},
   371  		{
   372  			// Overriding the nameserver with a localhost address means use the container's
   373  			// loopback interface, not the host's.
   374  			name:          "IPv4 localhost override",
   375  			input:         "nameserver 10.0.0.1",
   376  			ipv6:          false,
   377  			overrideNS:    []string{"127.0.0.53"},
   378  			expExtServers: []ExtDNSEntry{mke("127.0.0.53", false)},
   379  		},
   380  		{
   381  			name:          "IPv4 localhost, ipv6 enabled",
   382  			input:         "nameserver 127.0.0.53",
   383  			ipv6:          true,
   384  			expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
   385  		},
   386  		{
   387  			name:  "IPv6 addr, IPv6 enabled",
   388  			input: "nameserver fd14:6e0e:f855::1",
   389  			ipv6:  true,
   390  			// Note that there are no ext servers in this case, the internal resolver
   391  			// will only look up container names. The default nameservers aren't added
   392  			// because the host's IPv6 nameserver remains in the container's resolv.conf,
   393  			// (because only IPv4 ext servers are currently allowed).
   394  		},
   395  		{
   396  			name:          "IPv4 and IPv6 localhost, IPv6 disabled",
   397  			input:         "nameserver 127.0.0.53\nnameserver ::1",
   398  			ipv6:          false,
   399  			expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
   400  		},
   401  		{
   402  			name:          "IPv4 and IPv6 localhost, ipv6 enabled",
   403  			input:         "nameserver 127.0.0.53\nnameserver ::1",
   404  			ipv6:          true,
   405  			expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
   406  		},
   407  		{
   408  			name:          "IPv4 localhost, IPv6 private, IPv6 enabled",
   409  			input:         "nameserver 127.0.0.53\nnameserver fd3e:2d1a:1f5a::1",
   410  			ipv6:          true,
   411  			expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
   412  		},
   413  		{
   414  			name:          "IPv4 localhost, IPv6 private, IPv6 disabled",
   415  			input:         "nameserver 127.0.0.53\nnameserver fd3e:2d1a:1f5a::1",
   416  			ipv6:          false,
   417  			expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
   418  		},
   419  		{
   420  			name:  "No host nameserver, no iv6",
   421  			input: "",
   422  			ipv6:  false,
   423  			expExtServers: []ExtDNSEntry{
   424  				mke("8.8.8.8", false),
   425  				mke("8.8.4.4", false),
   426  			},
   427  		},
   428  		{
   429  			name:  "No host nameserver, iv6",
   430  			input: "",
   431  			ipv6:  true,
   432  			expExtServers: []ExtDNSEntry{
   433  				mke("8.8.8.8", false),
   434  				mke("8.8.4.4", false),
   435  				mke("2001:4860:4860::8888", false),
   436  				mke("2001:4860:4860::8844", false),
   437  			},
   438  		},
   439  		{
   440  			name:          "ndots present and required",
   441  			input:         "nameserver 127.0.0.53\noptions ndots:1",
   442  			reqdOptions:   []string{"ndots:0"},
   443  			expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
   444  		},
   445  		{
   446  			name:          "ndots missing but required",
   447  			input:         "nameserver 127.0.0.53",
   448  			reqdOptions:   []string{"ndots:0"},
   449  			expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
   450  		},
   451  		{
   452  			name:            "ndots host, override and required",
   453  			input:           "nameserver 127.0.0.53",
   454  			reqdOptions:     []string{"ndots:0"},
   455  			overrideOptions: []string{"ndots:2"},
   456  			expExtServers:   []ExtDNSEntry{mke("127.0.0.53", true)},
   457  		},
   458  		{
   459  			name:          "Extra required options",
   460  			input:         "nameserver 127.0.0.53\noptions trust-ad",
   461  			reqdOptions:   []string{"ndots:0", "attempts:3", "edns0", "trust-ad"},
   462  			expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
   463  		},
   464  	}
   465  
   466  	for _, tc := range testcases {
   467  		t.Run(tc.name, func(t *testing.T) {
   468  			tc := tc
   469  			rc, err := Parse(bytes.NewBuffer([]byte(tc.input)), "/etc/resolv.conf")
   470  			assert.NilError(t, err)
   471  
   472  			if tc.intNameServer == "" {
   473  				tc.intNameServer = "127.0.0.11"
   474  			}
   475  			if len(tc.overrideNS) > 0 {
   476  				rc.OverrideNameServers(s2a(tc.overrideNS))
   477  			}
   478  			if len(tc.overrideOptions) > 0 {
   479  				rc.OverrideOptions(tc.overrideOptions)
   480  			}
   481  			intNS := netip.MustParseAddr(tc.intNameServer)
   482  			extNameServers, err := rc.TransformForIntNS(tc.ipv6, intNS, tc.reqdOptions)
   483  			if tc.expErr != "" {
   484  				assert.Check(t, is.ErrorContains(err, tc.expErr))
   485  				return
   486  			}
   487  			assert.NilError(t, err)
   488  
   489  			d := t.TempDir()
   490  			path := filepath.Join(d, "resolv.conf")
   491  			err = rc.WriteFile(path, "", 0644)
   492  			assert.NilError(t, err)
   493  
   494  			content, err := os.ReadFile(path)
   495  			assert.NilError(t, err)
   496  			assert.Check(t, golden.String(string(content), t.Name()+".golden"))
   497  			assert.Check(t, is.DeepEqual(extNameServers, tc.expExtServers,
   498  				cmpopts.EquateComparable(netip.Addr{})))
   499  		})
   500  	}
   501  }
   502  
   503  func TestRCRead(t *testing.T) {
   504  	d := t.TempDir()
   505  	path := filepath.Join(d, "resolv.conf")
   506  
   507  	// Try to read a nonexistent file, equivalent to an empty file.
   508  	_, err := Load(path)
   509  	assert.Check(t, is.ErrorIs(err, fs.ErrNotExist))
   510  
   511  	err = os.WriteFile(path, []byte("options edns0"), 0644)
   512  	assert.NilError(t, err)
   513  
   514  	// Read that file in the constructor.
   515  	rc, err := Load(path)
   516  	assert.NilError(t, err)
   517  	assert.Check(t, is.DeepEqual(rc.Options(), []string{"edns0"}))
   518  
   519  	// Pass in an os.File, check the path is extracted.
   520  	file, err := os.Open(path)
   521  	assert.NilError(t, err)
   522  	defer file.Close()
   523  	rc, err = Parse(file, "")
   524  	assert.NilError(t, err)
   525  	assert.Check(t, is.Equal(rc.md.SourcePath, path))
   526  }
   527  
   528  func TestRCInvalidNS(t *testing.T) {
   529  	d := t.TempDir()
   530  
   531  	// A resolv.conf with an invalid nameserver address.
   532  	rc, err := Parse(bytes.NewBuffer([]byte("nameserver 1.2.3.4.5")), "")
   533  	assert.NilError(t, err)
   534  
   535  	path := filepath.Join(d, "resolv.conf")
   536  	err = rc.WriteFile(path, "", 0644)
   537  	assert.NilError(t, err)
   538  
   539  	content, err := os.ReadFile(path)
   540  	assert.NilError(t, err)
   541  	assert.Check(t, golden.String(string(content), t.Name()+".golden"))
   542  }
   543  
   544  func TestRCSetHeader(t *testing.T) {
   545  	rc, err := Parse(bytes.NewBuffer([]byte("nameserver 127.0.0.53")), "/etc/resolv.conf")
   546  	assert.NilError(t, err)
   547  
   548  	rc.SetHeader("# This is a comment.")
   549  	d := t.TempDir()
   550  	path := filepath.Join(d, "resolv.conf")
   551  	err = rc.WriteFile(path, "", 0644)
   552  	assert.NilError(t, err)
   553  
   554  	content, err := os.ReadFile(path)
   555  	assert.NilError(t, err)
   556  	assert.Check(t, golden.String(string(content), t.Name()+".golden"))
   557  }
   558  
   559  func TestRCUnknownDirectives(t *testing.T) {
   560  	const input = `
   561  something unexpected
   562  nameserver 127.0.0.53
   563  options ndots:1
   564  unrecognised thing
   565  `
   566  	rc, err := Parse(bytes.NewBuffer([]byte(input)), "/etc/resolv.conf")
   567  	assert.NilError(t, err)
   568  
   569  	d := t.TempDir()
   570  	path := filepath.Join(d, "resolv.conf")
   571  	err = rc.WriteFile(path, "", 0644)
   572  	assert.NilError(t, err)
   573  
   574  	content, err := os.ReadFile(path)
   575  	assert.NilError(t, err)
   576  	assert.Check(t, golden.String(string(content), t.Name()+".golden"))
   577  }