Files
nip/bosh-release/src/xip/xip_test.go
Brian Cunnie 1251afa774 DNS server: allow returning _multiple_ AAAA records
Previously the DNS server only returned the first AAAA record of a
customized domain; now it will return all the AAAA records.
2020-12-17 07:54:08 -08:00

497 lines
19 KiB
Go

package xip_test
import (
"encoding/binary"
"math/rand"
"net"
"strings"
"xip/xip"
. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
. "github.com/onsi/gomega"
"golang.org/x/net/dns/dnsmessage"
)
var _ = Describe("Xip", func() {
var (
err error
queryBuilder dnsmessage.Builder
queryType dnsmessage.Type
name string
nameArray [255]byte
packedQuery []byte
packedResponse []byte
logMessage string
response dnsmessage.Message
expectedResponse dnsmessage.Message
headerId uint16
question dnsmessage.Question
)
rand.Seed(GinkgoRandomSeed()) // Set to ginkgo's seed so that it's different each test & we can reproduce failures if necessary
Describe("QueryResponse()", func() {
BeforeEach(func() {
headerId = uint16(rand.Int31())
expectedResponse = dnsmessage.Message{
Header: dnsmessage.Header{
ID: headerId,
Response: true,
OpCode: 0,
Authoritative: true,
Truncated: false,
RecursionDesired: true,
RecursionAvailable: false,
},
Authorities: []dnsmessage.Resource{},
Additionals: []dnsmessage.Resource{},
}
})
JustBeforeEach(func() {
// This JustBeforeEach is way too long; I know.
// Set up the DNS query
queryBuilder = dnsmessage.NewBuilder(nil, dnsmessage.Header{
ID: headerId,
Response: false,
OpCode: 0,
Authoritative: false,
Truncated: false,
RecursionDesired: true,
RecursionAvailable: false,
RCode: 0,
})
queryBuilder.EnableCompression()
question = dnsmessage.Question{
Name: dnsmessage.Name{
Data: nameArray,
Length: uint8(len(name)),
},
Type: queryType,
Class: dnsmessage.ClassINET,
}
err = queryBuilder.StartQuestions()
Expect(err).ToNot(HaveOccurred())
err = queryBuilder.Question(question)
Expect(err).ToNot(HaveOccurred())
packedQuery, err = queryBuilder.Finish()
Expect(err).ToNot(HaveOccurred())
// Do preliminary setup of the expected response
expectedResponse.ID = headerId
expectedResponse.Questions = append(expectedResponse.Questions, question)
// The heart of the code: call QueryResponse()
packedResponse, logMessage, err = xip.QueryResponse(packedQuery)
Expect(err).ToNot(HaveOccurred())
err = response.Unpack(packedResponse)
Expect(err).ToNot(HaveOccurred())
})
When("it cannot Unpack() the query", func() {
BeforeEach(func() {
// This BeforeEach() serves no purpose other than preventing the JustBeforeEach() from complaining
name = "this-name-does-not-matter."
nameArray = [255]byte{} // zero-out the array otherwise tests will fail with leftovers from longer "name"s
copy(nameArray[:], name)
queryType = dnsmessage.TypeA
})
It("returns an error", func() {
_, logMessage, err = xip.QueryResponse([]byte{})
// I suspect the following may be brittle, and I would have been
// better off with Expect(err).To(HaveOccurred())
Expect(logMessage).To(Equal(""))
Expect(err).To(MatchError("unpacking header: id: insufficient data for base length type"))
})
})
When("the A record can be found", func() {
BeforeEach(func() {
name = "127.0.0.1.sslip.io."
nameArray = [255]byte{} // zero-out the array otherwise tests will fail with leftovers from longer "name"s
copy(nameArray[:], name)
queryType = dnsmessage.TypeA
expectedResponse.Answers = append(expectedResponse.Answers, dnsmessage.Resource{
Header: dnsmessage.ResourceHeader{
Name: dnsmessage.Name{
Data: nameArray,
Length: uint8(len(name)),
},
Type: queryType,
Class: dnsmessage.ClassINET,
TTL: 604800,
Length: 4,
},
Body: &dnsmessage.AResource{A: [4]byte{127, 0, 0, 1}},
})
})
It("should return the correct expectedResponse", func() {
Expect(err).ToNot(HaveOccurred())
Expect(logMessage).To(Equal("TypeA 127.0.0.1.sslip.io. ? 127.0.0.1"))
// break the sections out to make debugging easier
Expect(response.Header).To(Equal(expectedResponse.Header))
Expect(response.Questions).To(Equal(expectedResponse.Questions))
Expect(response.Answers).To(Equal(expectedResponse.Answers))
Expect(response.Authorities).To(Equal(expectedResponse.Authorities))
Expect(response.Additionals).To(Equal(expectedResponse.Additionals))
// and now the whole enchilada
Expect(response).To(Equal(expectedResponse))
})
})
When("the AAAA record can be found", func() {
BeforeEach(func() {
name = "--1.sslip.io."
nameArray = [255]byte{} // zero-out the array otherwise tests will fail with leftovers from longer "name"s
copy(nameArray[:], name)
queryType = dnsmessage.TypeAAAA
expectedResponse.Answers = append(expectedResponse.Answers, dnsmessage.Resource{
Header: dnsmessage.ResourceHeader{
Name: dnsmessage.Name{
Data: nameArray,
Length: uint8(len(name)),
},
Type: queryType,
Class: dnsmessage.ClassINET,
TTL: 604800,
Length: 16,
},
Body: &dnsmessage.AAAAResource{AAAA: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}},
})
})
It("should return the correct expectedResponse", func() {
Expect(err).ToNot(HaveOccurred())
Expect(logMessage).To(Equal("TypeAAAA --1.sslip.io. ? ::1"))
// break the sections out to make debugging easier
Expect(response.Header).To(Equal(expectedResponse.Header))
Expect(response.Questions).To(Equal(expectedResponse.Questions))
Expect(response.Answers).To(Equal(expectedResponse.Answers))
Expect(response.Authorities).To(Equal(expectedResponse.Authorities))
Expect(response.Additionals).To(Equal(expectedResponse.Additionals))
// and now the whole enchilada
Expect(response).To(Equal(expectedResponse))
})
})
When("an A or an AAAA record cannot be found", func() {
BeforeEach(func() {
name = "not-an-ip.sslip.io."
nameArray = [255]byte{} // zero-out the array otherwise tests will fail with leftovers from longer "name"s
copy(nameArray[:], name)
queryType = dnsmessage.TypeA
expectedSOA := xip.SOAResource(name)
expectedAuthority := dnsmessage.Resource{
Header: dnsmessage.ResourceHeader{
Name: dnsmessage.Name{
Data: nameArray,
Length: uint8(len(name)),
},
Type: dnsmessage.TypeSOA,
Class: dnsmessage.ClassINET,
TTL: 604800,
Length: 45,
},
Body: &expectedSOA,
}
expectedResponse.Authorities = append(expectedResponse.Authorities, expectedAuthority)
})
It("returns no answers, but returns an authoritative section", func() {
Expect(err).ToNot(HaveOccurred())
Expect(logMessage).To(Equal("TypeA not-an-ip.sslip.io. ? nil, SOA"))
Expect(len(response.Questions)).To(Equal(1))
Expect(response.Questions[0]).To(Equal(question))
// break test down for easier debugging
Expect(len(response.Answers)).To(Equal(0))
Expect(len(response.Authorities)).To(Equal(1))
Expect(response.Authorities[0].Header.Name).To(Equal(expectedResponse.Authorities[0].Header.Name))
Expect(response.Authorities[0].Header).To(Equal(expectedResponse.Authorities[0].Header))
Expect(response.Authorities[0].Body).To(Equal(expectedResponse.Authorities[0].Body))
Expect(response.Authorities[0]).To(Equal(expectedResponse.Authorities[0]))
// I've made a decision to not populate the Additionals section because it's too much work
// (And I don't think it's necessary)
Expect(len(response.Additionals)).To(Equal(0))
})
})
When("an ANY record is requested", func() {
BeforeEach(func() {
queryType = dnsmessage.TypeALL
})
It("responds that it's not implemented because it should be deprecated (RFC 8482)", func() {
Expect(err).ToNot(HaveOccurred())
Expect(response.RCode).To(Equal(dnsmessage.RCodeNotImplemented))
Expect(len(response.Answers)).To(Equal(0))
Expect(len(response.Authorities)).To(Equal(0))
Expect(len(response.Additionals)).To(Equal(0))
})
})
When("a record is requested but there's no record to return (e.g. SRV, HINFO)", func() {
BeforeEach(func() {
name = "no-srv-record.sslip.io."
nameArray = [255]byte{} // zero-out the array otherwise tests will fail with leftovers from longer "name"s
copy(nameArray[:], name)
queryType = dnsmessage.TypeSRV
expectedSOA := xip.SOAResource(name)
expectedAuthority := dnsmessage.Resource{
Header: dnsmessage.ResourceHeader{
Name: dnsmessage.Name{
Data: nameArray,
Length: uint8(len(name)),
},
Type: dnsmessage.TypeSOA,
Class: dnsmessage.ClassINET,
TTL: 604800,
Length: 45,
},
Body: &expectedSOA,
}
expectedResponse.Authorities = append(expectedResponse.Authorities, expectedAuthority)
})
It("responds with no answers but with an authority", func() {
Expect(err).ToNot(HaveOccurred())
Expect(logMessage).To(Equal("TypeSRV no-srv-record.sslip.io. ? nil, SOA"))
Expect(len(response.Questions)).To(Equal(1))
Expect(response.Questions[0]).To(Equal(question))
// break test down for easier debugging
Expect(len(response.Answers)).To(Equal(0))
Expect(len(response.Authorities)).To(Equal(1))
Expect(response.Authorities[0].Header.Name).To(Equal(expectedResponse.Authorities[0].Header.Name))
Expect(response.Authorities[0].Header).To(Equal(expectedResponse.Authorities[0].Header))
Expect(response.Authorities[0].Body).To(Equal(expectedResponse.Authorities[0].Body))
Expect(response.Authorities[0]).To(Equal(expectedResponse.Authorities[0]))
// I've made a decision to not populate the Additionals section because it's too much work
// (And I don't think it's necessary)
Expect(len(response.Additionals)).To(Equal(0))
})
})
})
Describe("ResponseHeader()", func() {
It("returns a header with the ID", func() {
headerId = uint16(rand.Int31())
Expect(xip.ResponseHeader(dnsmessage.Header{
ID: headerId,
Response: false,
OpCode: 0,
Authoritative: false,
Truncated: false,
RecursionDesired: false,
RecursionAvailable: false,
}, dnsmessage.RCodeSuccess)).To(Equal(dnsmessage.Header{
ID: headerId, // taken from the query
Response: true,
OpCode: 0,
Authoritative: true,
Truncated: false,
RecursionDesired: false, // taken from the query
RecursionAvailable: false,
RCode: 0,
}))
})
It("returns the header with the passed-in RCode", func() {
Expect(xip.ResponseHeader(dnsmessage.Header{}, dnsmessage.RCodeNotImplemented).
RCode).To(Equal(dnsmessage.RCodeNotImplemented))
})
})
Describe("MXResource()", func() {
It("returns the MX resource", func() {
query := "xyz"
mx := xip.MXResource(query)
var mxHostBytes [255]byte
copy(mxHostBytes[:], query)
Expect(len(mx)).To(Equal(1))
Expect(mx[0].MX.Length).To(Equal(uint8(3))) // "xyz" has 3 letters
Expect(mx[0].MX.Data).To(Equal(mxHostBytes))
})
When("sslip.io is the domain being queried", func() {
It("returns sslip.io's custom MX records", func() {
mx := xip.MXResource("sslip.io.")
Expect(len(mx)).To(Equal(2))
Expect(mx[0].MX.Data).To(Equal(xip.Customizations["sslip.io."].MX[0].MX.Data))
})
})
})
Describe("NSResources()", func() {
It("returns a map of the name servers", func() {
ns := xip.NSResources()
for _, nameServer := range xip.NameServers {
var nameServerBytes [255]byte
copy(nameServerBytes[:], nameServer)
Expect(ns[nameServer].NS.Data).To(Equal(nameServerBytes))
}
})
})
Describe("SOAResource()", func() {
It("returns the SOA resource for the domain in question", func() {
domain := "example.com."
soa := xip.SOAResource(domain)
var domainBytes [255]byte
copy(domainBytes[:], domain)
Expect(soa.NS.Data).To(Equal(domainBytes))
})
})
Describe("TXTResources()", func() {
It("returns no TXT resources", func() {
domain := "example.com."
_, err := xip.TXTResources(domain)
Expect(err).To(HaveOccurred())
})
When("queried for the sslip.io domain", func() {
It("returns mail-related TXT resources for the sslip.io domain", func() {
domain := "sslip.io."
txt, err := xip.TXTResources(domain)
Expect(err).To(Not(HaveOccurred()))
Expect(len(txt)).To(Equal(2))
Expect(txt[0].TXT[0]).To(MatchRegexp("protonmail-verification="))
Expect(txt[1].TXT[0]).To(MatchRegexp("v=spf1"))
})
})
When("a domain has been customized", func() { // Unnecessary, but confirms Golang's behavior for me, a doubting Thomas
customDomain := "some-crazy-domain-name-no-really.io."
xip.Customizations[customDomain] = xip.DomainCustomization{}
It("returns no TXT resources", func() {
_, err := xip.TXTResources(customDomain)
Expect(err).To(HaveOccurred())
})
delete(xip.Customizations, customDomain) // clean-up
})
})
Describe("NameToA()", func() {
DescribeTable("when it succeeds",
func(fqdn string, expectedA dnsmessage.AResource) {
ipv4Answer, err := xip.NameToA(fqdn)
Expect(err).ToNot(HaveOccurred())
Expect(*ipv4Answer).To(Equal(expectedA))
},
// sslip.io website
Entry("sslip.io", "sslip.io.", dnsmessage.AResource{A: [4]byte{78, 46, 204, 247}}),
// nameservers
Entry("ns-aws", "ns-aws.nono.io.", dnsmessage.AResource{A: [4]byte{52, 0, 56, 137}}),
Entry("ns-azure", "ns-azure.nono.io.", dnsmessage.AResource{A: [4]byte{52, 187, 42, 158}}),
Entry("ns-gce", "ns-gce.nono.io.", dnsmessage.AResource{A: [4]byte{104, 155, 144, 4}}),
// dots
Entry("loopback", "127.0.0.1", dnsmessage.AResource{A: [4]byte{127, 0, 0, 1}}),
Entry("255 with domain", "255.254.253.252.com", dnsmessage.AResource{A: [4]byte{255, 254, 253, 252}}),
Entry(`"This" network, pre-and-post`, "nono.io.0.1.2.3.sslip.io", dnsmessage.AResource{A: [4]byte{0, 1, 2, 3}}),
Entry("private network, two IPs, grabs the leftmost", "nono.io.172.16.0.30.172.31.255.255.sslip.io", dnsmessage.AResource{A: [4]byte{172, 16, 0, 30}}),
// dashes
Entry("shared address with dashes", "100-64-1-2", dnsmessage.AResource{A: [4]byte{100, 64, 1, 2}}),
Entry("link-local with domain", "169-254-168-253-com", dnsmessage.AResource{A: [4]byte{169, 254, 168, 253}}),
Entry("IETF protocol assignments with domain and www", "www-192-0-0-1-com", dnsmessage.AResource{A: [4]byte{192, 0, 0, 1}}),
// dots-and-dashes, mix-and-matches
Entry("Pandaxin's paradox", "minio-01.192-168-1-100.sslip.io", dnsmessage.AResource{A: [4]byte{192, 168, 1, 100}}),
)
DescribeTable("when it does not match an IP address",
func(fqdn string) {
_, err := xip.NameToA(fqdn)
Expect(err).To(MatchError("record not found"))
},
Entry("empty string", ""),
Entry("bare domain", "nono.io"),
Entry("canonical domain", "sslip.io"),
Entry("www", "www.sslip.io"),
Entry("a lone number", "538.sslip.io"),
Entry("too big", "256.254.253.252"),
Entry("NS but no dot", "ns-aws.nono.io"),
Entry("NS + cruft at beginning", "p-ns-aws.nono.io"),
Entry("test-net address with dots-and-dashes mixed", "www-192.0-2.3.example-me.com"),
)
})
Describe("NameToAAAA()", func() {
DescribeTable("when it succeeds",
func(fqdn string, expectedAAAA dnsmessage.AAAAResource) {
ipv6Answers, err := xip.NameToAAAA(fqdn)
Expect(err).ToNot(HaveOccurred())
Expect(len(ipv6Answers)).To(Equal(1))
Expect(ipv6Answers[0]).To(Equal(expectedAAAA))
},
// sslip.io website
Entry("sslip.io", "sslip.io.", xip.Customizations["sslip.io."].AAAA[0]),
// dashes only
Entry("loopback", "--1", dnsmessage.AAAAResource{AAAA: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}}),
Entry("ff with domain", "fffe-fdfc-fbfa-f9f8-f7f6-f5f4-f3f2-f1f0.com", dnsmessage.AAAAResource{AAAA: [16]byte{255, 254, 253, 252, 251, 250, 249, 248, 247, 246, 245, 244, 243, 242, 241, 240}}),
Entry("ff with domain and pre", "www.fffe-fdfc-fbfa-f9f8-f7f6-f5f4-f3f2-f1f0.com", dnsmessage.AAAAResource{AAAA: [16]byte{255, 254, 253, 252, 251, 250, 249, 248, 247, 246, 245, 244, 243, 242, 241, 240}}),
Entry("ff with domain dashes", "1.www-fffe-fdfc-fbfa-f9f8-f7f6-f5f4-f3f2-f1f0-1.com", dnsmessage.AAAAResource{AAAA: [16]byte{255, 254, 253, 252, 251, 250, 249, 248, 247, 246, 245, 244, 243, 242, 241, 240}}),
Entry("Browsing the logs", "2006-41d0-2-e01e--56dB-3598.sSLIP.io.", dnsmessage.AAAAResource{AAAA: [16]byte{32, 6, 65, 208, 0, 2, 224, 30, 0, 0, 0, 0, 86, 219, 53, 152}}),
Entry("Browsing the logs", "1-2-3--4-5-6.sSLIP.io.", dnsmessage.AAAAResource{AAAA: [16]byte{0, 1, 0, 2, 0, 3, 0, 0, 0, 0, 0, 4, 0, 5, 0, 6}}),
Entry("Browsing the logs", "1--2-3-4-5-6.sSLIP.io.", dnsmessage.AAAAResource{AAAA: [16]byte{0, 1, 0, 0, 0, 0, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6}}),
)
DescribeTable("when it does not match an IP address",
func(fqdn string) {
_, err := xip.NameToAAAA(fqdn)
//ipv4Answer, err := xip.NameToA(fqdn)
Expect(err).To(MatchError("record not found"))
//Expect(ipv4Answer).To(Equal(dnsmessage.AAAAResource{})) // is this important to test?
},
Entry("empty string", ""),
Entry("bare domain", "nono.io"),
Entry("canonical domain", "sslip.io"),
Entry("www", "www.sslip.io"),
Entry("a 1 without double-dash", "-1"),
Entry("too big", "--g"),
)
When("using randomly generated IPv6 addresses (fuzz testing)", func() {
It("should succeed every time", func() {
for i := 0; i < 1000; i++ {
addr := randomIPv6Address()
ipv6Answers, err := xip.NameToAAAA(strings.ReplaceAll(addr.String(), ":", "-"))
Expect(err).ToNot(HaveOccurred())
Expect(ipv6Answers[0].AAAA[:]).To(Equal([]uint8(addr)))
}
})
})
When("There is more than one AAAA record", func() {
It("returns them all", func() {
fqdn := random8ByteString()
xip.Customizations[fqdn] = xip.DomainCustomization{
//copy(xip.Customizations[fqdn].AAAA, dnsmessage.AAAAResource)
AAAA: []dnsmessage.AAAAResource{
{AAAA: [16]byte{1}},
{AAAA: [16]byte{2}},
},
}
ipv6Addrs, err := xip.NameToAAAA(fqdn)
Expect(err).ToNot(HaveOccurred())
Expect(len(ipv6Addrs)).To(Equal(2))
Expect(ipv6Addrs[0].AAAA).To(Equal([16]byte{1}))
Expect(ipv6Addrs[1].AAAA).To(Equal([16]byte{2}))
delete(xip.Customizations, fqdn)
})
})
})
})
func randomIPv6Address() net.IP {
upperHalf := make([]byte, 8)
lowerHalf := make([]byte, 8)
binary.LittleEndian.PutUint64(upperHalf, rand.Uint64())
binary.LittleEndian.PutUint64(lowerHalf, rand.Uint64())
ipv6 := net.IP(append(upperHalf, lowerHalf...))
// IPv6 addrs have a lot of all-zero two-byte sections
// So we zero-out ~50% of the sections
for i := 0; i < 8; i++ {
if rand.Int()%2 == 0 {
for j := 0; j < 2; j++ {
ipv6[i*2+j] = 0
}
}
}
return ipv6
}
// random8ByteString() returns an 8-char string consisting solely of the letters a-z.
func random8ByteString() string {
var randomString []byte
for i := 0; i < 8; i++ {
// 97 == ascii 'a', and there are 26 letters in the alphabet
randomString = append(randomString, byte(97+rand.Intn(26)))
}
return string(randomString)
}