_acme-challenge. TXT records return NS not SOA

This change is to enable wildcard certificates via DNS challenge.

My earlier attempt failed: when queried for TXT for
`_acme-challenge.127-0-0-1.sslip.io`, I returned an authoritative
response with the Authorities section containing the SOA record, which
signaled, "There is no TXT record, of that I am sure." Even though I had
configured an NS record `_acme-challenge.127-0-0-1.sslip.io` to return
`127-0-0-1.sslip.io`.

Now I return an authoritative response with an NS record, not an SOA
record, in the response.

But that's still not enough, and I'd like to do the following changes:

- When queried for a DNS-01 challenge, e.g.
`_acme-challenge.127-0-0-1.sslip.io`, I return a _non-authoritative_
response with the NS record. This is the behavior of, say, `dig ns
sslip.io @a0.nic.io.`

- When queried for any NS records, I return an Additionals section
with the IP addresses.
This commit is contained in:
Brian Cunnie
2021-01-12 06:17:37 -08:00
parent 0d0acfe318
commit f42417da7e
2 changed files with 117 additions and 14 deletions

View File

@@ -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
}

View File

@@ -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) {