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