Skip to content

TRANSIP: Write TXT records unquoted (v5 regression of #2708) - #4824

Merged
TomOnTime merged 2 commits into
DNSControl:mainfrom
cafferata:fix/transip-txt-quoting
Aug 28, 2026
Merged

TomOnTime merged 2 commits into
DNSControl:mainfrom
cafferata:fix/transip-txt-quoting

Conversation

@cafferata

Copy link
Copy Markdown
Member

What this fixes

Since v5.0.0 the TransIP provider writes TXT record content in RFC presentation format (enclosed in double-quotes). TransIP stores that content verbatim, so the quote characters become literal data in DNS. Every TXT record managed via TransIP (SPF, DKIM, DMARC, verification tokens) is served with a leading/trailing ", which breaks anything that expects a bare value. For example, an SPF validator reports No SPF record found / should include .... This is a regression of #2708 ("TRANSIP: Fix TXT quoting").

Root cause, in providers/transip/transipProvider.go recordToNative(): config.GetRDATA().String() returns the quoted presentation form for TXT. #2708 had set this field to the unquoted form; the v5 RDATA refactor reintroduced the quoting.

The fix

Mirror the inwx provider (another provider that stores raw TXT):

  • Write the raw, unquoted value with GetTargetTXTJoined() for TXT.
  • Read it back with the TxtDontParse flag so the content is parsed as raw data. The flag only affects TXT; other record types are unchanged.

Reproduction

$ dig +noall +answer TXT deprobleemoplosser.nl @ns0.transip.net
deprobleemoplosser.nl.  86400  IN  TXT  "\"v=spf1 include:_spf.protonmail.ch include:spf.flowmailer.net -all\""

The stored value is "v=spf1 include:_spf.protonmail.ch include:spf.flowmailer.net -all", including the surrounding " characters. dnscontrol preview/push print the normalized model (v=spf1 ...), so the diff looks correct; only dig reveals the literal quotes, and a re-run reports no drift, so the zone stays silently broken.

Testing

  • Updated TestNativeToRecordUsesV3RecordConfig (the TXT case now reflects the raw/unquoted contract) and added TestRecordToNativeTXTUnquoted (write-side plus round-trip) and TestNativeToRecordHealsLegacyQuotedTXT (a legacy quoted value is read back with quotes intact so a push rewrites it unquoted).
  • Regenerated the recordToNative golden file with -update; the only change is TXT content fields losing their enclosing quotes.
  • go test ./providers/transip/... and go build ./... pass.
  • Verified against a live TransIP zone: after this change the affected TXT records are served unquoted, and a subsequent preview shows no quoting drift.

@blackshadev blackshadev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lgtm. Due to limited time I am not able to test it. But the code looks good to me.

@cafferata

Copy link
Copy Markdown
Member Author

Hi @blackshadev, good to cross paths again here. You mentioned you didn't have time to test this, so let me help out by sharing the results of the testing I ran, including against a live TransIP zone.

Unit tests (go test ./providers/transip/...):

--- PASS: TestRecordToNativeGolden/dnscontrol.nl
--- PASS: TestNativeToRecordUsesV3RecordConfig  (A, TXT, MX)
--- PASS: TestRecordToNativeTXTUnquoted
--- PASS: TestNativeToRecordHealsLegacyQuotedTXT
ok  github.com/DNSControl/dnscontrol/v5/providers/transip

The golden regeneration touches only TXT content fields (enclosing quotes removed); other record types are unchanged.

Live TransIP zone. Before this change, a push on v5.0.0 stored the quotes as record data:

$ dig +noall +answer TXT deprobleemoplosser.nl @ns0.transip.net
deprobleemoplosser.nl.  86400  IN  TXT  "\"v=spf1 include:_spf.protonmail.ch include:spf.flowmailer.net -all\""

After a push built from this branch:

$ dig +noall +answer TXT deprobleemoplosser.nl @ns0.transip.net
deprobleemoplosser.nl.         86400  IN  TXT  "v=spf1 include:_spf.protonmail.ch include:spf.flowmailer.net -all"
return.deprobleemoplosser.nl.  86400  IN  TXT  "v=spf1 include:spf.flowmailer.net ~all"

DKIM and Microsoft/Google verification TXT records healed the same way across ~16 zones.

Idempotency. Re-running preview with the patched binary reports no quoting drift on the now-unquoted records. The only remaining diff on my zones is an unrelated np= tag that the v5 DMARC_BUILDER adds.

Happy to provide more detail if useful.

Comment thread providers/transip/transipProvider.go Outdated
Comment on lines 280 to 295
func recordToNative(config *models.RecordConfig) (domain.DNSEntry, error) {
// TransIP stores the TXT "content" field verbatim, so it must be the raw,
// unquoted value. GetRDATA().String() would emit the RFC presentation form
// (enclosed in double-quotes), which TransIP would then serve as literal
// data, breaking SPF/DKIM/DMARC. Use the unquoted target instead.
content := config.GetRDATA().String()
if config.Type == "TXT" {
content = config.GetTargetTXTJoined()
}
return domain.DNSEntry{
Name: config.Name,
Expire: int(config.TTL),
Type: config.Type,
Content: config.GetRDATA().String(),
Content: content,
}, nil
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Rename "config" to "rc".
  • Use a var/switch instead of if/then.
  • Less verbose comments.
Suggested change
func recordToNative(config *models.RecordConfig) (domain.DNSEntry, error) {
// TransIP stores the TXT "content" field verbatim, so it must be the raw,
// unquoted value. GetRDATA().String() would emit the RFC presentation form
// (enclosed in double-quotes), which TransIP would then serve as literal
// data, breaking SPF/DKIM/DMARC. Use the unquoted target instead.
content := config.GetRDATA().String()
if config.Type == "TXT" {
content = config.GetTargetTXTJoined()
}
return domain.DNSEntry{
Name: config.Name,
Expire: int(config.TTL),
Type: config.Type,
Content: config.GetRDATA().String(),
Content: content,
}, nil
}
func recordToNative(rc *models.RecordConfig) (domain.DNSEntry, error) {
var content string
switch rc.TypeNum {
case dnsv2.TypeTXT:
// TransIP stores the TXT "content" field verbatim.
content = rc.GetTargetTXTJoined()
default:
content = rc.GetRDATA().String()
}
return domain.DNSEntry{
Name: rc.Name,
Expire: int(rc.TTL),
Type: rc.Type,
Content: content,
}, nil
}

@@ -10,6 +10,7 @@ import (

"github.com/DNSControl/dnscontrol/v5/models"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"github.com/DNSControl/dnscontrol/v5/models"
dnsv2 "codeberg.org/miekg/dns"
"github.com/DNSControl/dnscontrol/v5/models"

Comment thread providers/transip/transipProvider.go Outdated
Comment on lines +298 to +300
// TransIP returns TXT content unquoted (see recordToNative), so parse it as
// raw data. TxtDontParse only affects TXT; other types are unchanged.
return dc.NewRecordConfigParse(dc.LabelFromShort(entry.Name), uint32(entry.Expire), entry.Type, entry.Content, nrc.Flags{TxtDontParse: true})

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Style: newline before nrc.Flags{}. (I need to add this to the style guide)
Suggested change
// TransIP returns TXT content unquoted (see recordToNative), so parse it as
// raw data. TxtDontParse only affects TXT; other types are unchanged.
return dc.NewRecordConfigParse(dc.LabelFromShort(entry.Name), uint32(entry.Expire), entry.Type, entry.Content, nrc.Flags{TxtDontParse: true})
// TransIP returns TXT content unquoted (see recordToNative), so parse it as raw data.
return dc.NewRecordConfigParse(dc.LabelFromShort(entry.Name), uint32(entry.Expire), entry.Type, entry.Content,
nrc.Flags{TxtDontParse: true})
}

@TomOnTime

Copy link
Copy Markdown
Collaborator

This is a common bug. Sadly I can't find an automated way to test it. The best I can do is this: https://docs.dnscontrol.org/developer-info/testing-txt-records

That said, the code looks good. I just have some style suggestions.

The v5 RDATA refactor set the TransIP DNSEntry Content to config.GetRDATA().String(), which for TXT emits the RFC presentation form (enclosed in double-quotes). TransIP stores that verbatim, so the quotes end up as literal record data and break SPF/DKIM/DMARC. Write the raw, unquoted value with GetTargetTXTJoined() and read it back with the TxtDontParse flag, mirroring the inwx provider.
@cafferata
cafferata force-pushed the fix/transip-txt-quoting branch from 42b90a4 to 80850c5 Compare August 28, 2026 14:24
@cafferata

Copy link
Copy Markdown
Member Author

Thanks for the review, @TomOnTime. I've applied all three suggestions:

  • Added the dnsv2 "codeberg.org/miekg/dns" import.
  • recordToNative: renamed config to rc, replaced the if/then with a switch rc.TypeNum on dnsv2.TypeTXT, and trimmed the comment.
  • nativeToRecord: shortened the comment and put nrc.Flags{} on its own line.

Squashed into the existing commit and pushed. go build, go vet, and go test ./providers/transip/... all pass.

@cafferata
cafferata requested a review from TomOnTime August 28, 2026 14:28
@TomOnTime

Copy link
Copy Markdown
Collaborator

Perfect!

@TomOnTime
TomOnTime merged commit b81370f into DNSControl:main Aug 28, 2026
8 checks passed
@cafferata
cafferata deleted the fix/transip-txt-quoting branch August 28, 2026 14:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

3 participants