mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-11-19 00:08:37 +08:00
simplify null mx handling
defer null MX creation adds doc note about special Null MX restriction separate and reorder change types
This commit is contained in:
parent
6ef0648778
commit
6d2e67d9fb
2 changed files with 42 additions and 117 deletions
|
|
@ -109,3 +109,7 @@ D("example.com", REG_INWX, DnsProvider(DSP_CF),
|
|||
);
|
||||
```
|
||||
{% endcode %}
|
||||
|
||||
## Notes
|
||||
|
||||
INWX enforces the [RFC 7505](https://www.rfc-editor.org/rfc/rfc7505.html#section-3) MUST NOT guidance regarding publishing both null MX and regular MX records. If a push would result in mixed null MX and regular MX records in the zone, the API responds with `FAILURE! (2308) Data management policy violation` and the record will not be persisted.
|
||||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -231,109 +230,11 @@ func (api *inwxAPI) deleteRecord(RecordID int) error {
|
|||
return api.client.Nameservers.DeleteRecord(RecordID)
|
||||
}
|
||||
|
||||
// appendDeleteCorrection is a helper function to append delete corrections to the list of corrections
|
||||
func (api *inwxAPI) appendDeleteCorrection(corrections []*models.Correction, rec *models.RecordConfig, removals map[string]struct{}) ([]*models.Correction, map[string]struct{}) {
|
||||
// prevent duplicate delete instructions
|
||||
if _, found := removals[rec.ToComparableNoTTL()]; found {
|
||||
return corrections, removals
|
||||
}
|
||||
corrections = append(corrections, &models.Correction{
|
||||
Msg: color.RedString("- DELETE %s %s %s ttl=%d", rec.GetLabelFQDN(), rec.Type, rec.ToComparableNoTTL(), rec.TTL),
|
||||
F: func() error {
|
||||
return api.deleteRecord(rec.Original.(goinwx.NameserverRecord).ID)
|
||||
},
|
||||
})
|
||||
removals[rec.ToComparableNoTTL()] = struct{}{}
|
||||
return corrections, removals
|
||||
}
|
||||
|
||||
// isNullMX checks if a record is a null MX record.
|
||||
func isNullMX(rec *models.RecordConfig) bool {
|
||||
return rec.Type == "MX" && rec.MxPreference == 0 && rec.GetTargetField() == "."
|
||||
}
|
||||
|
||||
// MXCorrections generates required delete corrections when a MX change can not be applied in an updateRecord call.
|
||||
func (api *inwxAPI) MXCorrections(dc *models.DomainConfig, foundRecords models.Records, corrections []*models.Correction) ([]*models.Correction, models.Records, error) {
|
||||
|
||||
// If a null MX is present in the zone, we have to take special care of any
|
||||
// planned MX changes: No non-null MX records can be added until the null
|
||||
// MX is deleted. If a null MX is planned to be added and the diff is
|
||||
// trying to replace an existing regular MX, we need to delete the existing
|
||||
// MX record because an update would be rejected with "2308 Data management policy violation"
|
||||
|
||||
removals := make(map[string]struct{})
|
||||
tempRecords := []*models.RecordConfig{}
|
||||
|
||||
// Detect Null MX in foundRecords
|
||||
nullMXInFound := slices.ContainsFunc(foundRecords.GetByType("MX"), isNullMX)
|
||||
|
||||
// Detect Null MX and regular MX in desired records
|
||||
nullMXInDesired := false
|
||||
regularMXInDesired := false
|
||||
for _, rec := range dc.Records.GetByType("MX") {
|
||||
if isNullMX(rec) {
|
||||
nullMXInDesired = true
|
||||
} else {
|
||||
regularMXInDesired = true
|
||||
}
|
||||
}
|
||||
|
||||
// invalid state. Null MX and regular MX are both present in the configuration
|
||||
if nullMXInDesired && regularMXInDesired {
|
||||
return nil, nil, fmt.Errorf("desired configuration contains both Null MX and regular MX records")
|
||||
}
|
||||
|
||||
if nullMXInFound && !nullMXInDesired {
|
||||
// Null MX exists in foundRecords, but desired configuration contains only regular MX records
|
||||
// Safe to delete the Null MX record
|
||||
for _, rec := range foundRecords {
|
||||
if isNullMX(rec) {
|
||||
corrections, removals = api.appendDeleteCorrection(corrections, rec, removals)
|
||||
}
|
||||
}
|
||||
} else if !nullMXInFound && nullMXInDesired {
|
||||
// Null MX is being added, ensure all existing MX records are deleted
|
||||
for _, rec := range foundRecords {
|
||||
if rec.Type == "MX" {
|
||||
corrections, removals = api.appendDeleteCorrection(corrections, rec, removals)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mxRecords := foundRecords.GetByType("MX")
|
||||
mxonlyDc, err := dc.Copy()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
mxonlyDc.Records = mxonlyDc.Records.GetByType("MX")
|
||||
|
||||
mxchanges, _, err := diff2.ByRecord(mxRecords, mxonlyDc, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for _, change := range mxchanges {
|
||||
if change.Type == diff2.CHANGE {
|
||||
// INWX will not apply a MX preference update of >=1 to 0. The updateRecord
|
||||
// endpoint will not report an error, so the zone and config will be out of
|
||||
// sync unless we handle this as a delete then create
|
||||
if change.New[0].MxPreference == 0 && change.Old[0].MxPreference != 0 {
|
||||
corrections, removals = api.appendDeleteCorrection(corrections, change.Old[0], removals)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We need to remove the RRs already in corrections
|
||||
for _, rec := range foundRecords {
|
||||
if _, found := removals[rec.ToComparableNoTTL()]; !found {
|
||||
tempRecords = append(tempRecords, rec)
|
||||
}
|
||||
}
|
||||
|
||||
cleanedRecords := models.Records(tempRecords)
|
||||
return corrections, cleanedRecords, nil
|
||||
}
|
||||
|
||||
// AutoDnssecToggle enables and disables AutoDNSSEC for INWX domains.
|
||||
func (api *inwxAPI) AutoDnssecToggle(dc *models.DomainConfig, corrections []*models.Correction) ([]*models.Correction, error) {
|
||||
|
||||
|
|
@ -369,42 +270,60 @@ func (api *inwxAPI) AutoDnssecToggle(dc *models.DomainConfig, corrections []*mod
|
|||
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
|
||||
func (api *inwxAPI) GetZoneRecordsCorrections(dc *models.DomainConfig, foundRecords models.Records) ([]*models.Correction, int, error) {
|
||||
|
||||
// INWX support for Null MX requires special handling. MX preference changes
|
||||
// of >0 to 0 are silently dropped, so if the change includes a null MX record
|
||||
// we have to delete then create. Corrections are compiled separately for
|
||||
// deletes, changes and creates and then assembled in that order.
|
||||
corrections := []*models.Correction{}
|
||||
creates := []*models.Correction{}
|
||||
deferred := []*models.Correction{}
|
||||
|
||||
corrections, records, err := api.MXCorrections(dc, foundRecords, corrections)
|
||||
corrections, err := api.AutoDnssecToggle(dc, corrections)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
corrections, err = api.AutoDnssecToggle(dc, corrections)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
changes, actualChangeCount, err := diff2.ByRecord(records, dc, nil)
|
||||
changes, actualChangeCount, err := diff2.ByRecord(foundRecords, dc, nil)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
for _, change := range changes {
|
||||
changeMsgs := change.MsgsJoined
|
||||
dcName := dc.Name
|
||||
switch change.Type {
|
||||
case diff2.REPORT:
|
||||
corrections = append(corrections, &models.Correction{Msg: changeMsgs})
|
||||
case diff2.CHANGE:
|
||||
recID := change.Old[0].Original.(goinwx.NameserverRecord).ID
|
||||
corrections = append(corrections, &models.Correction{
|
||||
Msg: changeMsgs,
|
||||
F: func() error {
|
||||
return api.updateRecord(recID, change.New[0])
|
||||
},
|
||||
})
|
||||
oldRec := change.Old[0]
|
||||
newRec := change.New[0]
|
||||
if isNullMX(newRec) || isNullMX(oldRec) {
|
||||
// changing to or from a Null MX has to be delete then create
|
||||
corrections = append(corrections, &models.Correction{
|
||||
Msg: color.RedString("- DELETE %s %s %s ttl=%d", oldRec.GetLabelFQDN(), oldRec.Type, oldRec.ToComparableNoTTL(), oldRec.TTL),
|
||||
F: func() error {
|
||||
return api.deleteRecord(oldRec.Original.(goinwx.NameserverRecord).ID)
|
||||
},
|
||||
})
|
||||
deferred = append(deferred, &models.Correction{
|
||||
Msg: color.GreenString("+ CREATE %s %s %s ttl=%d", newRec.GetLabelFQDN(), newRec.Type, newRec.ToComparableNoTTL(), newRec.TTL),
|
||||
F: func() error {
|
||||
return api.createRecord(dc.Name, newRec)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
recID := oldRec.Original.(goinwx.NameserverRecord).ID
|
||||
corrections = append(corrections, &models.Correction{
|
||||
Msg: changeMsgs,
|
||||
F: func() error {
|
||||
return api.updateRecord(recID, newRec)
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
case diff2.CREATE:
|
||||
changeNew := change.New[0]
|
||||
corrections = append(corrections, &models.Correction{
|
||||
creates = append(creates, &models.Correction{
|
||||
Msg: changeMsgs,
|
||||
F: func() error {
|
||||
return api.createRecord(dcName, changeNew)
|
||||
return api.createRecord(dc.Name, change.New[0])
|
||||
},
|
||||
})
|
||||
case diff2.DELETE:
|
||||
|
|
@ -417,6 +336,8 @@ func (api *inwxAPI) GetZoneRecordsCorrections(dc *models.DomainConfig, foundReco
|
|||
panic(fmt.Sprintf("unhandled change.Type %s", change.Type))
|
||||
}
|
||||
}
|
||||
corrections = append(corrections, creates...)
|
||||
corrections = append(corrections, deferred...)
|
||||
return corrections, actualChangeCount, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue