diff --git a/bosh-release/src/sslip.io-dns-server/xip/xip.go b/bosh-release/src/sslip.io-dns-server/xip/xip.go index f1c0892..d4f0e77 100644 --- a/bosh-release/src/sslip.io-dns-server/xip/xip.go +++ b/bosh-release/src/sslip.io-dns-server/xip/xip.go @@ -189,7 +189,7 @@ func processQuestion(q dnsmessage.Question, b *dnsmessage.Builder) (logMessage s // 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 = noAnswersOnlyAuthorities(q, b, &logMessage) + err = noAnswerOnlySoaAuthority(q, b, &logMessage) return } else { err = b.StartAnswers() @@ -220,7 +220,7 @@ func processQuestion(q dnsmessage.Question, b *dnsmessage.Builder) (logMessage s nameToAAAAs, err = NameToAAAA(q.Name.String()) if err != nil { // There's only one possible error this can be: ErrNotFound - err = noAnswersOnlyAuthorities(q, b, &logMessage) + err = noAnswerOnlySoaAuthority(q, b, &logMessage) return } else { err = b.StartAnswers() @@ -262,7 +262,7 @@ func processQuestion(q dnsmessage.Question, b *dnsmessage.Builder) (logMessage s var cname *dnsmessage.CNAMEResource cname, err = CNAMEResource(q.Name.String()) if err != nil { - err = noAnswersOnlyAuthorities(q, b, &logMessage) + err = noAnswerOnlySoaAuthority(q, b, &logMessage) return } err = b.CNAMEResource(dnsmessage.ResourceHeader{ @@ -341,6 +341,9 @@ func processQuestion(q dnsmessage.Question, b *dnsmessage.Builder) (logMessage s } case dnsmessage.TypeTXT: { + // if the TXT record is customized, we return that + // if it's an "_acme-challenge." TXT, we return no answer but an NS authority not SOA authority + // otherwise we return the usual `noAnswerOnlySoaAuthority` err = b.StartAnswers() if err != nil { return @@ -348,7 +351,11 @@ func processQuestion(q dnsmessage.Question, b *dnsmessage.Builder) (logMessage s var txts []dnsmessage.TXTResource txts, err = TXTResources(q.Name.String()) if err != nil { - err = noAnswersOnlyAuthorities(q, b, &logMessage) + if IsAcmeChallenge(q.Name.String()) { + err = noAnswerOnlyNsAuthority(q, b, &logMessage) + return + } + err = noAnswerOnlySoaAuthority(q, b, &logMessage) return } var logMessageTXTss []string @@ -377,7 +384,7 @@ func processQuestion(q dnsmessage.Question, b *dnsmessage.Builder) (logMessage s { // default is the same case as an A/AAAA record which is not found, // i.e. we return no answers, but we return an authority section - err = noAnswersOnlyAuthorities(q, b, &logMessage) + err = noAnswerOnlySoaAuthority(q, b, &logMessage) return } } @@ -471,24 +478,28 @@ func MxResources(fqdnString string) []dnsmessage.MXResource { } } -func NSResources(fqdnString string) []dnsmessage.NSResource { - if dns01ChallengeRE.Match([]byte(fqdnString)) { - strippedFqdn := dns01ChallengeRE.ReplaceAllString(fqdnString, "") +func IsAcmeChallenge(fqdnString string) bool { + if dns01ChallengeRE.MatchString(fqdnString) { _, errIPv4 := NameToA(fqdnString) _, errIPv6 := NameToAAAA(fqdnString) if errIPv4 == nil || errIPv6 == nil { - ns, _ := dnsmessage.NewName(strippedFqdn) - return []dnsmessage.NSResource{ - {NS: ns}} + return true } } + return false +} + +func NSResources(fqdnString string) []dnsmessage.NSResource { + if IsAcmeChallenge(fqdnString) { + strippedFqdn := dns01ChallengeRE.ReplaceAllString(fqdnString, "") + ns, _ := dnsmessage.NewName(strippedFqdn) + return []dnsmessage.NSResource{{NS: ns}} + } return NameServers } // SOAResource returns the hard-coded (except MNAME) SOA func SOAResource(fqdnString string) dnsmessage.SOAResource { - var domainBytes [255]byte - copy(domainBytes[:], fqdnString) mname, _ := dnsmessage.NewName(fqdnString) return dnsmessage.SOAResource{ NS: mname, @@ -509,7 +520,7 @@ func TXTResources(fqdnString string) ([]dnsmessage.TXTResource, error) { return nil, ErrNotFound } -func noAnswersOnlyAuthorities(q dnsmessage.Question, b *dnsmessage.Builder, logMessage *string) error { +func noAnswerOnlySoaAuthority(q dnsmessage.Question, b *dnsmessage.Builder, logMessage *string) error { err := b.StartAuthorities() if err != nil { return err @@ -527,3 +538,31 @@ func noAnswersOnlyAuthorities(q dnsmessage.Question, b *dnsmessage.Builder, logM *logMessage += "nil, SOA" return nil } + +// noAnswerOnlyNsAuthority is a corner-case when it's a query for a TXT record for +// a domain that has "_acme-challenge." in it; in that case we return an NS record +// to the domain queried, e.g. "127-0-0-1.sslip.io", which has the "_acme-challenge." +// stripped. +func noAnswerOnlyNsAuthority(q dnsmessage.Question, b *dnsmessage.Builder, logMessage *string) error { + err := b.StartAuthorities() + if err != nil { + return err + } + var nsNames []string + nses := NSResources(q.Name.String()) + for _, ns := range nses { + nsNames = append(nsNames, ns.NS.String()) + err = b.NSResource(dnsmessage.ResourceHeader{ + Name: q.Name, + Type: dnsmessage.TypeNS, + Class: dnsmessage.ClassINET, + TTL: 604800, // 60 * 60 * 24 * 7 == 1 week; it's not gonna change + Length: 0, + }, ns) + if err != nil { + return err + } + } + *logMessage += "nil, NS " + strings.Join(nsNames, ", ") + return nil +} diff --git a/bosh-release/src/sslip.io-dns-server/xip/xip_test.go b/bosh-release/src/sslip.io-dns-server/xip/xip_test.go index 8e97a4a..3ff1936 100644 --- a/bosh-release/src/sslip.io-dns-server/xip/xip_test.go +++ b/bosh-release/src/sslip.io-dns-server/xip/xip_test.go @@ -264,6 +264,41 @@ var _ = Describe("Xip", func() { Expect(len(response.Additionals)).To(Equal(0)) }) }) + When("an '_acme-challenge.' with an embedded IP address is requested", func() { + BeforeEach(func() { + name = "_acme-challenge.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.TypeTXT + + expectedNSes := xip.NSResources(name) + Expect(len(expectedNSes)).To(Equal(1)) + expectedAuthority := dnsmessage.Resource{ + Header: dnsmessage.ResourceHeader{ + Name: dnsmessage.Name{ + Data: nameArray, + Length: uint8(len(name)), + }, + Type: dnsmessage.TypeNS, + Class: dnsmessage.ClassINET, + TTL: 604800, + Length: 20, + }, + Body: &expectedNSes[0], + } + expectedResponse.Authorities = append(expectedResponse.Authorities, expectedAuthority) + }) + It("responds with no answers but with an authority of an NS server", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(logMessage).To(Equal("TypeTXT _acme-challenge.127-0-0-1.sslip.io. ? nil, NS 127-0-0-1.sslip.io.")) + 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])) + }) + }) }) Describe("ResponseHeader()", func() { @@ -503,6 +538,35 @@ var _ = Describe("Xip", func() { }) }) + Describe("IsAcmeChallenge()", func() { + When("the domain doesn't have '_acme-challenge.' in it", func() { + It("returns false", func() { + randomDomain := random8ByteString() + ".com." + Expect(xip.IsAcmeChallenge(randomDomain)).To(BeFalse()) + }) + It("returns false even when there are embedded IPs", func() { + randomDomain := "127.0.0.1." + random8ByteString() + ".com." + Expect(xip.IsAcmeChallenge(randomDomain)).To(BeFalse()) + }) + }) + When("it has '_acme-challenge.' in it", func() { + When("it does NOT have any embedded IPs", func() { + It("returns false", func() { + randomDomain := "_acme-challenge." + random8ByteString() + ".com." + Expect(xip.IsAcmeChallenge(randomDomain)).To(BeFalse()) + }) + }) + When("it has embedded IPs", func() { + It("returns true", func() { + randomDomain := "_acme-challenge.127.0.0.1." + random8ByteString() + ".com." + Expect(xip.IsAcmeChallenge(randomDomain)).To(BeTrue()) + randomDomain = "_acme-challenge.fe80--1." + random8ByteString() + ".com." + Expect(xip.IsAcmeChallenge(randomDomain)).To(BeTrue()) + }) + }) + }) + }) + Describe("NameToAAAA()", func() { DescribeTable("when it succeeds", func(fqdn string, expectedAAAA dnsmessage.AAAAResource) {