Home | History | Annotate | Download | only in dns
      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.
     21 package dns
     22 
     23 import (
     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 
     40 func init() {
     41 	resolver.Register(NewBuilder())
     42 }
     43 
     44 const (
     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 
     53 var (
     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.
     64 func NewBuilder() resolver.Builder {
     65 	return &dnsBuilder{freq: defaultFreq}
     66 }
     67 
     68 type 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.
     74 func (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".
    118 func (b *dnsBuilder) Scheme() string {
    119 	return "dns"
    120 }
    121 
    122 // ipResolver watches for the name resolution update for an IP address.
    123 type 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.
    132 func (i *ipResolver) ResolveNow(opt resolver.ResolveNowOption) {
    133 	select {
    134 	case i.rn <- struct{}{}:
    135 	default:
    136 	}
    137 }
    138 
    139 // Close closes the ipResolver.
    140 func (i *ipResolver) Close() {
    141 	close(i.q)
    142 }
    143 
    144 func (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.
    156 type 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.
    177 func (d *dnsResolver) ResolveNow(opt resolver.ResolveNowOption) {
    178 	select {
    179 	case d.rn <- struct{}{}:
    180 	default:
    181 	}
    182 }
    183 
    184 // Close closes the dnsResolver.
    185 func (d *dnsResolver) Close() {
    186 	d.cancel()
    187 	d.wg.Wait()
    188 	d.t.Stop()
    189 }
    190 
    191 func (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 
    208 func (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 
    234 func (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 
    253 func (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 
    272 func (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.
    286 func 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"
    306 func 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 
    333 type 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 
    340 func 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 
    352 func chosenByPercentage(a *int) bool {
    353 	if a == nil {
    354 		return true
    355 	}
    356 	return grpcrand.Intn(100)+1 <= *a
    357 }
    358 
    359 func 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