🐞 TXT Records: only ONE string per record

Previously we were returning one TXT record with multiple strings for
_sslip.io_. That did not work for ProtonMail's domain verification.

It seems a convention that each TXT record has one string. _google.com_,
for example, has a separate TXT record for each string.

It turns out I had misunderstood the
[StackExchange](https://serverfault.com/questions/815841/multiple-txt-fields-for-same-subdomain)
thread.

fixes (from ProtonMail domain verification):

> Verification did not succeed, please try again in an hour.
This commit is contained in:
Brian Cunnie
2020-12-16 09:10:31 -08:00
parent 8da410c029
commit dc20d97adf
2 changed files with 38 additions and 36 deletions

View File

@@ -31,7 +31,7 @@ type DomainCustomization struct {
AAAA []dnsmessage.AAAAResource
CNAME dnsmessage.CNAMEResource
MX []dnsmessage.MXResource
TXT dnsmessage.TXTResource
TXT []dnsmessage.TXTResource
}
type DomainCustomizations map[string]DomainCustomization
@@ -80,14 +80,12 @@ var (
},
},
},
// although multiple TXT records with multiple strings are allowed, we're sticking
// with a single TXT record with multiple strings to simplify things, just like AWS
// does: https://serverfault.com/questions/815841/multiple-txt-fields-for-same-subdomain
TXT: dnsmessage.TXTResource{
TXT: []string{
"protonmail-verification=ce0ca3f5010aa7a2cf8bcc693778338ffde73e26", // protonmail verification; don't delete
"v=spf1 include:_spf.protonmail.ch mx ~all",
},
// Although multiple TXT records with multiple strings are allowed, we're sticking
// with a multiple TXT records with a single string apiece because that's what ProtonMail requires
// and that's what google.com does.
TXT: []dnsmessage.TXTResource{
{TXT: []string{"protonmail-verification=ce0ca3f5010aa7a2cf8bcc693778338ffde73e26"}}, // ProtonMail verification; don't delete
{TXT: []string{"v=spf1 include:_spf.protonmail.ch mx ~all"}}, // Sender Policy Framework
},
},
// nameserver addresses; we get queries for those every once in a while
@@ -307,29 +305,33 @@ func processQuestion(q dnsmessage.Question, b *dnsmessage.Builder) (logMessage s
if err != nil {
return
}
var txt dnsmessage.TXTResource
txt, err = TXTResource(q.Name.String())
var txts []dnsmessage.TXTResource
txts, err = TXTResources(q.Name.String())
if err != nil {
err = noAnswersOnlyAuthorities(q, b, &logMessage)
return
}
err = b.TXTResource(dnsmessage.ResourceHeader{
Name: q.Name,
Type: dnsmessage.TypeTXT,
Class: dnsmessage.ClassINET,
// aggressively expire (5 mins) TXT records, long enough to obtain a Let's Encrypt cert,
// but short enough to free up frequently-used domains (e.g. 192.168.0.1.sslip.io) for the next user
TTL: 300,
Length: 0,
}, txt)
if err != nil {
return
var logMessageTXTss []string
for _, txt := range txts {
err = b.TXTResource(dnsmessage.ResourceHeader{
Name: q.Name,
Type: dnsmessage.TypeTXT,
Class: dnsmessage.ClassINET,
// aggressively expire (5 mins) TXT records, long enough to obtain a Let's Encrypt cert,
// but short enough to free up frequently-used domains (e.g. 192.168.0.1.sslip.io) for the next user
TTL: 300,
Length: 0,
}, txt)
if err != nil {
return
}
var logMessageTXTs []string
for _, TXTstring := range txt.TXT {
logMessageTXTs = append(logMessageTXTs, TXTstring)
}
logMessageTXTss = append(logMessageTXTss, `TXT "`+strings.Join(logMessageTXTs, `", "`)+`"`)
}
var logMessageTXTs []string
for _, TXTstring := range txt.TXT {
logMessageTXTs = append(logMessageTXTs, TXTstring)
}
logMessage += `TXT "` + strings.Join(logMessageTXTs, `", "`) + `"`
logMessage += strings.Join(logMessageTXTss, " ")
}
default:
{
@@ -459,12 +461,12 @@ func SOAResource(fqdnString string) dnsmessage.SOAResource {
}
}
func TXTResource(fqdnString string) (dnsmessage.TXTResource, error) {
func TXTResources(fqdnString string) ([]dnsmessage.TXTResource, error) {
// is it a customized TXT record? If so, return early
if domain, ok := Customizations[fqdnString]; ok {
return domain.TXT, nil
}
return dnsmessage.TXTResource{}, ErrNotFound
return nil, ErrNotFound
}
func noAnswersOnlyAuthorities(q dnsmessage.Question, b *dnsmessage.Builder, logMessage *string) error {

View File

@@ -334,27 +334,27 @@ var _ = Describe("Xip", func() {
})
})
Describe("TXTResource()", func() {
Describe("TXTResources()", func() {
It("returns no TXT resources", func() {
domain := "example.com."
_, err := xip.TXTResource(domain)
_, err := xip.TXTResources(domain)
Expect(err).To(HaveOccurred())
})
When("queried for the sslip.io domain", func() {
It("returns mail-related TXT resources for the sslip.io domain", func() {
domain := "sslip.io."
txt, err := xip.TXTResource(domain)
txt, err := xip.TXTResources(domain)
Expect(err).To(Not(HaveOccurred()))
Expect(len(txt.TXT)).To(Equal(2))
Expect(txt.TXT[0]).To(MatchRegexp("protonmail-verification="))
Expect(txt.TXT[1]).To(MatchRegexp("v=spf1"))
Expect(len(txt)).To(Equal(2))
Expect(txt[0].TXT[0]).To(MatchRegexp("protonmail-verification="))
Expect(txt[1].TXT[0]).To(MatchRegexp("v=spf1"))
})
})
When("a domain has been customized", func() { // Unnecessary, but confirms Golang's behavior for me, a doubting Thomas
customDomain := "some-crazy-domain-name-no-really.io."
xip.Customizations[customDomain] = xip.DomainCustomization{}
It("returns no TXT resources", func() {
_, err := xip.TXTResource(customDomain)
_, err := xip.TXTResources(customDomain)
Expect(err).To(HaveOccurred())
})
delete(xip.Customizations, customDomain) // clean-up