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