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