Skip to content

feat(cloudflare): add support for MX records #5283

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
99af754
feat(cloudflare): add support for MX records
arthlr Apr 13, 2025
2fa91d0
test(txt): add additional TXT and MX record test cases
arthlr Apr 13, 2025
2f657d1
feat(endpoint): implement parsing for MX and SRV records with structu…
arthlr Apr 15, 2025
558553b
fix(txt): remove TXT record type from supported types in NewTXTRegistry
arthlr Apr 15, 2025
42e7e51
refactor(digitalocean): streamline MX record handling
arthlr Apr 15, 2025
12affc5
refactor(cloudflare): improve error handling in change creation
arthlr Apr 15, 2025
65e903e
fix(endpoint): return all parsed SRV targets instead of a single target
arthlr Apr 16, 2025
bb5a509
test(endpoint): add parsing tests for MX and SRV records
arthlr Apr 16, 2025
aab81a7
fix(endpoint): streamline MX and SRV record validation and parsing
arthlr Apr 24, 2025
364f1aa
fix(digital_ocean): simplify MX record parsing
arthlr Apr 24, 2025
5c2c3b9
fix(docs): update link to CRD source in MX record documentation
arthlr May 30, 2025
ddda28a
fix(cloudflare): improve error handling for MX record parsing
arthlr May 30, 2025
a63a9b9
fix(cloudflare): improve error message formatting for MX record parsing
arthlr May 30, 2025
cb611d7
refactor(endpoint): rename ParseMXRecord to NewMXTarget and update re…
arthlr Jun 10, 2025
9571205
fix(endpoint): update NewMXTarget to return pointer and adjust tests …
arthlr Jun 21, 2025
bd08258
refactor(cloudflare): consolidate proxyEnabled and proxyDisabled vari…
arthlr Jun 23, 2025
58b996b
fix(endpoint): update TestNewMXTarget to reflect changes in MXTarget …
arthlr Jun 23, 2025
03b2066
fix(digitalocean): improve MX record handling by adjusting error hand…
arthlr Jun 23, 2025
d6ae998
refactor(endpoint): change MXTarget fields to unexported and update N…
arthlr Jun 23, 2025
852a53a
refactor(cloudflare): update groupByNameAndTypeWithCustomHostnames to…
arthlr Jun 23, 2025
fcd0340
test(cloudflare): enhance test cover
arthlr Jun 23, 2025
1d7d8e4
refactor(endpoint): remove unused SRVTarget struct from endpoint.go
arthlr Jun 23, 2025
7912f0b
refactor(endpoint): rename NewMXTarget to NewMXRecord for clarity and…
arthlr Jun 23, 2025
fa5d134
Update docs/sources/mx-record.md
arthlr Jun 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/sources/mx-record.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# MX record with CRD source

You can create and manage MX records with the help of [CRD source](../sources/crd.md)
and `DNSEndpoint` CRD. Currently, this feature is only supported by `aws`, `azure`, `google` and `digitalocean` providers.
and `DNSEndpoint` CRD. Currently, this feature is only supported by `aws`, `azure`, `cloudflare`, `digitalocean` and `google` providers.

In order to start managing MX records you need to set the `--managed-record-types=MX` flag.

