blob: ca90eeaca002e333b4f4b3d411bb68b47b5d7c36 [file] [log] [blame]
// Copyright Istio 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 client
import (
"fmt"
"net"
"reflect"
"strings"
"testing"
"time"
)
import (
"github.com/miekg/dns"
"go.uber.org/atomic"
)
import (
dnsProto "github.com/apache/dubbo-go-pixiu/pkg/dns/proto"
"github.com/apache/dubbo-go-pixiu/pkg/test"
)
var testAgentDNSAddr = "127.0.0.1:15053"
func TestDNS(t *testing.T) {
initDNS(t)
testCases := []struct {
name string
host string
id int
queryAAAA bool
expected []dns.RR
expectResolutionFailure int
expectExternalResolution bool
modifyReq func(msg *dns.Msg)
}{
{
name: "success: non k8s host in local cache",
host: "www.google.com.",
expected: a("www.google.com.", []net.IP{net.ParseIP("1.1.1.1").To4()}),
},
{
name: "success: non k8s host with search namespace yields cname+A record",
host: "www.google.com.ns1.svc.cluster.local.",
expected: append(cname("www.google.com.ns1.svc.cluster.local.", "www.google.com."),
a("www.google.com.", []net.IP{net.ParseIP("1.1.1.1").To4()})...),
},
{
name: "success: non k8s host not in local cache",
host: "www.bing.com.",
expectExternalResolution: true,
},
{
name: "success: k8s host - fqdn",
host: "productpage.ns1.svc.cluster.local.",
expected: a("productpage.ns1.svc.cluster.local.", []net.IP{net.ParseIP("9.9.9.9").To4()}),
},
{
name: "success: k8s host - name.namespace",
host: "productpage.ns1.",
expected: a("productpage.ns1.", []net.IP{net.ParseIP("9.9.9.9").To4()}),
},
{
name: "success: k8s host - shortname",
host: "productpage.",
expected: a("productpage.", []net.IP{net.ParseIP("9.9.9.9").To4()}),
},
{
name: "success: k8s host (name.namespace) with search namespace yields cname+A record",
host: "productpage.ns1.ns1.svc.cluster.local.",
expected: append(cname("productpage.ns1.ns1.svc.cluster.local.", "productpage.ns1."),
a("productpage.ns1.", []net.IP{net.ParseIP("9.9.9.9").To4()})...),
},
{
name: "success: AAAA query for IPv4 k8s host (name.namespace) with search namespace",
host: "productpage.ns1.ns1.svc.cluster.local.",
queryAAAA: true,
},
{
name: "success: k8s host - non local namespace - name.namespace",
host: "example.ns2.",
expected: a("example.ns2.", []net.IP{net.ParseIP("10.10.10.10").To4()}),
},
{
name: "success: k8s host - non local namespace - fqdn",
host: "example.ns2.svc.cluster.local.",
expected: a("example.ns2.svc.cluster.local.", []net.IP{net.ParseIP("10.10.10.10").To4()}),
},
{
name: "success: k8s host - non local namespace - name.namespace.svc",
host: "example.ns2.svc.",
expected: a("example.ns2.svc.", []net.IP{net.ParseIP("10.10.10.10").To4()}),
},
{
name: "failure: k8s host - non local namespace - shortname",
host: "example.",
expectResolutionFailure: dns.RcodeNameError,
},
{
name: "success: alt host - name",
host: "svc-with-alt.",
expected: a("svc-with-alt.", []net.IP{net.ParseIP("15.15.15.15").To4()}),
},
{
name: "success: alt host - name.namespace",
host: "svc-with-alt.ns1.",
expected: a("svc-with-alt.ns1.", []net.IP{net.ParseIP("15.15.15.15").To4()}),
},
{
name: "success: alt host - name.namespace.svc",
host: "svc-with-alt.ns1.svc.",
expected: a("svc-with-alt.ns1.svc.", []net.IP{net.ParseIP("15.15.15.15").To4()}),
},
{
name: "success: alt host - name.namespace.svc.cluster.local",
host: "svc-with-alt.ns1.svc.cluster.local.",
expected: a("svc-with-alt.ns1.svc.cluster.local.", []net.IP{net.ParseIP("15.15.15.15").To4()}),
},
{
name: "success: alt host - name.namespace.svc.clusterset.local",
host: "svc-with-alt.ns1.svc.clusterset.local.",
expected: a("svc-with-alt.ns1.svc.clusterset.local.", []net.IP{net.ParseIP("15.15.15.15").To4()}),
},
{
name: "success: remote cluster k8s svc - same ns and different domain - fqdn",
host: "details.ns2.svc.cluster.remote.",
id: 2,
expected: a("details.ns2.svc.cluster.remote.",
[]net.IP{
net.ParseIP("13.13.13.13").To4(),
net.ParseIP("14.14.14.14").To4(),
net.ParseIP("12.12.12.12").To4(),
net.ParseIP("11.11.11.11").To4(),
}),
},
{
name: "success: remote cluster k8s svc round robin",
host: "details.ns2.svc.cluster.remote.",
id: 1,
expected: a("details.ns2.svc.cluster.remote.",
[]net.IP{
net.ParseIP("13.13.13.13").To4(),
net.ParseIP("14.14.14.14").To4(),
net.ParseIP("11.11.11.11").To4(),
net.ParseIP("12.12.12.12").To4(),
}),
},
{
name: "failure: remote cluster k8s svc - same ns and different domain - name.namespace",
host: "details.ns2.",
expectResolutionFailure: dns.RcodeNameError, // on home machines, the ISP may resolve to some generic webpage. So this test may fail on laptops
},
{
name: "success: TypeA query returns A records only",
host: "dual.localhost.",
expected: a("dual.localhost.", []net.IP{net.ParseIP("2.2.2.2").To4()}),
},
{
name: "success: wild card returns A record correctly",
host: "foo.wildcard.",
expected: a("foo.wildcard.", []net.IP{net.ParseIP("10.10.10.10").To4()}),
},
{
name: "success: specific wild card returns A record correctly",
host: "a.b.wildcard.",
expected: a("a.b.wildcard.", []net.IP{net.ParseIP("11.11.11.11").To4()}),
},
{
name: "success: wild card with domain returns A record correctly",
host: "foo.svc.mesh.company.net.",
expected: a("foo.svc.mesh.company.net.", []net.IP{net.ParseIP("10.1.2.3").To4()}),
},
{
name: "success: wild card with namespace with domain returns A record correctly",
host: "foo.foons.svc.mesh.company.net.",
expected: a("foo.foons.svc.mesh.company.net.", []net.IP{net.ParseIP("10.1.2.3").To4()}),
},
{
name: "success: wild card with search domain returns A record correctly",
host: "foo.svc.mesh.company.net.ns1.svc.cluster.local.",
expected: append(cname("*.svc.mesh.company.net.ns1.svc.cluster.local.", "*.svc.mesh.company.net."),
a("foo.svc.mesh.company.net.ns1.svc.cluster.local.", []net.IP{net.ParseIP("10.1.2.3").To4()})...),
},
{
name: "success: TypeAAAA query returns AAAA records only",
host: "dual.localhost.",
queryAAAA: true,
expected: aaaa("dual.localhost.", []net.IP{net.ParseIP("2001:db8:0:0:0:ff00:42:8329")}),
},
{
// This is not a NXDOMAIN, but empty response
name: "success: Error response if only AAAA records exist for typeA",
host: "ipv6.localhost.",
},
{
// This is not a NXDOMAIN, but empty response
name: "success: Error response if only A records exist for typeAAAA",
host: "ipv4.localhost.",
queryAAAA: true,
},
{
name: "udp: large request",
host: "giant.",
// Upstream UDP server returns big response, we cannot serve it. Compliant server would truncate it.
expectResolutionFailure: dns.RcodeServerFailure,
},
{
name: "tcp: large request",
host: "giant.",
expected: giantResponse,
},
{
name: "large request edns",
host: "giant.",
expectResolutionFailure: dns.RcodeSuccess,
expected: giantResponse,
modifyReq: func(msg *dns.Msg) {
msg.SetEdns0(dns.MaxMsgSize, false)
},
},
{
name: "large request truncated",
host: "giant-tc.",
expectResolutionFailure: dns.RcodeSuccess,
expected: giantResponse[:29],
},
{
name: "success: hostname with a period",
host: "example.localhost.",
expected: a("example.localhost.", []net.IP{net.ParseIP("3.3.3.3").To4()}),
},
}
clients := []dns.Client{
{
Timeout: 3 * time.Second,
Net: "udp",
UDPSize: 65535,
},
{
Timeout: 3 * time.Second,
Net: "tcp",
},
}
currentID := atomic.NewInt32(0)
oldID := dns.Id
dns.Id = func() uint16 {
return uint16(currentID.Inc())
}
defer func() { dns.Id = oldID }()
for i := range clients {
for _, tt := range testCases {
// Test is for explicit network
if (strings.HasPrefix(tt.name, "udp") || strings.HasPrefix(tt.name, "tcp")) && !strings.HasPrefix(tt.name, clients[i].Net) {
continue
}
t.Run(clients[i].Net+"-"+tt.name, func(t *testing.T) {
m := new(dns.Msg)
q := dns.TypeA
if tt.queryAAAA {
q = dns.TypeAAAA
}
m.SetQuestion(tt.host, q)
if tt.modifyReq != nil {
tt.modifyReq(m)
}
if tt.id != 0 {
currentID.Store(int32(tt.id))
defer func() { currentID.Store(0) }()
}
res, _, err := clients[i].Exchange(m, testAgentDNSAddr)
if res != nil {
t.Log("size: ", len(res.Answer))
}
if err != nil {
t.Errorf("Failed to resolve query for %s: %v", tt.host, err)
} else {
for _, answer := range res.Answer {
if answer.Header().Class != dns.ClassINET {
t.Errorf("expected class INET for all responses, got %+v for host %s", answer.Header(), tt.host)
}
}
if tt.expectExternalResolution {
// just make sure that the response has a valid DNS response from upstream resolvers
if res.Rcode != dns.RcodeSuccess {
t.Errorf("upstream dns resolution for %s failed: %v", tt.host, res)
}
} else {
if tt.expectResolutionFailure > 0 && tt.expectResolutionFailure != res.Rcode {
t.Errorf("expected resolution failure does not match with response code for %s: expected: %v, got: %v",
tt.host, tt.expectResolutionFailure, res.Rcode)
}
if !equalsDNSrecords(res.Answer, tt.expected) {
t.Log(res)
t.Errorf("dns responses for %s do not match. \n got %v\nwant %v", tt.host, res.Answer, tt.expected)
}
}
}
})
}
}
}
// Baseline:
//
// ~150us via agent if cached for A/AAAA
// ~300us via agent when doing the cname redirect
// 5-6ms to upstream resolver directly
// 6-7ms via agent to upstream resolver (cache miss)
//
// Also useful for load testing is using dnsperf. This can be run with:
//
// docker run -v $PWD:$PWD -w $PWD --network host quay.io/ssro/dnsperf dnsperf -p 15053 -d input -c 100 -l 30
//
// where `input` contains dns queries to run, such as `echo.default. A`
func BenchmarkDNS(t *testing.B) {
initDNS(t)
t.Run("via-agent-cache-miss", func(b *testing.B) {
bench(b, testAgentDNSAddr, "www.bing.com.")
})
t.Run("via-agent-cache-hit-fqdn", func(b *testing.B) {
bench(b, testAgentDNSAddr, "www.google.com.")
})
t.Run("via-agent-cache-hit-cname", func(b *testing.B) {
bench(b, testAgentDNSAddr, "www.google.com.ns1.svc.cluster.local.")
})
}
func bench(t *testing.B, nameserver string, hostname string) {
errs := 0
nrs := 0
nxdomain := 0
cnames := 0
c := dns.Client{
Timeout: 1 * time.Second,
}
for i := 0; i < t.N; i++ {
toResolve := hostname
redirect:
m := new(dns.Msg)
m.SetQuestion(toResolve, dns.TypeA)
res, _, err := c.Exchange(m, nameserver)
if err != nil {
errs++
} else if len(res.Answer) == 0 {
nrs++
} else {
for _, a := range res.Answer {
if arec, ok := a.(*dns.A); !ok {
// check if this is a cname redirect. If so, repeat the resolution
// assuming the client does not see/respect the inlined A record in the response.
if crec, ok := a.(*dns.CNAME); !ok {
errs++
} else {
cnames++
toResolve = crec.Target
goto redirect
}
} else {
if arec.Hdr.Rrtype != dns.RcodeSuccess {
nxdomain++
}
}
}
}
}
if errs+nrs > 0 {
t.Log("Sent", t.N, "err", errs, "no response", nrs, "nxdomain", nxdomain, "cname redirect", cnames)
}
}
var giantResponse = func() []dns.RR {
ips := make([]net.IP, 0)
for i := 0; i < 64; i++ {
ips = append(ips, net.ParseIP(fmt.Sprintf("240.0.0.%d", i)).To4())
}
return a("aaaaaaaaaaaa.aaaaaa.", ips)
}()
func makeUpstream(t test.Failer, responses map[string]string) string {
mux := dns.NewServeMux()
mux.HandleFunc(".", func(resp dns.ResponseWriter, msg *dns.Msg) {
answer := new(dns.Msg)
answer.SetReply(msg)
answer.Rcode = dns.RcodeNameError
if err := resp.WriteMsg(answer); err != nil {
t.Fatalf("err: %s", err)
}
})
for hn, desiredResp := range responses {
mux.HandleFunc(hn, func(resp dns.ResponseWriter, msg *dns.Msg) {
answer := dns.Msg{
Answer: a(hn, []net.IP{net.ParseIP(desiredResp).To4()}),
}
answer.SetReply(msg)
answer.Rcode = dns.RcodeSuccess
if err := resp.WriteMsg(&answer); err != nil {
t.Fatalf("err: %s", err)
}
})
}
mux.HandleFunc("giant.", func(resp dns.ResponseWriter, msg *dns.Msg) {
answer := &dns.Msg{
Answer: giantResponse,
}
answer.SetReply(msg)
answer.Rcode = dns.RcodeSuccess
if err := resp.WriteMsg(answer); err != nil {
t.Fatalf("err: %s", err)
}
})
mux.HandleFunc("giant-tc.", func(resp dns.ResponseWriter, msg *dns.Msg) {
answer := &dns.Msg{
Answer: giantResponse,
}
answer.SetReply(msg)
answer.Rcode = dns.RcodeSuccess
answer.Truncate(size("udp", msg))
if err := resp.WriteMsg(answer); err != nil {
t.Fatalf("err: %s", err)
}
})
up := make(chan struct{})
server := &dns.Server{
Addr: "127.0.0.1:0",
Net: "udp",
Handler: mux,
NotifyStartedFunc: func() { close(up) },
ReadTimeout: time.Second,
WriteTimeout: time.Second,
}
go func() {
if err := server.ListenAndServe(); err != nil {
log.Warnf("listen error: %v", err)
}
}()
select {
case <-time.After(time.Second * 10):
t.Fatalf("setup timeout")
case <-up:
}
t.Cleanup(func() { _ = server.Shutdown() })
server.Addr = server.PacketConn.LocalAddr().String()
// Setup TCP server on same port
up = make(chan struct{})
tcp := &dns.Server{
Addr: server.Addr,
Net: "tcp",
Handler: mux,
NotifyStartedFunc: func() { close(up) },
}
go func() {
if err := tcp.ListenAndServe(); err != nil {
log.Warnf("listen error: %v", err)
}
}()
select {
case <-time.After(time.Second * 10):
t.Fatalf("setup timeout")
case <-up:
}
t.Cleanup(func() { _ = tcp.Shutdown() })
t.Cleanup(func() { _ = server.Shutdown() })
tcp.Addr = server.PacketConn.LocalAddr().String()
return server.Addr
}
func initDNS(t test.Failer) *LocalDNSServer {
srv := makeUpstream(t, map[string]string{"www.bing.com.": "1.1.1.1"})
testAgentDNS, err := NewLocalDNSServer("ns1", "ns1.svc.cluster.local", "localhost:15053")
if err != nil {
t.Fatal(err)
}
testAgentDNS.resolvConfServers = []string{srv}
testAgentDNS.StartDNS()
testAgentDNS.searchNamespaces = []string{"ns1.svc.cluster.local", "svc.cluster.local", "cluster.local"}
testAgentDNS.UpdateLookupTable(&dnsProto.NameTable{
Table: map[string]*dnsProto.NameTable_NameInfo{
"www.google.com": {
Ips: []string{"1.1.1.1"},
Registry: "External",
},
"productpage.ns1.svc.cluster.local": {
Ips: []string{"9.9.9.9"},
Registry: "Kubernetes",
Namespace: "ns1",
Shortname: "productpage",
},
"example.ns2.svc.cluster.local": {
Ips: []string{"10.10.10.10"},
Registry: "Kubernetes",
Namespace: "ns2",
Shortname: "example",
},
"details.ns2.svc.cluster.remote": {
Ips: []string{"11.11.11.11", "12.12.12.12", "13.13.13.13", "14.14.14.14"},
Registry: "Kubernetes",
Namespace: "ns2",
Shortname: "details",
},
"svc-with-alt.ns1.svc.cluster.local": {
Ips: []string{"15.15.15.15"},
Registry: "Kubernetes",
Namespace: "ns1",
Shortname: "svc-with-alt",
AltHosts: []string{
"svc-with-alt.ns1.svc.clusterset.local",
},
},
"ipv6.localhost": {
Ips: []string{"2001:db8:0:0:0:ff00:42:8329"},
Registry: "External",
},
"dual.localhost": {
Ips: []string{"2.2.2.2", "2001:db8:0:0:0:ff00:42:8329"},
Registry: "External",
},
"ipv4.localhost": {
Ips: []string{"2.2.2.2"},
Registry: "External",
},
"*.b.wildcard": {
Ips: []string{"11.11.11.11"},
Registry: "External",
},
"*.wildcard": {
Ips: []string{"10.10.10.10"},
Registry: "External",
},
"*.svc.mesh.company.net": {
Ips: []string{"10.1.2.3"},
Registry: "External",
},
"example.localhost.": {
Ips: []string{"3.3.3.3"},
Registry: "External",
},
},
})
t.Cleanup(testAgentDNS.Close)
return testAgentDNS
}
// reflect.DeepEqual doesn't seem to work well for dns.RR
// as the Rdlength field is not updated in the a(), or aaaa() calls.
// so zero them out before doing reflect.Deepequal
func equalsDNSrecords(got []dns.RR, want []dns.RR) bool {
for i := range got {
got[i].Header().Rdlength = 0
}
return reflect.DeepEqual(got, want)
}