From 67acbb7f47ca5d3c40dc5d512589fb70a988c7a6 Mon Sep 17 00:00:00 2001 From: Brian Cunnie Date: Wed, 16 Sep 2020 20:04:25 -0700 Subject: [PATCH] Golang: use `dnsmessage.Builder` - It automatically populates the header for us, which would have been a big headache to do manually. - Switched `ENOTFOUND` to `ErrNotFound`, and updated the error message as well. As sad as it was to make this switch, I must acknowledge that I'm coding in Go, not C, and I should follow its conventions. - TWO OF THE TESTS ARE BROKEN. I know, I'll fix them soon. I should have fixed the tests first, then the code, but I was overeager. --- src/xip/xip.go | 99 +++++++++++++++++++++++++++++---------------- src/xip/xip_test.go | 19 ++++++--- 2 files changed, 78 insertions(+), 40 deletions(-) diff --git a/src/xip/xip.go b/src/xip/xip.go index 2383e9d..d4a46ed 100644 --- a/src/xip/xip.go +++ b/src/xip/xip.go @@ -15,52 +15,77 @@ import ( // https://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses var ipv4RE = regexp.MustCompile(`(^|[.-])(((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])[.-]){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))($|[.-])`) var ipv6RE = regexp.MustCompile(`(^|[.-])(([0-9a-fA-F]{1,4}-){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}-){1,7}-|([0-9a-fA-F]{1,4}-){1,6}-[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}-){1,5}(-[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}-){1,4}(-[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}-){1,3}(-[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}-){1,2}(-[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}-((-[0-9a-fA-F]{1,4}){1,6})|-((-[0-9a-fA-F]{1,4}){1,7}|-)|fe80-(-[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|--(ffff(-0{1,4}){0,1}-){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}-){1,4}-((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))($|[.-])`) +var ErrNotFound = errors.New("record not found") // QueryResponse takes in a raw (packed) DNS query and returns a raw (packed) // DNS response It takes in the raw data to offload as much as possible from // main(). main() is hard to unit test, but functions like QueryResponse are // easy. func QueryResponse(queryBytes []byte) ([]byte, error) { - var query dnsmessage.Message + var queryHeader dnsmessage.Header + var err error + var response []byte - err := query.Unpack(queryBytes) - if err != nil { + p := dnsmessage.Parser{} + if queryHeader, err = p.Start(queryBytes); err != nil { return nil, err } - response := dnsmessage.Message{ - Header: ResponseHeader(query), - Questions: query.Questions, - } - - for _, question := range query.Questions { - fqdn := string(question.Name.Data[:question.Name.Length]) - switch question.Type { + b := dnsmessage.NewBuilder(response, ResponseHeader(queryHeader)) + b.EnableCompression() + for { + q, err := p.Question() + if err == dnsmessage.ErrSectionDone { + break + } + if err != nil { + return nil, err + } + switch q.Type { case dnsmessage.TypeA: { - nameToA, err := NameToA(fqdn) + nameToA, err := NameToA(q.Name.String()) if err != nil { - // note that this could be written more efficiently; however, I wrote it - // to accommodate 'if err != nil' codepath. My first version was 'if err == nil', - // and it flummoxed me. - response.Answers = append(response.Answers, dnsmessage.Resource{}) + // There's only one possible error this can be: ErrNotFound. note that + // this could be written more efficiently; however, I wrote it to + // accommodate 'if err != nil' convention. My first version was 'if + // err == nil', and it flummoxed me. + err = b.StartAuthorities() + if err != nil { + return nil, err + } + err = b.SOAResource(dnsmessage.ResourceHeader{ + Name: q.Name, + Type: dnsmessage.TypeA, + Class: dnsmessage.ClassINET, + TTL: 604800, // 60 * 60 * 24 * 7 == 1 week; it's not gonna change + Length: 0, + }, SOAResource(q.Name.String())) + if err != nil { + return nil, err + } } else { - response.Answers = append(response.Answers, dnsmessage.Resource{ - Header: dnsmessage.ResourceHeader{ - Name: question.Name, - Type: dnsmessage.TypeA, - Class: dnsmessage.ClassINET, - TTL: 300, - Length: 4, // A records are always 4 bytes - }, - Body: nameToA, - }) + err = b.StartAnswers() + if err != nil { + return nil, err + } + err = b.AResource(dnsmessage.ResourceHeader{ + Name: q.Name, + Type: dnsmessage.TypeA, + Class: dnsmessage.ClassINET, + TTL: 604800, // 60 * 60 * 24 * 7 == 1 week; it's not gonna change + Length: 0, + }, *nameToA) + if err != nil { + panic(err.Error()) + return nil, err + } } } } } - responseBytes, err := response.Pack() + responseBytes, err := b.Finish() // I couldn't figure an easy way to test the error condition in Ginkgo. Sue me. if err != nil { return nil, err @@ -71,9 +96,9 @@ func QueryResponse(queryBytes []byte) ([]byte, error) { // ResponseHeader returns a pre-fab DNS response header. Note that we're always // authoritative and therefore recursion is never available. We're able to // "white label" domains by indiscriminately matching every query that comes -// our way. Not being recursive has the added benefit of not worrying -// being used as an amplifier in a DDOS attack -func ResponseHeader(query dnsmessage.Message) dnsmessage.Header { +// our way. Not being recursive has the added benefit of not being usable as an +// amplifier in a DDOS attack +func ResponseHeader(query dnsmessage.Header) dnsmessage.Header { return dnsmessage.Header{ ID: query.ID, Response: true, @@ -85,10 +110,11 @@ func ResponseHeader(query dnsmessage.Message) dnsmessage.Header { } } +// NameToA returns either an AResource that matched the hostname or ErrNotFound func NameToA(fqdnString string) (*dnsmessage.AResource, error) { fqdn := []byte(fqdnString) if !ipv4RE.Match(fqdn) { - return &dnsmessage.AResource{}, errors.New("ENOTFOUND") // I can't help it; I love the old-style UNIX errors + return &dnsmessage.AResource{}, ErrNotFound } match := string(ipv4RE.FindSubmatch(fqdn)[2]) @@ -98,10 +124,12 @@ func NameToA(fqdnString string) (*dnsmessage.AResource, error) { return &dnsmessage.AResource{A: [4]byte{ipv4address[0], ipv4address[1], ipv4address[2], ipv4address[3]}}, nil } +// NameToAAAA NameToA returns either an AAAAResource that matched the hostname +// or ErrNotFound func NameToAAAA(fqdnString string) (*dnsmessage.AAAAResource, error) { fqdn := []byte(fqdnString) if !ipv6RE.Match(fqdn) { - return &dnsmessage.AAAAResource{}, errors.New("ENOTFOUND") // I can't help it; I love the old-style UNIX errors + return &dnsmessage.AAAAResource{}, ErrNotFound } match := string(ipv6RE.FindSubmatch(fqdn)[2]) @@ -115,13 +143,14 @@ func NameToAAAA(fqdnString string) (*dnsmessage.AAAAResource, error) { return &AAAAR, nil } -func SOAResource(domain string) *dnsmessage.SOAResource { +// SOAResource returns the hard-coded SOA +func SOAResource(domain string) dnsmessage.SOAResource { var domainArray [255]byte copy(domainArray[:], domain) - hostmaster := "briancunnie@gmail.com" + hostmaster := "briancunnie.gmail.com." var mboxArray [255]byte copy(mboxArray[:], hostmaster) - return &dnsmessage.SOAResource{ + return dnsmessage.SOAResource{ NS: dnsmessage.Name{ Data: domainArray, Length: uint8(len(domain)), diff --git a/src/xip/xip_test.go b/src/xip/xip_test.go index 1406e06..b42205e 100644 --- a/src/xip/xip_test.go +++ b/src/xip/xip_test.go @@ -1,7 +1,6 @@ package xip_test import ( - "errors" "math/rand" "github.com/cunnie/sslip.io/src/xip" @@ -99,12 +98,22 @@ var _ = Describe("Xip", func() { Expect(err).To(Not(HaveOccurred())) Expect(response).To(Equal(expectedResponse)) }) + When("There is A record cannot be found", func() { + BeforeEach(func() { + name = "not-an-ip.sslip.io." + }) + It("returns the no answers, but returns an authoritative section", func() { + Expect(err).To(Not(HaveOccurred())) + Expect(response.Answers).To(Equal([]dnsmessage.Resource{})) + Expect(response.Authorities).To(Equal(xip.SOAResource(name))) + }) + }) }) - Describe("responseHeader()", func() { + Describe("ResponseHeader()", func() { It("returns a header with the ID", func() { query.ID = uint16(rand.Int31()) - Expect(xip.ResponseHeader(query)).To(Equal(dnsmessage.Header{ + Expect(xip.ResponseHeader(query.Header)).To(Equal(dnsmessage.Header{ ID: query.ID, Response: true, OpCode: 0, @@ -139,7 +148,7 @@ var _ = Describe("Xip", func() { DescribeTable("when it does not match an IP address", func(fqdn string) { _, err := xip.NameToA(fqdn) - Expect(err).To(MatchError(errors.New("ENOTFOUND"))) + Expect(err).To(MatchError("record not found")) }, Entry("empty string", ""), Entry("bare domain", "nono.io"), @@ -167,7 +176,7 @@ var _ = Describe("Xip", func() { func(fqdn string) { _, err := xip.NameToAAAA(fqdn) //ipv4Answer, err := xip.NameToA(fqdn) - Expect(err).To(MatchError(errors.New("ENOTFOUND"))) + Expect(err).To(MatchError("record not found")) //Expect(ipv4Answer).To(Equal(dnsmessage.AAAAResource{})) // is this important to test? }, Entry("empty string", ""),