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