123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497 |
- // Package resolvconf is used to generate a container's /etc/resolv.conf file.
- //
- // Constructor Load and Parse read a resolv.conf file from the filesystem or
- // a reader respectively, and return a ResolvConf object.
- //
- // The ResolvConf object can then be updated with overrides for nameserver,
- // search domains, and DNS options.
- //
- // ResolvConf can then be transformed to make it suitable for legacy networking,
- // a network with an internal nameserver, or used as-is for host networking.
- //
- // This package includes methods to write the file for the container, along with
- // a hash that can be used to detect modifications made by the user to avoid
- // overwriting those updates.
- package resolvconf
- import (
- "bufio"
- "bytes"
- "context"
- "fmt"
- "io"
- "io/fs"
- "net/netip"
- "os"
- "strconv"
- "strings"
- "text/template"
- "github.com/containerd/log"
- "github.com/docker/docker/errdefs"
- "github.com/docker/docker/pkg/ioutils"
- "github.com/opencontainers/go-digest"
- "github.com/pkg/errors"
- )
- // Fallback nameservers, to use if none can be obtained from the host or command
- // line options.
- var (
- defaultIPv4NSs = []netip.Addr{
- netip.MustParseAddr("8.8.8.8"),
- netip.MustParseAddr("8.8.4.4"),
- }
- defaultIPv6NSs = []netip.Addr{
- netip.MustParseAddr("2001:4860:4860::8888"),
- netip.MustParseAddr("2001:4860:4860::8844"),
- }
- )
- // ResolvConf represents a resolv.conf file. It can be constructed by
- // reading a resolv.conf file, using method Parse().
- type ResolvConf struct {
- nameServers []netip.Addr
- search []string
- options []string
- other []string // Unrecognised directives from the host's file, if any.
- md metadata
- }
- // ExtDNSEntry represents a nameserver address that was removed from the
- // container's resolv.conf when it was transformed by TransformForIntNS(). These
- // are addresses read from the host's file, or applied via an override ('--dns').
- type ExtDNSEntry struct {
- Addr netip.Addr
- HostLoopback bool // The address is loopback, in the host's namespace.
- }
- func (ed ExtDNSEntry) String() string {
- if ed.HostLoopback {
- return fmt.Sprintf("host(%s)", ed.Addr)
- }
- return ed.Addr.String()
- }
- // metadata is used to track where components of the generated file have come
- // from, in order to generate comments in the file for debug/info. Struct members
- // are exported for use by 'text/template'.
- type metadata struct {
- SourcePath string
- Header string
- NSOverride bool
- SearchOverride bool
- OptionsOverride bool
- NDotsFrom string
- UsedDefaultNS bool
- Transform string
- InvalidNSs []string
- ExtNameServers []ExtDNSEntry
- }
- // Load opens a file at path and parses it as a resolv.conf file.
- // On error, the returned ResolvConf will be zero-valued.
- func Load(path string) (ResolvConf, error) {
- f, err := os.Open(path)
- if err != nil {
- return ResolvConf{}, err
- }
- defer f.Close()
- return Parse(f, path)
- }
- // Parse parses a resolv.conf file from reader.
- // path is optional if reader is an *os.File.
- // On error, the returned ResolvConf will be zero-valued.
- func Parse(reader io.Reader, path string) (ResolvConf, error) {
- var rc ResolvConf
- rc.md.SourcePath = path
- if path == "" {
- if namer, ok := reader.(interface{ Name() string }); ok {
- rc.md.SourcePath = namer.Name()
- }
- }
- scanner := bufio.NewScanner(reader)
- for scanner.Scan() {
- rc.processLine(scanner.Text())
- }
- if err := scanner.Err(); err != nil {
- return ResolvConf{}, errdefs.System(err)
- }
- if _, ok := rc.Option("ndots"); ok {
- rc.md.NDotsFrom = "host"
- }
- return rc, nil
- }
- // SetHeader sets the content to be included verbatim at the top of the
- // generated resolv.conf file. No formatting or checking is done on the
- // string. It must be valid resolv.conf syntax. (Comments must have '#'
- // or ';' in the first column of each line).
- //
- // For example:
- //
- // SetHeader("# My resolv.conf\n# This file was generated.")
- func (rc *ResolvConf) SetHeader(c string) {
- rc.md.Header = c
- }
- // NameServers returns addresses used in nameserver directives.
- func (rc *ResolvConf) NameServers() []netip.Addr {
- return append([]netip.Addr(nil), rc.nameServers...)
- }
- // OverrideNameServers replaces the current set of nameservers.
- func (rc *ResolvConf) OverrideNameServers(nameServers []netip.Addr) {
- rc.nameServers = nameServers
- rc.md.NSOverride = true
- }
- // Search returns the current DNS search domains.
- func (rc *ResolvConf) Search() []string {
- return append([]string(nil), rc.search...)
- }
- // OverrideSearch replaces the current DNS search domains.
- func (rc *ResolvConf) OverrideSearch(search []string) {
- var filtered []string
- for _, s := range search {
- if s != "." {
- filtered = append(filtered, s)
- }
- }
- rc.search = filtered
- rc.md.SearchOverride = true
- }
- // Options returns the current options.
- func (rc *ResolvConf) Options() []string {
- return append([]string(nil), rc.options...)
- }
- // Option finds the last option named search, and returns (value, true) if
- // found, else ("", false). Options are treated as "name:value", where the
- // ":value" may be omitted.
- //
- // For example, for "ndots:1 edns0":
- //
- // Option("ndots") -> ("1", true)
- // Option("edns0") -> ("", true)
- func (rc *ResolvConf) Option(search string) (string, bool) {
- for i := len(rc.options) - 1; i >= 0; i -= 1 {
- k, v, _ := strings.Cut(rc.options[i], ":")
- if k == search {
- return v, true
- }
- }
- return "", false
- }
- // OverrideOptions replaces the current DNS options.
- func (rc *ResolvConf) OverrideOptions(options []string) {
- rc.options = append([]string(nil), options...)
- rc.md.NDotsFrom = ""
- if _, exists := rc.Option("ndots"); exists {
- rc.md.NDotsFrom = "override"
- }
- rc.md.OptionsOverride = true
- }
- // AddOption adds a single DNS option.
- func (rc *ResolvConf) AddOption(option string) {
- if len(option) > 6 && option[:6] == "ndots:" {
- rc.md.NDotsFrom = "internal"
- }
- rc.options = append(rc.options, option)
- }
- // TransformForLegacyNw makes sure the resolv.conf file will be suitable for
- // use in a legacy network (one that has no internal resolver).
- // - Remove loopback addresses inherited from the host's resolv.conf, because
- // they'll only work in the host's namespace.
- // - Remove IPv6 addresses if !ipv6.
- // - Add default nameservers if there are no addresses left.
- func (rc *ResolvConf) TransformForLegacyNw(ipv6 bool) {
- rc.md.Transform = "legacy"
- if rc.md.NSOverride {
- return
- }
- var filtered []netip.Addr
- for _, addr := range rc.nameServers {
- if !addr.IsLoopback() && (!addr.Is6() || ipv6) {
- filtered = append(filtered, addr)
- }
- }
- rc.nameServers = filtered
- if len(rc.nameServers) == 0 {
- log.G(context.TODO()).Info("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers")
- rc.nameServers = defaultNSAddrs(ipv6)
- rc.md.UsedDefaultNS = true
- }
- }
- // TransformForIntNS makes sure the resolv.conf file will be suitable for
- // use in a network sandbox that has an internal DNS resolver.
- // - Add internalNS as a nameserver.
- // - Remove other nameservers, stashing them as ExtNameServers for the
- // internal resolver to use.
- // - Mark ExtNameServers that must be used in the host namespace.
- // - If no ExtNameServer addresses are found, use the defaults.
- // - Return an error if an "ndots" option inherited from the host's config, or
- // supplied in an override is not valid.
- // - Ensure there's an 'options' value for each entry in reqdOptions. If the
- // option includes a ':', and an option with a matching prefix exists, it
- // is not modified.
- func (rc *ResolvConf) TransformForIntNS(
- ipv6 bool,
- internalNS netip.Addr,
- reqdOptions []string,
- ) ([]ExtDNSEntry, error) {
- // The transformed config must list the internal nameserver.
- newNSs := []netip.Addr{internalNS}
- // Filter out other nameservers, keeping them for use as upstream nameservers by the
- // internal nameserver.
- rc.md.ExtNameServers = nil
- for _, addr := range rc.nameServers {
- // Extract this NS. Mark addresses that did not come from an override, but will
- // definitely not work in the container's namespace as 'HostLoopback'. Upstream
- // requests for these servers will be made in the host's network namespace. (So,
- // '--dns 127.0.0.53' means use a nameserver listening on the container's
- // loopback interface. But, if the host's resolv.conf contains 'nameserver
- // 127.0.0.53', the host's resolver will be used.)
- rc.md.ExtNameServers = append(rc.md.ExtNameServers, ExtDNSEntry{
- Addr: addr,
- HostLoopback: !rc.md.NSOverride && (addr.IsLoopback() || (addr.Is6() && !ipv6) || addr.Zone() != ""),
- })
- }
- rc.nameServers = newNSs
- // If there are no external nameservers, and the only nameserver left is the
- // internal resolver, use the defaults as ext nameservers.
- if len(rc.md.ExtNameServers) == 0 && len(rc.nameServers) == 1 {
- log.G(context.TODO()).Info("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers")
- for _, addr := range defaultNSAddrs(ipv6) {
- rc.md.ExtNameServers = append(rc.md.ExtNameServers, ExtDNSEntry{Addr: addr})
- }
- rc.md.UsedDefaultNS = true
- }
- // For each option required by the nameserver, add it if not already present. If
- // the option is already present, don't override it. Apart from ndots - if the
- // ndots value is invalid and an ndots option is required, replace the existing
- // value.
- for _, opt := range reqdOptions {
- optName, _, _ := strings.Cut(opt, ":")
- if optName == "ndots" {
- rc.options = removeInvalidNDots(rc.options)
- // No need to update rc.md.NDotsFrom, if there is no ndots option remaining,
- // it'll be set to "internal" when the required value is added.
- }
- if _, exists := rc.Option(optName); !exists {
- rc.AddOption(opt)
- }
- }
- rc.md.Transform = "internal resolver"
- return append([]ExtDNSEntry(nil), rc.md.ExtNameServers...), nil
- }
- // Generate returns content suitable for writing to a resolv.conf file. If comments
- // is true, the file will include header information if supplied, and a trailing
- // comment that describes how the file was constructed and lists external resolvers.
- func (rc *ResolvConf) Generate(comments bool) ([]byte, error) {
- s := struct {
- Md *metadata
- NameServers []netip.Addr
- Search []string
- Options []string
- Other []string
- Overrides []string
- Comments bool
- }{
- Md: &rc.md,
- NameServers: rc.nameServers,
- Search: rc.search,
- Options: rc.options,
- Other: rc.other,
- Comments: comments,
- }
- if rc.md.NSOverride {
- s.Overrides = append(s.Overrides, "nameservers")
- }
- if rc.md.SearchOverride {
- s.Overrides = append(s.Overrides, "search")
- }
- if rc.md.OptionsOverride {
- s.Overrides = append(s.Overrides, "options")
- }
- const templateText = `{{if .Comments}}{{with .Md.Header}}{{.}}
- {{end}}{{end}}{{range .NameServers -}}
- nameserver {{.}}
- {{end}}{{with .Search -}}
- search {{join . " "}}
- {{end}}{{with .Options -}}
- options {{join . " "}}
- {{end}}{{with .Other -}}
- {{join . "\n"}}
- {{end}}{{if .Comments}}
- # Based on host file: '{{.Md.SourcePath}}'{{with .Md.Transform}} ({{.}}){{end}}
- {{if .Md.UsedDefaultNS -}}
- # Used default nameservers.
- {{end -}}
- {{with .Md.ExtNameServers -}}
- # ExtServers: {{.}}
- {{end -}}
- {{with .Md.InvalidNSs -}}
- # Invalid nameservers: {{.}}
- {{end -}}
- # Overrides: {{.Overrides}}
- {{with .Md.NDotsFrom -}}
- # Option ndots from: {{.}}
- {{end -}}
- {{end -}}
- `
- funcs := template.FuncMap{"join": strings.Join}
- var buf bytes.Buffer
- templ, err := template.New("summary").Funcs(funcs).Parse(templateText)
- if err != nil {
- return nil, errdefs.System(err)
- }
- if err := templ.Execute(&buf, s); err != nil {
- return nil, errdefs.System(err)
- }
- return buf.Bytes(), nil
- }
- // WriteFile generates content and writes it to path. If hashPath is non-zero, it
- // also writes a file containing a hash of the content, to enable UserModified()
- // to determine whether the file has been modified.
- func (rc *ResolvConf) WriteFile(path, hashPath string, perm os.FileMode) error {
- content, err := rc.Generate(true)
- if err != nil {
- return err
- }
- // Write the resolv.conf file - it's bind-mounted into the container, so can't
- // move a temp file into place, just have to truncate and write it.
- if err := os.WriteFile(path, content, perm); err != nil {
- return errdefs.System(err)
- }
- // Write the hash file.
- if hashPath != "" {
- hashFile, err := ioutils.NewAtomicFileWriter(hashPath, perm)
- if err != nil {
- return errdefs.System(err)
- }
- defer hashFile.Close()
- digest := digest.FromBytes(content)
- if _, err = hashFile.Write([]byte(digest)); err != nil {
- return err
- }
- }
- return nil
- }
- // UserModified can be used to determine whether the resolv.conf file has been
- // modified since it was generated. It returns false with no error if the file
- // matches the hash, true with no error if the file no longer matches the hash,
- // and false with an error if the result cannot be determined.
- func UserModified(rcPath, rcHashPath string) (bool, error) {
- currRCHash, err := os.ReadFile(rcHashPath)
- if err != nil {
- // If the hash file doesn't exist, can only assume it hasn't been written
- // yet (so, the user hasn't modified the file it hashes).
- if errors.Is(err, fs.ErrNotExist) {
- return false, nil
- }
- return false, errors.Wrapf(err, "failed to read hash file %s", rcHashPath)
- }
- expected, err := digest.Parse(string(currRCHash))
- if err != nil {
- return false, errors.Wrapf(err, "failed to parse hash file %s", rcHashPath)
- }
- v := expected.Verifier()
- currRC, err := os.Open(rcPath)
- if err != nil {
- return false, errors.Wrapf(err, "failed to open %s to check for modifications", rcPath)
- }
- defer currRC.Close()
- if _, err := io.Copy(v, currRC); err != nil {
- return false, errors.Wrapf(err, "failed to hash %s to check for modifications", rcPath)
- }
- return !v.Verified(), nil
- }
- func (rc *ResolvConf) processLine(line string) {
- fields := strings.Fields(line)
- // Strip blank lines and comments.
- if len(fields) == 0 || fields[0][0] == '#' || fields[0][0] == ';' {
- return
- }
- switch fields[0] {
- case "nameserver":
- if len(fields) < 2 {
- return
- }
- if addr, err := netip.ParseAddr(fields[1]); err != nil {
- rc.md.InvalidNSs = append(rc.md.InvalidNSs, fields[1])
- } else {
- rc.nameServers = append(rc.nameServers, addr)
- }
- case "domain":
- // 'domain' is an obsolete name for 'search'.
- fallthrough
- case "search":
- if len(fields) < 2 {
- return
- }
- // Only the last 'search' directive is used.
- rc.search = fields[1:]
- case "options":
- if len(fields) < 2 {
- return
- }
- // Accumulate options.
- rc.options = append(rc.options, fields[1:]...)
- default:
- // Copy anything that's not a recognised directive.
- rc.other = append(rc.other, line)
- }
- }
- func defaultNSAddrs(ipv6 bool) []netip.Addr {
- var addrs []netip.Addr
- addrs = append(addrs, defaultIPv4NSs...)
- if ipv6 {
- addrs = append(addrs, defaultIPv6NSs...)
- }
- return addrs
- }
- // removeInvalidNDots filters ill-formed "ndots" settings from options.
- // The backing array of the options slice is reused.
- func removeInvalidNDots(options []string) []string {
- n := 0
- for _, opt := range options {
- k, v, _ := strings.Cut(opt, ":")
- if k == "ndots" {
- ndots, err := strconv.Atoi(v)
- if err != nil || ndots < 0 {
- continue
- }
- }
- options[n] = opt
- n++
- }
- clear(options[n:]) // Zero out the obsolete elements, for GC.
- return options[:n]
- }
|