Summary
The query answer path does not split TXT/SPF rdata into ≤255-byte DNS character-strings. Any TXT (or SPF) record whose rdata exceeds 255 bytes fails to serialize, and go53 returns no response at all (UDP and TCP) for that qname. Resolvers therefore report SERVFAIL.
Real-world impact: every 2048-bit DKIM record is unreachable (a 2048-bit v=DKIM1;...;p=<base64> value is ~400+ bytes). 1024-bit DKIM (<255 bytes) resolves fine, which is why the bug went unnoticed. Long SPF/verification TXT values are affected too.
Observed on v0.79.0.
Severity
Critical. A single short, valid record silently disables answering for that name across all transports, breaking email authentication (DKIM) for affected domains. Because the failure is a silent drop of an otherwise valid, common record, it is also a latent availability footgun for any zone hosting long TXT values.
Root cause
chunkTXT() (which correctly splits long strings into ≤255-byte character-strings) lives in internal/rrbuilder.go and is used by the zone-build / AXFR / signing RR construction — but it is never called on the live query answer path.
The answer path builds the RR with the full string as a single character-string:
zone/rtypes/txt.go (Lookup) → &dns.TXT{ ..., Txt: []string{rec.Text} }
zone/rtypes/spf.go (Lookup) → &dns.SPF{ ..., Txt: []string{rec.Text} }
Call chain for a normal query:
dns/handler.go zone.LookupRecord(q.Qtype, q.Name)
-> zone/zone.go LookupRecord: rtypes.Get(rrtype).Lookup(name)
-> zone/rtypes/txt.go Lookup: &dns.TXT{ Txt: []string{rec.Text} } // <-- single unchunked string
When rec.Text is > 255 bytes, miekg/dns refuses to pack the character-string (255-byte limit), the whole message fails to serialize, and the handler writes nothing back to the client. This is independent of DNSSEC — the failure is in packing the TXT/SPF RR itself.
grep -rn chunkTXT zone/rtypes/ → no hits. chunkTXT is only referenced in internal/rrbuilder.go.
Note: the earlier "TXT chunking" fix (v0.77.3, commit 3c1269d) added chunkTXT to internal/rrbuilder.go only and missed the serve path, so the bug persists on the actual query hot path.
Reproduction
- Create a zone and add a TXT record with rdata > 255 bytes, e.g. a 2048-bit DKIM key:
<selector>._domainkey TXT "v=DKIM1;k=rsa;p=<~392 base64 chars>"
- Query it:
dig @<go53> TXT <selector>._domainkey.<zone> — also try +tcp and +dnssec.
- Observed: no answer on UDP or TCP (client times out / SERVFAIL upstream).
Expected: the TXT RRset returned with the value split into multiple ≤255-byte character-strings (which resolvers concatenate).
- A short TXT at the same name (e.g.
_dmarc) answers normally — confirming it is length-related, not zone/record-existence.
Proposed fix
Apply chunkTXT() on the answer path so serve and zone-build agree:
zone/rtypes/txt.go: Txt: chunkTXT(rec.Text) instead of Txt: []string{rec.Text}
zone/rtypes/spf.go: same for the SPF RR
chunkTXT is safe for short values (returns the string unchanged when ≤255 bytes), so this is a drop-in. It would be worth centralizing the TXT/SPF RR construction so internal/rrbuilder.go and zone/rtypes/* cannot diverge again, and adding a regression test that queries a >255-byte TXT end-to-end (pack + wire round-trip), asserting multiple character-strings are emitted.
Workaround (until a release)
Rotate affected domains to a 1024-bit DKIM key so the rdata packs under 255 bytes.
Filed from downstream operations after a production DKIM outage; root cause traced to the serve path. Happy to open a PR for the chunkTXT serve-path fix + regression test.
Summary
The query answer path does not split TXT/SPF rdata into ≤255-byte DNS character-strings. Any TXT (or SPF) record whose rdata exceeds 255 bytes fails to serialize, and go53 returns no response at all (UDP and TCP) for that qname. Resolvers therefore report SERVFAIL.
Real-world impact: every 2048-bit DKIM record is unreachable (a 2048-bit
v=DKIM1;...;p=<base64>value is ~400+ bytes). 1024-bit DKIM (<255 bytes) resolves fine, which is why the bug went unnoticed. Long SPF/verification TXT values are affected too.Observed on v0.79.0.
Severity
Critical. A single short, valid record silently disables answering for that name across all transports, breaking email authentication (DKIM) for affected domains. Because the failure is a silent drop of an otherwise valid, common record, it is also a latent availability footgun for any zone hosting long TXT values.
Root cause
chunkTXT()(which correctly splits long strings into ≤255-byte character-strings) lives ininternal/rrbuilder.goand is used by the zone-build / AXFR / signing RR construction — but it is never called on the live query answer path.The answer path builds the RR with the full string as a single character-string:
zone/rtypes/txt.go(Lookup) →&dns.TXT{ ..., Txt: []string{rec.Text} }zone/rtypes/spf.go(Lookup) →&dns.SPF{ ..., Txt: []string{rec.Text} }Call chain for a normal query:
When
rec.Textis > 255 bytes,miekg/dnsrefuses to pack the character-string (255-byte limit), the whole message fails to serialize, and the handler writes nothing back to the client. This is independent of DNSSEC — the failure is in packing the TXT/SPF RR itself.grep -rn chunkTXT zone/rtypes/→ no hits.chunkTXTis only referenced ininternal/rrbuilder.go.Note: the earlier "TXT chunking" fix (v0.77.3, commit 3c1269d) added
chunkTXTtointernal/rrbuilder.goonly and missed the serve path, so the bug persists on the actual query hot path.Reproduction
<selector>._domainkey TXT "v=DKIM1;k=rsa;p=<~392 base64 chars>"dig @<go53> TXT <selector>._domainkey.<zone>— also try+tcpand+dnssec.Expected: the TXT RRset returned with the value split into multiple ≤255-byte character-strings (which resolvers concatenate).
_dmarc) answers normally — confirming it is length-related, not zone/record-existence.Proposed fix
Apply
chunkTXT()on the answer path so serve and zone-build agree:zone/rtypes/txt.go:Txt: chunkTXT(rec.Text)instead ofTxt: []string{rec.Text}zone/rtypes/spf.go: same for the SPF RRchunkTXTis safe for short values (returns the string unchanged when ≤255 bytes), so this is a drop-in. It would be worth centralizing the TXT/SPF RR construction sointernal/rrbuilder.goandzone/rtypes/*cannot diverge again, and adding a regression test that queries a >255-byte TXT end-to-end (pack + wire round-trip), asserting multiple character-strings are emitted.Workaround (until a release)
Rotate affected domains to a 1024-bit DKIM key so the rdata packs under 255 bytes.
Filed from downstream operations after a production DKIM outage; root cause traced to the serve path. Happy to open a PR for the
chunkTXTserve-path fix + regression test.