blob: 985b1c6ea53ac715c93278d55dd840cc99771966 [file] [log] [blame]
// Copyright 2017 The Netstack Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package dns
import (
"testing"
"time"
"golang.org/x/net/dns/dnsmessage"
)
const (
example = "example.com."
fooExample = "foo.example.com."
insertAll = "insertAll"
lookup = "lookup"
prune = "prune"
)
func mustMakeResourceHeader(nameStr string, rhType dnsmessage.Type, ttl uint32) dnsmessage.ResourceHeader {
name, err := dnsmessage.NewName(nameStr)
if err != nil {
panic(err)
}
return dnsmessage.ResourceHeader{
Name: name,
Type: rhType,
Class: dnsmessage.ClassINET,
TTL: ttl,
}
}
func makeTypeAResource(hName string, ttl uint32, A [4]byte) dnsmessage.Resource {
return dnsmessage.Resource{
Header: mustMakeResourceHeader(hName, dnsmessage.TypeA, ttl),
Body: &dnsmessage.AResource{
A,
},
}
}
func makeTypeAAAAResource(hName string, ttl uint32, AAAA [16]byte) dnsmessage.Resource {
return dnsmessage.Resource{
Header: mustMakeResourceHeader(hName, dnsmessage.TypeAAAA, ttl),
Body: &dnsmessage.AAAAResource{
AAAA,
},
}
}
func mustMakeCNAMEResource(hName, cName string, ttl uint32) dnsmessage.Resource {
name, err := dnsmessage.NewName(cName)
if err != nil {
panic(err)
}
return dnsmessage.Resource{
Header: mustMakeResourceHeader(hName, dnsmessage.TypeCNAME, ttl),
Body: &dnsmessage.CNAMEResource{
CNAME: name,
},
}
}
func makeSOAResource(hName string, ttl, minTTL uint32) dnsmessage.Resource {
return dnsmessage.Resource{
Header: mustMakeResourceHeader(hName, dnsmessage.TypeA, ttl),
Body: &dnsmessage.SOAResource{MinTTL: minTTL},
}
}
func makeQuestion(nameStr string) dnsmessage.Question {
name, err := dnsmessage.NewName(nameStr)
if err != nil {
panic(err)
}
return dnsmessage.Question{
Name: name,
Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET,
}
}
type checkParams struct {
funcName string
gotResources []dnsmessage.Resource
wantLen int
wantName dnsmessage.Name
}
func check(t *testing.T, err error, params checkParams) {
t.Helper()
if err != nil {
t.Errorf("after cache.%s, cache.lookup returned error: %s", params.funcName, err)
}
if len(params.gotResources) != params.wantLen {
t.Errorf("got cache.%s with the resource length = %d, want %d.", params.funcName, len(params.gotResources), params.wantLen)
}
for _, rr := range params.gotResources {
if rr.Header.Name != params.wantName {
t.Errorf("got cache.%s(...) = %s, want %s", params.funcName, rr.Header.Name, params.wantName)
}
}
}
var (
smallTestResources = []dnsmessage.Resource{
makeTypeAResource(example, 5, [4]byte{127, 0, 0, 1}),
makeTypeAResource(example, 5, [4]byte{127, 0, 0, 2}),
}
soaAuthority = makeSOAResource(example, 5, 12)
exampleQuestion = makeQuestion(example)
fooExampleQuestion = makeQuestion(fooExample)
)
// Tests a simple insert and lookup pair.
func TestLookup(t *testing.T) {
cache := makeCache()
cache.insertAll(smallTestResources)
visited := make(map[dnsmessage.Name]struct{})
rrs, err := cache.lookup(exampleQuestion, visited, 0)
check(t, err, checkParams{
funcName: lookup,
gotResources: rrs,
wantLen: 2,
wantName: exampleQuestion.Name,
})
}
// Tests that entries are pruned when they expire, and not before.
func TestExpires(t *testing.T) {
cache := makeCache()
// These records expire at 5 seconds.
testTime := time.Now()
origTimeNow := timeNow
defer func() { timeNow = origTimeNow }()
timeNow = func() time.Time { return testTime }
cache.insertAll(smallTestResources)
// Still there after t=4 seconds.
testTime = testTime.Add(4 * time.Second)
cache.prune()
{
visited := make(map[dnsmessage.Name]struct{})
rrs, err := cache.lookup(exampleQuestion, visited, 0)
check(t, err, checkParams{
funcName: prune,
gotResources: rrs,
wantLen: 2,
wantName: exampleQuestion.Name,
})
}
// Gone after t=6 seconds.
testTime = testTime.Add(2 * time.Second)
cache.prune()
{
visited := make(map[dnsmessage.Name]struct{})
rrs, err := cache.lookup(exampleQuestion, visited, 0)
check(t, err, checkParams{
funcName: prune,
gotResources: rrs,
wantLen: 0,
wantName: exampleQuestion.Name,
})
}
}
// Tests that a Resource Record with the name of an existing CNAMERecord is inserted and the existing CNAMERecord is overwritten.
func TestInsertWithExistingCNAMEResource(t *testing.T) {
cache := makeCache()
cache.insertAll(smallTestResources)
cache.insertAll([]dnsmessage.Resource{mustMakeCNAMEResource(fooExample, example, 5)})
{
visited := make(map[dnsmessage.Name]struct{})
rrs, err := cache.lookup(fooExampleQuestion, visited, 0)
check(t, err, checkParams{
funcName: lookup,
gotResources: rrs,
wantLen: 2,
wantName: exampleQuestion.Name,
})
}
// Insert a CNAMEResource with the name of an existing CNAMEResource.
anotherExample := "anotherExample"
cache.insertAll([]dnsmessage.Resource{makeTypeAResource(anotherExample, 6, [4]byte{127, 0, 0, 3}), mustMakeCNAMEResource(fooExample, anotherExample, 5)})
{
visited := make(map[dnsmessage.Name]struct{})
name, err := dnsmessage.NewName(anotherExample)
if err != nil {
t.Fatalf("dnsmessage.NewName(%s) failed with error :%s", anotherExample, err)
}
rrs, err := cache.lookup(fooExampleQuestion, visited, 0)
check(t, err, checkParams{
funcName: insertAll,
gotResources: rrs,
wantLen: 1,
wantName: name,
})
}
// Insert a TypeAResource with the name of an existing CNAMEResource.
cache.insertAll([]dnsmessage.Resource{makeTypeAResource(fooExample, 5, [4]byte{127, 0, 0, 4})})
{
visited := make(map[dnsmessage.Name]struct{})
rrs, err := cache.lookup(fooExampleQuestion, visited, 0)
check(t, err, checkParams{
funcName: insertAll,
gotResources: rrs,
wantLen: 1,
wantName: fooExampleQuestion.Name,
})
}
}
// Tests that a CNAMEResource with the name of existing Resources Records is inserted and the existing Resource Records are overwritten.
func TestInsertCNAMEResourceWithExistingTypeAResources(t *testing.T) {
cache := makeCache()
cache.insertAll(smallTestResources)
{
visited := make(map[dnsmessage.Name]struct{})
rrs, err := cache.lookup(exampleQuestion, visited, 0)
check(t, err, checkParams{
funcName: lookup,
gotResources: rrs,
wantLen: 2,
wantName: exampleQuestion.Name,
})
}
// Insert a CNAMEResource with the name of existing TypeAResource and TypeAResources are overwritten.
cache.insertAll([]dnsmessage.Resource{mustMakeCNAMEResource(example, fooExample, 5)})
{
visited := make(map[dnsmessage.Name]struct{})
rrs, err := cache.lookup(exampleQuestion, visited, 0)
check(t, err, checkParams{
funcName: insertAll,
gotResources: rrs,
wantLen: 0,
wantName: exampleQuestion.Name,
})
}
}
// Tests that we can't insert more than maxEntries entries, but after pruning old ones, we can insert again.
func TestMaxEntries(t *testing.T) {
cache := makeCache()
testTime := time.Now()
origTimeNow := timeNow
defer func() { timeNow = origTimeNow }()
timeNow = func() time.Time { return testTime }
// One record that expires at 10 seconds.
cache.insertAll([]dnsmessage.Resource{
makeTypeAResource(example, 10, [4]byte{127, 0, 0, 1}),
})
// A bunch that expire at 5 seconds.
for i := 0; i < maxEntries; i++ {
cache.insertAll([]dnsmessage.Resource{
makeTypeAResource(example, 5, [4]byte{byte(i >> 24), byte(i >> 16), byte(i >> 8), byte(i)}),
})
}
{
visited := make(map[dnsmessage.Name]struct{})
rrs, err := cache.lookup(exampleQuestion, visited, 0)
check(t, err, checkParams{
funcName: insertAll,
gotResources: rrs,
wantLen: maxEntries,
wantName: exampleQuestion.Name,
})
}
// Cache is at capacity. Can't insert anymore.
cache.insertAll([]dnsmessage.Resource{
makeTypeAResource(fooExample, 5, [4]byte{192, 168, 0, 1}),
})
{
visited := make(map[dnsmessage.Name]struct{})
rrs, err := cache.lookup(fooExampleQuestion, visited, 0)
check(t, err, checkParams{
funcName: insertAll,
gotResources: rrs,
wantLen: 0,
wantName: exampleQuestion.Name,
})
}
// Advance the clock so the 5 second entries expire. Insert should succeed.
testTime = testTime.Add(6 * time.Second)
cache.insertAll([]dnsmessage.Resource{
makeTypeAResource(fooExample, 5, [4]byte{192, 168, 0, 1}),
})
{
visited := make(map[dnsmessage.Name]struct{})
rrs, err := cache.lookup(fooExampleQuestion, visited, 0)
check(t, err, checkParams{
funcName: insertAll,
gotResources: rrs,
wantLen: 1,
wantName: fooExampleQuestion.Name,
})
}
}
// Tests that we get results when looking up a domain alias.
func TestCNAME(t *testing.T) {
cache := makeCache()
cache.insertAll(smallTestResources)
// One CNAME record that points at an existing record.
cache.insertAll([]dnsmessage.Resource{
mustMakeCNAMEResource(fooExample, example, 10),
})
visited := make(map[dnsmessage.Name]struct{})
rrs, err := cache.lookup(fooExampleQuestion, visited, 0)
check(t, err, checkParams{
funcName: lookup,
gotResources: rrs,
wantLen: 2,
wantName: exampleQuestion.Name,
})
}
// Tests that there is a loop in the CNAME aliases.
func TestCNAMELoop(t *testing.T) {
cache := makeCache()
cache.insertAll(smallTestResources)
cache.insertAll([]dnsmessage.Resource{mustMakeCNAMEResource(fooExample, example, 5)})
{
visited := make(map[dnsmessage.Name]struct{})
rrs, err := cache.lookup(fooExampleQuestion, visited, 0)
check(t, err, checkParams{
funcName: lookup,
gotResources: rrs,
wantLen: 2,
wantName: exampleQuestion.Name,
})
}
// Form a loop of CNAME.
cache.insertAll([]dnsmessage.Resource{mustMakeCNAMEResource(example, fooExample, 5)})
{
visited := make(map[dnsmessage.Name]struct{})
if _, err := cache.lookup(fooExampleQuestion, visited, 0); err != errCNAMELoop {
t.Errorf("got cache.lookup failed with error = %v, want %s", err, errCNAMELoop)
}
}
}
// Tests that the level of CNAMEResource exceeds maxCNAMELevel.
func TestCNAMELevel(t *testing.T) {
cache := makeCache()
name := 'z'
cache.insertAll(smallTestResources)
cache.insertAll([]dnsmessage.Resource{mustMakeCNAMEResource(string(name), example, 5)})
for i := 0; i < maxCNAMELevel-1; i++ {
cache.insertAll([]dnsmessage.Resource{mustMakeCNAMEResource(string(name-1), string(name), 5)})
name--
}
{
visited := make(map[dnsmessage.Name]struct{})
rrs, err := cache.lookup(makeQuestion(string(name)), visited, 0)
check(t, err, checkParams{
funcName: lookup,
gotResources: rrs,
wantLen: 2,
wantName: exampleQuestion.Name,
})
}
// Add one more level of CNAME aliases.
cache.insertAll([]dnsmessage.Resource{mustMakeCNAMEResource(string(name-1), string(name), 5)})
name--
{
visited := make(map[dnsmessage.Name]struct{})
if _, err := cache.lookup(makeQuestion(string(name)), visited, 0); err != errCNAMELevel {
t.Errorf("got cache.lookup failed with error = %s, want %s", err, errCNAMELevel)
}
}
}
// Tests that duplicate CNAMEResoures aren't allowed, so that no duplicate {A,AAAA}Resource will be returned.
func TestDupeCNAME(t *testing.T) {
cache := makeCache()
cache.insertAll(smallTestResources)
{
visited := make(map[dnsmessage.Name]struct{})
rrs, err := cache.lookup(exampleQuestion, visited, 0)
check(t, err, checkParams{
funcName: lookup,
gotResources: rrs,
wantLen: 2,
wantName: exampleQuestion.Name,
})
}
cache.insertAll([]dnsmessage.Resource{
mustMakeCNAMEResource(fooExample, example, 5),
})
{
visited := make(map[dnsmessage.Name]struct{})
rrs, err := cache.lookup(fooExampleQuestion, visited, 0)
check(t, err, checkParams{
funcName: lookup,
gotResources: rrs,
wantLen: 2,
wantName: exampleQuestion.Name,
})
}
// Insert fooExample CNAME example again with different ttl.
cache.insertAll([]dnsmessage.Resource{
mustMakeCNAMEResource(fooExample, example, 10),
})
{
visited := make(map[dnsmessage.Name]struct{})
rrs, err := cache.lookup(fooExampleQuestion, visited, 0)
check(t, err, checkParams{
funcName: lookup,
gotResources: rrs,
wantLen: 2,
wantName: exampleQuestion.Name,
})
}
}
// Tests that the cache doesn't store multiple identical AResource.
func TestDupeAResource(t *testing.T) {
cache := makeCache()
cache.insertAll(smallTestResources)
cache.insertAll(smallTestResources)
visited := make(map[dnsmessage.Name]struct{})
rrs, err := cache.lookup(exampleQuestion, visited, 0)
check(t, err, checkParams{
funcName: lookup,
gotResources: rrs,
wantLen: 2,
wantName: exampleQuestion.Name,
})
}
// Tests that we can insert and expire negative resources.
func TestNegative(t *testing.T) {
cache := makeCache()
// The negative record expires at 12 seconds (taken from the SOA authority resource).
testTime := time.Now()
origTimeNow := timeNow
defer func() { timeNow = origTimeNow }()
timeNow = func() time.Time { return testTime }
cache.insertNegative(exampleQuestion, dnsmessage.Message{
Questions: []dnsmessage.Question{exampleQuestion},
Authorities: []dnsmessage.Resource{soaAuthority},
})
// Still there after t=11 seconds.
testTime = testTime.Add(11 * time.Second)
cache.prune()
{
visited := make(map[dnsmessage.Name]struct{})
rrs, err := cache.lookup(exampleQuestion, visited, 0)
check(t, err, checkParams{
funcName: prune,
gotResources: rrs,
wantLen: 1,
wantName: exampleQuestion.Name,
})
}
// Gone after t=13 seconds.
testTime = testTime.Add(2 * time.Second)
cache.prune()
{
visited := make(map[dnsmessage.Name]struct{})
rrs, err := cache.lookup(exampleQuestion, visited, 0)
check(t, err, checkParams{
funcName: prune,
gotResources: rrs,
wantLen: 0,
wantName: exampleQuestion.Name,
})
}
}
// Tests that a negative resource is replaced when we have an actual resource for that query.
func TestNegativeUpdate(t *testing.T) {
cache := makeCache()
cache.insertNegative(exampleQuestion, dnsmessage.Message{
Questions: []dnsmessage.Question{exampleQuestion},
Authorities: []dnsmessage.Resource{soaAuthority},
})
cache.insertAll(smallTestResources)
visited := make(map[dnsmessage.Name]struct{})
rrs, err := cache.lookup(exampleQuestion, visited, 0)
check(t, err, checkParams{
funcName: lookup,
gotResources: rrs,
wantLen: 2,
wantName: exampleQuestion.Name,
})
}