Expand Down
48 changes: 38 additions & 10 deletions endpoint/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ func (ttl TTL) IsConfigured() bool {
// Targets is a representation of a list of targets for an endpoint.
type Targets []string

// MXTarget represents a single MX (Mail Exchange) record target, including its priority and host.
type MXTarget struct {
priority uint16
host string
}

// NewTargets is a convenience method to create a new Targets object from a vararg of strings
func NewTargets(target ...string) Targets {
t := make(Targets, 0, len(target))
Expand Down Expand Up @@ -394,22 +400,44 @@ func (e *Endpoint) CheckEndpoint() bool {
return true
}

// NewMXRecord parses a string representation of an MX record target (e.g., "10 mail.example.com")
// and returns an MXTarget struct. Returns an error if the input is invalid.
func NewMXRecord(target string) (*MXTarget, error) {
parts := strings.Fields(strings.TrimSpace(target))
if len(parts) != 2 {
return nil, fmt.Errorf("invalid MX record target: %s. MX records must have a preference value and a host, e.g. '10 example.com'", target)
}

priority, err := strconv.ParseUint(parts[0], 10, 16)
if err != nil {
return nil, fmt.Errorf("invalid integer value in target: %s", target)
}

return &MXTarget{
priority: uint16(priority),
host: parts[1],
}, nil
}

// GetPriority returns the priority of the MX record target.
func (m *MXTarget) GetPriority() *uint16 {
return &m.priority
}

// GetHost returns the host of the MX record target.
func (m *MXTarget) GetHost() *string {
return &m.host
}

func (t Targets) ValidateMXRecord() bool {
for _, target := range t {
// MX records must have a preference value to indicate priority, e.g. "10 example.com"
// as per https://www.rfc-editor.org/rfc/rfc974.txt
targetParts := strings.Fields(strings.TrimSpace(target))
if len(targetParts) != 2 {
log.Debugf("Invalid MX record target: %s. MX records must have a preference value to indicate priority, e.g. '10 example.com'", target)
return false
}
preferenceRaw := targetParts[0]
_, err := strconv.ParseUint(preferenceRaw, 10, 16)
_, err := NewMXRecord(target)
if err != nil {
log.Debugf("Invalid SRV record target: %s. Invalid integer value in target.", target)
log.Debugf("Invalid MX record target: %s. %v", target, err)
return false
}
}

return true
}

Expand Down
110 changes: 110 additions & 0 deletions endpoint/endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -815,3 +815,113 @@ func TestPDNScheckEndpoint(t *testing.T) {
assert.Equal(t, tt.expected, actual)
}
}

func TestNewMXTarget(t *testing.T) {
tests := []struct {
description string
target string
expected *MXTarget
expectError bool
}{
{
description: "Valid MX record",
target: "10 example.com",
expected: &MXTarget{priority: 10, host: "example.com"},
expectError: false,
},
{
description: "Invalid MX record with missing priority",
target: "example.com",
expectError: true,
},
{
description: "Invalid MX record with non-integer priority",
target: "abc example.com",
expectError: true,
},
{
description: "Invalid MX record with too many parts",
target: "10 example.com extra",
expectError: true,
},
{
description: "Missing host",
target: "10 ",
expected: nil,
expectError: true,
},
}

for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
actual, err := NewMXRecord(tt.target)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expected, actual)
}
})
}
}

func TestCheckEndpoint(t *testing.T) {
tests := []struct {
description string
endpoint Endpoint
expected bool
}{
{
description: "Valid MX record target",
endpoint: Endpoint{
DNSName: "example.com",
RecordType: RecordTypeMX,
Targets: Targets{"10 example.com"},
},
expected: true,
},
{
description: "Invalid MX record target",
endpoint: Endpoint{
DNSName: "example.com",
RecordType: RecordTypeMX,
Targets: Targets{"example.com"},
},
expected: false,
},
{
description: "Valid SRV record target",
endpoint: Endpoint{
DNSName: "_service._tcp.example.com",
RecordType: RecordTypeSRV,
Targets: Targets{"10 5 5060 example.com"},
},
expected: true,
},
{
description: "Invalid SRV record target",
endpoint: Endpoint{
DNSName: "_service._tcp.example.com",
RecordType: RecordTypeSRV,
Targets: Targets{"10 5 example.com"},
},
expected: false,
},
{
description: "Non-MX/SRV record type",
endpoint: Endpoint{
DNSName: "example.com",
RecordType: RecordTypeA,
Targets: Targets{"192.168.1.1"},
},
expected: true,
},
}

for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
actual := tt.endpoint.CheckEndpoint()
assert.Equal(t, tt.expected, actual)
})
}
}
Loading
Loading