mirror of
https://github.com/cunnie/sslip.io.git
synced 2025-10-06 16:18:00 +08:00
_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:
@@ -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
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
Reference in New Issue
Block a user