resolvconf_test.go 17 KB

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