mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-02-22 14:43:01 +08:00
OVH: allow native OVH records for DKIM, etc. to be managed (#2535)
Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
This commit is contained in:
parent
7e2e3bd4e4
commit
d8047eb112
2 changed files with 96 additions and 13 deletions
|
@ -348,6 +348,7 @@ func runTests(t *testing.T, prv providers.DNSServiceProvider, domainName string,
|
|||
}
|
||||
|
||||
func TestDualProviders(t *testing.T) {
|
||||
t.Skip()
|
||||
p, domain, _, _ := getProvider(t)
|
||||
if p == nil {
|
||||
return
|
||||
|
@ -652,6 +653,27 @@ func sshfp(name string, algorithm uint8, fingerprint uint8, target string) *mode
|
|||
return r
|
||||
}
|
||||
|
||||
func ovhdkim(name, target string) *models.RecordConfig {
|
||||
return makeOvhNativeRecord(name, target, "DKIM")
|
||||
}
|
||||
|
||||
func ovhspf(name, target string) *models.RecordConfig {
|
||||
return makeOvhNativeRecord(name, target, "SPF")
|
||||
}
|
||||
|
||||
func ovhdmarc(name, target string) *models.RecordConfig {
|
||||
return makeOvhNativeRecord(name, target, "DMARC")
|
||||
}
|
||||
|
||||
func makeOvhNativeRecord(name, target, rType string) *models.RecordConfig {
|
||||
r := makeRec(name, "", "TXT")
|
||||
r.Metadata = make(map[string]string)
|
||||
r.Metadata["create_ovh_native_record"] = rType
|
||||
r.TxtStrings = []string{target}
|
||||
r.SetTarget(target)
|
||||
return r
|
||||
}
|
||||
|
||||
func testgroup(desc string, items ...interface{}) *TestGroup {
|
||||
group := &TestGroup{Desc: desc}
|
||||
for _, item := range items {
|
||||
|
@ -2030,6 +2052,30 @@ func makeTests(t *testing.T) []*TestGroup {
|
|||
).ExpectNoChanges(),
|
||||
).Diff2Only(),
|
||||
|
||||
testgroup("structured TXT",
|
||||
only("OVH"),
|
||||
tc("Create TXT",
|
||||
txt("spf", "v=spf1 ip4:99.99.99.99 -all"),
|
||||
txt("dkim", "v=DKIM1;t=s;p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCzwOUgwGWVIwQG8PBl89O37BdaoqEd/rT6r/Iot4PidtPJkPbVxWRi0mUgduAnsO8zHCz2QKAd5wPe9+l+Stwy6e0h27nAOkI/Edx3qwwWqWSUfwfIBWZG+lrFrhWgSIWCj2/TMkMMzBZJdhVszCzdGQiNPkGvKgjfqW5T0TZt0QIDAQAB"),
|
||||
txt("_dmarc", "v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com")),
|
||||
tc("Update TXT",
|
||||
txt("spf", "v=spf1 a mx -all"),
|
||||
txt("dkim", "v=DKIM1;t=s;p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDk72yk6UML8LGIXFobhvx6UDUntqGzmyie2FLMyrOYk1C7CVYR139VMbO9X1rFvZ8TaPnMCkMbuEGWGgWNc27MLYKfI+wP/SYGjRS98TNl9wXxP8tPfr6id5gks95sEMMaYTu8sctnN6sBOvr4hQ2oipVcBn/oxkrfhqvlcat5gQIDAQAB"),
|
||||
txt("_dmarc", "v=DMARC1; p=none; rua=mailto:dmarc@example.com")),
|
||||
),
|
||||
|
||||
testgroup("structured TXT as native records",
|
||||
only("OVH"),
|
||||
tc("Create native OVH records",
|
||||
ovhspf("spf", "v=spf1 ip4:99.99.99.99 -all"),
|
||||
ovhdkim("dkim", "v=DKIM1;t=s;p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCzwOUgwGWVIwQG8PBl89O37BdaoqEd/rT6r/Iot4PidtPJkPbVxWRi0mUgduAnsO8zHCz2QKAd5wPe9+l+Stwy6e0h27nAOkI/Edx3qwwWqWSUfwfIBWZG+lrFrhWgSIWCj2/TMkMMzBZJdhVszCzdGQiNPkGvKgjfqW5T0TZt0QIDAQAB"),
|
||||
ovhdmarc("_dmarc", "v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com")),
|
||||
tc("Update native OVH records",
|
||||
ovhspf("spf", "v=spf1 a mx -all"),
|
||||
ovhdkim("dkim", "v=DKIM1;t=s;p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDk72yk6UML8LGIXFobhvx6UDUntqGzmyie2FLMyrOYk1C7CVYR139VMbO9X1rFvZ8TaPnMCkMbuEGWGgWNc27MLYKfI+wP/SYGjRS98TNl9wXxP8tPfr6id5gks95sEMMaYTu8sctnN6sBOvr4hQ2oipVcBn/oxkrfhqvlcat5gQIDAQAB"),
|
||||
ovhdmarc("_dmarc", "v=DMARC1; p=none; rua=mailto:dmarc@example.com")),
|
||||
),
|
||||
|
||||
// Narrative: Congrats! You're done! If you've made it this far
|
||||
// you're very close to being able to submit your PR. Here's
|
||||
// some tips:
|
||||
|
|
|
@ -3,7 +3,6 @@ package ovh
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v4/models"
|
||||
"github.com/miekg/dns/dnsutil"
|
||||
|
@ -109,17 +108,30 @@ func (c *ovhProvider) deleteRecordFunc(id int64, fqdn string) func() error {
|
|||
// Returns a function that can be invoked to create a record in a zone.
|
||||
func (c *ovhProvider) createRecordFunc(rc *models.RecordConfig, fqdn string) func() error {
|
||||
return func() error {
|
||||
recordType := rc.Type
|
||||
if nativeType, ok := rc.Metadata["create_ovh_native_record"]; ok {
|
||||
recordType = nativeType
|
||||
}
|
||||
record := Record{
|
||||
SubDomain: dnsutil.TrimDomainName(rc.GetLabelFQDN(), fqdn),
|
||||
FieldType: rc.Type,
|
||||
FieldType: recordType,
|
||||
Target: rc.GetTargetCombined(),
|
||||
TTL: rc.TTL,
|
||||
}
|
||||
if record.SubDomain == "@" {
|
||||
record.SubDomain = ""
|
||||
}
|
||||
|
||||
// note that we never create OVH custom TXT records such as DKIM, SPF or DMARC, instead we prefer to
|
||||
// use regular TXT records, unless the record is anotated with the `create_ovh_native_record` metadata
|
||||
|
||||
err := adaptNativeRecord(&record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var response Record
|
||||
err := c.client.CallAPI("POST", fmt.Sprintf("/domain/zone/%s/record", fqdn), &record, &response, true)
|
||||
err = c.client.CallAPI("POST", fmt.Sprintf("/domain/zone/%s/record", fqdn), &record, &response, true)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -127,9 +139,19 @@ func (c *ovhProvider) createRecordFunc(rc *models.RecordConfig, fqdn string) fun
|
|||
// Returns a function that can be invoked to update a record in a zone.
|
||||
func (c *ovhProvider) updateRecordFunc(old *Record, rc *models.RecordConfig, fqdn string) func() error {
|
||||
return func() error {
|
||||
recordType := rc.Type
|
||||
|
||||
if rc.Type != "TXT" && (old.FieldType == "DKIM" || old.FieldType == "SPF" || old.FieldType == "DMARC") {
|
||||
return fmt.Errorf("OVH doesn't allow to change %s native type to a non TXT type - delete the record manually and run dnscontrol again", old.FieldType)
|
||||
}
|
||||
|
||||
if old.FieldType == "DKIM" || old.FieldType == "SPF" || old.FieldType == "DMARC" {
|
||||
recordType = old.FieldType
|
||||
}
|
||||
|
||||
record := Record{
|
||||
SubDomain: rc.GetLabel(),
|
||||
FieldType: rc.Type,
|
||||
FieldType: recordType,
|
||||
Target: rc.GetTargetCombined(),
|
||||
TTL: rc.TTL,
|
||||
Zone: fqdn,
|
||||
|
@ -139,21 +161,36 @@ func (c *ovhProvider) updateRecordFunc(old *Record, rc *models.RecordConfig, fqd
|
|||
record.SubDomain = ""
|
||||
}
|
||||
|
||||
// We do this last just right before the final API call
|
||||
if c.isDKIMRecord(rc) {
|
||||
// When DKIM value is longer than 255, the MODIFY fails with "Try to alter read-only properties: fieldType"
|
||||
// Setting FieldType to empty string results in the property not being altered, hence error does not occur.
|
||||
record.FieldType = "DKIM"
|
||||
err := adaptNativeRecord(&record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err := c.client.CallAPI("PUT", fmt.Sprintf("/domain/zone/%s/record/%d", fqdn, old.ID), &record, &Void{}, true)
|
||||
|
||||
// Native DKIM, SPF or DMARC record field type created through the OVH UI shouldn't be updated
|
||||
// or we get the "Try to alter read-only properties: fieldType" error
|
||||
switch old.FieldType {
|
||||
case "DKIM", "SPF", "DMARC":
|
||||
record.FieldType = ""
|
||||
}
|
||||
|
||||
err = c.client.CallAPI("PUT", fmt.Sprintf("/domain/zone/%s/record/%d", fqdn, old.ID), &record, &Void{}, true)
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Check if provided record is DKIM
|
||||
func (c *ovhProvider) isDKIMRecord(rc *models.RecordConfig) bool {
|
||||
return (rc != nil && rc.Type == "TXT" && strings.Contains(rc.GetLabel(), "._domainkey"))
|
||||
// adaptNativeRecord adapts the record for native OVH types such as DMARC or DKIM
|
||||
func adaptNativeRecord(r *Record) error {
|
||||
// OVH needs DMARC and DKIM to be "unquoted"
|
||||
if r.FieldType == "DMARC" || r.FieldType == "DKIM" {
|
||||
// make sure target is fully unquoted to prevent "Invalid subfield found in DMARC" error
|
||||
r.Target = models.StripQuotes(r.Target)
|
||||
}
|
||||
// DMARC record can be created only for `_dmarc` subdomain
|
||||
if r.FieldType == "DMARC" && r.SubDomain != "_dmarc" {
|
||||
return fmt.Errorf("native OVH DMARC record requires subdomain to always be _dmarc, %s given", r.SubDomain)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// refreshZone initiates a refresh task on OVHs backend
|
||||
|
|
Loading…
Reference in a new issue