github.com/containerd/nerdctl@v1.7.7/pkg/resolvconf/resolvconf.go (about) 1 /* 2 Copyright The containerd Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 /* 18 Portions from https://github.com/moby/moby/blob/6014c1e29dc34dffa77fb5749cc3281c1b4854ac/libnetwork/resolvconf/resolvconf.go 19 Copyright (C) Docker/Moby authors. 20 Licensed under the Apache License, Version 2.0 21 NOTICE: https://github.com/moby/moby/blob/6014c1e29dc34dffa77fb5749cc3281c1b4854ac/NOTICE 22 */ 23 24 // Package resolvconf provides utility code to query and update DNS configuration in /etc/resolv.conf 25 // originally from https://github.com/moby/moby/blob/6014c1e29dc34dffa77fb5749cc3281c1b4854ac/libnetwork/resolvconf/resolvconf.go 26 package resolvconf 27 28 import ( 29 "bytes" 30 "crypto/sha256" 31 "encoding/hex" 32 "io" 33 "os" 34 "regexp" 35 "strings" 36 "sync" 37 38 "github.com/containerd/log" 39 ) 40 41 const ( 42 // defaultPath is the default path to the resolv.conf that contains information to resolve DNS. See Path(). 43 defaultPath = "/etc/resolv.conf" 44 // alternatePath is a path different from defaultPath, that may be used to resolve DNS. See Path(). 45 alternatePath = "/run/systemd/resolve/resolv.conf" 46 ) 47 48 // constants for the IP address type 49 const ( 50 IP = iota // IPv4 and IPv6 51 IPv4 52 IPv6 53 ) 54 55 var ( 56 detectSystemdResolvConfOnce sync.Once 57 pathAfterSystemdDetection = defaultPath 58 ) 59 60 // Path returns the path to the resolv.conf file that libnetwork should use. 61 // 62 // When /etc/resolv.conf contains 127.0.0.53 as the only nameserver, then 63 // it is assumed systemd-resolved manages DNS. Because inside the container 127.0.0.53 64 // is not a valid DNS server, Path() returns /run/systemd/resolve/resolv.conf 65 // which is the resolv.conf that systemd-resolved generates and manages. 66 // Otherwise Path() returns /etc/resolv.conf. 67 // 68 // Errors are silenced as they will inevitably resurface at future open/read calls. 69 // 70 // More information at https://www.freedesktop.org/software/systemd/man/systemd-resolved.service.html#/etc/resolv.conf 71 func Path() string { 72 detectSystemdResolvConfOnce.Do(func() { 73 candidateResolvConf, err := os.ReadFile(defaultPath) 74 if err != nil { 75 // silencing error as it will resurface at next calls trying to read defaultPath 76 return 77 } 78 ns := GetNameservers(candidateResolvConf, IP) 79 if len(ns) == 1 && ns[0] == "127.0.0.53" { 80 pathAfterSystemdDetection = alternatePath 81 log.L.Debugf("detected 127.0.0.53 nameserver, assuming systemd-resolved, so using resolv.conf: %s", alternatePath) 82 } 83 }) 84 return pathAfterSystemdDetection 85 } 86 87 const ( 88 // ipLocalhost is a regex pattern for IPv4 or IPv6 loopback range. 89 ipLocalhost = `((127\.([0-9]{1,3}\.){2}[0-9]{1,3})|(::1)$)` 90 ipv4NumBlock = `(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)` 91 ipv4Address = `(` + ipv4NumBlock + `\.){3}` + ipv4NumBlock 92 93 // This is not an IPv6 address verifier as it will accept a super-set of IPv6, and also 94 // will *not match* IPv4-Embedded IPv6 Addresses (RFC6052), but that and other variants 95 // -- e.g. other link-local types -- either won't work in containers or are unnecessary. 96 // For readability and sufficiency for Docker purposes this seemed more reasonable than a 97 // 1000+ character regexp with exact and complete IPv6 validation 98 ipv6Address = `([0-9A-Fa-f]{0,4}:){2,7}([0-9A-Fa-f]{0,4})(%\w+)?` 99 ) 100 101 var ( 102 // Note: the default IPv4 & IPv6 resolvers are set to Google's Public DNS 103 defaultIPv4Dns = []string{"nameserver 8.8.8.8", "nameserver 8.8.4.4"} 104 defaultIPv6Dns = []string{"nameserver 2001:4860:4860::8888", "nameserver 2001:4860:4860::8844"} 105 106 localhostNSRegexp = regexp.MustCompile(`(?m)^nameserver\s+` + ipLocalhost + `\s*\n*`) 107 nsIPv6Regexp = regexp.MustCompile(`(?m)^nameserver\s+` + ipv6Address + `\s*\n*`) 108 nsRegexp = regexp.MustCompile(`^\s*nameserver\s*((` + ipv4Address + `)|(` + ipv6Address + `))\s*$`) 109 nsIPv6Regexpmatch = regexp.MustCompile(`^\s*nameserver\s*((` + ipv6Address + `))\s*$`) 110 nsIPv4Regexpmatch = regexp.MustCompile(`^\s*nameserver\s*((` + ipv4Address + `))\s*$`) 111 searchRegexp = regexp.MustCompile(`^\s*search\s*(([^\s]+\s*)*)$`) 112 optionsRegexp = regexp.MustCompile(`^\s*options\s*(([^\s]+\s*)*)$`) 113 ) 114 115 var lastModified struct { 116 sync.Mutex 117 sha256 string 118 contents []byte 119 } 120 121 // File contains the resolv.conf content and its hash 122 type File struct { 123 Content []byte 124 Hash string 125 } 126 127 // Get returns the contents of /etc/resolv.conf and its hash 128 func Get() (*File, error) { 129 return GetSpecific(Path()) 130 } 131 132 // GetSpecific returns the contents of the user specified resolv.conf file and its hash 133 func GetSpecific(path string) (*File, error) { 134 resolv, err := os.ReadFile(path) 135 if err != nil { 136 return nil, err 137 } 138 hash, err := hashData(bytes.NewReader(resolv)) 139 if err != nil { 140 return nil, err 141 } 142 return &File{Content: resolv, Hash: hash}, nil 143 } 144 145 // GetIfChanged retrieves the host /etc/resolv.conf file, checks against the last hash 146 // and, if modified since last check, returns the bytes and new hash. 147 // This feature is used by the resolv.conf updater for containers 148 func GetIfChanged() (*File, error) { 149 lastModified.Lock() 150 defer lastModified.Unlock() 151 152 resolv, err := os.ReadFile(Path()) 153 if err != nil { 154 return nil, err 155 } 156 newHash, err := hashData(bytes.NewReader(resolv)) 157 if err != nil { 158 return nil, err 159 } 160 if lastModified.sha256 != newHash { 161 lastModified.sha256 = newHash 162 lastModified.contents = resolv 163 return &File{Content: resolv, Hash: newHash}, nil 164 } 165 // nothing changed, so return no data 166 return nil, nil 167 } 168 169 // GetLastModified retrieves the last used contents and hash of the host resolv.conf. 170 // Used by containers updating on restart 171 func GetLastModified() *File { 172 lastModified.Lock() 173 defer lastModified.Unlock() 174 175 return &File{Content: lastModified.contents, Hash: lastModified.sha256} 176 } 177 178 // FilterResolvDNS cleans up the config in resolvConf. It has two main jobs: 179 // 1. It looks for localhost (127.*|::1) entries in the provided 180 // resolv.conf, removing local nameserver entries, and, if the resulting 181 // cleaned config has no defined nameservers left, adds default DNS entries 182 // 2. Given the caller provides the enable/disable state of IPv6, the filter 183 // code will remove all IPv6 nameservers if it is not enabled for containers 184 func FilterResolvDNS(resolvConf []byte, ipv6Enabled bool) (*File, error) { 185 cleanedResolvConf := localhostNSRegexp.ReplaceAll(resolvConf, []byte{}) 186 // if IPv6 is not enabled, also clean out any IPv6 address nameserver 187 if !ipv6Enabled { 188 cleanedResolvConf = nsIPv6Regexp.ReplaceAll(cleanedResolvConf, []byte{}) 189 } 190 // if the resulting resolvConf has no more nameservers defined, add appropriate 191 // default DNS servers for IPv4 and (optionally) IPv6 192 if len(GetNameservers(cleanedResolvConf, IP)) == 0 { 193 log.L.Infof("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers: %v", defaultIPv4Dns) 194 dns := defaultIPv4Dns 195 if ipv6Enabled { 196 log.L.Infof("IPv6 enabled; Adding default IPv6 external servers: %v", defaultIPv6Dns) 197 dns = append(dns, defaultIPv6Dns...) 198 } 199 cleanedResolvConf = append(cleanedResolvConf, []byte("\n"+strings.Join(dns, "\n"))...) 200 } 201 hash, err := hashData(bytes.NewReader(cleanedResolvConf)) 202 if err != nil { 203 return nil, err 204 } 205 return &File{Content: cleanedResolvConf, Hash: hash}, nil 206 } 207 208 // getLines parses input into lines and strips away comments. 209 func getLines(input []byte, commentMarker []byte) [][]byte { 210 lines := bytes.Split(input, []byte("\n")) 211 var output [][]byte 212 for _, currentLine := range lines { 213 var commentIndex = bytes.Index(currentLine, commentMarker) 214 if commentIndex == -1 { 215 output = append(output, currentLine) 216 } else { 217 output = append(output, currentLine[:commentIndex]) 218 } 219 } 220 return output 221 } 222 223 // GetNameservers returns nameservers (if any) listed in /etc/resolv.conf 224 func GetNameservers(resolvConf []byte, kind int) []string { 225 nameservers := []string{} 226 for _, line := range getLines(resolvConf, []byte("#")) { 227 var ns [][]byte 228 if kind == IP { 229 ns = nsRegexp.FindSubmatch(line) 230 } else if kind == IPv4 { 231 ns = nsIPv4Regexpmatch.FindSubmatch(line) 232 } else if kind == IPv6 { 233 ns = nsIPv6Regexpmatch.FindSubmatch(line) 234 } 235 if len(ns) > 0 { 236 nameservers = append(nameservers, string(ns[1])) 237 } 238 } 239 return nameservers 240 } 241 242 // GetNameserversAsCIDR returns nameservers (if any) listed in 243 // /etc/resolv.conf as CIDR blocks (e.g., "1.2.3.4/32") 244 // This function's output is intended for net.ParseCIDR 245 func GetNameserversAsCIDR(resolvConf []byte) []string { 246 nameservers := []string{} 247 for _, nameserver := range GetNameservers(resolvConf, IP) { 248 var address string 249 // If IPv6, strip zone if present 250 if strings.Contains(nameserver, ":") { 251 address = strings.Split(nameserver, "%")[0] + "/128" 252 } else { 253 address = nameserver + "/32" 254 } 255 nameservers = append(nameservers, address) 256 } 257 return nameservers 258 } 259 260 // GetSearchDomains returns search domains (if any) listed in /etc/resolv.conf 261 // If more than one search line is encountered, only the contents of the last 262 // one is returned. 263 func GetSearchDomains(resolvConf []byte) []string { 264 domains := []string{} 265 for _, line := range getLines(resolvConf, []byte("#")) { 266 match := searchRegexp.FindSubmatch(line) 267 if match == nil { 268 continue 269 } 270 domains = strings.Fields(string(match[1])) 271 } 272 return domains 273 } 274 275 // GetOptions returns options (if any) listed in /etc/resolv.conf 276 // If more than one options line is encountered, only the contents of the last 277 // one is returned. 278 func GetOptions(resolvConf []byte) []string { 279 options := []string{} 280 for _, line := range getLines(resolvConf, []byte("#")) { 281 match := optionsRegexp.FindSubmatch(line) 282 if match == nil { 283 continue 284 } 285 options = strings.Fields(string(match[1])) 286 } 287 return options 288 } 289 290 // Build writes a configuration file to path containing a "nameserver" entry 291 // for every element in dns, a "search" entry for every element in 292 // dnsSearch, and an "options" entry for every element in dnsOptions. 293 func Build(path string, dns, dnsSearch, dnsOptions []string) (*File, error) { 294 content := bytes.NewBuffer(nil) 295 if len(dnsSearch) > 0 { 296 if searchString := strings.Join(dnsSearch, " "); strings.Trim(searchString, " ") != "." { 297 if _, err := content.WriteString("search " + searchString + "\n"); err != nil { 298 return nil, err 299 } 300 } 301 } 302 for _, dns := range dns { 303 if _, err := content.WriteString("nameserver " + dns + "\n"); err != nil { 304 return nil, err 305 } 306 } 307 if len(dnsOptions) > 0 { 308 if optsString := strings.Join(dnsOptions, " "); strings.Trim(optsString, " ") != "" { 309 if _, err := content.WriteString("options " + optsString + "\n"); err != nil { 310 return nil, err 311 } 312 } 313 } 314 315 hash, err := hashData(bytes.NewReader(content.Bytes())) 316 if err != nil { 317 return nil, err 318 } 319 320 return &File{Content: content.Bytes(), Hash: hash}, os.WriteFile(path, content.Bytes(), 0644) 321 } 322 323 func hashData(src io.Reader) (string, error) { 324 h := sha256.New() 325 if _, err := io.Copy(h, src); err != nil { 326 return "", err 327 } 328 return "sha256:" + hex.EncodeToString(h.Sum(nil)), nil 329 }