/*
 *
 * 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 ( rand grpclbstate ) var ( // EnableSRVLookups controls whether the DNS resolver attempts to fetch gRPCLB // addresses from SRV records. Must not be changed after init time. EnableSRVLookups = false // MinResolutionInterval is the minimum interval at which re-resolutions are // allowed. This helps to prevent excessive re-resolution. MinResolutionInterval = 30 * time.Second // ResolvingTimeout specifies the maximum duration for a DNS resolution request. // If the timeout expires before a response is received, the request will be canceled. // // It is recommended to set this value at application startup. Avoid modifying this variable // after initialization as it's not thread-safe for concurrent modification. ResolvingTimeout = 30 * time.Second logger = grpclog.Component("dns") ) func () { resolver.Register(NewBuilder()) internal.TimeAfterFunc = time.After internal.TimeNowFunc = time.Now internal.TimeUntilFunc = time.Until internal.NewNetResolver = newNetResolver internal.AddressDialer = addressDialer } 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 addressDialer = func( string) func(context.Context, string, string) (net.Conn, error) { return func( context.Context, , string) (net.Conn, error) { var net.Dialer return .DialContext(, , ) } } var newNetResolver = func( string) (internal.NetResolver, error) { if == "" { return net.DefaultResolver, nil } , , := parseTarget(, defaultDNSSvrPort) if != nil { return nil, } := net.JoinHostPort(, ) return &net.Resolver{ PreferGo: true, Dial: internal.AddressDialer(), }, 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(); == nil { := []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), enableServiceConfig: envconfig.EnableTXTServiceConfig && !.DisableServiceConfig, } .resolver, = internal.NewNetResolver(.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" } // 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 internal.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 warn lookup (READ the lookup // function pointers) inside watcher() goroutine has data race with // replaceNetFunc (WRITE the lookup function pointers). wg sync.WaitGroup enableServiceConfig 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.Time if == nil { // Success resolving, wait for the next ResolveNow. However, also wait 30 // seconds at the very least to prevent constantly re-resolving. = 1 = internal.TimeNowFunc().Add(MinResolutionInterval) select { case <-.ctx.Done(): return case <-.rn: } } else { // Poll on an error found in DNS Resolver or an error received from // ClientConn. = internal.TimeNowFunc().Add(backoff.DefaultExponential.Backoff()) ++ } select { case <-.ctx.Done(): return case <-internal.TimeAfterFunc(internal.TimeUntilFunc()): } } } func ( *dnsResolver) ( context.Context) ([]resolver.Address, error) { // Skip this particular host to avoid timeouts with some versions of // systemd-resolved. if !EnableSRVLookups || .host == "metadata.google.internal." { return nil, nil } var []resolver.Address , , := .resolver.LookupSRV(, "grpclb", "tcp", .host) if != nil { = handleDNSError(, "SRV") // may become nil return nil, } for , := range { , := .resolver.LookupHost(, .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 != nil { return nil, fmt.Errorf("dns: error parsing A record IP address %v: %v", , ) } := + ":" + strconv.Itoa(int(.Port)) = append(, resolver.Address{Addr: , ServerName: .Target}) } } return , nil } func ( error, string) error { , := .(*net.DNSError) if && !.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) ( context.Context) *serviceconfig.ParseResult { , := .resolver.LookupTXT(, 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) ( context.Context) ([]resolver.Address, error) { , := .resolver.LookupHost(, .host) if != nil { = handleDNSError(, "A") return nil, } := make([]resolver.Address, 0, len()) for , := range { , := formatIP() if != nil { return nil, fmt.Errorf("dns: error parsing A record IP address %v: %v", , ) } := + ":" + .port = append(, resolver.Address{Addr: }) } return , nil } func ( *dnsResolver) () (*resolver.State, error) { , := context.WithTimeout(.ctx, ResolvingTimeout) defer () , := .lookupSRV() , := .lookupHost() if != nil && ( != nil || len() == 0) { return nil, } := resolver.State{Addresses: } if len() > 0 { = grpclbstate.Set(, &grpclbstate.State{BalancerAddresses: }) } if .enableServiceConfig { .ServiceConfig = .lookupTXT() } return &, nil } // formatIP returns an error if addr is not a valid textual representation of // an IP address. If addr is an IPv4 address, return the addr and error = nil. // If addr is an IPv6 address, return the addr enclosed in square brackets and // error = nil. func ( string) (string, error) { , := netip.ParseAddr() if != nil { return "", } if .Is4() { return , nil } return "[" + + "]", nil } // 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 "", "", internal.ErrMissingAddr } if , := netip.ParseAddr(); == 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 "", "", internal.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 rand.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 }