grpc-go/internal/resolver/dns/dns_resolver_test.go

1377 lines
42 KiB
Go

/*
*
* 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_test
import (
"context"
"errors"
"fmt"
"net"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"google.golang.org/grpc/balancer"
grpclbstate "google.golang.org/grpc/balancer/grpclb/state"
"google.golang.org/grpc/internal"
"google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/internal/resolver/dns"
dnsinternal "google.golang.org/grpc/internal/resolver/dns/internal"
"google.golang.org/grpc/internal/testutils"
"google.golang.org/grpc/resolver"
dnspublic "google.golang.org/grpc/resolver/dns"
"google.golang.org/grpc/serviceconfig"
_ "google.golang.org/grpc" // To initialize internal.ParseServiceConfig
)
const (
txtBytesLimit = 255
defaultTestTimeout = 10 * time.Second
defaultTestShortTimeout = 10 * time.Millisecond
colonDefaultPort = ":443"
)
type s struct {
grpctest.Tester
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
// Override the default net.Resolver with a test resolver.
func overrideNetResolver(t *testing.T, r *testNetResolver) {
origNetResolver := dnsinternal.NewNetResolver
dnsinternal.NewNetResolver = func(string) (dnsinternal.NetResolver, error) { return r, nil }
t.Cleanup(func() { dnsinternal.NewNetResolver = origNetResolver })
}
// Override the DNS minimum resolution interval used by the resolver.
func overrideResolutionInterval(t *testing.T, d time.Duration) {
origMinResInterval := dns.MinResolutionInterval
dnspublic.SetMinResolutionInterval(d)
t.Cleanup(func() { dnspublic.SetMinResolutionInterval(origMinResInterval) })
}
// Override the timer used by the DNS resolver to fire after a duration of d.
func overrideTimeAfterFunc(t *testing.T, d time.Duration) {
origTimeAfter := dnsinternal.TimeAfterFunc
dnsinternal.TimeAfterFunc = func(time.Duration) <-chan time.Time {
return time.After(d)
}
t.Cleanup(func() { dnsinternal.TimeAfterFunc = origTimeAfter })
}
// Override the timer used by the DNS resolver as follows:
// - use the durChan to read the duration that the resolver wants to wait for
// - use the timerChan to unblock the wait on the timer
func overrideTimeAfterFuncWithChannel(t *testing.T) (durChan chan time.Duration, timeChan chan time.Time) {
origTimeAfter := dnsinternal.TimeAfterFunc
durChan = make(chan time.Duration, 1)
timeChan = make(chan time.Time)
dnsinternal.TimeAfterFunc = func(d time.Duration) <-chan time.Time {
select {
case durChan <- d:
default:
}
return timeChan
}
t.Cleanup(func() { dnsinternal.TimeAfterFunc = origTimeAfter })
return durChan, timeChan
}
// Override the current time used by the DNS resolver.
func overrideTimeNowFunc(t *testing.T, now time.Time) {
origTimeNowFunc := dnsinternal.TimeNowFunc
dnsinternal.TimeNowFunc = func() time.Time { return now }
t.Cleanup(func() { dnsinternal.TimeNowFunc = origTimeNowFunc })
}
// Override the remaining wait time to allow re-resolution by DNS resolver.
// Use the timeChan to read the time until resolver needs to wait for
// and return 0 wait time.
func overrideTimeUntilFuncWithChannel(t *testing.T) (timeChan chan time.Time) {
timeCh := make(chan time.Time, 1)
origTimeUntil := dnsinternal.TimeUntilFunc
dnsinternal.TimeUntilFunc = func(t time.Time) time.Duration {
timeCh <- t
return 0
}
t.Cleanup(func() { dnsinternal.TimeUntilFunc = origTimeUntil })
return timeCh
}
func enableSRVLookups(t *testing.T) {
origEnableSRVLookups := dns.EnableSRVLookups
dns.EnableSRVLookups = true
t.Cleanup(func() { dns.EnableSRVLookups = origEnableSRVLookups })
}
// Builds a DNS resolver for target and returns a couple of channels to read the
// state and error pushed by the resolver respectively.
func buildResolverWithTestClientConn(t *testing.T, target string) (resolver.Resolver, chan resolver.State, chan error) {
t.Helper()
b := resolver.Get("dns")
if b == nil {
t.Fatalf("Resolver for dns:/// scheme not registered")
}
stateCh := make(chan resolver.State, 1)
updateStateF := func(s resolver.State) error {
select {
case stateCh <- s:
default:
}
return nil
}
errCh := make(chan error, 1)
reportErrorF := func(err error) {
select {
case errCh <- err:
default:
}
}
tcc := &testutils.ResolverClientConn{Logger: t, UpdateStateF: updateStateF, ReportErrorF: reportErrorF}
r, err := b.Build(resolver.Target{URL: *testutils.MustParseURL(fmt.Sprintf("dns:///%s", target))}, tcc, resolver.BuildOptions{})
if err != nil {
t.Fatalf("Failed to build DNS resolver for target %q: %v\n", target, err)
}
t.Cleanup(func() { r.Close() })
return r, stateCh, errCh
}
// Waits for a state update from the DNS resolver and verifies the following:
// - wantAddrs matches the list of addresses in the update
// - wantBalancerAddrs matches the list of grpclb addresses in the update
// - wantSC matches the service config in the update
func verifyUpdateFromResolver(ctx context.Context, t *testing.T, stateCh chan resolver.State, wantAddrs, wantBalancerAddrs []resolver.Address, wantSC string) {
t.Helper()
var state resolver.State
select {
case <-ctx.Done():
t.Fatal("Timeout when waiting for a state update from the resolver")
case state = <-stateCh:
}
if !cmp.Equal(state.Addresses, wantAddrs, cmpopts.EquateEmpty()) {
t.Fatalf("Got addresses: %+v, want: %+v", state.Addresses, wantAddrs)
}
if gs := grpclbstate.Get(state); gs == nil {
if len(wantBalancerAddrs) > 0 {
t.Fatalf("Got no grpclb addresses. Want %d", len(wantBalancerAddrs))
}
} else {
if !cmp.Equal(gs.BalancerAddresses, wantBalancerAddrs) {
t.Fatalf("Got grpclb addresses %+v, want %+v", gs.BalancerAddresses, wantBalancerAddrs)
}
}
if wantSC == "{}" {
if state.ServiceConfig != nil && state.ServiceConfig.Config != nil {
t.Fatalf("Got service config:\n%s \nWant service config: {}", cmp.Diff(nil, state.ServiceConfig.Config))
}
} else if wantSC != "" {
wantSCParsed := internal.ParseServiceConfig.(func(string) *serviceconfig.ParseResult)(wantSC)
if !internal.EqualServiceConfigForTesting(state.ServiceConfig.Config, wantSCParsed.Config) {
t.Fatalf("Got service config:\n%s \nWant service config:\n%s", cmp.Diff(nil, state.ServiceConfig.Config), cmp.Diff(nil, wantSCParsed.Config))
}
}
}
// This is the service config used by the fake net.Resolver in its TXT record.
// - it contains an array of 5 entries
// - the first three will be dropped by the DNS resolver as part of its
// canarying rule matching functionality:
// - the client language does not match in the first entry
// - the percentage is set to 0 in the second entry
// - the client host name does not match in the third entry
// - the fourth and fifth entries will match the canarying rules, and therefore
// the fourth entry will be used as it will be the first matching entry.
const txtRecordGood = `
[
{
"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
}
]
}
}
]`
// This is the matched portion of the above TXT record entry.
const scJSON = `
{
"loadBalancingPolicy": "round_robin",
"methodConfig": [
{
"name": [
{
"service": "foo"
}
],
"waitForReady": true,
"timeout": "1s"
},
{
"name": [
{
"service": "bar"
}
],
"waitForReady": false
}
]
}`
// This service config contains three entries, but none of the match the DNS
// resolver's canarying rules and hence the resulting service config pushed by
// the DNS resolver will be an empty one.
const txtRecordNonMatching = `
[
{
"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"
}
]
}
}
]`
// Tests the scenario where a name resolves to a list of addresses, possibly
// some grpclb addresses as well, and a service config. The test verifies that
// the expected update is pushed to the channel.
func (s) TestDNSResolver_Basic(t *testing.T) {
tests := []struct {
name string
target string
hostLookupTable map[string][]string
srvLookupTable map[string][]*net.SRV
txtLookupTable map[string][]string
wantAddrs []resolver.Address
wantBalancerAddrs []resolver.Address
wantSC string
}{
{
name: "default_port",
target: "foo.bar.com",
hostLookupTable: map[string][]string{
"foo.bar.com": {"1.2.3.4", "5.6.7.8"},
},
txtLookupTable: map[string][]string{
"_grpc_config.foo.bar.com": txtRecordServiceConfig(txtRecordGood),
},
wantAddrs: []resolver.Address{{Addr: "1.2.3.4" + colonDefaultPort}, {Addr: "5.6.7.8" + colonDefaultPort}},
wantBalancerAddrs: nil,
wantSC: scJSON,
},
{
name: "specified_port",
target: "foo.bar.com:1234",
hostLookupTable: map[string][]string{
"foo.bar.com": {"1.2.3.4", "5.6.7.8"},
},
txtLookupTable: map[string][]string{
"_grpc_config.foo.bar.com": txtRecordServiceConfig(txtRecordGood),
},
wantAddrs: []resolver.Address{{Addr: "1.2.3.4:1234"}, {Addr: "5.6.7.8:1234"}},
wantBalancerAddrs: nil,
wantSC: scJSON,
},
{
name: "ipv4_with_SRV_and_single_grpclb_address",
target: "srv.ipv4.single.fake",
hostLookupTable: map[string][]string{
"srv.ipv4.single.fake": {"2.4.6.8"},
"ipv4.single.fake": {"1.2.3.4"},
},
srvLookupTable: map[string][]*net.SRV{
"_grpclb._tcp.srv.ipv4.single.fake": {&net.SRV{Target: "ipv4.single.fake", Port: 1234}},
},
txtLookupTable: map[string][]string{
"_grpc_config.srv.ipv4.single.fake": txtRecordServiceConfig(txtRecordGood),
},
wantAddrs: []resolver.Address{{Addr: "2.4.6.8" + colonDefaultPort}},
wantBalancerAddrs: []resolver.Address{{Addr: "1.2.3.4:1234", ServerName: "ipv4.single.fake"}},
wantSC: scJSON,
},
{
name: "ipv4_with_SRV_and_multiple_grpclb_address",
target: "srv.ipv4.multi.fake",
hostLookupTable: map[string][]string{
"ipv4.multi.fake": {"1.2.3.4", "5.6.7.8", "9.10.11.12"},
},
srvLookupTable: map[string][]*net.SRV{
"_grpclb._tcp.srv.ipv4.multi.fake": {&net.SRV{Target: "ipv4.multi.fake", Port: 1234}},
},
txtLookupTable: map[string][]string{
"_grpc_config.srv.ipv4.multi.fake": txtRecordServiceConfig(txtRecordGood),
},
wantAddrs: nil,
wantBalancerAddrs: []resolver.Address{
{Addr: "1.2.3.4:1234", ServerName: "ipv4.multi.fake"},
{Addr: "5.6.7.8:1234", ServerName: "ipv4.multi.fake"},
{Addr: "9.10.11.12:1234", ServerName: "ipv4.multi.fake"},
},
wantSC: scJSON,
},
{
name: "ipv6_with_SRV_and_single_grpclb_address",
target: "srv.ipv6.single.fake",
hostLookupTable: map[string][]string{
"srv.ipv6.single.fake": nil,
"ipv6.single.fake": {"2607:f8b0:400a:801::1001"},
},
srvLookupTable: map[string][]*net.SRV{
"_grpclb._tcp.srv.ipv6.single.fake": {&net.SRV{Target: "ipv6.single.fake", Port: 1234}},
},
txtLookupTable: map[string][]string{
"_grpc_config.srv.ipv6.single.fake": txtRecordServiceConfig(txtRecordNonMatching),
},
wantAddrs: nil,
wantBalancerAddrs: []resolver.Address{{Addr: "[2607:f8b0:400a:801::1001]:1234", ServerName: "ipv6.single.fake"}},
wantSC: "{}",
},
{
name: "ipv6_with_SRV_and_multiple_grpclb_address",
target: "srv.ipv6.multi.fake",
hostLookupTable: map[string][]string{
"srv.ipv6.multi.fake": nil,
"ipv6.multi.fake": {"2607:f8b0:400a:801::1001", "2607:f8b0:400a:801::1002", "2607:f8b0:400a:801::1003"},
},
srvLookupTable: map[string][]*net.SRV{
"_grpclb._tcp.srv.ipv6.multi.fake": {&net.SRV{Target: "ipv6.multi.fake", Port: 1234}},
},
txtLookupTable: map[string][]string{
"_grpc_config.srv.ipv6.multi.fake": txtRecordServiceConfig(txtRecordNonMatching),
},
wantAddrs: nil,
wantBalancerAddrs: []resolver.Address{
{Addr: "[2607:f8b0:400a:801::1001]:1234", ServerName: "ipv6.multi.fake"},
{Addr: "[2607:f8b0:400a:801::1002]:1234", ServerName: "ipv6.multi.fake"},
{Addr: "[2607:f8b0:400a:801::1003]:1234", ServerName: "ipv6.multi.fake"},
},
wantSC: "{}",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
overrideTimeAfterFunc(t, 2*defaultTestTimeout)
overrideNetResolver(t, &testNetResolver{
hostLookupTable: test.hostLookupTable,
srvLookupTable: test.srvLookupTable,
txtLookupTable: test.txtLookupTable,
})
enableSRVLookups(t)
_, stateCh, _ := buildResolverWithTestClientConn(t, test.target)
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
verifyUpdateFromResolver(ctx, t, stateCh, test.wantAddrs, test.wantBalancerAddrs, test.wantSC)
})
}
}
// Tests the case where the channel returns an error for the update pushed by
// the DNS resolver. Verifies that the DNS resolver backs off before trying to
// resolve. Once the channel returns a nil error, the test verifies that the DNS
// resolver does not backoff anymore.
func (s) TestDNSResolver_ExponentialBackoff(t *testing.T) {
tests := []struct {
name string
target string
hostLookupTable map[string][]string
txtLookupTable map[string][]string
wantAddrs []resolver.Address
wantSC string
}{
{
name: "happy case default port",
target: "foo.bar.com",
hostLookupTable: map[string][]string{"foo.bar.com": {"1.2.3.4", "5.6.7.8"}},
txtLookupTable: map[string][]string{
"_grpc_config.foo.bar.com": txtRecordServiceConfig(txtRecordGood),
},
wantAddrs: []resolver.Address{{Addr: "1.2.3.4" + colonDefaultPort}, {Addr: "5.6.7.8" + colonDefaultPort}},
wantSC: scJSON,
},
{
name: "happy case specified port",
target: "foo.bar.com:1234",
hostLookupTable: map[string][]string{"foo.bar.com": {"1.2.3.4", "5.6.7.8"}},
txtLookupTable: map[string][]string{
"_grpc_config.foo.bar.com": txtRecordServiceConfig(txtRecordGood),
},
wantAddrs: []resolver.Address{{Addr: "1.2.3.4:1234"}, {Addr: "5.6.7.8:1234"}},
wantSC: scJSON,
},
{
name: "happy case another default port",
target: "srv.ipv4.single.fake",
hostLookupTable: map[string][]string{
"srv.ipv4.single.fake": {"2.4.6.8"},
"ipv4.single.fake": {"1.2.3.4"},
},
txtLookupTable: map[string][]string{
"_grpc_config.srv.ipv4.single.fake": txtRecordServiceConfig(txtRecordGood),
},
wantAddrs: []resolver.Address{{Addr: "2.4.6.8" + colonDefaultPort}},
wantSC: scJSON,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
durChan, timeChan := overrideTimeAfterFuncWithChannel(t)
overrideNetResolver(t, &testNetResolver{
hostLookupTable: test.hostLookupTable,
txtLookupTable: test.txtLookupTable,
})
// Set the test clientconn to return error back to the resolver when
// it pushes an update on the channel.
var returnNilErr atomic.Bool
updateStateF := func(s resolver.State) error {
if returnNilErr.Load() {
return nil
}
return balancer.ErrBadResolverState
}
tcc := &testutils.ResolverClientConn{Logger: t, UpdateStateF: updateStateF}
b := resolver.Get("dns")
if b == nil {
t.Fatalf("Resolver for dns:/// scheme not registered")
}
r, err := b.Build(resolver.Target{URL: *testutils.MustParseURL(fmt.Sprintf("dns:///%s", test.target))}, tcc, resolver.BuildOptions{})
if err != nil {
t.Fatalf("Failed to build DNS resolver for target %q: %v\n", test.target, err)
}
defer r.Close()
// Expect the DNS resolver to backoff and attempt to re-resolve.
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
const retries = 10
var prevDur time.Duration
for i := 0; i < retries; i++ {
select {
case <-ctx.Done():
t.Fatalf("(Iteration: %d): Timeout when waiting for DNS resolver to backoff", i)
case dur := <-durChan:
if dur <= prevDur {
t.Fatalf("(Iteration: %d): Unexpected decrease in amount of time to backoff", i)
}
}
if i == retries-1 {
// Update resolver.ClientConn to not return an error
// anymore before last resolution retry to ensure that
// last resolution attempt doesn't back off.
returnNilErr.Store(true)
}
// Unblock the DNS resolver's backoff by pushing the current time.
timeChan <- time.Now()
}
// Verify that the DNS resolver does not backoff anymore.
sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
defer sCancel()
select {
case <-durChan:
t.Fatal("Unexpected DNS resolver backoff")
case <-sCtx.Done():
}
})
}
}
// Tests the case where the DNS resolver is asked to re-resolve by invoking the
// ResolveNow method.
func (s) TestDNSResolver_ResolveNow(t *testing.T) {
const target = "foo.bar.com"
overrideResolutionInterval(t, 0)
overrideTimeAfterFunc(t, 0)
tr := &testNetResolver{
hostLookupTable: map[string][]string{
"foo.bar.com": {"1.2.3.4", "5.6.7.8"},
},
txtLookupTable: map[string][]string{
"_grpc_config.foo.bar.com": txtRecordServiceConfig(txtRecordGood),
},
}
overrideNetResolver(t, tr)
r, stateCh, _ := buildResolverWithTestClientConn(t, target)
// Verify that the first update pushed by the resolver matches expectations.
wantAddrs := []resolver.Address{{Addr: "1.2.3.4" + colonDefaultPort}, {Addr: "5.6.7.8" + colonDefaultPort}}
wantSC := scJSON
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
verifyUpdateFromResolver(ctx, t, stateCh, wantAddrs, nil, wantSC)
// Update state in the fake net.Resolver to return only one address and a
// new service config.
tr.UpdateHostLookupTable(map[string][]string{target: {"1.2.3.4"}})
tr.UpdateTXTLookupTable(map[string][]string{
"_grpc_config.foo.bar.com": txtRecordServiceConfig(`[{"serviceConfig":{"loadBalancingPolicy": "grpclb"}}]`),
})
// Ask the resolver to re-resolve and verify that the new update matches
// expectations.
r.ResolveNow(resolver.ResolveNowOptions{})
wantAddrs = []resolver.Address{{Addr: "1.2.3.4" + colonDefaultPort}}
wantSC = `{"loadBalancingPolicy": "grpclb"}`
verifyUpdateFromResolver(ctx, t, stateCh, wantAddrs, nil, wantSC)
// Update state in the fake resolver to return no addresses and the same
// service config as before.
tr.UpdateHostLookupTable(map[string][]string{target: nil})
// Ask the resolver to re-resolve and verify that the new update matches
// expectations.
r.ResolveNow(resolver.ResolveNowOptions{})
verifyUpdateFromResolver(ctx, t, stateCh, nil, nil, wantSC)
}
// Tests the case where the given name is an IP address and verifies that the
// update pushed by the DNS resolver meets expectations.
func (s) TestIPResolver(t *testing.T) {
tests := []struct {
name string
target string
wantAddr []resolver.Address
}{
{
name: "localhost ipv4 default port",
target: "127.0.0.1",
wantAddr: []resolver.Address{{Addr: "127.0.0.1:443"}},
},
{
name: "localhost ipv4 non-default port",
target: "127.0.0.1:12345",
wantAddr: []resolver.Address{{Addr: "127.0.0.1:12345"}},
},
{
name: "localhost ipv6 default port no brackets",
target: "::1",
wantAddr: []resolver.Address{{Addr: "[::1]:443"}},
},
{
name: "localhost ipv6 default port with brackets",
target: "[::1]",
wantAddr: []resolver.Address{{Addr: "[::1]:443"}},
},
{
name: "localhost ipv6 non-default port",
target: "[::1]:12345",
wantAddr: []resolver.Address{{Addr: "[::1]:12345"}},
},
{
name: "ipv6 default port no brackets",
target: "2001:db8:85a3::8a2e:370:7334",
wantAddr: []resolver.Address{{Addr: "[2001:db8:85a3::8a2e:370:7334]:443"}},
},
{
name: "ipv6 default port with brackets",
target: "[2001:db8:85a3::8a2e:370:7334]",
wantAddr: []resolver.Address{{Addr: "[2001:db8:85a3::8a2e:370:7334]:443"}},
},
{
name: "ipv6 non-default port with brackets",
target: "[2001:db8:85a3::8a2e:370:7334]:12345",
wantAddr: []resolver.Address{{Addr: "[2001:db8:85a3::8a2e:370:7334]:12345"}},
},
{
name: "abbreviated ipv6 address",
target: "[2001:db8::1]:http",
wantAddr: []resolver.Address{{Addr: "[2001:db8::1]:http"}},
},
{
name: "ipv6 with zone and port",
target: "[fe80::1%25eth0]:1234",
wantAddr: []resolver.Address{{Addr: "[fe80::1%eth0]:1234"}},
},
{
name: "ipv6 with zone and default port",
target: "fe80::1%25eth0",
wantAddr: []resolver.Address{{Addr: "[fe80::1%eth0]:443"}},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
overrideResolutionInterval(t, 0)
overrideTimeAfterFunc(t, 2*defaultTestTimeout)
r, stateCh, _ := buildResolverWithTestClientConn(t, test.target)
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
verifyUpdateFromResolver(ctx, t, stateCh, test.wantAddr, nil, "")
// Attempt to re-resolve should not result in a state update.
r.ResolveNow(resolver.ResolveNowOptions{})
sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
defer sCancel()
select {
case <-sCtx.Done():
case s := <-stateCh:
t.Fatalf("Unexpected state update from the resolver: %+v", s)
}
})
}
}
// Tests the DNS resolver builder with different target names.
func (s) TestResolverBuild(t *testing.T) {
tests := []struct {
name string
target string
wantErr string
}{
{
name: "valid url",
target: "www.google.com",
},
{
name: "host port",
target: "foo.bar:12345",
},
{
name: "ipv4 address with default port",
target: "127.0.0.1",
},
{
name: "ipv6 address without brackets and default port",
target: "::",
},
{
name: "ipv4 address with non-default port",
target: "127.0.0.1:12345",
},
{
name: "localhost ipv6 with brackets",
target: "[::1]:80",
},
{
name: "ipv6 address with brackets",
target: "[2001:db8:a0b:12f0::1]:21",
},
{
name: "empty host with port",
target: ":80",
},
{
name: "ipv6 address with zone",
target: "[fe80::1%25lo0]:80",
},
{
name: "url with port",
target: "golang.org:http",
},
{
name: "ipv6 address with non integer port",
target: "[2001:db8::1]:http",
},
{
name: "address ends with colon",
target: "[2001:db8::1]:",
wantErr: dnsinternal.ErrEndsWithColon.Error(),
},
{
name: "address contains only a colon",
target: ":",
wantErr: dnsinternal.ErrEndsWithColon.Error(),
},
{
name: "empty address",
target: "",
wantErr: dnsinternal.ErrMissingAddr.Error(),
},
{
name: "invalid address",
target: "[2001:db8:a0b:12f0::1",
wantErr: "invalid target address",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
overrideTimeAfterFunc(t, 2*defaultTestTimeout)
b := resolver.Get("dns")
if b == nil {
t.Fatalf("Resolver for dns:/// scheme not registered")
}
tcc := &testutils.ResolverClientConn{Logger: t}
r, err := b.Build(resolver.Target{URL: *testutils.MustParseURL(fmt.Sprintf("dns:///%s", test.target))}, tcc, resolver.BuildOptions{})
if err != nil {
if test.wantErr == "" {
t.Fatalf("DNS resolver build for target %q failed with error: %v", test.target, err)
}
if !strings.Contains(err.Error(), test.wantErr) {
t.Fatalf("DNS resolver build for target %q failed with error: %v, wantErr: %s", test.target, err, test.wantErr)
}
return
}
if err == nil && test.wantErr != "" {
t.Fatalf("DNS resolver build for target %q succeeded when expected to fail with error: %s", test.target, test.wantErr)
}
r.Close()
})
}
}
// Tests scenarios where fetching of service config is enabled or disabled, and
// verifies that the expected update is pushed by the DNS resolver.
func (s) TestDisableServiceConfig(t *testing.T) {
tests := []struct {
name string
target string
hostLookupTable map[string][]string
txtLookupTable map[string][]string
disableServiceConfig bool
wantAddrs []resolver.Address
wantSC string
}{
{
name: "false",
target: "foo.bar.com",
hostLookupTable: map[string][]string{"foo.bar.com": {"1.2.3.4", "5.6.7.8"}},
txtLookupTable: map[string][]string{
"_grpc_config.foo.bar.com": txtRecordServiceConfig(txtRecordGood),
},
disableServiceConfig: false,
wantAddrs: []resolver.Address{{Addr: "1.2.3.4" + colonDefaultPort}, {Addr: "5.6.7.8" + colonDefaultPort}},
wantSC: scJSON,
},
{
name: "true",
target: "foo.bar.com",
hostLookupTable: map[string][]string{"foo.bar.com": {"1.2.3.4", "5.6.7.8"}},
txtLookupTable: map[string][]string{
"_grpc_config.foo.bar.com": txtRecordServiceConfig(txtRecordGood),
},
disableServiceConfig: true,
wantAddrs: []resolver.Address{{Addr: "1.2.3.4" + colonDefaultPort}, {Addr: "5.6.7.8" + colonDefaultPort}},
wantSC: "{}",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
overrideTimeAfterFunc(t, 2*defaultTestTimeout)
overrideNetResolver(t, &testNetResolver{
hostLookupTable: test.hostLookupTable,
txtLookupTable: test.txtLookupTable,
})
b := resolver.Get("dns")
if b == nil {
t.Fatalf("Resolver for dns:/// scheme not registered")
}
stateCh := make(chan resolver.State, 1)
updateStateF := func(s resolver.State) error {
stateCh <- s
return nil
}
tcc := &testutils.ResolverClientConn{Logger: t, UpdateStateF: updateStateF}
r, err := b.Build(resolver.Target{URL: *testutils.MustParseURL(fmt.Sprintf("dns:///%s", test.target))}, tcc, resolver.BuildOptions{DisableServiceConfig: test.disableServiceConfig})
if err != nil {
t.Fatalf("Failed to build DNS resolver for target %q: %v\n", test.target, err)
}
defer r.Close()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
verifyUpdateFromResolver(ctx, t, stateCh, test.wantAddrs, nil, test.wantSC)
})
}
}
// Tests the case where a TXT lookup is expected to return an error. Verifies
// that errors are ignored with the corresponding env var is set.
func (s) TestTXTError(t *testing.T) {
for _, ignore := range []bool{false, true} {
t.Run(fmt.Sprintf("%v", ignore), func(t *testing.T) {
overrideTimeAfterFunc(t, 2*defaultTestTimeout)
overrideNetResolver(t, &testNetResolver{hostLookupTable: map[string][]string{"ipv4.single.fake": {"1.2.3.4"}}})
origTXTIgnore := envconfig.TXTErrIgnore
envconfig.TXTErrIgnore = ignore
defer func() { envconfig.TXTErrIgnore = origTXTIgnore }()
// There is no entry for "ipv4.single.fake" in the txtLookupTbl
// maintained by the fake net.Resolver. So, a TXT lookup for this
// name will return an error.
_, stateCh, _ := buildResolverWithTestClientConn(t, "ipv4.single.fake")
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
var state resolver.State
select {
case <-ctx.Done():
t.Fatal("Timeout when waiting for a state update from the resolver")
case state = <-stateCh:
}
if ignore {
if state.ServiceConfig != nil {
t.Fatalf("Received non-nil service config: %+v; want nil", state.ServiceConfig)
}
} else {
if state.ServiceConfig == nil || state.ServiceConfig.Err == nil {
t.Fatalf("Received service config %+v; want non-nil error", state.ServiceConfig)
}
}
})
}
}
// Tests different cases for a user's dial target that specifies a non-empty
// authority (or Host field of the URL).
func (s) TestCustomAuthority(t *testing.T) {
tests := []struct {
name string
authority string
wantAuthority string
wantBuildErr bool
}{
{
name: "authority with default DNS port",
authority: "4.3.2.1:53",
wantAuthority: "4.3.2.1:53",
},
{
name: "authority with non-default DNS port",
authority: "4.3.2.1:123",
wantAuthority: "4.3.2.1:123",
},
{
name: "authority with no port",
authority: "4.3.2.1",
wantAuthority: "4.3.2.1:53",
},
{
name: "ipv6 authority with no port",
authority: "::1",
wantAuthority: "[::1]:53",
},
{
name: "ipv6 authority with brackets and no port",
authority: "[::1]",
wantAuthority: "[::1]:53",
},
{
name: "ipv6 authority with brackets and non-default DNS port",
authority: "[::1]:123",
wantAuthority: "[::1]:123",
},
{
name: "host name with no port",
authority: "dnsserver.com",
wantAuthority: "dnsserver.com:53",
},
{
name: "no host port and non-default port",
authority: ":123",
wantAuthority: "localhost:123",
},
{
name: "only colon",
authority: ":",
wantAuthority: "",
wantBuildErr: true,
},
{
name: "ipv6 name ending in colon",
authority: "[::1]:",
wantAuthority: "",
wantBuildErr: true,
},
{
name: "host name ending in colon",
authority: "dnsserver.com:",
wantAuthority: "",
wantBuildErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
overrideTimeAfterFunc(t, 2*defaultTestTimeout)
// Override the address dialer to verify the authority being passed.
origAddressDialer := dnsinternal.AddressDialer
errChan := make(chan error, 1)
dnsinternal.AddressDialer = func(authority string) func(ctx context.Context, network, address string) (net.Conn, error) {
if authority != test.wantAuthority {
errChan <- fmt.Errorf("wrong custom authority passed to resolver. target: %s got authority: %s want authority: %s", test.authority, authority, test.wantAuthority)
} else {
errChan <- nil
}
return func(ctx context.Context, network, address string) (net.Conn, error) {
return nil, errors.New("no need to dial")
}
}
defer func() { dnsinternal.AddressDialer = origAddressDialer }()
b := resolver.Get("dns")
if b == nil {
t.Fatalf("Resolver for dns:/// scheme not registered")
}
tcc := &testutils.ResolverClientConn{Logger: t}
endpoint := "foo.bar.com"
target := resolver.Target{URL: *testutils.MustParseURL(fmt.Sprintf("dns://%s/%s", test.authority, endpoint))}
r, err := b.Build(target, tcc, resolver.BuildOptions{})
if (err != nil) != test.wantBuildErr {
t.Fatalf("DNS resolver build for target %+v returned error %v: wantErr: %v\n", target, err, test.wantBuildErr)
}
if err != nil {
return
}
defer r.Close()
if err := <-errChan; err != nil {
t.Fatal(err)
}
})
}
}
// TestRateLimitedResolve exercises the rate limit enforced on re-resolution
// requests. It sets the re-resolution rate to a small value and repeatedly
// calls ResolveNow() and ensures only the expected number of resolution
// requests are made.
func (s) TestRateLimitedResolve(t *testing.T) {
const target = "foo.bar.com"
_, timeChan := overrideTimeAfterFuncWithChannel(t)
tr := &testNetResolver{
lookupHostCh: testutils.NewChannel(),
hostLookupTable: map[string][]string{target: {"1.2.3.4", "5.6.7.8"}},
}
overrideNetResolver(t, tr)
r, stateCh, _ := buildResolverWithTestClientConn(t, target)
// Wait for the first resolution request to be done. This happens as part
// of the first iteration of the for loop in watcher().
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if _, err := tr.lookupHostCh.Receive(ctx); err != nil {
t.Fatalf("Timed out waiting for lookup() call.")
}
// Call Resolve Now 100 times, shouldn't continue onto next iteration of
// watcher, thus shouldn't lookup again.
for i := 0; i <= 100; i++ {
r.ResolveNow(resolver.ResolveNowOptions{})
}
continueCtx, continueCancel := context.WithTimeout(context.Background(), defaultTestShortTimeout)
defer continueCancel()
if _, err := tr.lookupHostCh.Receive(continueCtx); err == nil {
t.Fatalf("Should not have looked up again as DNS Min Res Rate timer has not gone off.")
}
// Make the DNSMinResRate timer fire immediately, by sending the current
// time on it. This will unblock the resolver which is currently blocked on
// the DNS Min Res Rate timer going off, which will allow it to continue to
// the next iteration of the watcher loop.
select {
case timeChan <- time.Now():
case <-ctx.Done():
t.Fatal("Timed out waiting for the DNS resolver to block on DNS Min Res Rate to elapse")
}
// Now that DNS Min Res Rate timer has gone off, it should lookup again.
if _, err := tr.lookupHostCh.Receive(ctx); err != nil {
t.Fatalf("Timed out waiting for lookup() call.")
}
// Resolve Now 1000 more times, shouldn't lookup again as DNS Min Res Rate
// timer has not gone off.
for i := 0; i < 1000; i++ {
r.ResolveNow(resolver.ResolveNowOptions{})
}
continueCtx, continueCancel = context.WithTimeout(context.Background(), defaultTestShortTimeout)
defer continueCancel()
if _, err := tr.lookupHostCh.Receive(continueCtx); err == nil {
t.Fatalf("Should not have looked up again as DNS Min Res Rate timer has not gone off.")
}
// Make the DNSMinResRate timer fire immediately again.
select {
case timeChan <- time.Now():
case <-ctx.Done():
t.Fatal("Timed out waiting for the DNS resolver to block on DNS Min Res Rate to elapse")
}
// Now that DNS Min Res Rate timer has gone off, it should lookup again.
if _, err := tr.lookupHostCh.Receive(ctx); err != nil {
t.Fatalf("Timed out waiting for lookup() call.")
}
wantAddrs := []resolver.Address{{Addr: "1.2.3.4" + colonDefaultPort}, {Addr: "5.6.7.8" + colonDefaultPort}}
var state resolver.State
select {
case <-ctx.Done():
t.Fatal("Timeout when waiting for a state update from the resolver")
case state = <-stateCh:
}
if !cmp.Equal(state.Addresses, wantAddrs, cmpopts.EquateEmpty()) {
t.Fatalf("Got addresses: %+v, want: %+v", state.Addresses, wantAddrs)
}
}
// Test verifies that when the DNS resolver gets an error from the underlying
// net.Resolver, it reports the error to the channel and backs off and retries.
func (s) TestReportError(t *testing.T) {
durChan, timeChan := overrideTimeAfterFuncWithChannel(t)
overrideNetResolver(t, &testNetResolver{})
const target = "notfoundaddress"
_, _, errorCh := buildResolverWithTestClientConn(t, target)
// Should receive first error.
ctx, ctxCancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer ctxCancel()
select {
case <-ctx.Done():
t.Fatal("Timeout when waiting for an error from the resolver")
case err := <-errorCh:
if !strings.Contains(err.Error(), "hostLookup error") {
t.Fatalf(`ReportError(err=%v) called; want err contains "hostLookupError"`, err)
}
}
// Expect the DNS resolver to backoff and attempt to re-resolve. Every time,
// the DNS resolver will receive the same error from the net.Resolver and is
// expected to push it to the channel.
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
const retries = 10
var prevDur time.Duration
for i := 0; i < retries; i++ {
select {
case <-ctx.Done():
t.Fatalf("(Iteration: %d): Timeout when waiting for DNS resolver to backoff", i)
case dur := <-durChan:
if dur <= prevDur {
t.Fatalf("(Iteration: %d): Unexpected decrease in amount of time to backoff", i)
}
}
// Unblock the DNS resolver's backoff by pushing the current time.
timeChan <- time.Now()
select {
case <-ctx.Done():
t.Fatal("Timeout when waiting for an error from the resolver")
case err := <-errorCh:
if !strings.Contains(err.Error(), "hostLookup error") {
t.Fatalf(`ReportError(err=%v) called; want err contains "hostLookupError"`, err)
}
}
}
}
// Override the default dns.ResolvingTimeout with a test duration.
func overrideResolveTimeoutDuration(t *testing.T, dur time.Duration) {
t.Helper()
origDur := dns.ResolvingTimeout
dnspublic.SetResolvingTimeout(dur)
t.Cleanup(func() { dnspublic.SetResolvingTimeout(origDur) })
}
// Test verifies that the DNS resolver gets timeout error when net.Resolver
// takes too long to resolve a target.
func (s) TestResolveTimeout(t *testing.T) {
// Set DNS resolving timeout duration to 7ms
timeoutDur := 7 * time.Millisecond
overrideResolveTimeoutDuration(t, timeoutDur)
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
// We are trying to resolve hostname which takes infinity time to resolve.
const target = "infinity"
// Define a testNetResolver with lookupHostCh, an unbuffered channel,
// so we can block the resolver until reaching timeout.
tr := &testNetResolver{
lookupHostCh: testutils.NewChannelWithSize(0),
hostLookupTable: map[string][]string{target: {"1.2.3.4"}},
}
overrideNetResolver(t, tr)
_, _, errCh := buildResolverWithTestClientConn(t, target)
select {
case <-ctx.Done():
t.Fatal("Timeout when waiting for the DNS resolver to timeout")
case err := <-errCh:
if err == nil || !strings.Contains(err.Error(), "context deadline exceeded") {
t.Fatalf(`Expected to see Timeout error; got: %v`, err)
}
}
}
// Test verifies that changing [MinResolutionInterval] variable correctly effects
// the resolution behaviour
func (s) TestMinResolutionInterval(t *testing.T) {
const target = "foo.bar.com"
overrideResolutionInterval(t, 1*time.Millisecond)
tr := &testNetResolver{
hostLookupTable: map[string][]string{
"foo.bar.com": {"1.2.3.4", "5.6.7.8"},
},
txtLookupTable: map[string][]string{
"_grpc_config.foo.bar.com": txtRecordServiceConfig(txtRecordGood),
},
}
overrideNetResolver(t, tr)
r, stateCh, _ := buildResolverWithTestClientConn(t, target)
wantAddrs := []resolver.Address{{Addr: "1.2.3.4" + colonDefaultPort}, {Addr: "5.6.7.8" + colonDefaultPort}}
wantSC := scJSON
for i := 0; i < 5; i++ {
// set context timeout slightly higher than the min resolution interval to make sure resolutions
// happen successfully
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
verifyUpdateFromResolver(ctx, t, stateCh, wantAddrs, nil, wantSC)
r.ResolveNow(resolver.ResolveNowOptions{})
}
}
// TestMinResolutionInterval_NoExtraDelay verifies that there is no extra delay
// between two resolution requests apart from [MinResolutionInterval].
func (s) TestMinResolutionInterval_NoExtraDelay(t *testing.T) {
tr := &testNetResolver{
hostLookupTable: map[string][]string{
"foo.bar.com": {"1.2.3.4", "5.6.7.8"},
},
txtLookupTable: map[string][]string{
"_grpc_config.foo.bar.com": txtRecordServiceConfig(txtRecordGood),
},
}
overrideNetResolver(t, tr)
// Override time.Now() to return a zero value for time. This will allow us
// to verify that the call to time.Until is made with the exact
// [MinResolutionInterval] that we expect.
overrideTimeNowFunc(t, time.Time{})
// Override time.Until() to read the time passed to it
// and return immediately without any delay
timeCh := overrideTimeUntilFuncWithChannel(t)
r, stateCh, errorCh := buildResolverWithTestClientConn(t, "foo.bar.com")
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
// Ensure that the first resolution happens.
select {
case <-ctx.Done():
t.Fatal("Timeout when waiting for DNS resolver")
case err := <-errorCh:
t.Fatalf("Unexpected error from resolver, %v", err)
case <-stateCh:
}
// Request re-resolution and verify that the resolver waits for
// [MinResolutionInterval].
r.ResolveNow(resolver.ResolveNowOptions{})
select {
case <-ctx.Done():
t.Fatal("Timeout when waiting for DNS resolver")
case gotTime := <-timeCh:
wantTime := time.Time{}.Add(dns.MinResolutionInterval)
if !gotTime.Equal(wantTime) {
t.Fatalf("DNS resolver waits for %v time before re-resolution, want %v", gotTime, wantTime)
}
}
// Ensure that the re-resolution request actually happens.
select {
case <-ctx.Done():
t.Fatal("Timeout when waiting for an error from the resolver")
case err := <-errorCh:
t.Fatalf("Unexpected error from resolver, %v", err)
case <-stateCh:
}
}