🐞 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 AAAA []dnsmessage.AAAAResource
CNAME dnsmessage.CNAMEResource CNAME dnsmessage.CNAMEResource
MX []dnsmessage.MXResource MX []dnsmessage.MXResource
TXT dnsmessage.TXTResource TXT []dnsmessage.TXTResource
} }
type DomainCustomizations map[string]DomainCustomization type DomainCustomizations map[string]DomainCustomization
@@ -80,14 +80,12 @@ var (
}, },
}, },
}, },
// although multiple TXT records with multiple strings are allowed, we're sticking // 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 // with a multiple TXT records with a single string apiece because that's what ProtonMail requires
// does: https://serverfault.com/questions/815841/multiple-txt-fields-for-same-subdomain // and that's what google.com does.
TXT: dnsmessage.TXTResource{ TXT: []dnsmessage.TXTResource{
TXT: []string{ {TXT: []string{"protonmail-verification=ce0ca3f5010aa7a2cf8bcc693778338ffde73e26"}}, // ProtonMail verification; don't delete
"protonmail-verification=ce0ca3f5010aa7a2cf8bcc693778338ffde73e26", // protonmail verification; don't delete {TXT: []string{"v=spf1 include:_spf.protonmail.ch mx ~all"}}, // Sender Policy Framework
"v=spf1 include:_spf.protonmail.ch mx ~all",
},
}, },
}, },
// nameserver addresses; we get queries for those every once in a while // 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 { if err != nil {
return return
} }
var txt dnsmessage.TXTResource var txts []dnsmessage.TXTResource
txt, err = TXTResource(q.Name.String()) txts, err = TXTResources(q.Name.String())
if err != nil { if err != nil {
err = noAnswersOnlyAuthorities(q, b, &logMessage) err = noAnswersOnlyAuthorities(q, b, &logMessage)
return return
} }
err = b.TXTResource(dnsmessage.ResourceHeader{ var logMessageTXTss []string
Name: q.Name, for _, txt := range txts {
Type: dnsmessage.TypeTXT, err = b.TXTResource(dnsmessage.ResourceHeader{
Class: dnsmessage.ClassINET, Name: q.Name,
// aggressively expire (5 mins) TXT records, long enough to obtain a Let's Encrypt cert, Type: dnsmessage.TypeTXT,
// but short enough to free up frequently-used domains (e.g. 192.168.0.1.sslip.io) for the next user Class: dnsmessage.ClassINET,
TTL: 300, // aggressively expire (5 mins) TXT records, long enough to obtain a Let's Encrypt cert,
Length: 0, // but short enough to free up frequently-used domains (e.g. 192.168.0.1.sslip.io) for the next user
}, txt) TTL: 300,
if err != nil { Length: 0,
return }, 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 logMessage += strings.Join(logMessageTXTss, " ")
for _, TXTstring := range txt.TXT {
logMessageTXTs = append(logMessageTXTs, TXTstring)
}
logMessage += `TXT "` + strings.Join(logMessageTXTs, `", "`) + `"`
} }
default: 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 // is it a customized TXT record? If so, return early
if domain, ok := Customizations[fqdnString]; ok { if domain, ok := Customizations[fqdnString]; ok {
return domain.TXT, nil return domain.TXT, nil
} }
return dnsmessage.TXTResource{}, ErrNotFound return nil, ErrNotFound
} }
func noAnswersOnlyAuthorities(q dnsmessage.Question, b *dnsmessage.Builder, logMessage *string) error { 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() { It("returns no TXT resources", func() {
domain := "example.com." domain := "example.com."
_, err := xip.TXTResource(domain) _, err := xip.TXTResources(domain)
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
}) })
When("queried for the sslip.io domain", func() { When("queried for the sslip.io domain", func() {
It("returns mail-related TXT resources for the sslip.io domain", func() { It("returns mail-related TXT resources for the sslip.io domain", func() {
domain := "sslip.io." domain := "sslip.io."
txt, err := xip.TXTResource(domain) txt, err := xip.TXTResources(domain)
Expect(err).To(Not(HaveOccurred())) Expect(err).To(Not(HaveOccurred()))
Expect(len(txt.TXT)).To(Equal(2)) Expect(len(txt)).To(Equal(2))
Expect(txt.TXT[0]).To(MatchRegexp("protonmail-verification=")) Expect(txt[0].TXT[0]).To(MatchRegexp("protonmail-verification="))
Expect(txt.TXT[1]).To(MatchRegexp("v=spf1")) 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 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." customDomain := "some-crazy-domain-name-no-really.io."
xip.Customizations[customDomain] = xip.DomainCustomization{} xip.Customizations[customDomain] = xip.DomainCustomization{}
It("returns no TXT resources", func() { It("returns no TXT resources", func() {
_, err := xip.TXTResource(customDomain) _, err := xip.TXTResources(customDomain)
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
}) })
delete(xip.Customizations, customDomain) // clean-up delete(xip.Customizations, customDomain) // clean-up