1/* 2 * 3 * Copyright 2017 gRPC authors. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 */ 18 19// Package dns implements a dns resolver to be installed as the default resolver 20// in grpc. 21package dns 22 23import ( 24 "encoding/json" 25 "errors" 26 "fmt" 27 "net" 28 "os" 29 "strconv" 30 "strings" 31 "sync" 32 "time" 33 34 "golang.org/x/net/context" 35 "google.golang.org/grpc/grpclog" 36 "google.golang.org/grpc/internal/grpcrand" 37 "google.golang.org/grpc/resolver" 38) 39 40func init() { 41 resolver.Register(NewBuilder()) 42} 43 44const ( 45 defaultPort = "443" 46 defaultFreq = time.Minute * 30 47 golang = "GO" 48 // In DNS, service config is encoded in a TXT record via the mechanism 49 // described in RFC-1464 using the attribute name grpc_config. 50 txtAttribute = "grpc_config=" 51) 52 53var ( 54 errMissingAddr = errors.New("dns resolver: missing address") 55 56 // Addresses ending with a colon that is supposed to be the separator 57 // between host and port is not allowed. E.g. "::" is a valid address as 58 // it is an IPv6 address (host only) and "[::]:" is invalid as it ends with 59 // a colon as the host and port separator 60 errEndsWithColon = errors.New("dns resolver: missing port after port-separator colon") 61) 62 63// NewBuilder creates a dnsBuilder which is used to factory DNS resolvers. 64func NewBuilder() resolver.Builder { 65 return &dnsBuilder{freq: defaultFreq} 66} 67 68type dnsBuilder struct { 69 // frequency of polling the DNS server. 70 freq time.Duration 71} 72 73// Build creates and starts a DNS resolver that watches the name resolution of the target. 74func (b *dnsBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOption) (resolver.Resolver, error) { 75 if target.Authority != "" { 76 return nil, fmt.Errorf("Default DNS resolver does not support custom DNS server") 77 } 78 host, port, err := parseTarget(target.Endpoint) 79 if err != nil { 80 return nil, err 81 } 82 83 // IP address. 84 if net.ParseIP(host) != nil { 85 host, _ = formatIP(host) 86 addr := []resolver.Address{{Addr: host + ":" + port}} 87 i := &ipResolver{ 88 cc: cc, 89 ip: addr, 90 rn: make(chan struct{}, 1), 91 q: make(chan struct{}), 92 } 93 cc.NewAddress(addr) 94 go i.watcher() 95 return i, nil 96 } 97 98 // DNS address (non-IP). 99 ctx, cancel := context.WithCancel(context.Background()) 100 d := &dnsResolver{ 101 freq: b.freq, 102 host: host, 103 port: port, 104 ctx: ctx, 105 cancel: cancel, 106 cc: cc, 107 t: time.NewTimer(0), 108 rn: make(chan struct{}, 1), 109 disableServiceConfig: opts.DisableServiceConfig, 110 } 111 112 d.wg.Add(1) 113 go d.watcher() 114 return d, nil 115} 116 117// Scheme returns the naming scheme of this resolver builder, which is "dns". 118func (b *dnsBuilder) Scheme() string { 119 return "dns" 120} 121 122// ipResolver watches for the name resolution update for an IP address. 123type ipResolver struct { 124 cc resolver.ClientConn 125 ip []resolver.Address 126 // rn channel is used by ResolveNow() to force an immediate resolution of the target. 127 rn chan struct{} 128 q chan struct{} 129} 130 131// ResolveNow resend the address it stores, no resolution is needed. 132func (i *ipResolver) ResolveNow(opt resolver.ResolveNowOption) { 133 select { 134 case i.rn <- struct{}{}: 135 default: 136 } 137} 138 139// Close closes the ipResolver. 140func (i *ipResolver) Close() { 141 close(i.q) 142} 143 144func (i *ipResolver) watcher() { 145 for { 146 select { 147 case <-i.rn: 148 i.cc.NewAddress(i.ip) 149 case <-i.q: 150 return 151 } 152 } 153} 154 155// dnsResolver watches for the name resolution update for a non-IP target. 156type dnsResolver struct { 157 freq time.Duration 158 host string 159 port string 160 ctx context.Context 161 cancel context.CancelFunc 162 cc resolver.ClientConn 163 // rn channel is used by ResolveNow() to force an immediate resolution of the target. 164 rn chan struct{} 165 t *time.Timer 166 // wg is used to enforce Close() to return after the watcher() goroutine has finished. 167 // Otherwise, data race will be possible. [Race Example] in dns_resolver_test we 168 // replace the real lookup functions with mocked ones to facilitate testing. 169 // If Close() doesn't wait for watcher() goroutine finishes, race detector sometimes 170 // will warns lookup (READ the lookup function pointers) inside watcher() goroutine 171 // has data race with replaceNetFunc (WRITE the lookup function pointers). 172 wg sync.WaitGroup 173 disableServiceConfig bool 174} 175 176// ResolveNow invoke an immediate resolution of the target that this dnsResolver watches. 177func (d *dnsResolver) ResolveNow(opt resolver.ResolveNowOption) { 178 select { 179 case d.rn <- struct{}{}: 180 default: 181 } 182} 183 184// Close closes the dnsResolver. 185func (d *dnsResolver) Close() { 186 d.cancel() 187 d.wg.Wait() 188 d.t.Stop() 189} 190 191func (d *dnsResolver) watcher() { 192 defer d.wg.Done() 193 for { 194 select { 195 case <-d.ctx.Done(): 196 return 197 case <-d.t.C: 198 case <-d.rn: 199 } 200 result, sc := d.lookup() 201 // Next lookup should happen after an interval defined by d.freq. 202 d.t.Reset(d.freq) 203 d.cc.NewServiceConfig(sc) 204 d.cc.NewAddress(result) 205 } 206} 207 208func (d *dnsResolver) lookupSRV() []resolver.Address { 209 var newAddrs []resolver.Address 210 _, srvs, err := lookupSRV(d.ctx, "grpclb", "tcp", d.host) 211 if err != nil { 212 grpclog.Infof("grpc: failed dns SRV record lookup due to %v.\n", err) 213 return nil 214 } 215 for _, s := range srvs { 216 lbAddrs, err := lookupHost(d.ctx, s.Target) 217 if err != nil { 218 grpclog.Infof("grpc: failed load balancer address dns lookup due to %v.\n", err) 219 continue 220 } 221 for _, a := range lbAddrs { 222 a, ok := formatIP(a) 223 if !ok { 224 grpclog.Errorf("grpc: failed IP parsing due to %v.\n", err) 225 continue 226 } 227 addr := a + ":" + strconv.Itoa(int(s.Port)) 228 newAddrs = append(newAddrs, resolver.Address{Addr: addr, Type: resolver.GRPCLB, ServerName: s.Target}) 229 } 230 } 231 return newAddrs 232} 233 234func (d *dnsResolver) lookupTXT() string { 235 ss, err := lookupTXT(d.ctx, d.host) 236 if err != nil { 237 grpclog.Infof("grpc: failed dns TXT record lookup due to %v.\n", err) 238 return "" 239 } 240 var res string 241 for _, s := range ss { 242 res += s 243 } 244 245 // TXT record must have "grpc_config=" attribute in order to be used as service config. 246 if !strings.HasPrefix(res, txtAttribute) { 247 grpclog.Warningf("grpc: TXT record %v missing %v attribute", res, txtAttribute) 248 return "" 249 } 250 return strings.TrimPrefix(res, txtAttribute) 251} 252 253func (d *dnsResolver) lookupHost() []resolver.Address { 254 var newAddrs []resolver.Address 255 addrs, err := lookupHost(d.ctx, d.host) 256 if err != nil { 257 grpclog.Warningf("grpc: failed dns A record lookup due to %v.\n", err) 258 return nil 259 } 260 for _, a := range addrs { 261 a, ok := formatIP(a) 262 if !ok { 263 grpclog.Errorf("grpc: failed IP parsing due to %v.\n", err) 264 continue 265 } 266 addr := a + ":" + d.port 267 newAddrs = append(newAddrs, resolver.Address{Addr: addr}) 268 } 269 return newAddrs 270} 271 272func (d *dnsResolver) lookup() ([]resolver.Address, string) { 273 newAddrs := d.lookupSRV() 274 // Support fallback to non-balancer address. 275 newAddrs = append(newAddrs, d.lookupHost()...) 276 if d.disableServiceConfig { 277 return newAddrs, "" 278 } 279 sc := d.lookupTXT() 280 return newAddrs, canaryingSC(sc) 281} 282 283// formatIP returns ok = false if addr is not a valid textual representation of an IP address. 284// If addr is an IPv4 address, return the addr and ok = true. 285// If addr is an IPv6 address, return the addr enclosed in square brackets and ok = true. 286func formatIP(addr string) (addrIP string, ok bool) { 287 ip := net.ParseIP(addr) 288 if ip == nil { 289 return "", false 290 } 291 if ip.To4() != nil { 292 return addr, true 293 } 294 return "[" + addr + "]", true 295} 296 297// parseTarget takes the user input target string, returns formatted host and port info. 298// If target doesn't specify a port, set the port to be the defaultPort. 299// If target is in IPv6 format and host-name is enclosed in sqarue brackets, brackets 300// are strippd when setting the host. 301// examples: 302// target: "www.google.com" returns host: "www.google.com", port: "443" 303// target: "ipv4-host:80" returns host: "ipv4-host", port: "80" 304// target: "[ipv6-host]" returns host: "ipv6-host", port: "443" 305// target: ":80" returns host: "localhost", port: "80" 306func parseTarget(target string) (host, port string, err error) { 307 if target == "" { 308 return "", "", errMissingAddr 309 } 310 if ip := net.ParseIP(target); ip != nil { 311 // target is an IPv4 or IPv6(without brackets) address 312 return target, defaultPort, nil 313 } 314 if host, port, err = net.SplitHostPort(target); err == nil { 315 if port == "" { 316 // If the port field is empty (target ends with colon), e.g. "[::1]:", this is an error. 317 return "", "", errEndsWithColon 318 } 319 // target has port, i.e ipv4-host:port, [ipv6-host]:port, host-name:port 320 if host == "" { 321 // Keep consistent with net.Dial(): If the host is empty, as in ":80", the local system is assumed. 322 host = "localhost" 323 } 324 return host, port, nil 325 } 326 if host, port, err = net.SplitHostPort(target + ":" + defaultPort); err == nil { 327 // target doesn't have port 328 return host, port, nil 329 } 330 return "", "", fmt.Errorf("invalid target address %v, error info: %v", target, err) 331} 332 333type rawChoice struct { 334 ClientLanguage *[]string `json:"clientLanguage,omitempty"` 335 Percentage *int `json:"percentage,omitempty"` 336 ClientHostName *[]string `json:"clientHostName,omitempty"` 337 ServiceConfig *json.RawMessage `json:"serviceConfig,omitempty"` 338} 339 340func containsString(a *[]string, b string) bool { 341 if a == nil { 342 return true 343 } 344 for _, c := range *a { 345 if c == b { 346 return true 347 } 348 } 349 return false 350} 351 352func chosenByPercentage(a *int) bool { 353 if a == nil { 354 return true 355 } 356 return grpcrand.Intn(100)+1 <= *a 357} 358 359func canaryingSC(js string) string { 360 if js == "" { 361 return "" 362 } 363 var rcs []rawChoice 364 err := json.Unmarshal([]byte(js), &rcs) 365 if err != nil { 366 grpclog.Warningf("grpc: failed to parse service config json string due to %v.\n", err) 367 return "" 368 } 369 cliHostname, err := os.Hostname() 370 if err != nil { 371 grpclog.Warningf("grpc: failed to get client hostname due to %v.\n", err) 372 return "" 373 } 374 var sc string 375 for _, c := range rcs { 376 if !containsString(c.ClientLanguage, golang) || 377 !chosenByPercentage(c.Percentage) || 378 !containsString(c.ClientHostName, cliHostname) || 379 c.ServiceConfig == nil { 380 continue 381 } 382 sc = string(*c.ServiceConfig) 383 break 384 } 385 return sc 386} 387