//go:build (linux && !android) || freebsd package dns import ( "os" "path/filepath" "testing" "github.com/stretchr/testify/assert" ) func Test_parseResolvConf(t *testing.T) { testCases := []struct { input string expectedSearch []string expectedNS []string expectedOther []string }{ { input: `domain example.org search example.org nameserver 192.168.0.1 `, expectedSearch: []string{"example.org"}, expectedNS: []string{"192.168.0.1"}, expectedOther: []string{}, }, { input: `# This is /run/systemd/resolve/resolv.conf managed by man:systemd-resolved(8). # Do not edit. # # This file might be symlinked as /etc/resolv.conf. If you're looking at # /etc/resolv.conf and seeing this text, you have followed the symlink. # # This is a dynamic resolv.conf file for connecting local clients directly to # all known uplink DNS servers. This file lists all configured search domains. # # Third party programs should typically not access this file directly, but only # through the symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a # different way, replace this symlink by a static file or a different symlink. # # See man:systemd-resolved.service(8) for details about the supported modes of # operation for /etc/resolv.conf. nameserver 192.168.2.1 nameserver 100.81.99.197 search netbird.cloud `, expectedSearch: []string{"netbird.cloud"}, expectedNS: []string{"192.168.2.1", "100.81.99.197"}, expectedOther: []string{}, }, { input: `# This is /run/systemd/resolve/resolv.conf managed by man:systemd-resolved(8). # Do not edit. # # This file might be symlinked as /etc/resolv.conf. If you're looking at # /etc/resolv.conf and seeing this text, you have followed the symlink. # # This is a dynamic resolv.conf file for connecting local clients directly to # all known uplink DNS servers. This file lists all configured search domains. # # Third party programs should typically not access this file directly, but only # through the symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a # different way, replace this symlink by a static file or a different symlink. # # See man:systemd-resolved.service(8) for details about the supported modes of # operation for /etc/resolv.conf. nameserver 192.168.2.1 nameserver 100.81.99.197 search netbird.cloud options debug `, expectedSearch: []string{"netbird.cloud"}, expectedNS: []string{"192.168.2.1", "100.81.99.197"}, expectedOther: []string{"options debug"}, }, } for _, testCase := range testCases { testCase := testCase t.Run("test", func(t *testing.T) { t.Parallel() tmpResolvConf := filepath.Join(t.TempDir(), "resolv.conf") err := os.WriteFile(tmpResolvConf, []byte(testCase.input), 0644) if err != nil { t.Fatal(err) } cfg, err := parseResolvConfFile(tmpResolvConf) if err != nil { t.Fatal(err) } ok := compareLists(cfg.searchDomains, testCase.expectedSearch) if !ok { t.Errorf("invalid parse result for search domains, expected: %v, got: %v", testCase.expectedSearch, cfg.searchDomains) } ok = compareLists(cfg.nameServers, testCase.expectedNS) if !ok { t.Errorf("invalid parse result for ns domains, expected: %v, got: %v", testCase.expectedNS, cfg.nameServers) } ok = compareLists(cfg.others, testCase.expectedOther) if !ok { t.Errorf("invalid parse result for others, expected: %v, got: %v", testCase.expectedOther, cfg.others) } }) } } func compareLists(search []string, search2 []string) bool { if len(search) != len(search2) { return false } for i, v := range search { if v != search2[i] { return false } } return true } func Test_emptyFile(t *testing.T) { cfg, err := parseResolvConfFile("/tmp/nothing") if err == nil { t.Errorf("expected error, got nil") } if len(cfg.others) != 0 || len(cfg.searchDomains) != 0 || len(cfg.nameServers) != 0 { t.Errorf("expected empty config, got %v", cfg) } } func Test_symlink(t *testing.T) { input := `# This is /run/systemd/resolve/resolv.conf managed by man:systemd-resolved(8). # Do not edit. # # This file might be symlinked as /etc/resolv.conf. If you're looking at # /etc/resolv.conf and seeing this text, you have followed the symlink. # # This is a dynamic resolv.conf file for connecting local clients directly to # all known uplink DNS servers. This file lists all configured search domains. # # Third party programs should typically not access this file directly, but only # through the symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a # different way, replace this symlink by a static file or a different symlink. # # See man:systemd-resolved.service(8) for details about the supported modes of # operation for /etc/resolv.conf. nameserver 192.168.0.1 ` tmpResolvConf := filepath.Join(t.TempDir(), "resolv.conf") err := os.WriteFile(tmpResolvConf, []byte(input), 0644) if err != nil { t.Fatal(err) } tmpLink := filepath.Join(t.TempDir(), "symlink") err = os.Symlink(tmpResolvConf, tmpLink) if err != nil { t.Fatal(err) } cfg, err := parseResolvConfFile(tmpLink) if err != nil { t.Fatal(err) } if len(cfg.nameServers) != 1 { t.Errorf("unexpected resolv.conf content: %v", cfg) } } func TestPrepareOptionsWithTimeout(t *testing.T) { tests := []struct { name string others []string timeout int attempts int expected []string }{ { name: "Append new options with timeout and attempts", others: []string{"some config"}, timeout: 2, attempts: 2, expected: []string{"some config", "options timeout:2 attempts:2"}, }, { name: "Modify existing options to exclude rotate and include timeout and attempts", others: []string{"some config", "options rotate someother"}, timeout: 3, attempts: 2, expected: []string{"some config", "options attempts:2 timeout:3 someother"}, }, { name: "Existing options with timeout and attempts are updated", others: []string{"some config", "options timeout:4 attempts:3"}, timeout: 5, attempts: 4, expected: []string{"some config", "options timeout:5 attempts:4"}, }, { name: "Modify existing options, add missing attempts before timeout", others: []string{"some config", "options timeout:4"}, timeout: 4, attempts: 3, expected: []string{"some config", "options attempts:3 timeout:4"}, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { result := prepareOptionsWithTimeout(tc.others, tc.timeout, tc.attempts) assert.Equal(t, tc.expected, result) }) } } func TestRemoveFirstNbNameserver(t *testing.T) { testCases := []struct { name string content string ipToRemove string expected string }{ { name: "Unrelated nameservers with comments and options", content: `# This is a comment options rotate nameserver 1.1.1.1 # Another comment nameserver 8.8.4.4 search example.com`, ipToRemove: "9.9.9.9", expected: `# This is a comment options rotate nameserver 1.1.1.1 # Another comment nameserver 8.8.4.4 search example.com`, }, { name: "First nameserver matches", content: `search example.com nameserver 9.9.9.9 # oof, a comment nameserver 8.8.4.4 options attempts:5`, ipToRemove: "9.9.9.9", expected: `search example.com # oof, a comment nameserver 8.8.4.4 options attempts:5`, }, { name: "Target IP not the first nameserver", // nolint:dupword content: `# Comment about the first nameserver nameserver 8.8.4.4 # Comment before our target nameserver 9.9.9.9 options timeout:2`, ipToRemove: "9.9.9.9", // nolint:dupword expected: `# Comment about the first nameserver nameserver 8.8.4.4 # Comment before our target nameserver 9.9.9.9 options timeout:2`, }, { name: "Only nameserver matches", content: `options debug nameserver 9.9.9.9 search localdomain`, ipToRemove: "9.9.9.9", expected: `options debug nameserver 9.9.9.9 search localdomain`, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { tempDir := t.TempDir() tempFile := filepath.Join(tempDir, "resolv.conf") err := os.WriteFile(tempFile, []byte(tc.content), 0644) assert.NoError(t, err) err = removeFirstNbNameserver(tempFile, tc.ipToRemove) assert.NoError(t, err) content, err := os.ReadFile(tempFile) assert.NoError(t, err) assert.Equal(t, tc.expected, string(content), "The resulting content should match the expected output.") }) } }