resolvconf.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  1. // Package resolvconf is used to generate a container's /etc/resolv.conf file.
  2. //
  3. // Constructor Load and Parse read a resolv.conf file from the filesystem or
  4. // a reader respectively, and return a ResolvConf object.
  5. //
  6. // The ResolvConf object can then be updated with overrides for nameserver,
  7. // search domains, and DNS options.
  8. //
  9. // ResolvConf can then be transformed to make it suitable for legacy networking,
  10. // a network with an internal nameserver, or used as-is for host networking.
  11. //
  12. // This package includes methods to write the file for the container, along with
  13. // a hash that can be used to detect modifications made by the user to avoid
  14. // overwriting those updates.
  15. package resolvconf
  16. import (
  17. "bufio"
  18. "bytes"
  19. "context"
  20. _ "embed"
  21. "fmt"
  22. "io"
  23. "io/fs"
  24. "net/netip"
  25. "os"
  26. "slices"
  27. "strconv"
  28. "strings"
  29. "text/template"
  30. "github.com/containerd/log"
  31. "github.com/docker/docker/errdefs"
  32. "github.com/docker/docker/pkg/ioutils"
  33. "github.com/opencontainers/go-digest"
  34. "github.com/pkg/errors"
  35. )
  36. // Fallback nameservers, to use if none can be obtained from the host or command
  37. // line options.
  38. var (
  39. defaultIPv4NSs = []netip.Addr{
  40. netip.MustParseAddr("8.8.8.8"),
  41. netip.MustParseAddr("8.8.4.4"),
  42. }
  43. defaultIPv6NSs = []netip.Addr{
  44. netip.MustParseAddr("2001:4860:4860::8888"),
  45. netip.MustParseAddr("2001:4860:4860::8844"),
  46. }
  47. )
  48. // ResolvConf represents a resolv.conf file. It can be constructed by
  49. // reading a resolv.conf file, using method Parse().
  50. type ResolvConf struct {
  51. nameServers []netip.Addr
  52. search []string
  53. options []string
  54. other []string // Unrecognised directives from the host's file, if any.
  55. md metadata
  56. }
  57. // ExtDNSEntry represents a nameserver address that was removed from the
  58. // container's resolv.conf when it was transformed by TransformForIntNS(). These
  59. // are addresses read from the host's file, or applied via an override ('--dns').
  60. type ExtDNSEntry struct {
  61. Addr netip.Addr
  62. HostLoopback bool // The address is loopback, in the host's namespace.
  63. }
  64. func (ed ExtDNSEntry) String() string {
  65. if ed.HostLoopback {
  66. return fmt.Sprintf("host(%s)", ed.Addr)
  67. }
  68. return ed.Addr.String()
  69. }
  70. // metadata is used to track where components of the generated file have come
  71. // from, in order to generate comments in the file for debug/info. Struct members
  72. // are exported for use by 'text/template'.
  73. type metadata struct {
  74. SourcePath string
  75. Header string
  76. NSOverride bool
  77. SearchOverride bool
  78. OptionsOverride bool
  79. NDotsFrom string
  80. UsedDefaultNS bool
  81. Transform string
  82. InvalidNSs []string
  83. ExtNameServers []ExtDNSEntry
  84. }
  85. // Load opens a file at path and parses it as a resolv.conf file.
  86. // On error, the returned ResolvConf will be zero-valued.
  87. func Load(path string) (ResolvConf, error) {
  88. f, err := os.Open(path)
  89. if err != nil {
  90. return ResolvConf{}, err
  91. }
  92. defer f.Close()
  93. return Parse(f, path)
  94. }
  95. // Parse parses a resolv.conf file from reader.
  96. // path is optional if reader is an *os.File.
  97. // On error, the returned ResolvConf will be zero-valued.
  98. func Parse(reader io.Reader, path string) (ResolvConf, error) {
  99. var rc ResolvConf
  100. rc.md.SourcePath = path
  101. if path == "" {
  102. if namer, ok := reader.(interface{ Name() string }); ok {
  103. rc.md.SourcePath = namer.Name()
  104. }
  105. }
  106. scanner := bufio.NewScanner(reader)
  107. for scanner.Scan() {
  108. rc.processLine(scanner.Text())
  109. }
  110. if err := scanner.Err(); err != nil {
  111. return ResolvConf{}, errdefs.System(err)
  112. }
  113. if _, ok := rc.Option("ndots"); ok {
  114. rc.md.NDotsFrom = "host"
  115. }
  116. return rc, nil
  117. }
  118. // SetHeader sets the content to be included verbatim at the top of the
  119. // generated resolv.conf file. No formatting or checking is done on the
  120. // string. It must be valid resolv.conf syntax. (Comments must have '#'
  121. // or ';' in the first column of each line).
  122. //
  123. // For example:
  124. //
  125. // SetHeader("# My resolv.conf\n# This file was generated.")
  126. func (rc *ResolvConf) SetHeader(c string) {
  127. rc.md.Header = c
  128. }
  129. // NameServers returns addresses used in nameserver directives.
  130. func (rc *ResolvConf) NameServers() []netip.Addr {
  131. return slices.Clone(rc.nameServers)
  132. }
  133. // OverrideNameServers replaces the current set of nameservers.
  134. func (rc *ResolvConf) OverrideNameServers(nameServers []netip.Addr) {
  135. rc.nameServers = nameServers
  136. rc.md.NSOverride = true
  137. }
  138. // Search returns the current DNS search domains.
  139. func (rc *ResolvConf) Search() []string {
  140. return slices.Clone(rc.search)
  141. }
  142. // OverrideSearch replaces the current DNS search domains.
  143. func (rc *ResolvConf) OverrideSearch(search []string) {
  144. var filtered []string
  145. for _, s := range search {
  146. if s != "." {
  147. filtered = append(filtered, s)
  148. }
  149. }
  150. rc.search = filtered
  151. rc.md.SearchOverride = true
  152. }
  153. // Options returns the current options.
  154. func (rc *ResolvConf) Options() []string {
  155. return slices.Clone(rc.options)
  156. }
  157. // Option finds the last option named search, and returns (value, true) if
  158. // found, else ("", false). Options are treated as "name:value", where the
  159. // ":value" may be omitted.
  160. //
  161. // For example, for "ndots:1 edns0":
  162. //
  163. // Option("ndots") -> ("1", true)
  164. // Option("edns0") -> ("", true)
  165. func (rc *ResolvConf) Option(search string) (string, bool) {
  166. for i := len(rc.options) - 1; i >= 0; i -= 1 {
  167. k, v, _ := strings.Cut(rc.options[i], ":")
  168. if k == search {
  169. return v, true
  170. }
  171. }
  172. return "", false
  173. }
  174. // OverrideOptions replaces the current DNS options.
  175. func (rc *ResolvConf) OverrideOptions(options []string) {
  176. rc.options = slices.Clone(options)
  177. rc.md.NDotsFrom = ""
  178. if _, exists := rc.Option("ndots"); exists {
  179. rc.md.NDotsFrom = "override"
  180. }
  181. rc.md.OptionsOverride = true
  182. }
  183. // AddOption adds a single DNS option.
  184. func (rc *ResolvConf) AddOption(option string) {
  185. if len(option) > 6 && option[:6] == "ndots:" {
  186. rc.md.NDotsFrom = "internal"
  187. }
  188. rc.options = append(rc.options, option)
  189. }
  190. // TransformForLegacyNw makes sure the resolv.conf file will be suitable for
  191. // use in a legacy network (one that has no internal resolver).
  192. // - Remove loopback addresses inherited from the host's resolv.conf, because
  193. // they'll only work in the host's namespace.
  194. // - Remove IPv6 addresses if !ipv6.
  195. // - Add default nameservers if there are no addresses left.
  196. func (rc *ResolvConf) TransformForLegacyNw(ipv6 bool) {
  197. rc.md.Transform = "legacy"
  198. if rc.md.NSOverride {
  199. return
  200. }
  201. var filtered []netip.Addr
  202. for _, addr := range rc.nameServers {
  203. if !addr.IsLoopback() && (!addr.Is6() || ipv6) {
  204. filtered = append(filtered, addr)
  205. }
  206. }
  207. rc.nameServers = filtered
  208. if len(rc.nameServers) == 0 {
  209. log.G(context.TODO()).Info("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers")
  210. rc.nameServers = defaultNSAddrs(ipv6)
  211. rc.md.UsedDefaultNS = true
  212. }
  213. }
  214. // TransformForIntNS makes sure the resolv.conf file will be suitable for
  215. // use in a network sandbox that has an internal DNS resolver.
  216. // - Add internalNS as a nameserver.
  217. // - Remove other nameservers, stashing them as ExtNameServers for the
  218. // internal resolver to use. (Apart from IPv6 nameservers, if keepIPv6.)
  219. // - Mark ExtNameServers that must be used in the host namespace.
  220. // - If no ExtNameServer addresses are found, use the defaults.
  221. // - Return an error if an "ndots" option inherited from the host's config, or
  222. // supplied in an override is not valid.
  223. // - Ensure there's an 'options' value for each entry in reqdOptions. If the
  224. // option includes a ':', and an option with a matching prefix exists, it
  225. // is not modified.
  226. func (rc *ResolvConf) TransformForIntNS(
  227. keepIPv6 bool,
  228. internalNS netip.Addr,
  229. reqdOptions []string,
  230. ) ([]ExtDNSEntry, error) {
  231. // The transformed config must list the internal nameserver.
  232. newNSs := []netip.Addr{internalNS}
  233. // Filter out other nameservers, keeping them for use as upstream nameservers by the
  234. // internal nameserver.
  235. rc.md.ExtNameServers = nil
  236. for _, addr := range rc.nameServers {
  237. // The internal resolver only uses IPv4 addresses so, keep IPv6 nameservers in
  238. // the container's file if keepIPv6, else drop them.
  239. if addr.Is6() {
  240. if keepIPv6 {
  241. newNSs = append(newNSs, addr)
  242. }
  243. } else {
  244. // Extract this NS. Mark loopback addresses that did not come from an override as
  245. // 'HostLoopback'. Upstream requests for these servers will be made in the host's
  246. // network namespace. (So, '--dns 127.0.0.53' means use a nameserver listening on
  247. // the container's loopback interface. But, if the host's resolv.conf contains
  248. // 'nameserver 127.0.0.53', the host's resolver will be used.)
  249. //
  250. // TODO(robmry) - why only loopback addresses?
  251. // Addresses from the host's resolv.conf must be usable in the host's namespace,
  252. // and a lookup from the container's namespace is more expensive? And, for
  253. // example, if the host has a nameserver with an IPv6 LL address with a zone-id,
  254. // it won't work from the container's namespace (now, while the address is left in
  255. // the container's resolv.conf, or in future for the internal resolver).
  256. rc.md.ExtNameServers = append(rc.md.ExtNameServers, ExtDNSEntry{
  257. Addr: addr,
  258. HostLoopback: addr.IsLoopback() && !rc.md.NSOverride,
  259. })
  260. }
  261. }
  262. rc.nameServers = newNSs
  263. // If there are no external nameservers, and the only nameserver left is the
  264. // internal resolver, use the defaults as ext nameservers.
  265. if len(rc.md.ExtNameServers) == 0 && len(rc.nameServers) == 1 {
  266. log.G(context.TODO()).Info("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers")
  267. for _, addr := range defaultNSAddrs(keepIPv6) {
  268. rc.md.ExtNameServers = append(rc.md.ExtNameServers, ExtDNSEntry{Addr: addr})
  269. }
  270. rc.md.UsedDefaultNS = true
  271. }
  272. // Validate the ndots option from host config or overrides, if present.
  273. // TODO(robmry) - pre-existing behaviour, but ...
  274. // Validating ndots from an override is good, but not-liking something in the
  275. // host's resolv.conf isn't a reason to fail - just remove? (And it'll be
  276. // replaced by the value in reqdOptions, if given.)
  277. if ndots, exists := rc.Option("ndots"); exists {
  278. if n, err := strconv.Atoi(ndots); err != nil || n < 0 {
  279. return nil, errdefs.InvalidParameter(
  280. fmt.Errorf("invalid number for ndots option: %v", ndots))
  281. }
  282. }
  283. // For each option required by the nameserver, add it if not already
  284. // present (if the option already has a value don't change it).
  285. for _, opt := range reqdOptions {
  286. optName, _, _ := strings.Cut(opt, ":")
  287. if _, exists := rc.Option(optName); !exists {
  288. rc.AddOption(opt)
  289. }
  290. }
  291. rc.md.Transform = "internal resolver"
  292. return slices.Clone(rc.md.ExtNameServers), nil
  293. }
  294. // Generate returns content suitable for writing to a resolv.conf file. If comments
  295. // is true, the file will include header information if supplied, and a trailing
  296. // comment that describes how the file was constructed and lists external resolvers.
  297. func (rc *ResolvConf) Generate(comments bool) ([]byte, error) {
  298. s := struct {
  299. Md *metadata
  300. NameServers []netip.Addr
  301. Search []string
  302. Options []string
  303. Other []string
  304. Overrides []string
  305. Comments bool
  306. }{
  307. Md: &rc.md,
  308. NameServers: rc.nameServers,
  309. Search: rc.search,
  310. Options: rc.options,
  311. Other: rc.other,
  312. Comments: comments,
  313. }
  314. if rc.md.NSOverride {
  315. s.Overrides = append(s.Overrides, "nameservers")
  316. }
  317. if rc.md.SearchOverride {
  318. s.Overrides = append(s.Overrides, "search")
  319. }
  320. if rc.md.OptionsOverride {
  321. s.Overrides = append(s.Overrides, "options")
  322. }
  323. const templateText = `{{if .Comments}}{{with .Md.Header}}{{.}}
  324. {{end}}{{end}}{{range .NameServers -}}
  325. nameserver {{.}}
  326. {{end}}{{with .Search -}}
  327. search {{join . " "}}
  328. {{end}}{{with .Options -}}
  329. options {{join . " "}}
  330. {{end}}{{with .Other -}}
  331. {{join . "\n"}}
  332. {{end}}{{if .Comments}}
  333. # Based on host file: '{{.Md.SourcePath}}'{{with .Md.Transform}} ({{.}}){{end}}
  334. {{if .Md.UsedDefaultNS -}}
  335. # Used default nameservers.
  336. {{end -}}
  337. {{with .Md.ExtNameServers -}}
  338. # ExtServers: {{.}}
  339. {{end -}}
  340. {{with .Md.InvalidNSs -}}
  341. # Invalid nameservers: {{.}}
  342. {{end -}}
  343. # Overrides: {{.Overrides}}
  344. {{with .Md.NDotsFrom -}}
  345. # Option ndots from: {{.}}
  346. {{end -}}
  347. {{end -}}
  348. `
  349. funcs := template.FuncMap{"join": strings.Join}
  350. var buf bytes.Buffer
  351. templ, err := template.New("summary").Funcs(funcs).Parse(templateText)
  352. if err != nil {
  353. return nil, errdefs.System(err)
  354. }
  355. if err := templ.Execute(&buf, s); err != nil {
  356. return nil, errdefs.System(err)
  357. }
  358. return buf.Bytes(), nil
  359. }
  360. // WriteFile generates content and writes it to path. If hashPath is non-zero, it
  361. // also writes a file containing a hash of the content, to enable UserModified()
  362. // to determine whether the file has been modified.
  363. func (rc *ResolvConf) WriteFile(path, hashPath string, perm os.FileMode) error {
  364. content, err := rc.Generate(true)
  365. if err != nil {
  366. return err
  367. }
  368. // Write the resolv.conf file - it's bind-mounted into the container, so can't
  369. // move a temp file into place, just have to truncate and write it.
  370. if err := os.WriteFile(path, content, perm); err != nil {
  371. return errdefs.System(err)
  372. }
  373. // Write the hash file.
  374. if hashPath != "" {
  375. hashFile, err := ioutils.NewAtomicFileWriter(hashPath, perm)
  376. if err != nil {
  377. return errdefs.System(err)
  378. }
  379. defer hashFile.Close()
  380. digest := digest.FromBytes(content)
  381. if _, err = hashFile.Write([]byte(digest)); err != nil {
  382. return err
  383. }
  384. }
  385. return nil
  386. }
  387. // UserModified can be used to determine whether the resolv.conf file has been
  388. // modified since it was generated. It returns false with no error if the file
  389. // matches the hash, true with no error if the file no longer matches the hash,
  390. // and false with an error if the result cannot be determined.
  391. func UserModified(rcPath, rcHashPath string) (bool, error) {
  392. currRCHash, err := os.ReadFile(rcHashPath)
  393. if err != nil {
  394. // If the hash file doesn't exist, can only assume it hasn't been written
  395. // yet (so, the user hasn't modified the file it hashes).
  396. if errors.Is(err, fs.ErrNotExist) {
  397. return false, nil
  398. }
  399. return false, errors.Wrapf(err, "failed to read hash file %s", rcHashPath)
  400. }
  401. expected, err := digest.Parse(string(currRCHash))
  402. if err != nil {
  403. return false, errors.Wrapf(err, "failed to parse hash file %s", rcHashPath)
  404. }
  405. v := expected.Verifier()
  406. currRC, err := os.Open(rcPath)
  407. if err != nil {
  408. return false, errors.Wrapf(err, "failed to open %s to check for modifications", rcPath)
  409. }
  410. defer currRC.Close()
  411. if _, err := io.Copy(v, currRC); err != nil {
  412. return false, errors.Wrapf(err, "failed to hash %s to check for modifications", rcPath)
  413. }
  414. return !v.Verified(), nil
  415. }
  416. func (rc *ResolvConf) processLine(line string) {
  417. fields := strings.Fields(line)
  418. // Strip blank lines and comments.
  419. if len(fields) == 0 || fields[0][0] == '#' || fields[0][0] == ';' {
  420. return
  421. }
  422. switch fields[0] {
  423. case "nameserver":
  424. if len(fields) < 2 {
  425. return
  426. }
  427. if addr, err := netip.ParseAddr(fields[1]); err != nil {
  428. rc.md.InvalidNSs = append(rc.md.InvalidNSs, fields[1])
  429. } else {
  430. rc.nameServers = append(rc.nameServers, addr)
  431. }
  432. case "domain":
  433. // 'domain' is an obsolete name for 'search'.
  434. fallthrough
  435. case "search":
  436. if len(fields) < 2 {
  437. return
  438. }
  439. // Only the last 'search' directive is used.
  440. rc.search = fields[1:]
  441. case "options":
  442. if len(fields) < 2 {
  443. return
  444. }
  445. // Replace options from earlier directives.
  446. // TODO(robmry) - preserving incorrect behaviour, options should accumulate.
  447. // rc.options = append(rc.options, fields[1:]...)
  448. rc.options = fields[1:]
  449. default:
  450. // Copy anything that's not a recognised directive.
  451. rc.other = append(rc.other, line)
  452. }
  453. }
  454. func defaultNSAddrs(ipv6 bool) []netip.Addr {
  455. var addrs []netip.Addr
  456. addrs = append(addrs, defaultIPv4NSs...)
  457. if ipv6 {
  458. addrs = append(addrs, defaultIPv6NSs...)
  459. }
  460. return addrs
  461. }