From a6ef9bc1c651e54664c735da229c3393cf66fdb4 Mon Sep 17 00:00:00 2001 From: rvasikarla Date: Sun, 5 Apr 2026 18:12:04 -0500 Subject: [PATCH] fix: allow CNAME + other records when Cloudflare proxy is enabled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a CNAME record has cloudflare_proxy set to 'on' or 'full', Cloudflare flattens the CNAME internally and returns A/AAAA records to resolvers. The CNAME is never actually served, so the RFC 1034 §3.6.2 restriction (CNAME must be the only record at a name) does not apply. The checkCNAMEs validation now skips the CNAME conflict error for proxied Cloudflare CNAMEs, similar to the existing AKAMAICDN exception. Fixes #4181 --- pkg/normalize/validate.go | 10 ++++++++++ pkg/normalize/validate_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/pkg/normalize/validate.go b/pkg/normalize/validate.go index 7431e56750..232a9e8a08 100644 --- a/pkg/normalize/validate.go +++ b/pkg/normalize/validate.go @@ -636,12 +636,16 @@ func checkAutoDNSSEC(dc *models.DomainConfig) (errs []error) { func checkCNAMEs(dc *models.DomainConfig) (errs []error) { cnames := map[string]bool{} + proxiedCnames := map[string]bool{} for _, r := range dc.Records { if r.Type == "CNAME" { if cnames[r.GetLabel()] { errs = append(errs, fmt.Errorf("%s: cannot have multiple CNAMEs with same name: %s", r.FilePos, r.GetLabelFQDN())) } cnames[r.GetLabel()] = true + if p, ok := r.Metadata["cloudflare_proxy"]; ok && (p == "on" || p == "full") { + proxiedCnames[r.GetLabel()] = true + } } } for _, r := range dc.Records { @@ -650,6 +654,12 @@ func checkCNAMEs(dc *models.DomainConfig) (errs []error) { if r.Type == "AKAMAICDN" { continue } + // Cloudflare proxied (flattened) CNAMEs are resolved internally + // and never served as actual CNAME records, so the RFC 1034 §3.6.2 + // restriction does not apply. + if proxiedCnames[r.GetLabel()] { + continue + } errs = append(errs, fmt.Errorf("%s: cannot have CNAME and %s record with same name: %s", r.FilePos, r.Type, r.GetLabelFQDN())) } } diff --git a/pkg/normalize/validate_test.go b/pkg/normalize/validate_test.go index 383f74b72f..304ec2a219 100644 --- a/pkg/normalize/validate_test.go +++ b/pkg/normalize/validate_test.go @@ -342,6 +342,40 @@ func TestCNAMEMutex(t *testing.T) { } } +func TestCNAMECloudflareProxied(t *testing.T) { + // A proxied (flattened) CNAME should be allowed alongside other record types. + recCNAME := &models.RecordConfig{ + Type: "CNAME", + Metadata: map[string]string{"cloudflare_proxy": "on"}, + } + recCNAME.SetLabel("mail", "mail.example.com") + recCNAME.MustSetTarget("example.com.") + recMX := &models.RecordConfig{Type: "MX"} + recMX.SetLabel("mail", "mail.example.com") + recMX.MustSetTarget("smtp.example.com.") + dc := &models.DomainConfig{ + Name: "example.com", + Records: []*models.RecordConfig{recCNAME, recMX}, + } + errs := checkCNAMEs(dc) + if len(errs) != 0 { + t.Errorf("Expected no errors for proxied CNAME + MX, got: %v", errs) + } + + // A non-proxied CNAME should still fail. + recCNAME2 := &models.RecordConfig{Type: "CNAME"} + recCNAME2.SetLabel("mail", "mail.example.com") + recCNAME2.MustSetTarget("example.com.") + dc2 := &models.DomainConfig{ + Name: "example.com", + Records: []*models.RecordConfig{recCNAME2, recMX}, + } + errs2 := checkCNAMEs(dc2) + if len(errs2) == 0 { + t.Error("Expected error for non-proxied CNAME + MX, got none") + } +} + func TestCAAValidation(t *testing.T) { config := &models.DNSConfig{ Domains: []*models.DomainConfig{