diff --git a/resolver/dns/dns_resolver.go b/resolver/dns/dns_resolver.go new file mode 100644 index 000000000000..026789718e59 --- /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 000000000000..d25d840561b2 --- /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 000000000000..b466bc8f6d45 --- /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 000000000000..07fdcb03f88a --- /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 000000000000..fa34f14cad48 --- /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 000000000000..8e016709c1d7 --- /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 + } +}