resolvconf.go 15 KB

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