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.
This commit is contained in:
Brian Cunnie
2020-09-16 20:04:25 -07:00
parent 7b3fdd9c04
commit 67acbb7f47
2 changed files with 78 additions and 40 deletions

View File

@@ -15,52 +15,77 @@ import (
// https://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses // 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 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 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) // 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 // 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 // main(). main() is hard to unit test, but functions like QueryResponse are
// easy. // easy.
func QueryResponse(queryBytes []byte) ([]byte, error) { func QueryResponse(queryBytes []byte) ([]byte, error) {
var query dnsmessage.Message var queryHeader dnsmessage.Header
var err error
var response []byte
err := query.Unpack(queryBytes) p := dnsmessage.Parser{}
if err != nil { if queryHeader, err = p.Start(queryBytes); err != nil {
return nil, err return nil, err
} }
response := dnsmessage.Message{ b := dnsmessage.NewBuilder(response, ResponseHeader(queryHeader))
Header: ResponseHeader(query), b.EnableCompression()
Questions: query.Questions, for {
q, err := p.Question()
if err == dnsmessage.ErrSectionDone {
break
} }
if err != nil {
for _, question := range query.Questions { return nil, err
fqdn := string(question.Name.Data[:question.Name.Length]) }
switch question.Type { switch q.Type {
case dnsmessage.TypeA: case dnsmessage.TypeA:
{ {
nameToA, err := NameToA(fqdn) nameToA, err := NameToA(q.Name.String())
if err != nil { if err != nil {
// note that this could be written more efficiently; however, I wrote it // There's only one possible error this can be: ErrNotFound. note that
// to accommodate 'if err != nil' codepath. My first version was 'if err == nil', // this could be written more efficiently; however, I wrote it to
// and it flummoxed me. // accommodate 'if err != nil' convention. My first version was 'if
response.Answers = append(response.Answers, dnsmessage.Resource{}) // err == nil', and it flummoxed me.
} else { err = b.StartAuthorities()
response.Answers = append(response.Answers, dnsmessage.Resource{ if err != nil {
Header: dnsmessage.ResourceHeader{ return nil, err
Name: question.Name, }
err = b.SOAResource(dnsmessage.ResourceHeader{
Name: q.Name,
Type: dnsmessage.TypeA, Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET, Class: dnsmessage.ClassINET,
TTL: 300, TTL: 604800, // 60 * 60 * 24 * 7 == 1 week; it's not gonna change
Length: 4, // A records are always 4 bytes Length: 0,
}, }, SOAResource(q.Name.String()))
Body: nameToA, if err != nil {
}) return nil, err
}
} else {
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. // I couldn't figure an easy way to test the error condition in Ginkgo. Sue me.
if err != nil { if err != nil {
return nil, err 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 // ResponseHeader returns a pre-fab DNS response header. Note that we're always
// authoritative and therefore recursion is never available. We're able to // authoritative and therefore recursion is never available. We're able to
// "white label" domains by indiscriminately matching every query that comes // "white label" domains by indiscriminately matching every query that comes
// our way. Not being recursive has the added benefit of not worrying // our way. Not being recursive has the added benefit of not being usable as an
// being used as an amplifier in a DDOS attack // amplifier in a DDOS attack
func ResponseHeader(query dnsmessage.Message) dnsmessage.Header { func ResponseHeader(query dnsmessage.Header) dnsmessage.Header {
return dnsmessage.Header{ return dnsmessage.Header{
ID: query.ID, ID: query.ID,
Response: true, 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) { func NameToA(fqdnString string) (*dnsmessage.AResource, error) {
fqdn := []byte(fqdnString) fqdn := []byte(fqdnString)
if !ipv4RE.Match(fqdn) { 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]) 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 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) { func NameToAAAA(fqdnString string) (*dnsmessage.AAAAResource, error) {
fqdn := []byte(fqdnString) fqdn := []byte(fqdnString)
if !ipv6RE.Match(fqdn) { 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]) match := string(ipv6RE.FindSubmatch(fqdn)[2])
@@ -115,13 +143,14 @@ func NameToAAAA(fqdnString string) (*dnsmessage.AAAAResource, error) {
return &AAAAR, nil 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 var domainArray [255]byte
copy(domainArray[:], domain) copy(domainArray[:], domain)
hostmaster := "briancunnie@gmail.com" hostmaster := "briancunnie.gmail.com."
var mboxArray [255]byte var mboxArray [255]byte
copy(mboxArray[:], hostmaster) copy(mboxArray[:], hostmaster)
return &dnsmessage.SOAResource{ return dnsmessage.SOAResource{
NS: dnsmessage.Name{ NS: dnsmessage.Name{
Data: domainArray, Data: domainArray,
Length: uint8(len(domain)), Length: uint8(len(domain)),

View File

@@ -1,7 +1,6 @@
package xip_test package xip_test
import ( import (
"errors"
"math/rand" "math/rand"
"github.com/cunnie/sslip.io/src/xip" "github.com/cunnie/sslip.io/src/xip"
@@ -99,12 +98,22 @@ var _ = Describe("Xip", func() {
Expect(err).To(Not(HaveOccurred())) Expect(err).To(Not(HaveOccurred()))
Expect(response).To(Equal(expectedResponse)) 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() { It("returns a header with the ID", func() {
query.ID = uint16(rand.Int31()) 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, ID: query.ID,
Response: true, Response: true,
OpCode: 0, OpCode: 0,
@@ -139,7 +148,7 @@ var _ = Describe("Xip", func() {
DescribeTable("when it does not match an IP address", DescribeTable("when it does not match an IP address",
func(fqdn string) { func(fqdn string) {
_, err := xip.NameToA(fqdn) _, err := xip.NameToA(fqdn)
Expect(err).To(MatchError(errors.New("ENOTFOUND"))) Expect(err).To(MatchError("record not found"))
}, },
Entry("empty string", ""), Entry("empty string", ""),
Entry("bare domain", "nono.io"), Entry("bare domain", "nono.io"),
@@ -167,7 +176,7 @@ var _ = Describe("Xip", func() {
func(fqdn string) { func(fqdn string) {
_, err := xip.NameToAAAA(fqdn) _, err := xip.NameToAAAA(fqdn)
//ipv4Answer, err := xip.NameToA(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? //Expect(ipv4Answer).To(Equal(dnsmessage.AAAAResource{})) // is this important to test?
}, },
Entry("empty string", ""), Entry("empty string", ""),