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