github.com/kaisenlinux/docker.io@v0.0.0-20230510090727-ea55db55fac7/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/pkg/ioutils" 12 "github.com/docker/libnetwork/resolvconf/dns" 13 "github.com/docker/libnetwork/types" 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 func FilterResolvDNS(resolvConf []byte, ipv6Enabled bool) (*File, error) { 148 cleanedResolvConf := localhostNSRegexp.ReplaceAll(resolvConf, []byte{}) 149 // if IPv6 is not enabled, also clean out any IPv6 address nameserver 150 if !ipv6Enabled { 151 cleanedResolvConf = nsIPv6Regexp.ReplaceAll(cleanedResolvConf, []byte{}) 152 } 153 // if the resulting resolvConf has no more nameservers defined, add appropriate 154 // default DNS servers for IPv4 and (optionally) IPv6 155 if len(GetNameservers(cleanedResolvConf, types.IP)) == 0 { 156 logrus.Infof("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers: %v", defaultIPv4Dns) 157 dns := defaultIPv4Dns 158 if ipv6Enabled { 159 logrus.Infof("IPv6 enabled; Adding default IPv6 external servers: %v", defaultIPv6Dns) 160 dns = append(dns, defaultIPv6Dns...) 161 } 162 cleanedResolvConf = append(cleanedResolvConf, []byte("\n"+strings.Join(dns, "\n"))...) 163 } 164 hash, err := ioutils.HashData(bytes.NewReader(cleanedResolvConf)) 165 if err != nil { 166 return nil, err 167 } 168 return &File{Content: cleanedResolvConf, Hash: hash}, nil 169 } 170 171 // getLines parses input into lines and strips away comments. 172 func getLines(input []byte, commentMarker []byte) [][]byte { 173 lines := bytes.Split(input, []byte("\n")) 174 var output [][]byte 175 for _, currentLine := range lines { 176 var commentIndex = bytes.Index(currentLine, commentMarker) 177 if commentIndex == -1 { 178 output = append(output, currentLine) 179 } else { 180 output = append(output, currentLine[:commentIndex]) 181 } 182 } 183 return output 184 } 185 186 // GetNameservers returns nameservers (if any) listed in /etc/resolv.conf 187 func GetNameservers(resolvConf []byte, kind int) []string { 188 nameservers := []string{} 189 for _, line := range getLines(resolvConf, []byte("#")) { 190 var ns [][]byte 191 if kind == types.IP { 192 ns = nsRegexp.FindSubmatch(line) 193 } else if kind == types.IPv4 { 194 ns = nsIPv4Regexpmatch.FindSubmatch(line) 195 } else if kind == types.IPv6 { 196 ns = nsIPv6Regexpmatch.FindSubmatch(line) 197 } 198 if len(ns) > 0 { 199 nameservers = append(nameservers, string(ns[1])) 200 } 201 } 202 return nameservers 203 } 204 205 // GetNameserversAsCIDR returns nameservers (if any) listed in 206 // /etc/resolv.conf as CIDR blocks (e.g., "1.2.3.4/32") 207 // This function's output is intended for net.ParseCIDR 208 func GetNameserversAsCIDR(resolvConf []byte) []string { 209 nameservers := []string{} 210 for _, nameserver := range GetNameservers(resolvConf, types.IP) { 211 var address string 212 // If IPv6, strip zone if present 213 if strings.Contains(nameserver, ":") { 214 address = strings.Split(nameserver, "%")[0] + "/128" 215 } else { 216 address = nameserver + "/32" 217 } 218 nameservers = append(nameservers, address) 219 } 220 return nameservers 221 } 222 223 // GetSearchDomains returns search domains (if any) listed in /etc/resolv.conf 224 // If more than one search line is encountered, only the contents of the last 225 // one is returned. 226 func GetSearchDomains(resolvConf []byte) []string { 227 domains := []string{} 228 for _, line := range getLines(resolvConf, []byte("#")) { 229 match := searchRegexp.FindSubmatch(line) 230 if match == nil { 231 continue 232 } 233 domains = strings.Fields(string(match[1])) 234 } 235 return domains 236 } 237 238 // GetOptions returns options (if any) listed in /etc/resolv.conf 239 // If more than one options line is encountered, only the contents of the last 240 // one is returned. 241 func GetOptions(resolvConf []byte) []string { 242 options := []string{} 243 for _, line := range getLines(resolvConf, []byte("#")) { 244 match := optionsRegexp.FindSubmatch(line) 245 if match == nil { 246 continue 247 } 248 options = strings.Fields(string(match[1])) 249 } 250 return options 251 } 252 253 // Build writes a configuration file to path containing a "nameserver" entry 254 // for every element in dns, a "search" entry for every element in 255 // dnsSearch, and an "options" entry for every element in dnsOptions. 256 func Build(path string, dns, dnsSearch, dnsOptions []string) (*File, error) { 257 content := bytes.NewBuffer(nil) 258 if len(dnsSearch) > 0 { 259 if searchString := strings.Join(dnsSearch, " "); strings.Trim(searchString, " ") != "." { 260 if _, err := content.WriteString("search " + searchString + "\n"); err != nil { 261 return nil, err 262 } 263 } 264 } 265 for _, dns := range dns { 266 if _, err := content.WriteString("nameserver " + dns + "\n"); err != nil { 267 return nil, err 268 } 269 } 270 if len(dnsOptions) > 0 { 271 if optsString := strings.Join(dnsOptions, " "); strings.Trim(optsString, " ") != "" { 272 if _, err := content.WriteString("options " + optsString + "\n"); err != nil { 273 return nil, err 274 } 275 } 276 } 277 278 hash, err := ioutils.HashData(bytes.NewReader(content.Bytes())) 279 if err != nil { 280 return nil, err 281 } 282 283 return &File{Content: content.Bytes(), Hash: hash}, ioutil.WriteFile(path, content.Bytes(), 0644) 284 }