/*
 *
 * Copyright 2018 gRPC authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

// Package dns implements a dns resolver to be installed as the default resolver // in grpc.
package dns import ( grpclbstate ) // EnableSRVLookups controls whether the DNS resolver attempts to fetch gRPCLB // addresses from SRV records. Must not be changed after init time. var EnableSRVLookups = false var logger = grpclog.Component("dns") // Globals to stub out in tests. TODO: Perhaps these two can be combined into a // single variable for testing the resolver? var ( newTimer = time.NewTimer newTimerDNSResRate = time.NewTimer ) func () { resolver.Register(NewBuilder()) } const ( defaultPort = "443" defaultDNSSvrPort = "53" golang = "GO" // txtPrefix is the prefix string to be prepended to the host name for txt record lookup. txtPrefix = "_grpc_config." // In DNS, service config is encoded in a TXT record via the mechanism // described in RFC-1464 using the attribute name grpc_config. txtAttribute = "grpc_config=" ) var ( errMissingAddr = errors.New("dns resolver: missing address") // Addresses ending with a colon that is supposed to be the separator // between host and port is not allowed. E.g. "::" is a valid address as // it is an IPv6 address (host only) and "[::]:" is invalid as it ends with // a colon as the host and port separator errEndsWithColon = errors.New("dns resolver: missing port after port-separator colon") ) var ( defaultResolver netResolver = net.DefaultResolver // To prevent excessive re-resolution, we enforce a rate limit on DNS // resolution requests. minDNSResRate = 30 * time.Second ) var customAuthorityDialler = func( string) func( context.Context, , string) (net.Conn, error) { return func( context.Context, , string) (net.Conn, error) { var net.Dialer return .DialContext(, , ) } } var customAuthorityResolver = func( string) (netResolver, error) { , , := parseTarget(, defaultDNSSvrPort) if != nil { return nil, } := net.JoinHostPort(, ) return &net.Resolver{ PreferGo: true, Dial: customAuthorityDialler(), }, nil } // NewBuilder creates a dnsBuilder which is used to factory DNS resolvers. func () resolver.Builder { return &dnsBuilder{} } type dnsBuilder struct{} // Build creates and starts a DNS resolver that watches the name resolution of the target. func ( *dnsBuilder) ( resolver.Target, resolver.ClientConn, resolver.BuildOptions) (resolver.Resolver, error) { , , := parseTarget(.Endpoint(), defaultPort) if != nil { return nil, } // IP address. if , := formatIP(); { := []resolver.Address{{Addr: + ":" + }} .UpdateState(resolver.State{Addresses: }) return deadResolver{}, nil } // DNS address (non-IP). , := context.WithCancel(context.Background()) := &dnsResolver{ host: , port: , ctx: , cancel: , cc: , rn: make(chan struct{}, 1), disableServiceConfig: .DisableServiceConfig, } if .URL.Host == "" { .resolver = defaultResolver } else { .resolver, = customAuthorityResolver(.URL.Host) if != nil { return nil, } } .wg.Add(1) go .watcher() return , nil } // Scheme returns the naming scheme of this resolver builder, which is "dns". func ( *dnsBuilder) () string { return "dns" } type netResolver interface { LookupHost(ctx context.Context, host string) (addrs []string, err error) LookupSRV(ctx context.Context, service, proto, name string) (cname string, addrs []*net.SRV, err error) LookupTXT(ctx context.Context, name string) (txts []string, err error) } // deadResolver is a resolver that does nothing. type deadResolver struct{} func (deadResolver) (resolver.ResolveNowOptions) {} func (deadResolver) () {} // dnsResolver watches for the name resolution update for a non-IP target. type dnsResolver struct { host string port string resolver netResolver ctx context.Context cancel context.CancelFunc cc resolver.ClientConn // rn channel is used by ResolveNow() to force an immediate resolution of the target. rn chan struct{} // wg is used to enforce Close() to return after the watcher() goroutine has finished. // Otherwise, data race will be possible. [Race Example] in dns_resolver_test we // replace the real lookup functions with mocked ones to facilitate testing. // If Close() doesn't wait for watcher() goroutine finishes, race detector sometimes // will warns lookup (READ the lookup function pointers) inside watcher() goroutine // has data race with replaceNetFunc (WRITE the lookup function pointers). wg sync.WaitGroup disableServiceConfig bool } // ResolveNow invoke an immediate resolution of the target that this dnsResolver watches. func ( *dnsResolver) (resolver.ResolveNowOptions) { select { case .rn <- struct{}{}: default: } } // Close closes the dnsResolver. func ( *dnsResolver) () { .cancel() .wg.Wait() } func ( *dnsResolver) () { defer .wg.Done() := 1 for { , := .lookup() if != nil { // Report error to the underlying grpc.ClientConn. .cc.ReportError() } else { = .cc.UpdateState(*) } var *time.Timer if == nil { // Success resolving, wait for the next ResolveNow. However, also wait 30 seconds at the very least // to prevent constantly re-resolving. = 1 = newTimerDNSResRate(minDNSResRate) select { case <-.ctx.Done(): .Stop() return case <-.rn: } } else { // Poll on an error found in DNS Resolver or an error received from ClientConn. = newTimer(backoff.DefaultExponential.Backoff()) ++ } select { case <-.ctx.Done(): .Stop() return case <-.C: } } } func ( *dnsResolver) () ([]resolver.Address, error) { if !EnableSRVLookups { return nil, nil } var []resolver.Address , , := .resolver.LookupSRV(.ctx, "grpclb", "tcp", .host) if != nil { = handleDNSError(, "SRV") // may become nil return nil, } for , := range { , := .resolver.LookupHost(.ctx, .Target) if != nil { = handleDNSError(, "A") // may become nil if == nil { // If there are other SRV records, look them up and ignore this // one that does not exist. continue } return nil, } for , := range { , := formatIP() if ! { return nil, fmt.Errorf("dns: error parsing A record IP address %v", ) } := + ":" + strconv.Itoa(int(.Port)) = append(, resolver.Address{Addr: , ServerName: .Target}) } } return , nil } func ( error, string) error { if , := .(*net.DNSError); && !.IsTimeout && !.IsTemporary { // Timeouts and temporary errors should be communicated to gRPC to // attempt another DNS query (with backoff). Other errors should be // suppressed (they may represent the absence of a TXT record). return nil } if != nil { = fmt.Errorf("dns: %v record lookup error: %v", , ) logger.Info() } return } func ( *dnsResolver) () *serviceconfig.ParseResult { , := .resolver.LookupTXT(.ctx, txtPrefix+.host) if != nil { if envconfig.TXTErrIgnore { return nil } if = handleDNSError(, "TXT"); != nil { return &serviceconfig.ParseResult{Err: } } return nil } var string for , := range { += } // TXT record must have "grpc_config=" attribute in order to be used as service config. if !strings.HasPrefix(, txtAttribute) { logger.Warningf("dns: TXT record %v missing %v attribute", , txtAttribute) // This is not an error; it is the equivalent of not having a service config. return nil } := canaryingSC(strings.TrimPrefix(, txtAttribute)) return .cc.ParseServiceConfig() } func ( *dnsResolver) () ([]resolver.Address, error) { , := .resolver.LookupHost(.ctx, .host) if != nil { = handleDNSError(, "A") return nil, } := make([]resolver.Address, 0, len()) for , := range { , := formatIP() if ! { return nil, fmt.Errorf("dns: error parsing A record IP address %v", ) } := + ":" + .port = append(, resolver.Address{Addr: }) } return , nil } func ( *dnsResolver) () (*resolver.State, error) { , := .lookupSRV() , := .lookupHost() if != nil && ( != nil || len() == 0) { return nil, } := resolver.State{Addresses: } if len() > 0 { = grpclbstate.Set(, &grpclbstate.State{BalancerAddresses: }) } if !.disableServiceConfig { .ServiceConfig = .lookupTXT() } return &, nil } // formatIP returns ok = false if addr is not a valid textual representation of an IP address. // If addr is an IPv4 address, return the addr and ok = true. // If addr is an IPv6 address, return the addr enclosed in square brackets and ok = true. func ( string) ( string, bool) { := net.ParseIP() if == nil { return "", false } if .To4() != nil { return , true } return "[" + + "]", true } // parseTarget takes the user input target string and default port, returns formatted host and port info. // If target doesn't specify a port, set the port to be the defaultPort. // If target is in IPv6 format and host-name is enclosed in square brackets, brackets // are stripped when setting the host. // examples: // target: "www.google.com" defaultPort: "443" returns host: "www.google.com", port: "443" // target: "ipv4-host:80" defaultPort: "443" returns host: "ipv4-host", port: "80" // target: "[ipv6-host]" defaultPort: "443" returns host: "ipv6-host", port: "443" // target: ":80" defaultPort: "443" returns host: "localhost", port: "80" func (, string) (, string, error) { if == "" { return "", "", errMissingAddr } if := net.ParseIP(); != nil { // target is an IPv4 or IPv6(without brackets) address return , , nil } if , , = net.SplitHostPort(); == nil { if == "" { // If the port field is empty (target ends with colon), e.g. "[::1]:", this is an error. return "", "", errEndsWithColon } // target has port, i.e ipv4-host:port, [ipv6-host]:port, host-name:port if == "" { // Keep consistent with net.Dial(): If the host is empty, as in ":80", the local system is assumed. = "localhost" } return , , nil } if , , = net.SplitHostPort( + ":" + ); == nil { // target doesn't have port return , , nil } return "", "", fmt.Errorf("invalid target address %v, error info: %v", , ) } type rawChoice struct { ClientLanguage *[]string `json:"clientLanguage,omitempty"` Percentage *int `json:"percentage,omitempty"` ClientHostName *[]string `json:"clientHostName,omitempty"` ServiceConfig *json.RawMessage `json:"serviceConfig,omitempty"` } func ( *[]string, string) bool { if == nil { return true } for , := range * { if == { return true } } return false } func ( *int) bool { if == nil { return true } return grpcrand.Intn(100)+1 <= * } func ( string) string { if == "" { return "" } var []rawChoice := json.Unmarshal([]byte(), &) if != nil { logger.Warningf("dns: error parsing service config json: %v", ) return "" } , := os.Hostname() if != nil { logger.Warningf("dns: error getting client hostname: %v", ) return "" } var string for , := range { if !containsString(.ClientLanguage, golang) || !chosenByPercentage(.Percentage) || !containsString(.ClientHostName, ) || .ServiceConfig == nil { continue } = string(*.ServiceConfig) break } return }