DNS with new API (#1513)

dns resolver conforms to new resolver API
diff --git a/resolver/dns/dns_resolver.go b/resolver/dns/dns_resolver.go
new file mode 100644
index 0000000..0267897
--- /dev/null
+++ b/resolver/dns/dns_resolver.go
@@ -0,0 +1,377 @@
+/*
+ *
+ * Copyright 2017 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
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"math/rand"
+	"net"
+	"os"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"golang.org/x/net/context"
+	"google.golang.org/grpc/grpclog"
+	"google.golang.org/grpc/resolver"
+)
+
+func init() {
+	resolver.Register(NewBuilder())
+}
+
+const (
+	defaultPort = "443"
+	defaultFreq = time.Minute * 30
+	golang      = "GO"
+	// 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("missing address")
+)
+
+// NewBuilder creates a dnsBuilder which is used to factory DNS resolvers.
+func NewBuilder() resolver.Builder {
+	return &dnsBuilder{freq: defaultFreq}
+}
+
+type dnsBuilder struct {
+	// frequency of polling the DNS server.
+	freq time.Duration
+}
+
+// Build creates and starts a DNS resolver that watches the name resolution of the target.
+func (b *dnsBuilder) Build(target string, cc resolver.ClientConn, opts resolver.BuildOption) (resolver.Resolver, error) {
+	host, port, err := parseTarget(target)
+	if err != nil {
+		return nil, err
+	}
+
+	// IP address.
+	if net.ParseIP(host) != nil {
+		host, _ = formatIP(host)
+		addr := []resolver.Address{{Addr: host + ":" + port}}
+		i := &ipResolver{
+			cc: cc,
+			ip: addr,
+			rn: make(chan struct{}, 1),
+			q:  make(chan struct{}),
+		}
+		cc.NewAddress(addr)
+		go i.watcher()
+		return i, nil
+	}
+
+	// DNS address (non-IP).
+	ctx, cancel := context.WithCancel(context.Background())
+	d := &dnsResolver{
+		freq:   b.freq,
+		host:   host,
+		port:   port,
+		ctx:    ctx,
+		cancel: cancel,
+		cc:     cc,
+		t:      time.NewTimer(0),
+		rn:     make(chan struct{}, 1),
+	}
+
+	d.wg.Add(1)
+	go d.watcher()
+	return d, nil
+}
+
+// Scheme returns the naming scheme of this resolver builder, which is "dns".
+func (b *dnsBuilder) Scheme() string {
+	return "dns"
+}
+
+// ipResolver watches for the name resolution update for an IP address.
+type ipResolver struct {
+	cc resolver.ClientConn
+	ip []resolver.Address
+	// rn channel is used by ResolveNow() to force an immediate resolution of the target.
+	rn chan struct{}
+	q  chan struct{}
+}
+
+// ResolveNow resend the address it stores, no resolution is needed.
+func (i *ipResolver) ResolveNow(opt resolver.ResolveNowOption) {
+	select {
+	case i.rn <- struct{}{}:
+	default:
+	}
+}
+
+// Close closes the ipResolver.
+func (i *ipResolver) Close() {
+	close(i.q)
+}
+
+func (i *ipResolver) watcher() {
+	for {
+		select {
+		case <-i.rn:
+			i.cc.NewAddress(i.ip)
+		case <-i.q:
+			return
+		}
+	}
+}
+
+// dnsResolver watches for the name resolution update for a non-IP target.
+type dnsResolver struct {
+	freq   time.Duration
+	host   string
+	port   string
+	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{}
+	t  *time.Timer
+	// 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
+}
+
+// ResolveNow invoke an immediate resolution of the target that this dnsResolver watches.
+func (d *dnsResolver) ResolveNow(opt resolver.ResolveNowOption) {
+	select {
+	case d.rn <- struct{}{}:
+	default:
+	}
+}
+
+// Close closes the dnsResolver.
+func (d *dnsResolver) Close() {
+	d.cancel()
+	d.wg.Wait()
+	d.t.Stop()
+}
+
+func (d *dnsResolver) watcher() {
+	defer d.wg.Done()
+	for {
+		select {
+		case <-d.ctx.Done():
+			return
+		case <-d.t.C:
+		case <-d.rn:
+		}
+		result, sc := d.lookup()
+		// Next lookup should happen after an interval defined by d.freq.
+		d.t.Reset(d.freq)
+		d.cc.NewServiceConfig(string(sc))
+		d.cc.NewAddress(result)
+	}
+}
+
+func (d *dnsResolver) lookupSRV() []resolver.Address {
+	var newAddrs []resolver.Address
+	_, srvs, err := lookupSRV(d.ctx, "grpclb", "tcp", d.host)
+	if err != nil {
+		grpclog.Infof("grpc: failed dns SRV record lookup due to %v.\n", err)
+		return nil
+	}
+	for _, s := range srvs {
+		lbAddrs, err := lookupHost(d.ctx, s.Target)
+		if err != nil {
+			grpclog.Warningf("grpc: failed load banlacer address dns lookup due to %v.\n", err)
+			continue
+		}
+		for _, a := range lbAddrs {
+			a, ok := formatIP(a)
+			if !ok {
+				grpclog.Errorf("grpc: failed IP parsing due to %v.\n", err)
+				continue
+			}
+			addr := a + ":" + strconv.Itoa(int(s.Port))
+			newAddrs = append(newAddrs, resolver.Address{Addr: addr, Type: resolver.GRPCLB, ServerName: s.Target})
+		}
+	}
+	return newAddrs
+}
+
+func (d *dnsResolver) lookupTXT() string {
+	ss, err := lookupTXT(d.ctx, d.host)
+	if err != nil {
+		grpclog.Warningf("grpc: failed dns TXT record lookup due to %v.\n", err)
+		return ""
+	}
+	var res string
+	for _, s := range ss {
+		res += s
+	}
+
+	// TXT record must have "grpc_config=" attribute in order to be used as service config.
+	if !strings.HasPrefix(res, txtAttribute) {
+		grpclog.Warningf("grpc: TXT record %p missing %p attribute", res, txtAttribute)
+		return ""
+	}
+	return strings.TrimPrefix(res, txtAttribute)
+}
+
+func (d *dnsResolver) lookupHost() []resolver.Address {
+	var newAddrs []resolver.Address
+	addrs, err := lookupHost(d.ctx, d.host)
+	if err != nil {
+		grpclog.Warningf("grpc: failed dns A record lookup due to %v.\n", err)
+		return nil
+	}
+	for _, a := range addrs {
+		a, ok := formatIP(a)
+		if !ok {
+			grpclog.Errorf("grpc: failed IP parsing due to %v.\n", err)
+			continue
+		}
+		addr := a + ":" + d.port
+		newAddrs = append(newAddrs, resolver.Address{Addr: addr})
+	}
+	return newAddrs
+}
+
+func (d *dnsResolver) lookup() ([]resolver.Address, string) {
+	var newAddrs []resolver.Address
+	newAddrs = d.lookupSRV()
+	// Support fallback to non-balancer address.
+	newAddrs = append(newAddrs, d.lookupHost()...)
+	sc := d.lookupTXT()
+	return newAddrs, canaryingSC(sc)
+}
+
+// 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 formatIP(addr string) (addrIP string, ok bool) {
+	ip := net.ParseIP(addr)
+	if ip == nil {
+		return "", false
+	}
+	if ip.To4() != nil {
+		return addr, true
+	}
+	return "[" + addr + "]", true
+}
+
+// parseTarget takes the user input target string, 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 sqarue brackets, brackets
+// are strippd when setting the host.
+// examples:
+// target: "www.google.com" returns host: "www.google.com", port: "443"
+// target: "ipv4-host:80" returns host: "ipv4-host", port: "80"
+// target: "[ipv6-host]" returns host: "ipv6-host", port: "443"
+// target: ":80" returns host: "localhost", port: "80"
+// target: ":" returns host: "localhost", port: "443"
+func parseTarget(target string) (host, port string, err error) {
+	if target == "" {
+		return "", "", errMissingAddr
+	}
+	if ip := net.ParseIP(target); ip != nil {
+		// target is an IPv4 or IPv6(without brackets) address
+		return target, defaultPort, nil
+	}
+	if host, port, err = net.SplitHostPort(target); err == nil {
+		// target has port, i.e ipv4-host:port, [ipv6-host]:port, host-name:port
+		if host == "" {
+			// Keep consistent with net.Dial(): If the host is empty, as in ":80", the local system is assumed.
+			host = "localhost"
+		}
+		if port == "" {
+			// If the port field is empty(target ends with colon), e.g. "[::1]:", defaultPort is used.
+			port = defaultPort
+		}
+		return host, port, nil
+	}
+	if host, port, err = net.SplitHostPort(target + ":" + defaultPort); err == nil {
+		// target doesn't have port
+		return host, port, nil
+	}
+	return "", "", fmt.Errorf("invalid target address %v, error info: %v", target, err)
+}
+
+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 containsString(a *[]string, b string) bool {
+	if a == nil {
+		return true
+	}
+	for _, c := range *a {
+		if c == b {
+			return true
+		}
+	}
+	return false
+}
+
+func chosenByPercentage(a *int) bool {
+	if a == nil {
+		return true
+	}
+	s := rand.NewSource(time.Now().UnixNano())
+	r := rand.New(s)
+	if r.Intn(100)+1 > *a {
+		return false
+	}
+	return true
+}
+
+func canaryingSC(js string) string {
+	if js == "" {
+		return ""
+	}
+	var rcs []rawChoice
+	err := json.Unmarshal([]byte(js), &rcs)
+	if err != nil {
+		grpclog.Warningf("grpc: failed to parse service config json string due to %v.\n", err)
+		return ""
+	}
+	cliHostname, err := os.Hostname()
+	if err != nil {
+		grpclog.Warningf("grpc: failed to get client hostname due to %v.\n", err)
+		return ""
+	}
+	var sc string
+	for _, c := range rcs {
+		if !containsString(c.ClientLanguage, golang) ||
+			!chosenByPercentage(c.Percentage) ||
+			!containsString(c.ClientHostName, cliHostname) ||
+			c.ServiceConfig == nil {
+			continue
+		}
+		sc = string(*c.ServiceConfig)
+		break
+	}
+	return sc
+}
diff --git a/resolver/dns/dns_resolver_test.go b/resolver/dns/dns_resolver_test.go
new file mode 100644
index 0000000..d25d840
--- /dev/null
+++ b/resolver/dns/dns_resolver_test.go
@@ -0,0 +1,898 @@
+/*
+ *
+ * Copyright 2017 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
+
+import (
+	"fmt"
+	"net"
+	"os"
+	"reflect"
+	"sync"
+	"testing"
+	"time"
+
+	"google.golang.org/grpc/resolver"
+	"google.golang.org/grpc/test/leakcheck"
+)
+
+func TestMain(m *testing.M) {
+	cleanup := replaceNetFunc()
+	code := m.Run()
+	cleanup()
+	os.Exit(code)
+}
+
+const (
+	txtBytesLimit = 255
+)
+
+type testClientConn struct {
+	target string
+	m1     sync.Mutex
+	addrs  []resolver.Address
+	a      int
+	m2     sync.Mutex
+	sc     string
+	s      int
+}
+
+func (t *testClientConn) NewAddress(addresses []resolver.Address) {
+	t.m1.Lock()
+	defer t.m1.Unlock()
+	t.addrs = addresses
+	t.a++
+}
+
+func (t *testClientConn) getAddress() ([]resolver.Address, int) {
+	t.m1.Lock()
+	defer t.m1.Unlock()
+	return t.addrs, t.a
+}
+
+func (t *testClientConn) NewServiceConfig(serviceConfig string) {
+	t.m2.Lock()
+	defer t.m2.Unlock()
+	t.sc = serviceConfig
+	t.s++
+}
+
+func (t *testClientConn) getSc() (string, int) {
+	t.m2.Lock()
+	defer t.m2.Unlock()
+	return t.sc, t.s
+}
+
+var hostLookupTbl = struct {
+	sync.Mutex
+	tbl map[string][]string
+}{
+	tbl: map[string][]string{
+		"foo.bar.com":          {"1.2.3.4", "5.6.7.8"},
+		"ipv4.single.fake":     {"1.2.3.4"},
+		"srv.ipv4.single.fake": {"2.4.6.8"},
+		"ipv4.multi.fake":      {"1.2.3.4", "5.6.7.8", "9.10.11.12"},
+		"ipv6.single.fake":     {"2607:f8b0:400a:801::1001"},
+		"ipv6.multi.fake":      {"2607:f8b0:400a:801::1001", "2607:f8b0:400a:801::1002", "2607:f8b0:400a:801::1003"},
+	},
+}
+
+func hostLookup(host string) ([]string, error) {
+	hostLookupTbl.Lock()
+	defer hostLookupTbl.Unlock()
+	if addrs, cnt := hostLookupTbl.tbl[host]; cnt {
+		return addrs, nil
+	}
+	return nil, fmt.Errorf("failed to lookup host:%s resolution in hostLookupTbl", host)
+}
+
+var srvLookupTbl = struct {
+	sync.Mutex
+	tbl map[string][]*net.SRV
+}{
+	tbl: map[string][]*net.SRV{
+		"_grpclb._tcp.srv.ipv4.single.fake": {&net.SRV{Target: "ipv4.single.fake", Port: 1234}},
+		"_grpclb._tcp.srv.ipv4.multi.fake":  {&net.SRV{Target: "ipv4.multi.fake", Port: 1234}},
+		"_grpclb._tcp.srv.ipv6.single.fake": {&net.SRV{Target: "ipv6.single.fake", Port: 1234}},
+		"_grpclb._tcp.srv.ipv6.multi.fake":  {&net.SRV{Target: "ipv6.multi.fake", Port: 1234}},
+	},
+}
+
+func srvLookup(service, proto, name string) (string, []*net.SRV, error) {
+	cname := "_" + service + "._" + proto + "." + name
+	srvLookupTbl.Lock()
+	defer srvLookupTbl.Unlock()
+	if srvs, cnt := srvLookupTbl.tbl[cname]; cnt {
+		return cname, srvs, nil
+	}
+	return "", nil, fmt.Errorf("failed to lookup srv record for %s in srvLookupTbl", cname)
+}
+
+// div divides a byte slice into a slice of strings, each of which is of maximum
+// 255 bytes length, which is the length limit per TXT record in DNS.
+func div(b []byte) []string {
+	var r []string
+	for i := 0; i < len(b); i += txtBytesLimit {
+		if i+txtBytesLimit > len(b) {
+			r = append(r, string(b[i:]))
+		} else {
+			r = append(r, string(b[i:i+txtBytesLimit]))
+		}
+	}
+	return r
+}
+
+// scfs contains an array of service config file string in JSON format.
+// Notes about the scfs contents and usage:
+// scfs contains 4 service config file JSON strings for testing. Inside each
+// service config file, there are multiple choices. scfs[0:3] each contains 5
+// choices, and first 3 choices are nonmatching choices based on canarying rule,
+// while the last two are matched choices. scfs[3] only contains 3 choices, and
+// all of them are nonmatching based on canarying rule. For each of scfs[0:3],
+// the eventually returned service config, which is from the first of the two
+// matched choices, is stored in the corresponding scs element (e.g.
+// scfs[0]->scs[0]). scfs and scs elements are used in pair to test the dns
+// resolver functionality, with scfs as the input and scs used for validation of
+// the output. For scfs[3], it corresponds to empty service config, since there
+// isn't a matched choice.
+var (
+	scfs = []string{
+		`[
+	{
+		"clientLanguage": [
+			"CPP",
+			"JAVA"
+		],
+		"serviceConfig": {
+			"loadBalancingPolicy": "grpclb",
+			"methodConfig": [
+				{
+					"name": [
+						{
+							"service": "all"
+						}
+					],
+					"timeout": "1s"
+				}
+			]
+		}
+	},
+	{
+		"percentage": 0,
+		"serviceConfig": {
+			"loadBalancingPolicy": "grpclb",
+			"methodConfig": [
+				{
+					"name": [
+						{
+							"service": "all"
+						}
+					],
+					"timeout": "1s"
+				}
+			]
+		}
+	},
+	{
+		"clientHostName": [
+			"localhost"
+		],
+		"serviceConfig": {
+			"loadBalancingPolicy": "grpclb",
+			"methodConfig": [
+				{
+					"name": [
+						{
+							"service": "all"
+						}
+					],
+					"timeout": "1s"
+				}
+			]
+		}
+	},
+	{
+		"clientLanguage": [
+			"GO"
+		],
+		"percentage": 100,
+		"serviceConfig": {
+			"methodConfig": [
+				{
+					"name": [
+						{
+							"method": "bar"
+						}
+					],
+					"maxRequestMessageBytes": 1024,
+					"maxResponseMessageBytes": 1024
+				}
+			]
+		}
+	},
+	{
+		"serviceConfig": {
+			"loadBalancingPolicy": "round_robin",
+			"methodConfig": [
+				{
+					"name": [
+						{
+							"service": "foo",
+							"method": "bar"
+						}
+					],
+					"waitForReady": true
+				}
+			]
+		}
+	}
+]`,
+		`[
+	{
+		"clientLanguage": [
+			"CPP",
+			"JAVA"
+		],
+		"serviceConfig": {
+			"loadBalancingPolicy": "grpclb",
+			"methodConfig": [
+				{
+					"name": [
+						{
+							"service": "all"
+						}
+					],
+					"timeout": "1s"
+				}
+			]
+		}
+	},
+	{
+		"percentage": 0,
+		"serviceConfig": {
+			"loadBalancingPolicy": "grpclb",
+			"methodConfig": [
+				{
+					"name": [
+						{
+							"service": "all"
+						}
+					],
+					"timeout": "1s"
+				}
+			]
+		}
+	},
+	{
+		"clientHostName": [
+			"localhost"
+		],
+		"serviceConfig": {
+			"loadBalancingPolicy": "grpclb",
+			"methodConfig": [
+				{
+					"name": [
+						{
+							"service": "all"
+						}
+					],
+					"timeout": "1s"
+				}
+			]
+		}
+	},
+	{
+		"clientLanguage": [
+			"GO"
+		],
+		"percentage": 100,
+		"serviceConfig": {
+			"methodConfig": [
+				{
+					"name": [
+						{
+							"service": "foo",
+							"method": "bar"
+						}
+					],
+					"waitForReady": true,
+					"timeout": "1s",
+					"maxRequestMessageBytes": 1024,
+					"maxResponseMessageBytes": 1024
+				}
+			]
+		}
+	},
+	{
+		"serviceConfig": {
+			"loadBalancingPolicy": "round_robin",
+			"methodConfig": [
+				{
+					"name": [
+						{
+							"service": "foo",
+							"method": "bar"
+						}
+					],
+					"waitForReady": true
+				}
+			]
+		}
+	}
+]`,
+		`[
+	{
+		"clientLanguage": [
+			"CPP",
+			"JAVA"
+		],
+		"serviceConfig": {
+			"loadBalancingPolicy": "grpclb",
+			"methodConfig": [
+				{
+					"name": [
+						{
+							"service": "all"
+						}
+					],
+					"timeout": "1s"
+				}
+			]
+		}
+	},
+	{
+		"percentage": 0,
+		"serviceConfig": {
+			"loadBalancingPolicy": "grpclb",
+			"methodConfig": [
+				{
+					"name": [
+						{
+							"service": "all"
+						}
+					],
+					"timeout": "1s"
+				}
+			]
+		}
+	},
+	{
+		"clientHostName": [
+			"localhost"
+		],
+		"serviceConfig": {
+			"loadBalancingPolicy": "grpclb",
+			"methodConfig": [
+				{
+					"name": [
+						{
+							"service": "all"
+						}
+					],
+					"timeout": "1s"
+				}
+			]
+		}
+	},
+	{
+		"clientLanguage": [
+			"GO"
+		],
+		"percentage": 100,
+		"serviceConfig": {
+			"loadBalancingPolicy": "round_robin",
+			"methodConfig": [
+				{
+					"name": [
+						{
+							"service": "foo"
+						}
+					],
+					"waitForReady": true,
+					"timeout": "1s"
+				},
+				{
+					"name": [
+						{
+							"service": "bar"
+						}
+					],
+					"waitForReady": false
+				}
+			]
+		}
+	},
+	{
+		"serviceConfig": {
+			"loadBalancingPolicy": "round_robin",
+			"methodConfig": [
+				{
+					"name": [
+						{
+							"service": "foo",
+							"method": "bar"
+						}
+					],
+					"waitForReady": true
+				}
+			]
+		}
+	}
+]`,
+		`[
+	{
+		"clientLanguage": [
+			"CPP",
+			"JAVA"
+		],
+		"serviceConfig": {
+			"loadBalancingPolicy": "grpclb",
+			"methodConfig": [
+				{
+					"name": [
+						{
+							"service": "all"
+						}
+					],
+					"timeout": "1s"
+				}
+			]
+		}
+	},
+	{
+		"percentage": 0,
+		"serviceConfig": {
+			"loadBalancingPolicy": "grpclb",
+			"methodConfig": [
+				{
+					"name": [
+						{
+							"service": "all"
+						}
+					],
+					"timeout": "1s"
+				}
+			]
+		}
+	},
+	{
+		"clientHostName": [
+			"localhost"
+		],
+		"serviceConfig": {
+			"loadBalancingPolicy": "grpclb",
+			"methodConfig": [
+				{
+					"name": [
+						{
+							"service": "all"
+						}
+					],
+					"timeout": "1s"
+				}
+			]
+		}
+	}
+]`,
+	}
+)
+
+// scs contains an array of service config string in JSON format.
+var (
+	scs = []string{
+		`{
+			"methodConfig": [
+				{
+					"name": [
+						{
+							"method": "bar"
+						}
+					],
+					"maxRequestMessageBytes": 1024,
+					"maxResponseMessageBytes": 1024
+				}
+			]
+		}`,
+		`{
+			"methodConfig": [
+				{
+					"name": [
+						{
+							"service": "foo",
+							"method": "bar"
+						}
+					],
+					"waitForReady": true,
+					"timeout": "1s",
+					"maxRequestMessageBytes": 1024,
+					"maxResponseMessageBytes": 1024
+				}
+			]
+		}`,
+		`{
+			"loadBalancingPolicy": "round_robin",
+			"methodConfig": [
+				{
+					"name": [
+						{
+							"service": "foo"
+						}
+					],
+					"waitForReady": true,
+					"timeout": "1s"
+				},
+				{
+					"name": [
+						{
+							"service": "bar"
+						}
+					],
+					"waitForReady": false
+				}
+			]
+		}`,
+	}
+)
+
+// scLookupTbl is a set, which contains targets that have service config. Target
+// not in this set should not have service config.
+var scLookupTbl = map[string]bool{
+	"foo.bar.com":          true,
+	"srv.ipv4.single.fake": true,
+	"srv.ipv4.multi.fake":  true,
+	"no.attribute":         true,
+}
+
+// generateSCF generates a slice of strings (aggregately representing a single
+// service config file) for the input name, which mocks the result from a real
+// DNS TXT record lookup.
+func generateSCF(name string) []string {
+	var b []byte
+	switch name {
+	case "foo.bar.com":
+		b = []byte(scfs[0])
+	case "srv.ipv4.single.fake":
+		b = []byte(scfs[1])
+	case "srv.ipv4.multi.fake":
+		b = []byte(scfs[2])
+	default:
+		b = []byte(scfs[3])
+	}
+	if name == "no.attribute" {
+		return div(b)
+	}
+	return div(append([]byte(txtAttribute), b...))
+}
+
+// generateSC returns a service config string in JSON format for the input name.
+func generateSC(name string) string {
+	_, cnt := scLookupTbl[name]
+	if !cnt || name == "no.attribute" {
+		return ""
+	}
+	switch name {
+	case "foo.bar.com":
+		return scs[0]
+	case "srv.ipv4.single.fake":
+		return scs[1]
+	case "srv.ipv4.multi.fake":
+		return scs[2]
+	default:
+		return ""
+	}
+}
+
+var txtLookupTbl = struct {
+	sync.Mutex
+	tbl map[string][]string
+}{
+	tbl: map[string][]string{
+		"foo.bar.com":          generateSCF("foo.bar.com"),
+		"srv.ipv4.single.fake": generateSCF("srv.ipv4.single.fake"),
+		"srv.ipv4.multi.fake":  generateSCF("srv.ipv4.multi.fake"),
+		"srv.ipv6.single.fake": generateSCF("srv.ipv6.single.fake"),
+		"srv.ipv6.multi.fake":  generateSCF("srv.ipv6.multi.fake"),
+		"no.attribute":         generateSCF("no.attribute"),
+	},
+}
+
+func txtLookup(host string) ([]string, error) {
+	txtLookupTbl.Lock()
+	defer txtLookupTbl.Unlock()
+	if scs, cnt := txtLookupTbl.tbl[host]; cnt {
+		return scs, nil
+	}
+	return nil, fmt.Errorf("failed to lookup TXT:%s resolution in txtLookupTbl", host)
+}
+
+func TestResolve(t *testing.T) {
+	testDNSResolver(t)
+	testDNSResolveNow(t)
+	testIPResolver(t)
+}
+
+func testDNSResolver(t *testing.T) {
+	defer leakcheck.Check(t)
+	tests := []struct {
+		target   string
+		addrWant []resolver.Address
+		scWant   string
+	}{
+		{
+			"foo.bar.com",
+			[]resolver.Address{{Addr: "1.2.3.4" + colonDefaultPort}, {Addr: "5.6.7.8" + colonDefaultPort}},
+			generateSC("foo.bar.com"),
+		},
+		{
+			"foo.bar.com:1234",
+			[]resolver.Address{{Addr: "1.2.3.4:1234"}, {Addr: "5.6.7.8:1234"}},
+			generateSC("foo.bar.com"),
+		},
+		{
+			"srv.ipv4.single.fake",
+			[]resolver.Address{{Addr: "1.2.3.4:1234", Type: resolver.GRPCLB, ServerName: "ipv4.single.fake"}, {Addr: "2.4.6.8" + colonDefaultPort}},
+			generateSC("srv.ipv4.single.fake"),
+		},
+		{
+			"srv.ipv4.multi.fake",
+			[]resolver.Address{
+				{Addr: "1.2.3.4:1234", Type: resolver.GRPCLB, ServerName: "ipv4.multi.fake"},
+				{Addr: "5.6.7.8:1234", Type: resolver.GRPCLB, ServerName: "ipv4.multi.fake"},
+				{Addr: "9.10.11.12:1234", Type: resolver.GRPCLB, ServerName: "ipv4.multi.fake"},
+			},
+			generateSC("srv.ipv4.multi.fake"),
+		},
+		{
+			"srv.ipv6.single.fake",
+			[]resolver.Address{{Addr: "[2607:f8b0:400a:801::1001]:1234", Type: resolver.GRPCLB, ServerName: "ipv6.single.fake"}},
+			generateSC("srv.ipv6.single.fake"),
+		},
+		{
+			"srv.ipv6.multi.fake",
+			[]resolver.Address{
+				{Addr: "[2607:f8b0:400a:801::1001]:1234", Type: resolver.GRPCLB, ServerName: "ipv6.multi.fake"},
+				{Addr: "[2607:f8b0:400a:801::1002]:1234", Type: resolver.GRPCLB, ServerName: "ipv6.multi.fake"},
+				{Addr: "[2607:f8b0:400a:801::1003]:1234", Type: resolver.GRPCLB, ServerName: "ipv6.multi.fake"},
+			},
+			generateSC("srv.ipv6.multi.fake"),
+		},
+		{
+			"no.attribute",
+			nil,
+			generateSC("no.attribute"),
+		},
+	}
+
+	for _, a := range tests {
+		b := NewBuilder()
+		cc := &testClientConn{target: a.target}
+		r, err := b.Build(a.target, cc, resolver.BuildOption{})
+		if err != nil {
+			t.Fatalf("%v\n", err)
+		}
+		var addrs []resolver.Address
+		var cnt int
+		for {
+			addrs, cnt = cc.getAddress()
+			if cnt > 0 {
+				break
+			}
+			time.Sleep(time.Millisecond)
+		}
+		var sc string
+		for {
+			sc, cnt = cc.getSc()
+			if cnt > 0 {
+				break
+			}
+			time.Sleep(time.Millisecond)
+		}
+		if !reflect.DeepEqual(a.addrWant, addrs) {
+			t.Errorf("Resolved addresses of target: %q = %+v, want %+v\n", a.target, addrs, a.addrWant)
+		}
+		if !reflect.DeepEqual(a.scWant, sc) {
+			t.Errorf("Resolved service config of target: %q = %+v, want %+v\n", a.target, sc, a.scWant)
+		}
+		r.Close()
+	}
+}
+
+func mutateTbl(target string) func() {
+	hostLookupTbl.Lock()
+	oldHostTblEntry := hostLookupTbl.tbl[target]
+	hostLookupTbl.tbl[target] = hostLookupTbl.tbl[target][:len(oldHostTblEntry)-1]
+	hostLookupTbl.Unlock()
+	txtLookupTbl.Lock()
+	oldTxtTblEntry := txtLookupTbl.tbl[target]
+	txtLookupTbl.tbl[target] = []string{""}
+	txtLookupTbl.Unlock()
+
+	return func() {
+		hostLookupTbl.Lock()
+		hostLookupTbl.tbl[target] = oldHostTblEntry
+		hostLookupTbl.Unlock()
+		txtLookupTbl.Lock()
+		txtLookupTbl.tbl[target] = oldTxtTblEntry
+		txtLookupTbl.Unlock()
+	}
+}
+
+func testDNSResolveNow(t *testing.T) {
+	defer leakcheck.Check(t)
+	tests := []struct {
+		target   string
+		addrWant []resolver.Address
+		addrNext []resolver.Address
+		scWant   string
+		scNext   string
+	}{
+		{
+			"foo.bar.com",
+			[]resolver.Address{{Addr: "1.2.3.4" + colonDefaultPort}, {Addr: "5.6.7.8" + colonDefaultPort}},
+			[]resolver.Address{{Addr: "1.2.3.4" + colonDefaultPort}},
+			generateSC("foo.bar.com"),
+			"",
+		},
+	}
+
+	for _, a := range tests {
+		b := NewBuilder()
+		cc := &testClientConn{target: a.target}
+		r, err := b.Build(a.target, cc, resolver.BuildOption{})
+		if err != nil {
+			t.Fatalf("%v\n", err)
+		}
+		var addrs []resolver.Address
+		var cnt int
+		for {
+			addrs, cnt = cc.getAddress()
+			if cnt > 0 {
+				break
+			}
+			time.Sleep(time.Millisecond)
+		}
+		var sc string
+		for {
+			sc, cnt = cc.getSc()
+			if cnt > 0 {
+				break
+			}
+			time.Sleep(time.Millisecond)
+		}
+		if !reflect.DeepEqual(a.addrWant, addrs) {
+			t.Errorf("Resolved addresses of target: %q = %+v, want %+v\n", a.target, addrs, a.addrWant)
+		}
+		if !reflect.DeepEqual(a.scWant, sc) {
+			t.Errorf("Resolved service config of target: %q = %+v, want %+v\n", a.target, sc, a.scWant)
+		}
+		revertTbl := mutateTbl(a.target)
+		r.ResolveNow(resolver.ResolveNowOption{})
+		for {
+			addrs, cnt = cc.getAddress()
+			if cnt == 2 {
+				break
+			}
+			time.Sleep(time.Millisecond)
+		}
+		for {
+			sc, cnt = cc.getSc()
+			if cnt == 2 {
+				break
+			}
+			time.Sleep(time.Millisecond)
+		}
+		if !reflect.DeepEqual(a.addrNext, addrs) {
+			t.Errorf("Resolved addresses of target: %q = %+v, want %+v\n", a.target, addrs, a.addrNext)
+		}
+		if !reflect.DeepEqual(a.scNext, sc) {
+			t.Errorf("Resolved service config of target: %q = %+v, want %+v\n", a.target, sc, a.scNext)
+		}
+		revertTbl()
+		r.Close()
+	}
+}
+
+const colonDefaultPort = ":" + defaultPort
+
+func testIPResolver(t *testing.T) {
+	defer leakcheck.Check(t)
+	tests := []struct {
+		target string
+		want   []resolver.Address
+	}{
+		{"127.0.0.1", []resolver.Address{{Addr: "127.0.0.1" + colonDefaultPort}}},
+		{"127.0.0.1:12345", []resolver.Address{{Addr: "127.0.0.1:12345"}}},
+		{"::1", []resolver.Address{{Addr: "[::1]" + colonDefaultPort}}},
+		{"[::1]:12345", []resolver.Address{{Addr: "[::1]:12345"}}},
+		{"[::1]:", []resolver.Address{{Addr: "[::1]:443"}}},
+		{"2001:db8:85a3::8a2e:370:7334", []resolver.Address{{Addr: "[2001:db8:85a3::8a2e:370:7334]" + colonDefaultPort}}},
+		{"[2001:db8:85a3::8a2e:370:7334]", []resolver.Address{{Addr: "[2001:db8:85a3::8a2e:370:7334]" + colonDefaultPort}}},
+		{"[2001:db8:85a3::8a2e:370:7334]:12345", []resolver.Address{{Addr: "[2001:db8:85a3::8a2e:370:7334]:12345"}}},
+		{"[2001:db8::1]:http", []resolver.Address{{Addr: "[2001:db8::1]:http"}}},
+		// TODO(yuxuanli): zone support?
+	}
+
+	for _, v := range tests {
+		b := NewBuilder()
+		cc := &testClientConn{target: v.target}
+		r, err := b.Build(v.target, cc, resolver.BuildOption{})
+		if err != nil {
+			t.Fatalf("%v\n", err)
+		}
+		var addrs []resolver.Address
+		var cnt int
+		for {
+			addrs, cnt = cc.getAddress()
+			if cnt > 0 {
+				break
+			}
+			time.Sleep(time.Millisecond)
+		}
+		if !reflect.DeepEqual(v.want, addrs) {
+			t.Errorf("Resolved addresses of target: %q = %+v, want %+v\n", v.target, addrs, v.want)
+		}
+		r.ResolveNow(resolver.ResolveNowOption{})
+		for {
+			addrs, cnt = cc.getAddress()
+			if cnt == 2 {
+				break
+			}
+			time.Sleep(time.Millisecond)
+		}
+		if !reflect.DeepEqual(v.want, addrs) {
+			t.Errorf("Resolved addresses of target: %q = %+v, want %+v\n", v.target, addrs, v.want)
+		}
+		r.Close()
+	}
+}
+
+func TestResolveFunc(t *testing.T) {
+	defer leakcheck.Check(t)
+	tests := []struct {
+		addr string
+		want error
+	}{
+		// TODO(yuxuanli): More false cases?
+		{"www.google.com", nil},
+		{"foo.bar:12345", nil},
+		{"127.0.0.1", nil},
+		{"127.0.0.1:12345", nil},
+		{"[::1]:80", nil},
+		{"[2001:db8:a0b:12f0::1]:21", nil},
+		{":80", nil},
+		{"127.0.0...1:12345", nil},
+		{"[fe80::1%lo0]:80", nil},
+		{"golang.org:http", nil},
+		{"[2001:db8::1]:http", nil},
+		{":", nil},
+		{"", errMissingAddr},
+		{"[2001:db8:a0b:12f0::1", errForInvalidTarget},
+	}
+
+	b := NewBuilder()
+	for _, v := range tests {
+		cc := &testClientConn{target: v.addr}
+		r, err := b.Build(v.addr, cc, resolver.BuildOption{})
+		if err == nil {
+			r.Close()
+		}
+		if !reflect.DeepEqual(err, v.want) {
+			t.Errorf("Build(%q, cc, resolver.BuildOption{}) = %v, want %v", v.addr, err, v.want)
+		}
+	}
+}
diff --git a/resolver/dns/go17.go b/resolver/dns/go17.go
new file mode 100644
index 0000000..b466bc8
--- /dev/null
+++ b/resolver/dns/go17.go
@@ -0,0 +1,35 @@
+// +build go1.6, !go1.8
+
+/*
+ *
+ * Copyright 2017 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
+
+import (
+	"net"
+
+	"golang.org/x/net/context"
+)
+
+var (
+	lookupHost = func(ctx context.Context, host string) ([]string, error) { return net.LookupHost(host) }
+	lookupSRV  = func(ctx context.Context, service, proto, name string) (string, []*net.SRV, error) {
+		return net.LookupSRV(service, proto, name)
+	}
+	lookupTXT = func(ctx context.Context, name string) ([]string, error) { return net.LookupTXT(name) }
+)
diff --git a/resolver/dns/go17_test.go b/resolver/dns/go17_test.go
new file mode 100644
index 0000000..07fdcb0
--- /dev/null
+++ b/resolver/dns/go17_test.go
@@ -0,0 +1,52 @@
+// +build go1.6, !go1.8
+
+/*
+ *
+ * Copyright 2017 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
+
+import (
+	"fmt"
+	"net"
+
+	"golang.org/x/net/context"
+)
+
+var (
+	errForInvalidTarget = fmt.Errorf("invalid target address [2001:db8:a0b:12f0::1, error info: missing ']' in address [2001:db8:a0b:12f0::1:443")
+)
+
+func replaceNetFunc() func() {
+	oldLookupHost := lookupHost
+	oldLookupSRV := lookupSRV
+	oldLookupTXT := lookupTXT
+	lookupHost = func(ctx context.Context, host string) ([]string, error) {
+		return hostLookup(host)
+	}
+	lookupSRV = func(ctx context.Context, service, proto, name string) (string, []*net.SRV, error) {
+		return srvLookup(service, proto, name)
+	}
+	lookupTXT = func(ctx context.Context, host string) ([]string, error) {
+		return txtLookup(host)
+	}
+	return func() {
+		lookupHost = oldLookupHost
+		lookupSRV = oldLookupSRV
+		lookupTXT = oldLookupTXT
+	}
+}
diff --git a/resolver/dns/go18.go b/resolver/dns/go18.go
new file mode 100644
index 0000000..fa34f14
--- /dev/null
+++ b/resolver/dns/go18.go
@@ -0,0 +1,29 @@
+// +build go1.8
+
+/*
+ *
+ * Copyright 2017 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
+
+import "net"
+
+var (
+	lookupHost = net.DefaultResolver.LookupHost
+	lookupSRV  = net.DefaultResolver.LookupSRV
+	lookupTXT  = net.DefaultResolver.LookupTXT
+)
diff --git a/resolver/dns/go18_test.go b/resolver/dns/go18_test.go
new file mode 100644
index 0000000..8e01670
--- /dev/null
+++ b/resolver/dns/go18_test.go
@@ -0,0 +1,51 @@
+// +build go1.8
+
+/*
+ *
+ * Copyright 2017 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
+
+import (
+	"context"
+	"fmt"
+	"net"
+)
+
+var (
+	errForInvalidTarget = fmt.Errorf("invalid target address [2001:db8:a0b:12f0::1, error info: address [2001:db8:a0b:12f0::1:443: missing ']' in address")
+)
+
+func replaceNetFunc() func() {
+	oldLookupHost := lookupHost
+	oldLookupSRV := lookupSRV
+	oldLookupTXT := lookupTXT
+	lookupHost = func(ctx context.Context, host string) ([]string, error) {
+		return hostLookup(host)
+	}
+	lookupSRV = func(ctx context.Context, service, proto, name string) (string, []*net.SRV, error) {
+		return srvLookup(service, proto, name)
+	}
+	lookupTXT = func(ctx context.Context, host string) ([]string, error) {
+		return txtLookup(host)
+	}
+	return func() {
+		lookupHost = oldLookupHost
+		lookupSRV = oldLookupSRV
+		lookupTXT = oldLookupTXT
+	}
+}