Skip to content

Commit

Permalink
Delegate ALL "_acme-challenge." queries
Browse files Browse the repository at this point in the history
The purpose of this commit is to enable Let's Encrypt DNS-01 challenges
for wildcard certificates.

To accomplish that, we'd like to delegate queries for ALL types (e.g.
NS, SOA, A, AAAA) to the IP address of that server. For example, any
query for `_acme-challenge.52-0-56-137.sslip.io` would be delegated to
the DNS server `52-0-56-137.sslip.io` (whose IP address 52.0.56.137
would be supplied as well).

Thanks @normanr !

On a personal note, I feel the code is getting bloated again. Also, I'm
inconsistent with my parameters: `NSResponse()`, for example, has
arguments which it mutates (`response`), and which are returned
(`logMessage`). This offends my esthetics.

[#6]
  • Loading branch information
cunnie committed Jan 20, 2021
1 parent da96b45 commit 2d49626
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 80 deletions.
67 changes: 41 additions & 26 deletions bosh-release/src/sslip.io-dns-server/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,21 +153,6 @@ var _ = Describe("sslip.io-dns-server", func() {
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeNS example.com. \? ns-aws.nono.io., ns-azure.nono.io., ns-gce.nono.io.\n`))
})
})
When(`the NS record for an "_acme-challenge" domain is queried`, func() {
It(`returns the NS record of the query with the "_acme-challenge." stripped`, func() {
digArgs = "@localhost _acme-challenge.fe80--.sslip.io ns"
digCmd = exec.Command("dig", strings.Split(digArgs, " ")...)
digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter)
Expect(err).ToNot(HaveOccurred())
Eventually(digSession).Should(Say(`flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1`))
Eventually(digSession).Should(Say(`;; ANSWER SECTION:`))
Eventually(digSession).Should(Say(`fe80--.sslip.io.`))
Eventually(digSession).Should(Say(`;; ADDITIONAL SECTION:`))
Eventually(digSession).Should(Say(`fe80--.sslip.io..*fe80::\n`))
Eventually(digSession, 1).Should(Exit(0))
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeNS _acme-challenge.fe80--.sslip.io. \? fe80--.sslip.io.\n`))
})
})
When(`there are multiple TXT records returned (e.g. SPF for sslip.io)`, func() {
It("returns the custom TXT records", func() {
digArgs = "@localhost sslip.io txt +short"
Expand All @@ -192,17 +177,47 @@ var _ = Describe("sslip.io-dns-server", func() {
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeTXT sslip.io. \? \["protonmail-verification=ce0ca3f5010aa7a2cf8bcc693778338ffde73e26"\], \["v=spf1 include:_spf.protonmail.ch mx ~all"\]\n`))
})
})
When(`the TXT record for an "_acme-challenge" domain is queried`, func() {
It(`returns the NS record of the query with the "_acme-challenge." stripped`, func() {
digArgs = "@localhost _acme-challenge.127-0-0-1.sslip.io txt"
digCmd = exec.Command("dig", strings.Split(digArgs, " ")...)
digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter)
Expect(err).ToNot(HaveOccurred())
Eventually(digSession).Should(Say(`flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 1,`))
Eventually(digSession).Should(Say(`;; AUTHORITY SECTION:\n`))
Eventually(digSession).Should(Say(`^_acme-challenge.127-0-0-1.sslip.io. 604800 IN NS 127-0-0-1.sslip.io.\n`))
Eventually(digSession, 1).Should(Exit(0))
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeTXT _acme-challenge.127-0-0-1.sslip.io. \? nil, NS 127-0-0-1.sslip.io.\n`))
When(`a record for an "_acme-challenge" domain is queried`, func() {
When(`it's an NS record`, func() {
It(`returns the NS record of the query with the "_acme-challenge." stripped`, func() {
digArgs = "@localhost _acme-challenge.fe80--.sslip.io ns"
digCmd = exec.Command("dig", strings.Split(digArgs, " ")...)
digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter)
Expect(err).ToNot(HaveOccurred())
Eventually(digSession).Should(Say(`flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1`))
Eventually(digSession).Should(Say(`;; AUTHORITY SECTION:`))
Eventually(digSession).Should(Say(`fe80--.sslip.io.`))
Eventually(digSession).Should(Say(`;; ADDITIONAL SECTION:`))
Eventually(digSession).Should(Say(`fe80--.sslip.io..*fe80::\n`))
Eventually(digSession, 1).Should(Exit(0))
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeNS _acme-challenge.fe80--.sslip.io. \? nil, NS fe80--.sslip.io.\n`))
})
})
When(`it's a TXT record`, func() {
It(`returns the NS record of the query with the "_acme-challenge." stripped`, func() {
digArgs = "@localhost _acme-challenge.127-0-0-1.sslip.io txt"
digCmd = exec.Command("dig", strings.Split(digArgs, " ")...)
digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter)
Expect(err).ToNot(HaveOccurred())
Eventually(digSession).Should(Say(`flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 1,`))
Eventually(digSession).Should(Say(`;; AUTHORITY SECTION:\n`))
Eventually(digSession).Should(Say(`^_acme-challenge.127-0-0-1.sslip.io. 604800 IN NS 127-0-0-1.sslip.io.\n`))
Eventually(digSession, 1).Should(Exit(0))
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeTXT _acme-challenge.127-0-0-1.sslip.io. \? nil, NS 127-0-0-1.sslip.io.\n`))
})
})
When(`it's a A record`, func() {
It(`returns the NS record of the query with the "_acme-challenge." stripped`, func() {
digArgs = "@localhost _acme-challenge.127-0-0-1.sslip.io a"
digCmd = exec.Command("dig", strings.Split(digArgs, " ")...)
digSession, err = Start(digCmd, GinkgoWriter, GinkgoWriter)
Expect(err).ToNot(HaveOccurred())
Eventually(digSession).Should(Say(`flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 1,`))
Eventually(digSession).Should(Say(`;; AUTHORITY SECTION:\n`))
Eventually(digSession).Should(Say(`^_acme-challenge.127-0-0-1.sslip.io. 604800 IN NS 127-0-0-1.sslip.io.\n`))
Eventually(digSession, 1).Should(Exit(0))
Eventually(string(serverSession.Err.Contents())).Should(MatchRegexp(`TypeA _acme-challenge.127-0-0-1.sslip.io. \? nil, NS 127-0-0-1.sslip.io.\n`))
})
})
})
})
Expand Down
147 changes: 93 additions & 54 deletions bosh-release/src/sslip.io-dns-server/xip/xip.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ type DomainCustomization struct {

type DomainCustomizations map[string]DomainCustomization

// There's nothing like global variables to make my heart pound with joy.
// Some of these are global because they are, in essence, constants which
// I don't want to waste time recreating with every function call.
// But `Customizations` is a true global variable.
var (
ipv4REDots = regexp.MustCompile(`(^|[.-])(((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))($|[.-])`)
ipv4REDashes = regexp.MustCompile(`(^|[.-])(((25[0-5]|(2[0-4]|1?[0-9])?[0-9])-){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))($|[.-])`)
Expand Down Expand Up @@ -185,9 +189,15 @@ func QueryResponse(queryBytes []byte) (responseBytes []byte, logMessage string,
return responseBytes, logMessage, nil
}

func processQuestion(q dnsmessage.Question, response *Response) (string, error) {
func processQuestion(q dnsmessage.Question, response *Response) (logMessage string, _ error) {
var err error
var logMessage = q.Type.String() + " " + q.Name.String() + " ? "
logMessage = q.Type.String() + " " + q.Name.String() + " ? "
if IsAcmeChallenge(q.Name.String()) { // thanks @NormanR
// delegate everything to its stripped (remove "_acme-challenge.") address, e.g.
// dig _acme-challenge.127-0-0-1.sslip.io mx → NS 127-0-0-1.sslip.io
response.Header.Authoritative = false // we're delegating, so we're not authoritative
return NSResponse(q.Name, response, logMessage)
}
switch q.Type {
case dnsmessage.TypeA:
{
Expand Down Expand Up @@ -344,58 +354,7 @@ func processQuestion(q dnsmessage.Question, response *Response) (string, error)
}
case dnsmessage.TypeNS:
{
nameServers := NSResources(q.Name.String())
var logMessages []string
response.Answers = append(response.Answers,
func(b *dnsmessage.Builder) error {
for _, nameServer := range nameServers {
err = b.NSResource(dnsmessage.ResourceHeader{
Name: q.Name,
Type: dnsmessage.TypeNS,
Class: dnsmessage.ClassINET,
TTL: 604800, // 60 * 60 * 24 * 7 == 1 week; long TTL, these IP addrs don't change
Length: 0,
}, nameServer)
if err != nil {
return err
}
}
return nil
})
response.Additionals = append(response.Additionals,
func(b *dnsmessage.Builder) error {
for _, nameServer := range nameServers {
for _, aResource := range NameToA(nameServer.NS.String()) {
err = b.AResource(dnsmessage.ResourceHeader{
Name: nameServer.NS,
Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET,
TTL: 604800, // 60 * 60 * 24 * 7 == 1 week; long TTL, these IP addrs don't change
Length: 0,
}, aResource)
if err != nil {
return err
}
}
for _, aaaaResource := range NameToAAAA(nameServer.NS.String()) {
err = b.AAAAResource(dnsmessage.ResourceHeader{
Name: nameServer.NS,
Type: dnsmessage.TypeAAAA,
Class: dnsmessage.ClassINET,
TTL: 604800, // 60 * 60 * 24 * 7 == 1 week; long TTL, these IP addrs don't change
Length: 0,
}, aaaaResource)
if err != nil {
return err
}
}
}
return nil
})
for _, nameServer := range nameServers {
logMessages = append(logMessages, nameServer.NS.String())
}
return logMessage + strings.Join(logMessages, ", "), nil
return NSResponse(q.Name, response, logMessage)
}
case dnsmessage.TypeSOA:
{
Expand Down Expand Up @@ -509,6 +468,86 @@ func processQuestion(q dnsmessage.Question, response *Response) (string, error)
return "", errors.New("unexpectedly fell through processQuestion()")
}

// NSResponse sets the Answers/Authorities depending whether we're delegating or authoritative
// (whether it's an "_acme-challenge." domain or not). Either way, it supplies the Additionals
// (IP addresses of the nameservers).
func NSResponse(name dnsmessage.Name, response *Response, logMessage string) (string, error) {
nameServers := NSResources(name.String())
var logMessages []string
if response.Header.Authoritative {
// we're authoritative, so we reply with the answers
response.Answers = append(response.Answers,
func(b *dnsmessage.Builder) error {
for _, nameServer := range nameServers {
err := b.NSResource(dnsmessage.ResourceHeader{
Name: name,
Type: dnsmessage.TypeNS,
Class: dnsmessage.ClassINET,
TTL: 604800, // 60 * 60 * 24 * 7 == 1 week; long TTL, these IP addrs don't change
Length: 0,
}, nameServer)
if err != nil {
return err
}
}
return nil
})
} else {
// we're NOT authoritative, so we reply who is authoritative
response.Authorities = append(response.Authorities,
func(b *dnsmessage.Builder) error {
for _, nameServer := range nameServers {
err := b.NSResource(dnsmessage.ResourceHeader{
Name: name,
Type: dnsmessage.TypeNS,
Class: dnsmessage.ClassINET,
TTL: 604800, // 60 * 60 * 24 * 7 == 1 week; long TTL, these IP addrs don't change
Length: 0,
}, nameServer)
if err != nil {
return err
}
}
return nil
})
logMessage += "nil, NS " // we're not supplying an answer; we're supplying the NS record that's authoritative
}
response.Additionals = append(response.Additionals,
func(b *dnsmessage.Builder) error {
for _, nameServer := range nameServers {
for _, aResource := range NameToA(nameServer.NS.String()) {
err := b.AResource(dnsmessage.ResourceHeader{
Name: nameServer.NS,
Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET,
TTL: 604800, // 60 * 60 * 24 * 7 == 1 week; long TTL, these IP addrs don't change
Length: 0,
}, aResource)
if err != nil {
return err
}
}
for _, aaaaResource := range NameToAAAA(nameServer.NS.String()) {
err := b.AAAAResource(dnsmessage.ResourceHeader{
Name: nameServer.NS,
Type: dnsmessage.TypeAAAA,
Class: dnsmessage.ClassINET,
TTL: 604800, // 60 * 60 * 24 * 7 == 1 week; long TTL, these IP addrs don't change
Length: 0,
}, aaaaResource)
if err != nil {
return err
}
}
}
return nil
})
for _, nameServer := range nameServers {
logMessages = append(logMessages, nameServer.NS.String())
}
return logMessage + strings.Join(logMessages, ", "), nil
}

// ResponseHeader returns a pre-fab DNS response header.
// We are almost always authoritative (exception: _acme-challenge TXT records)
// We are not recursing
Expand Down

0 comments on commit 2d49626

Please sign in to comment.