github.com/tonistiigi/docker@v0.10.1-0.20240229224939-974013b0dc6a/libnetwork/sandbox_dns_unix.go (about) 1 //go:build !windows 2 3 package libnetwork 4 5 import ( 6 "context" 7 "io/fs" 8 "net/netip" 9 "os" 10 "path/filepath" 11 "strings" 12 13 "github.com/containerd/log" 14 "github.com/docker/docker/errdefs" 15 "github.com/docker/docker/libnetwork/etchosts" 16 "github.com/docker/docker/libnetwork/internal/resolvconf" 17 "github.com/docker/docker/libnetwork/types" 18 "github.com/pkg/errors" 19 ) 20 21 const ( 22 defaultPrefix = "/var/lib/docker/network/files" 23 dirPerm = 0o755 24 filePerm = 0o644 25 26 resolverIPSandbox = "127.0.0.11" 27 ) 28 29 // finishInitDNS is to be called after the container namespace has been created, 30 // before it the user process is started. The container's support for IPv6 can be 31 // determined at this point. 32 func (sb *Sandbox) finishInitDNS() error { 33 if err := sb.buildHostsFile(); err != nil { 34 return errdefs.System(err) 35 } 36 for _, ep := range sb.Endpoints() { 37 if err := sb.updateHostsFile(ep.getEtcHostsAddrs()); err != nil { 38 return errdefs.System(err) 39 } 40 } 41 return nil 42 } 43 44 func (sb *Sandbox) startResolver(restore bool) { 45 sb.resolverOnce.Do(func() { 46 var err error 47 // The embedded resolver is always started with proxyDNS set as true, even when the sandbox is only attached to 48 // an internal network. This way, it's the driver responsibility to make sure `connect` syscall fails fast when 49 // no external connectivity is available (eg. by not setting a default gateway). 50 sb.resolver = NewResolver(resolverIPSandbox, true, sb) 51 defer func() { 52 if err != nil { 53 sb.resolver = nil 54 } 55 }() 56 57 // In the case of live restore container is already running with 58 // right resolv.conf contents created before. Just update the 59 // external DNS servers from the restored sandbox for embedded 60 // server to use. 61 if !restore { 62 err = sb.rebuildDNS() 63 if err != nil { 64 log.G(context.TODO()).Errorf("Updating resolv.conf failed for container %s, %q", sb.ContainerID(), err) 65 return 66 } 67 } 68 sb.resolver.SetExtServers(sb.extDNS) 69 70 if err = sb.osSbox.InvokeFunc(sb.resolver.SetupFunc(0)); err != nil { 71 log.G(context.TODO()).Errorf("Resolver Setup function failed for container %s, %q", sb.ContainerID(), err) 72 return 73 } 74 75 if err = sb.resolver.Start(); err != nil { 76 log.G(context.TODO()).Errorf("Resolver Start failed for container %s, %q", sb.ContainerID(), err) 77 } 78 }) 79 } 80 81 func (sb *Sandbox) setupResolutionFiles() error { 82 // Create a hosts file that can be mounted during container setup. For most 83 // networking modes (not host networking) it will be re-created before the 84 // container start, once its support for IPv6 is known. 85 if sb.config.hostsPath == "" { 86 sb.config.hostsPath = defaultPrefix + "/" + sb.id + "/hosts" 87 } 88 dir, _ := filepath.Split(sb.config.hostsPath) 89 if err := createBasePath(dir); err != nil { 90 return err 91 } 92 if err := sb.buildHostsFile(); err != nil { 93 return err 94 } 95 96 return sb.setupDNS() 97 } 98 99 func (sb *Sandbox) buildHostsFile() error { 100 sb.restoreHostsPath() 101 102 dir, _ := filepath.Split(sb.config.hostsPath) 103 if err := createBasePath(dir); err != nil { 104 return err 105 } 106 107 // This is for the host mode networking 108 if sb.config.useDefaultSandBox && len(sb.config.extraHosts) == 0 { 109 // We are working under the assumption that the origin file option had been properly expressed by the upper layer 110 // if not here we are going to error out 111 if err := copyFile(sb.config.originHostsPath, sb.config.hostsPath); err != nil && !os.IsNotExist(err) { 112 return types.InternalErrorf("could not copy source hosts file %s to %s: %v", sb.config.originHostsPath, sb.config.hostsPath, err) 113 } 114 return nil 115 } 116 117 extraContent := make([]etchosts.Record, 0, len(sb.config.extraHosts)) 118 for _, extraHost := range sb.config.extraHosts { 119 extraContent = append(extraContent, etchosts.Record{Hosts: extraHost.name, IP: extraHost.IP}) 120 } 121 122 // Assume IPv6 support, unless it's definitely disabled. 123 buildf := etchosts.Build 124 if en, ok := sb.ipv6Enabled(); ok && !en { 125 buildf = etchosts.BuildNoIPv6 126 } 127 if err := buildf(sb.config.hostsPath, extraContent); err != nil { 128 return err 129 } 130 131 return sb.updateParentHosts() 132 } 133 134 func (sb *Sandbox) updateHostsFile(ifaceIPs []string) error { 135 if len(ifaceIPs) == 0 { 136 return nil 137 } 138 139 if sb.config.originHostsPath != "" { 140 return nil 141 } 142 143 // User might have provided a FQDN in hostname or split it across hostname 144 // and domainname. We want the FQDN and the bare hostname. 145 fqdn := sb.config.hostName 146 if sb.config.domainName != "" { 147 fqdn += "." + sb.config.domainName 148 } 149 hosts := fqdn 150 151 if hostName, _, ok := strings.Cut(fqdn, "."); ok { 152 hosts += " " + hostName 153 } 154 155 var extraContent []etchosts.Record 156 for _, ip := range ifaceIPs { 157 extraContent = append(extraContent, etchosts.Record{Hosts: hosts, IP: ip}) 158 } 159 160 sb.addHostsEntries(extraContent) 161 return nil 162 } 163 164 func (sb *Sandbox) addHostsEntries(recs []etchosts.Record) { 165 // Assume IPv6 support, unless it's definitely disabled. 166 if en, ok := sb.ipv6Enabled(); ok && !en { 167 var filtered []etchosts.Record 168 for _, rec := range recs { 169 if addr, err := netip.ParseAddr(rec.IP); err == nil && !addr.Is6() { 170 filtered = append(filtered, rec) 171 } 172 } 173 recs = filtered 174 } 175 if err := etchosts.Add(sb.config.hostsPath, recs); err != nil { 176 log.G(context.TODO()).Warnf("Failed adding service host entries to the running container: %v", err) 177 } 178 } 179 180 func (sb *Sandbox) deleteHostsEntries(recs []etchosts.Record) { 181 if err := etchosts.Delete(sb.config.hostsPath, recs); err != nil { 182 log.G(context.TODO()).Warnf("Failed deleting service host entries to the running container: %v", err) 183 } 184 } 185 186 func (sb *Sandbox) updateParentHosts() error { 187 var pSb *Sandbox 188 189 for _, update := range sb.config.parentUpdates { 190 // TODO(thaJeztah): was it intentional for this loop to re-use prior results of pSB? If not, we should make pSb local and always replace here. 191 if s, _ := sb.controller.GetSandbox(update.cid); s != nil { 192 pSb = s 193 } 194 if pSb == nil { 195 continue 196 } 197 // TODO(robmry) - filter out IPv6 addresses here if !sb.ipv6Enabled() but... 198 // - this is part of the implementation of '--link', which will be removed along 199 // with the rest of legacy networking. 200 // - IPv6 addresses shouldn't be allocated if IPv6 is not available in a container, 201 // and that change will come along later. 202 // - I think this may be dead code, it's not possible to start a parent container with 203 // '--link child' unless the child has already started ("Error response from daemon: 204 // Cannot link to a non running container"). So, when the child starts and this method 205 // is called with updates for parents, the parents aren't running and GetSandbox() 206 // returns nil.) 207 if err := etchosts.Update(pSb.config.hostsPath, update.ip, update.name); err != nil { 208 return err 209 } 210 } 211 212 return nil 213 } 214 215 func (sb *Sandbox) restoreResolvConfPath() { 216 if sb.config.resolvConfPath == "" { 217 sb.config.resolvConfPath = defaultPrefix + "/" + sb.id + "/resolv.conf" 218 } 219 sb.config.resolvConfHashFile = sb.config.resolvConfPath + ".hash" 220 } 221 222 func (sb *Sandbox) restoreHostsPath() { 223 if sb.config.hostsPath == "" { 224 sb.config.hostsPath = defaultPrefix + "/" + sb.id + "/hosts" 225 } 226 } 227 228 func (sb *Sandbox) setExternalResolvers(entries []resolvconf.ExtDNSEntry) { 229 sb.extDNS = make([]extDNSEntry, 0, len(entries)) 230 for _, entry := range entries { 231 sb.extDNS = append(sb.extDNS, extDNSEntry{ 232 IPStr: entry.Addr.String(), 233 HostLoopback: entry.HostLoopback, 234 }) 235 } 236 } 237 238 func (c *containerConfig) getOriginResolvConfPath() string { 239 if c.originResolvConfPath != "" { 240 return c.originResolvConfPath 241 } 242 // Fallback if not specified. 243 return resolvconf.Path() 244 } 245 246 // loadResolvConf reads the resolv.conf file at path, and merges in overrides for 247 // nameservers, options, and search domains. 248 func (sb *Sandbox) loadResolvConf(path string) (*resolvconf.ResolvConf, error) { 249 rc, err := resolvconf.Load(path) 250 if err != nil && !errors.Is(err, fs.ErrNotExist) { 251 return nil, err 252 } 253 // Proceed with rc, which might be zero-valued if path does not exist. 254 255 rc.SetHeader(`# Generated by Docker Engine. 256 # This file can be edited; Docker Engine will not make further changes once it 257 # has been modified.`) 258 if len(sb.config.dnsList) > 0 { 259 var dnsAddrs []netip.Addr 260 for _, ns := range sb.config.dnsList { 261 addr, err := netip.ParseAddr(ns) 262 if err != nil { 263 return nil, errors.Wrapf(err, "bad nameserver address %s", ns) 264 } 265 dnsAddrs = append(dnsAddrs, addr) 266 } 267 rc.OverrideNameServers(dnsAddrs) 268 } 269 if len(sb.config.dnsSearchList) > 0 { 270 rc.OverrideSearch(sb.config.dnsSearchList) 271 } 272 if len(sb.config.dnsOptionsList) > 0 { 273 rc.OverrideOptions(sb.config.dnsOptionsList) 274 } 275 return &rc, nil 276 } 277 278 // For a new sandbox, write an initial version of the container's resolv.conf. It'll 279 // be a copy of the host's file, with overrides for nameservers, options and search 280 // domains applied. 281 func (sb *Sandbox) setupDNS() error { 282 // Make sure the directory exists. 283 sb.restoreResolvConfPath() 284 dir, _ := filepath.Split(sb.config.resolvConfPath) 285 if err := createBasePath(dir); err != nil { 286 return err 287 } 288 289 rc, err := sb.loadResolvConf(sb.config.getOriginResolvConfPath()) 290 if err != nil { 291 return err 292 } 293 return rc.WriteFile(sb.config.resolvConfPath, sb.config.resolvConfHashFile, filePerm) 294 } 295 296 // Called when an endpoint has joined the sandbox. 297 func (sb *Sandbox) updateDNS(ipv6Enabled bool) error { 298 if mod, err := resolvconf.UserModified(sb.config.resolvConfPath, sb.config.resolvConfHashFile); err != nil || mod { 299 return err 300 } 301 302 // Load the host's resolv.conf as a starting point. 303 rc, err := sb.loadResolvConf(sb.config.getOriginResolvConfPath()) 304 if err != nil { 305 return err 306 } 307 // For host-networking, no further change is needed. 308 if !sb.config.useDefaultSandBox { 309 // The legacy bridge network has no internal nameserver. So, strip localhost 310 // nameservers from the host's config, then add default nameservers if there 311 // are none remaining. 312 rc.TransformForLegacyNw(ipv6Enabled) 313 } 314 return rc.WriteFile(sb.config.resolvConfPath, sb.config.resolvConfHashFile, filePerm) 315 } 316 317 // Embedded DNS server has to be enabled for this sandbox. Rebuild the container's resolv.conf. 318 func (sb *Sandbox) rebuildDNS() error { 319 // Don't touch the file if the user has modified it. 320 if mod, err := resolvconf.UserModified(sb.config.resolvConfPath, sb.config.resolvConfHashFile); err != nil || mod { 321 return err 322 } 323 324 // Load the host's resolv.conf as a starting point. 325 rc, err := sb.loadResolvConf(sb.config.getOriginResolvConfPath()) 326 if err != nil { 327 return err 328 } 329 330 // Check for IPv6 endpoints in this sandbox. If there are any, IPv6 nameservers 331 // will be left in the container's 'resolv.conf'. 332 // TODO(robmry) - preserving old behaviour, but ... 333 // IPv6 nameservers should be treated like IPv4 ones, and used as upstream 334 // servers for the internal resolver (if it has IPv6 connectivity). This 335 // doesn't need to depend on whether there are currently any IPv6 endpoints. 336 // Removing IPv6 nameservers from the container's resolv.conf will avoid the 337 // problem that musl-libc's resolver tries all nameservers in parallel, so an 338 // external IPv6 resolver can return NXDOMAIN before the internal resolver 339 // returns the address of a container. 340 ipv6 := false 341 for _, ep := range sb.endpoints { 342 if ep.network.enableIPv6 { 343 ipv6 = true 344 break 345 } 346 } 347 348 intNS, err := netip.ParseAddr(sb.resolver.NameServer()) 349 if err != nil { 350 return err 351 } 352 353 // Work out whether ndots has been set from host config or overrides. 354 _, sb.ndotsSet = rc.Option("ndots") 355 // Swap nameservers for the internal one, and make sure the required options are set. 356 var extNameServers []resolvconf.ExtDNSEntry 357 extNameServers, err = rc.TransformForIntNS(ipv6, intNS, sb.resolver.ResolverOptions()) 358 if err != nil { 359 return err 360 } 361 // Extract the list of nameservers that just got swapped out, and store them as 362 // upstream nameservers. 363 sb.setExternalResolvers(extNameServers) 364 365 // Write the file for the container - preserving old behaviour, not updating the 366 // hash file (so, no further updates will be made). 367 // TODO(robmry) - I think that's probably accidental, I can't find a reason for it, 368 // and the old resolvconf.Build() function wrote the file but not the hash, which 369 // is surprising. But, before fixing it, a guard/flag needs to be added to 370 // sb.updateDNS() to make sure that when an endpoint joins a sandbox that already 371 // has an internal resolver, the container's resolv.conf is still (re)configured 372 // for an internal resolver. 373 return rc.WriteFile(sb.config.resolvConfPath, "", filePerm) 374 } 375 376 func createBasePath(dir string) error { 377 return os.MkdirAll(dir, dirPerm) 378 } 379 380 func copyFile(src, dst string) error { 381 sBytes, err := os.ReadFile(src) 382 if err != nil { 383 return err 384 } 385 return os.WriteFile(dst, sBytes, filePerm) 386 }