Repository: StackExchange/dnscontrol
Pull request #2120 (2023-03-03 21:28:04)

BUG: Some DNS zones are downloaded twice
Fixes https://github.com/StackExchange/dnscontrol/issues/2214

Calls to GetDomainCorrections() are inconsistent. The provider shouldn't be expected to do so much work, and they often get things wrong.  Let's fix this by introducing a better function signature (GetZoneRecordsCorrections()) and use it in all providers.

Also, lets move the calls to GetZonesRecords() and GetDomainCorrections() to a central module rather than repeating ourselves.

<!--
Before you submit a pull request, please make sure you've run the following commands from the root directory.

go fmt ./...
go generate
go mod tidy
!-->




Patch:
diff --git a/commands/previewPush.go b/commands/previewPush.go
index 54304d82..137eb6d7 100644
--- a/commands/previewPush.go
+++ b/commands/previewPush.go
@@ -2,7 +2,6 @@ package commands
 
 import (
 	"fmt"
-	"log"
 	"os"
 	"strings"
 	"sync"
@@ -15,6 +14,7 @@ import (
 	"github.com/StackExchange/dnscontrol/v3/pkg/normalize"
 	"github.com/StackExchange/dnscontrol/v3/pkg/notifications"
 	"github.com/StackExchange/dnscontrol/v3/pkg/printer"
+	"github.com/StackExchange/dnscontrol/v3/pkg/zonerecs"
 	"github.com/StackExchange/dnscontrol/v3/providers"
 	"github.com/urfave/cli/v2"
 	"golang.org/x/exp/slices"
@@ -139,6 +139,7 @@ func run(args PreviewArgs, push bool, interactive bool, out printer.CLI) error {
 	var wg sync.WaitGroup
 	wg.Add(len(cfg.Domains))
 
+	// For each domain in dnsconfig.js...
 	for _, domain := range cfg.Domains {
 		// Run preview or push operations per domain as anonymous function, in preparation for the later use of goroutines.
 		// For now running this code is still sequential.
@@ -150,8 +151,16 @@ func run(args PreviewArgs, push bool, interactive bool, out printer.CLI) error {
 				return
 			}
 
+			err = domain.Punycode()
+			if err != nil {
+				return
+			}
+
+			// Correct the domain...
+
 			out.StartDomain(domain.UniqueName)
 			var providersWithExistingZone []*models.DNSProviderInstance
+			/// For each DSP...
 			for _, provider := range domain.DNSProviderInstances {
 				if !args.NoPopulate {
 					// preview run: check if zone is already there, if not print a warning
@@ -164,8 +173,8 @@ func run(args PreviewArgs, push bool, interactive bool, out printer.CLI) error {
 						aceZoneName, _ := idna.ToASCII(domain.Name)
 
 						if !slices.Contains(zones, aceZoneName) {
-							out.Warnf("DEBUG: zones: %v\n", zones)
-							out.Warnf("DEBUG: Name: %v\n", domain.Name)
+							//out.Warnf("DEBUG: zones: %v\n", zones)
+							//out.Warnf("DEBUG: Name: %v\n", domain.Name)
 
 							out.Warnf("Zone '%s' does not exist in the '%s' profile and will be added automatically.\n", domain.Name, provider.Name)
 							continue // continue with next provider, as we can not determine corrections without an existing zone
@@ -181,6 +190,8 @@ func run(args PreviewArgs, push bool, interactive bool, out printer.CLI) error {
 				providersWithExistingZone = append(providersWithExistingZone, provider)
 			}
 
+			// Correct the registrar...
+
 			nsList, err := nameservers.DetermineNameserversForProviders(domain, providersWithExistingZone)
 			if err != nil {
 				out.Errorf("ERROR: %s", err.Error())
@@ -190,20 +201,14 @@ func run(args PreviewArgs, push bool, interactive bool, out printer.CLI) error {
 			nameservers.AddNSRecords(domain)
 
 			for _, provider := range providersWithExistingZone {
-				dc, err := domain.Copy()
-				if err != nil {
-					out.Errorf("ERROR: %s", err.Error())
-					return
-				}
-				shouldrun := args.shouldRunProvider(provider.Name, dc)
+
+				shouldrun := args.shouldRunProvider(provider.Name, domain)
 				out.StartDNSProvider(provider.Name, !shouldrun)
 				if !shouldrun {
 					continue
 				}
 
-				/// This is where we should audit?
-
-				corrections, err := provider.Driver.GetDomainCorrections(dc)
+				corrections, err := zonerecs.CorrectZoneRecords(provider.Driver, domain)
 				out.EndProvider(provider.Name, len(corrections), err)
 				if err != nil {
 					anyErrors = true
@@ -212,6 +217,8 @@ func run(args PreviewArgs, push bool, interactive bool, out printer.CLI) error {
 				totalCorrections += len(corrections)
 				anyErrors = printOrRunCorrections(domain.Name, provider.Name, corrections, out, push, interactive, notifier) || anyErrors
 			}
+
+			//
 			run := args.shouldRunProvider(domain.RegistrarName, domain)
 			out.StartRegistrar(domain.RegistrarName, !run)
 			if !run {
@@ -221,11 +228,8 @@ func run(args PreviewArgs, push bool, interactive bool, out printer.CLI) error {
 				out.Warnf("No nameservers declared; skipping registrar. Add {no_ns:'true'} to force.\n")
 				return
 			}
-			dc, err := domain.Copy()
-			if err != nil {
-				log.Fatal(err)
-			}
-			corrections, err := domain.RegistrarInstance.Driver.GetRegistrarCorrections(dc)
+
+			corrections, err := domain.RegistrarInstance.Driver.GetRegistrarCorrections(domain)
 			out.EndProvider(domain.RegistrarName, len(corrections), err)
 			if err != nil {
 				anyErrors = true
diff --git a/documentation/providers/axfrddns.md b/documentation/providers/axfrddns.md
index d223f974..9408f3df 100644
--- a/documentation/providers/axfrddns.md
+++ b/documentation/providers/axfrddns.md
@@ -66,10 +66,12 @@ And update `creds.json` accordingly:
 ```json
 {
     "axfrddns-a": {
+        "TYPE": "AXFRDDNS",
         "transfer-key": "hmac-sha256:transfer-key-id:Base64EncodedSecret=",
         "update-key": "hmac-sha256:update-key-id:AnotherSecret="
     },
     "axfrddns-b": {
+        "TYPE": "AXFRDDNS",
         "transfer-key": "hmac-sha512:transfer-key-id-B:SmallSecret=",
         "update-key": "hmac-sha512:update-key-id-B:YetAnotherSecret="
     }
@@ -132,6 +134,16 @@ the following error message:
 Please consider adding default `nameservers` or an explicit `master` in `creds.json`.
 ```
 
+### Buggy DNS servers regarding CNAME updates
+
+When modifying a CNAME record, or when replacing an A record by a
+CNAME one in a single batched DDNS update, some DNS servers
+(e.g. Knot) will incorrectly reject the update. For this particular
+case, you might set the option `buggy-cname = "yes"` in `creds.json`.
+The changes will then be split in two DDNS updates, applied
+successively by the server. This will allow Knot to successfully apply
+the changes, but you will loose the atomic-update property.
+
 
 ## Server configuration examples
 
diff --git a/documentation/writing-providers.md b/documentation/writing-providers.md
index 1daed536..6ce353fb 100644
--- a/documentation/writing-providers.md
+++ b/documentation/writing-providers.md
@@ -14,41 +14,33 @@ you designate someone else as the maintainer). More details
 
 I'll ignore all the small stuff and get to the point.
 
-A provider's `GetDomainCorrections()` function is the workhorse
-of the provider.  It is what gets called by `dnscontrol preview`
+A typical provider implements 3 methods and DNSControl takes care of the rest:
+
+* GetZoneRecords() -- Download the list of DNS records and return them as a list of RecordConfig structs.
+* GetZoneRecordsCorrections() -- Generate a list of corrections.
+* GetNameservers() -- Query the API and return the list of parent nameservers.
+
+These three functions are all that's needed for `dnscontrol preview`
 and `dnscontrol push`.
 
-How does a provider's `GetDomainCorrections()` function work?
+The goal of `GetZoneRecords()` is to download all the DNS records,
+convert them to `models.RecordConfig` format, and return them as one big list
+(`models.Records`).
 
-The goal of `GetDomainCorrections()` is to return a list of
+The goal of `GetZoneRecordsCorrections()` is to return a list of
 corrections.  Each correction is a text string describing the change
 ("Delete CNAME record foo") and a function that, if called, will
-make the change (i.e. call the API and delete record foo).  Preview
-mode simply prints the text strings.  `dnscontrol push` prints the
+make the change (i.e. call the API and delete record foo).  `dnscontrol preview`
+simply prints the text strings.  `dnscontrol push` prints the
 strings and calls the functions. Because of how Go's functions work,
 the function will have everything it needs to make the change.
 Pretty cool, eh?
 
-So how does `GetDomainCorrections()` work?
-
-First, some terminology: The DNS records specified in the `dnsconfig.js`
-file are called the "desired" records. The DNS records stored at
-the DNS service provider are called the "existing" records.
-
-Every provider does the same basic process.  The function
-`GetDomainCorrections()` is called with a list of the desired DNS
-records (`dc.Records`).  It then contacts the provider's API and
-gathers the existing records. It converts the existing records into
-a list of `*models.RecordConfig`.
-
-Now that it has the desired and existing records in the appropriate
-format, `differ.IncrementalDiff(existingRecords)` is called and
-does all the hard work of understanding the DNS records and figuring
-out what changes need to be made.  It generates lists of adds,
-deletes, and changes.
-
-`GetDomainCorrections()` then generates the list of `models.Corrections()`
-and returns.  DNSControl takes care of the rest.
+Calculating the difference between existing and desired is difficult. Luckily
+the work is done for you.  `GetZoneRecordsCorrections()` calls a a function in
+the `pkg/diff2` module that generates a list of changes (usually an ADD,
+CHANGE, or DELETE) that can easily be turned into the API calls mentioned
+previously.
 
 So, what does all this mean?
 
@@ -61,11 +53,6 @@ If you are new to Go, there are plenty of providers you can copy
 from. In fact, many non-Go programmers
 [have learned Go by contributing to DNSControl](https://everythingsysadmin.com/2017/08/go-get-up-to-speed.html).
 
-Oh, and what if the API simply requires that the entire zonefile be uploaded
-every time?  We still generate the text descriptions of the changes (so that
-`dnscontrol preview` looks nice) but the functions are just no-ops, except
-for one that uploads the new zonefile.
-
 Now that you understand the general process, here are the details.
 
 ## Step 1: General advice
@@ -80,12 +67,44 @@ was confusing so we can update this document with advice for future
 authors (or even better, update [this document](https://github.com/StackExchange/dnscontrol/blob/master/documentation/writing-providers.md)
 yourself.)
 
+## NOTE: diff2
+
+We are in the process of changing how providers work. Sadly this document
+hasn't been fully updated yet.
+
+We are in the process of changing all providers from using `pkg/diff` to
+`pkg/diff2`.  diff2 is much easier to use as it does all the hard work for you.
+Providers are easier to write, there's less code for you to write, and fewer
+chances to make mistakes.
+
+New providers only need to implement diff2.  Older providers are implemented
+both ways, with a flag (`--diff2`) enabling the newer code.  Soon the new code
+will become the default, then the old code will be removed.
+
+The file `pkg/diff2/diff2.go` has instructions about how to use the new diff2 system.
+You can also do `grep diff2.By providers/*/*.go` to find providers that use
+the new system.
+
+Each DNS provider's API is different.  Some update one DNS record at a time.
+Others, the only change they permit is to upload the entire zone even if only one record changed!
+Others are somewhere in between: all records at a label must be updated at once, or all records
+in a RecordSet (the label + rType).  diff2 provides functions for all of these situations:
+
+diff2.ByRecord() -- Updates are done one DNS record at a time. New records are added. Changes and deletes refer to an ID assigned to the record by the provider.
+diff2.ByLabel() -- Updates are done for an entire label. Adds and changes are done by sending one or more records that will appear at that label (i.e. www.example.com). Deletes delete all records at that label.
+diff2.ByRecordSet() -- Similar to ByLabel() but updates are done on the label+type level. If www.example.com has 2 A records and 2 MX records, 
+
+
+
 
 ## Step 2: Pick a base provider
 
 Pick a similar provider as your base.  Providers basically fall
 into three general categories:
 
+NOTE: diff2 changes this.  For now, you can simply run `grep diff2.By providers/*/*.go` to see which
+providers use ByZone, ByLabel, ByRecord, ByRecordSet and pick a similar provider to copy from.
+
 * **zone:** The API requires you to upload the entire zone every time. (BIND, NAMECHEAP).
 * **incremental-record:** The API lets you add/change/delete individual DNS records. (CLOUDFLARE, DNSIMPLE, NAMEDOTCOM, GCLOUD, HEXONET)
 * **incremental-label:** Like incremental-record, but if there are
diff --git a/models/provider.go b/models/provider.go
index 9a254231..6adbb630 100644
--- a/models/provider.go
+++ b/models/provider.go
@@ -4,7 +4,7 @@ package models
 type DNSProvider interface {
 	GetNameservers(domain string) ([]*Nameserver, error)
 	GetZoneRecords(domain string) (Records, error)
-	GetDomainCorrections(dc *DomainConfig) ([]*Correction, error)
+	GetZoneRecordsCorrections(dc *DomainConfig, existing Records) ([]*Correction, error)
 }
 
 // Registrar is an interface for Registrar plug-ins.
diff --git a/models/record.go b/models/record.go
index f4b55040..b36e5520 100644
--- a/models/record.go
+++ b/models/record.go
@@ -555,6 +555,7 @@ func (recs Records) GroupedByFQDN() ([]string, map[string]Records) {
 }
 
 // PostProcessRecords does any post-processing of the downloaded DNS records.
+// Deprecated. zonerecords.CorrectZoneRecords() calls Downcase directly.
 func PostProcessRecords(recs []*RecordConfig) {
 	Downcase(recs)
 }
diff --git a/pkg/acme/acme.go b/pkg/acme/acme.go
index c76b7adb..7316658e 100644
--- a/pkg/acme/acme.go
+++ b/pkg/acme/acme.go
@@ -15,6 +15,7 @@ import (
 	"github.com/StackExchange/dnscontrol/v3/models"
 	"github.com/StackExchange/dnscontrol/v3/pkg/nameservers"
 	"github.com/StackExchange/dnscontrol/v3/pkg/notifications"
+	"github.com/StackExchange/dnscontrol/v3/pkg/zonerecs"
 	"github.com/go-acme/lego/certcrypto"
 	"github.com/go-acme/lego/certificate"
 	"github.com/go-acme/lego/challenge"
@@ -275,7 +276,7 @@ func (c *certManager) getCorrections(d *models.DomainConfig) ([]*models.Correcti
 		if err != nil {
 			return nil, err
 		}
-		corrections, err := p.Driver.GetDomainCorrections(dc)
+		corrections, err := zonerecs.CorrectZoneRecords(p.Driver, dc)
 		if err != nil {
 			return nil, err
 		}
diff --git a/pkg/txtutil/txtutil.go b/pkg/txtutil/txtutil.go
index 3e6f1527..cec100e1 100644
--- a/pkg/txtutil/txtutil.go
+++ b/pkg/txtutil/txtutil.go
@@ -5,6 +5,7 @@ import "github.com/StackExchange/dnscontrol/v3/models"
 // SplitSingleLongTxt finds TXT records with a single long string and splits it
 // into 255-octet chunks. This is used by providers that, when a user specifies
 // one long TXT string, split it into smaller strings behind the scenes.
+// This should be called from GetZoneRecordsCorrections().
 func SplitSingleLongTxt(records []*models.RecordConfig) {
 	for _, rc := range records {
 		if rc.HasFormatIdenticalToTXT() {
diff --git a/pkg/zonerecs/zonerecords.go b/pkg/zonerecs/zonerecords.go
new file mode 100644
index 00000000..920fb7c8
--- /dev/null
+++ b/pkg/zonerecs/zonerecords.go
@@ -0,0 +1,34 @@
+package zonerecs
+
+import (
+	"github.com/StackExchange/dnscontrol/v3/models"
+)
+
+// CorrectZoneRecords calls both GetZoneRecords, does any
+// post-processing, and then calls GetZoneRecordsCorrections.  The
+// name sucks because all the good names were taken.
+func CorrectZoneRecords(driver models.DNSProvider, dc *models.DomainConfig) ([]*models.Correction, error) {
+	existingRecords, err := driver.GetZoneRecords(dc.Name)
+	if err != nil {
+		return nil, err
+	}
+
+	// downcase
+	models.Downcase(existingRecords)
+
+	// Copy dc so that any corrections code that wants to
+	// modify the records may. For example, if the provider only
+	// supports certain TTL values, it will adjust the ones in
+	// dc.Records.
+	dc, err = dc.Copy()
+	if err != nil {
+		return nil, err
+	}
+
+	// punycode
+	dc.Punycode()
+	// FIXME(tlim) It is a waste to PunyCode every iteration.
+	// This should be moved to where the JavaScript is processed.
+
+	return driver.GetZoneRecordsCorrections(dc, existingRecords)
+}
diff --git a/providers/akamaiedgedns/akamaiEdgeDnsProvider.go b/providers/akamaiedgedns/akamaiEdgeDnsProvider.go
index 8b4535e9..055f3826 100644
--- a/providers/akamaiedgedns/akamaiEdgeDnsProvider.go
+++ b/providers/akamaiedgedns/akamaiEdgeDnsProvider.go
@@ -105,26 +105,13 @@ func (a *edgeDNSProvider) EnsureZoneExists(domain string) error {
 	return createZone(domain, a.contractID, a.groupID)
 }
 
-// GetDomainCorrections return a list of corrections. Each correction is a text string describing the change
-// and a function that, if called, will make the change.
-// “dnscontrol preview” simply prints the text strings.
-// "dnscontrol push" prints the strings and calls the functions.
-func (a *edgeDNSProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	err := dc.Punycode()
-	if err != nil {
-		return nil, err
-	}
-
-	existingRecords, err := getRecords(dc.Name)
-	if err != nil {
-		return nil, err
-	}
-
-	models.PostProcessRecords(existingRecords)
-	txtutil.SplitSingleLongTxt(dc.Records)
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (a *edgeDNSProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
+	txtutil.SplitSingleLongTxt(existingRecords)
 
 	var corrections []*models.Correction
 	var keysToUpdate map[models.RecordKey][]string
+	var err error
 	if !diff2.EnableDiff2 {
 		keysToUpdate, err = (diff.New(dc)).ChangedGroups(existingRecords)
 	} else {
diff --git a/providers/autodns/autoDnsProvider.go b/providers/autodns/autoDnsProvider.go
index d6d756ed..210e79e4 100644
--- a/providers/autodns/autoDnsProvider.go
+++ b/providers/autodns/autoDnsProvider.go
@@ -67,31 +67,12 @@ func New(settings map[string]string, _ json.RawMessage) (providers.DNSServicePro
 	return api, nil
 }
 
-// GetDomainCorrections returns the corrections for a domain.
-func (api *autoDNSProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	var changes []*models.RecordConfig
-
-	dc, err := dc.Copy()
-	if err != nil {
-		return nil, err
-	}
-
-	err = dc.Punycode()
-	if err != nil {
-		return nil, err
-	}
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (api *autoDNSProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
 	domain := dc.Name
-
-	// Get existing records
-	existingRecords, err := api.GetZoneRecords(domain)
-	if err != nil {
-		return nil, err
-	}
-
-	// Normalize
-	models.PostProcessRecords(existingRecords)
 	txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
 
+	var changes []*models.RecordConfig
 	var corrections []*models.Correction
 	if !diff2.EnableDiff2 {
 
diff --git a/providers/axfrddns/axfrddnsProvider.go b/providers/axfrddns/axfrddnsProvider.go
index c0b6a6b6..6a75f42d 100644
--- a/providers/axfrddns/axfrddnsProvider.go
+++ b/providers/axfrddns/axfrddnsProvider.go
@@ -29,6 +29,7 @@ import (
 	"github.com/StackExchange/dnscontrol/v3/pkg/printer"
 	"github.com/StackExchange/dnscontrol/v3/pkg/txtutil"
 	"github.com/StackExchange/dnscontrol/v3/providers"
+	"github.com/fatih/color"
 	"github.com/miekg/dns"
 )
 
@@ -56,13 +57,15 @@ var features = providers.DocumentationNotes{
 
 // axfrddnsProvider stores the client info for the provider.
 type axfrddnsProvider struct {
-	rand         *rand.Rand
-	master       string
-	updateMode   string
-	transferMode string
-	nameservers  []*models.Nameserver
-	transferKey  *Key
-	updateKey    *Key
+	rand                *rand.Rand
+	master              string
+	updateMode          string
+	transferMode        string
+	nameservers         []*models.Nameserver
+	transferKey         *Key
+	updateKey           *Key
+	hasDnssecRecords    bool
+	serverHasBuggyCNAME bool
 }
 
 func initAxfrDdns(config map[string]string, providermeta json.RawMessage) (providers.DNSServiceProvider, error) {
@@ -132,6 +135,12 @@ func initAxfrDdns(config map[string]string, providermeta json.RawMessage) (provi
 	if err != nil {
 		return nil, err
 	}
+	switch strings.ToLower(strings.TrimSpace(config["buggy-cname"])) {
+	case "yes", "true":
+		api.serverHasBuggyCNAME = true
+	default:
+		api.serverHasBuggyCNAME = false
+	}
 	for key := range config {
 		switch key {
 		case "master",
@@ -139,7 +148,9 @@ func initAxfrDdns(config map[string]string, providermeta json.RawMessage) (provi
 			"update-key",
 			"transfer-key",
 			"update-mode",
-			"transfer-mode":
+			"transfer-mode",
+			"domain",
+			"TYPE":
 			continue
 		default:
 			printer.Printf("[Warning] AXFRDDNS: unknown key in `creds.json` (%s)\n", key)
@@ -301,7 +312,7 @@ func (c *axfrddnsProvider) GetZoneRecords(domain string) (models.Records, error)
 
 	if len(foundRecords) >= 1 && foundRecords[len(foundRecords)-1].Type == "SOA" {
 		// The SOA is sent two times: as the first and the last record
-		// See section 2.2 of RFC5936
+		// See section 2.2 of RFC5936. We remove the later one.
 		foundRecords = foundRecords[:len(foundRecords)-1]
 	}
 
@@ -309,153 +320,309 @@ func (c *axfrddnsProvider) GetZoneRecords(domain string) (models.Records, error)
 		foundRecords = append(foundRecords, foundDNSSecRecords)
 	}
 
+	c.hasDnssecRecords = false
+	if len(foundRecords) >= 1 {
+		last := foundRecords[len(foundRecords)-1]
+		if last.Type == "TXT" &&
+			last.Name == dnssecDummyLabel &&
+			len(last.TxtStrings) == 1 &&
+			last.TxtStrings[0] == dnssecDummyTxt {
+			c.hasDnssecRecords = true
+			foundRecords = foundRecords[0:(len(foundRecords) - 1)]
+		}
+	}
+
 	return foundRecords, nil
 
 }
 
-// GetDomainCorrections returns a list of corrections to update a domain.
-func (c *axfrddnsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	dc.Punycode()
+// BuildCorrection return a Correction for a given set of DDNS update and the corresponding message.
+func (c *axfrddnsProvider) BuildCorrection(dc *models.DomainConfig, msgs []string, update *dns.Msg) *models.Correction {
+	return &models.Correction{
+		Msg: fmt.Sprintf("DDNS UPDATES to '%s' (primary master: '%s'). Changes:\n%s", dc.Name, c.master, strings.Join(msgs, "\n")),
+		F: func() error {
+
+			client := new(dns.Client)
+			client.Net = c.updateMode
+			client.Timeout = dnsTimeout
+			if c.updateKey != nil {
+				client.TsigSecret =
+					map[string]string{c.updateKey.id: c.updateKey.secret}
+				update.SetTsig(c.updateKey.id, c.updateKey.algo, 300, time.Now().Unix())
+				if c.updateKey.algo == dns.HmacMD5 {
+					client.TsigProvider = md5Provider(c.updateKey.secret)
+				}
+			}
 
-	foundRecords, err := c.GetZoneRecords(dc.Name)
-	if err != nil {
-		return nil, err
+			msg, _, err := client.Exchange(update, c.master)
+			if err != nil {
+				return err
+			}
+			if msg.MsgHdr.Rcode != 0 {
+				return fmt.Errorf("[Error] AXFRDDNS: nameserver refused to update the zone: %s (%d)",
+					dns.RcodeToString[msg.MsgHdr.Rcode],
+					msg.MsgHdr.Rcode)
+			}
+
+			return nil
+		},
 	}
+}
 
-	if len(foundRecords) >= 1 && foundRecords[0].Type == "SOA" {
-		// Ignoring the SOA, others providers  don't manage it either.
-		foundRecords = foundRecords[1:]
+// hasDeletionForName returns true if there exist a corrections for [name] which is a deletion
+func hasDeletionForName(changes diff2.ChangeList, name string) bool {
+	for _, change := range changes {
+		switch change.Type {
+		case diff2.DELETE:
+			if change.Old[0].Name == name {
+				return true
+			}
+		}
 	}
+	return false
+}
 
-	hasDnssecRecords := false
-	if len(foundRecords) >= 1 {
-		last := foundRecords[len(foundRecords)-1]
-		if last.Type == "TXT" &&
-			last.Name == dnssecDummyLabel &&
-			len(last.TxtStrings) == 1 &&
-			last.TxtStrings[0] == dnssecDummyTxt {
-			hasDnssecRecords = true
-			foundRecords = foundRecords[0:(len(foundRecords) - 1)]
+// hasNSDeletion returns true if there exist a correction that deletes or changes an NS record
+func hasNSDeletion(changes diff2.ChangeList) bool {
+	for _, change := range changes {
+		switch change.Type {
+		case diff2.CHANGE:
+			if change.Old[0].Type == "NS" && change.Old[0].Name == "@" {
+				return true
+			}
+		case diff2.DELETE:
+			if change.Old[0].Type == "NS" && change.Old[0].Name == "@" {
+				return true
+			}
+		case diff2.CREATE:
+		case diff2.REPORT:
 		}
 	}
+	return false
+}
+
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (c *axfrddnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, foundRecords models.Records) ([]*models.Correction, error) {
+	txtutil.SplitSingleLongTxt(foundRecords) // Autosplit long TXT records
+
+	// Ignoring the SOA, others providers don't manage it either.
+	if len(foundRecords) >= 1 && foundRecords[0].Type == "SOA" {
+		foundRecords = foundRecords[1:]
+	}
 
 	// TODO(tlim): This check should be done on all providers. Move to the global validation code.
-	if dc.AutoDNSSEC == "on" && !hasDnssecRecords {
+	if dc.AutoDNSSEC == "on" && !c.hasDnssecRecords {
 		printer.Printf("Warning: AUTODNSSEC is enabled, but no DNSKEY or RRSIG record was found in the AXFR answer!\n")
 	}
-	if dc.AutoDNSSEC == "off" && hasDnssecRecords {
+	if dc.AutoDNSSEC == "off" && c.hasDnssecRecords {
 		printer.Printf("Warning: AUTODNSSEC is disabled, but DNSKEY or RRSIG records were found in the AXFR answer!\n")
 	}
 
-	// Normalize
-	models.PostProcessRecords(foundRecords)
-	txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
-
-	var corrections []*models.Correction
-	var create, del, mod diff.Changeset
-	var differ diff.Differ
-	if !diff2.EnableDiff2 {
-		differ = diff.New(dc)
-	} else {
-		differ = diff.NewCompat(dc)
+	// An RFC2136-compliant server must silently ignore an
+	// update that inserts a non-CNAME RRset when a CNAME RR
+	// with the same name is present in the zone (and
+	// vice-versa). Therefore we prefer to first remove records
+	// and then insert new ones.
+	//
+	// Compliant servers must also silently ignore an update
+	// that removes the last NS record of a zone. Therefore we
+	// don't want to remove all NS records before inserting a
+	// new one. Then, when an update want to change a NS record,
+	// we first insert a dummy NS record that we will remove
+	// at the end of the batched update.
+
+	var msgs []string
+	var msgs2 []string
+	update := new(dns.Msg)
+	update.SetUpdate(dc.Name + ".")
+	update.Id = uint16(c.rand.Intn(math.MaxUint16))
+	update2 := new(dns.Msg)
+	update2.SetUpdate(dc.Name + ".")
+	update2.Id = uint16(c.rand.Intn(math.MaxUint16))
+	hasTwoCorrections := false
+
+	dummyNs1, err := dns.NewRR(dc.Name + ". IN NS 255.255.255.255")
+	if err != nil {
+		return nil, err
 	}
-	_, create, del, mod, err = differ.IncrementalDiff(foundRecords)
+	dummyNs2, err := dns.NewRR(dc.Name + ". IN NS 255.255.255.255")
 	if err != nil {
 		return nil, err
 	}
 
-	buf := &bytes.Buffer{}
-	// Print a list of changes. Generate an actual change that is the zone
-	changes := false
-	for _, i := range create {
-		changes = true
-		fmt.Fprintln(buf, i)
-	}
-	for _, i := range del {
-		changes = true
-		fmt.Fprintln(buf, i)
-	}
-	for _, i := range mod {
-		changes = true
-		fmt.Fprintln(buf, i)
-	}
-	msg := fmt.Sprintf("DDNS UPDATES to '%s' (primary master: '%s'). Changes:\n%s", dc.Name, c.master, buf)
-
-	if changes {
-
-		corrections = append(corrections,
-			&models.Correction{
-				Msg: msg,
-				F: func() error {
-
-					// An RFC2136-compliant server must silently ignore an
-					// update that inserts a non-CNAME RRset when a CNAME RR
-					// with the same name is present in the zone (and
-					// vice-versa). Therefore we prefer to first remove records
-					// and then insert new ones.
-					//
-					// Compliant servers must also silently ignore an update
-					// that removes the last NS record of a zone. Therefore we
-					// don't want to remove all NS records before inserting a
-					// new one. For the particular case of NS record, we prefer
-					// to insert new records before ot remove old ones.
-					//
-					// This remarks does not apply for "modified" NS records, as
-					// updates are processed one-by-one.
-					//
-					// This provider does not allow modifying the TTL of an NS
-					// record in a zone that defines only one NS. That would
-					// would require removing the single NS record, before
-					// adding the new one. But who does that anyway?
-
-					update := new(dns.Msg)
-					update.SetUpdate(dc.Name + ".")
-					update.Id = uint16(c.rand.Intn(math.MaxUint16))
-					for _, c := range create {
-						if c.Desired.Type == "NS" {
-							update.Insert([]dns.RR{c.Desired.ToRR()})
-						}
-					}
-					for _, c := range del {
-						update.Remove([]dns.RR{c.Existing.ToRR()})
-					}
-					for _, c := range mod {
-						update.Remove([]dns.RR{c.Existing.ToRR()})
-						update.Insert([]dns.RR{c.Desired.ToRR()})
-					}
-					for _, c := range create {
-						if c.Desired.Type != "NS" {
-							update.Insert([]dns.RR{c.Desired.ToRR()})
-						}
-					}
+	if !diff2.EnableDiff2 {
 
-					client := new(dns.Client)
-					client.Net = c.updateMode
-					client.Timeout = dnsTimeout
-					if c.updateKey != nil {
-						client.TsigSecret =
-							map[string]string{c.updateKey.id: c.updateKey.secret}
-						update.SetTsig(c.updateKey.id, c.updateKey.algo, 300, time.Now().Unix())
-						if c.updateKey.algo == dns.HmacMD5 {
-							client.TsigProvider = md5Provider(c.updateKey.secret)
-						}
-					}
+		// Legacy code with the old `diff`
 
-					msg, _, err := client.Exchange(update, c.master)
-					if err != nil {
-						return err
-					}
-					if msg.MsgHdr.Rcode != 0 {
-						return fmt.Errorf("[Error] AXFRDDNS: nameserver refused to update the zone: %s (%d)",
-							dns.RcodeToString[msg.MsgHdr.Rcode],
-							msg.MsgHdr.Rcode)
+		differ := diff.New(dc)
+		_, create, del, mod, err := differ.IncrementalDiff(foundRecords)
+		if err != nil {
+			return nil, err
+		}
+
+		changes := false
+		buf := &bytes.Buffer{}
+		buf2 := &bytes.Buffer{}
+
+		// See comment below about hasNSDeletion.
+		hasNSDeletion := false
+		for _, c := range create {
+			if c.Desired.Type == "NS" && c.Desired.Name == "@" {
+				hasNSDeletion = true
+				continue
+			}
+		}
+		for _, c := range del {
+			if c.Existing.Type == "NS" && c.Existing.Name == "@" {
+				hasNSDeletion = true
+				continue
+			}
+		}
+		for _, c := range mod {
+			if c.Existing.Type == "NS" && c.Existing.Name == "@" {
+				hasNSDeletion = true
+				continue
+			}
+		}
+
+		if hasNSDeletion {
+			update.Insert([]dns.RR{dummyNs1})
+		}
+
+		for _, change := range del {
+			changes = true
+			fmt.Fprintln(buf, change)
+			update.Remove([]dns.RR{change.Existing.ToRR()})
+		}
+		for _, change := range mod {
+			changes = true
+			if c.serverHasBuggyCNAME && change.Desired.Type == "CNAME" {
+				fmt.Fprintln(buf, change.String()+color.RedString(" (delete)"))
+				update.Remove([]dns.RR{change.Existing.ToRR()})
+				hasTwoCorrections = true
+				fmt.Fprintln(buf2, change.String()+color.GreenString(" (create)"))
+				update2.Insert([]dns.RR{change.Desired.ToRR()})
+			} else {
+				fmt.Fprintln(buf, change)
+				update.Remove([]dns.RR{change.Existing.ToRR()})
+				update.Insert([]dns.RR{change.Desired.ToRR()})
+			}
+		}
+		for _, change := range create {
+			changes = true
+			splitted := false
+			if c.serverHasBuggyCNAME && change.Desired.Type == "CNAME" {
+				for _, change2 := range del {
+					if change2.Existing.Name == change.Desired.Name {
+						splitted = true
+						break
 					}
+				}
+			}
+			if splitted {
+				hasTwoCorrections = true
+				fmt.Fprintln(buf2, change)
+				update2.Insert([]dns.RR{change.Desired.ToRR()})
+			} else {
+				fmt.Fprintln(buf, change)
+				update.Insert([]dns.RR{change.Desired.ToRR()})
+			}
+		}
+
+		if hasNSDeletion {
+			update.Remove([]dns.RR{dummyNs2})
+		}
+
+		if !changes {
+			return nil, nil
+		}
+
+		if hasTwoCorrections {
+
+			return []*models.Correction{
+				c.BuildCorrection(dc, []string{buf.String()}, update),
+				c.BuildCorrection(dc, []string{buf2.String()}, update2),
+			}, nil
+
+		}
+
+		return []*models.Correction{
+			c.BuildCorrection(dc, []string{buf.String()}, update),
+		}, nil
+
+	}
 
-					return nil
-				},
-			})
+	changes, err := diff2.ByRecord(foundRecords, dc, nil)
+	if err != nil {
+		return nil, err
+	}
+	if changes == nil {
+		return nil, nil
+	}
+
+	// A DNS server should silently ignore a DDNS update that removes
+	// the last NS record of a zone. Since modifying a record is
+	// implemented by successively a deletion of the old record and an
+	// insertion of the new one, then modifying all the NS record of a
+	// zone might will fail (even if the the deletion and insertion
+	// are grouped in a single batched update).
+	//
+	// To avoid this case, we will first insert a dummy NS record,
+	// that will be removed at the end of the batched updates. This
+	// record needs to inserted only when all NS records are touched
+	// The current implementation insert this dummy record as soon as
+	// a NS record is deleted or changed.
+	hasNSDeletion := hasNSDeletion(changes)
+
+	if hasNSDeletion {
+		update.Insert([]dns.RR{dummyNs1})
+	}
+
+	for _, change := range changes {
+		switch change.Type {
+		case diff2.DELETE:
+			msgs = append(msgs, change.Msgs[0])
+			update.Remove([]dns.RR{change.Old[0].ToRR()})
+		case diff2.CREATE:
+			if c.serverHasBuggyCNAME &&
+				change.New[0].Type == "CNAME" &&
+				hasDeletionForName(changes, change.New[0].Name) {
+				hasTwoCorrections = true
+				msgs2 = append(msgs2, change.Msgs[0])
+				update2.Insert([]dns.RR{change.New[0].ToRR()})
+			} else {
+				msgs = append(msgs, change.Msgs[0])
+				update.Insert([]dns.RR{change.New[0].ToRR()})
+			}
+		case diff2.CHANGE:
+			if c.serverHasBuggyCNAME && change.New[0].Type == "CNAME" {
+				msgs = append(msgs, change.Msgs[0]+color.RedString(" (delete)"))
+				update.Remove([]dns.RR{change.Old[0].ToRR()})
+				hasTwoCorrections = true
+				msgs2 = append(msgs2, change.Msgs[0]+color.GreenString(" (create)"))
+				update2.Insert([]dns.RR{change.New[0].ToRR()})
+			} else {
+				msgs = append(msgs, change.Msgs[0])
+				update.Remove([]dns.RR{change.Old[0].ToRR()})
+				update.Insert([]dns.RR{change.New[0].ToRR()})
+			}
+		case diff2.REPORT:
+			msgs = append(msgs, change.Msgs...)
+		}
 	}
 
-	return corrections, nil
+	if hasNSDeletion {
+		update.Remove([]dns.RR{dummyNs2})
+	}
+
+	if hasTwoCorrections {
+		return []*models.Correction{
+			c.BuildCorrection(dc, msgs, update),
+			c.BuildCorrection(dc, msgs2, update2),
+		}, nil
+	}
+	return []*models.Correction{
+		c.BuildCorrection(dc, msgs, update),
+	}, nil
 
 }
diff --git a/providers/azuredns/azureDnsProvider.go b/providers/azuredns/azureDnsProvider.go
index 0d80519e..cdddb9ad 100644
--- a/providers/azuredns/azureDnsProvider.go
+++ b/providers/azuredns/azureDnsProvider.go
@@ -25,6 +25,8 @@ type azurednsProvider struct {
 	zones          map[string]*adns.Zone
 	resourceGroup  *string
 	subscriptionID *string
+	rawRecords     map[string][]*adns.RecordSet
+	zoneName       map[string]string
 }
 
 func newAzureDNSDsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
@@ -47,7 +49,14 @@ func newAzureDNS(m map[string]string, metadata json.RawMessage) (*azurednsProvid
 		return nil, recordErr
 	}
 
-	api := &azurednsProvider{zonesClient: zonesClient, recordsClient: recordsClient, resourceGroup: to.StringPtr(rg), subscriptionID: to.StringPtr(subID)}
+	api := &azurednsProvider{
+		zonesClient:    zonesClient,
+		recordsClient:  recordsClient,
+		resourceGroup:  to.StringPtr(rg),
+		subscriptionID: to.StringPtr(subID),
+		rawRecords:     map[string][]*adns.RecordSet{},
+		zoneName:       map[string]string{},
+	}
 	err := api.getZones()
 	if err != nil {
 		return nil, err
@@ -157,6 +166,7 @@ func (a *azurednsProvider) GetZoneRecords(domain string) (models.Records, error)
 	if err != nil {
 		return nil, err
 	}
+
 	return existingRecords, nil
 }
 
@@ -166,40 +176,32 @@ func (a *azurednsProvider) getExistingRecords(domain string) (models.Records, []
 		return nil, nil, "", errNoExist{domain}
 	}
 	zoneName := *zone.Name
-	records, err := a.fetchRecordSets(zoneName)
+	rawRecords, err := a.fetchRecordSets(zoneName)
 	if err != nil {
 		return nil, nil, "", err
 	}
 
 	var existingRecords models.Records
-	for _, set := range records {
+	for _, set := range rawRecords {
 		existingRecords = append(existingRecords, nativeToRecords(set, zoneName)...)
 	}
 
-	// FIXME(tlim): PostProcessRecords is usually called in GetDomainCorrections.
-	models.PostProcessRecords(existingRecords)
+	a.rawRecords[domain] = rawRecords
+	a.zoneName[domain] = zoneName
 
-	// FIXME(tlim): The "records" return value is usually stored in RecordConfig.Original.
-	return existingRecords, records, zoneName, nil
+	return existingRecords, rawRecords, zoneName, nil
 }
 
-func (a *azurednsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	err := dc.Punycode()
-
-	if err != nil {
-		return nil, err
-	}
-
-	existingRecords, records, zoneName, err := a.getExistingRecords(dc.Name)
-	if err != nil {
-		return nil, err
-	}
-
-	txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (a *azurednsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
+	txtutil.SplitSingleLongTxt(existingRecords) // Autosplit long TXT records
 
 	var corrections []*models.Correction
 	if !diff2.EnableDiff2 {
 
+		records := a.rawRecords[dc.Name]
+		zoneName := a.zoneName[dc.Name]
+
 		differ := diff.New(dc)
 		namesToUpdate, err := differ.ChangedGroups(existingRecords)
 		if err != nil {
@@ -309,13 +311,6 @@ func (a *azurednsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mod
 						F: func() error {
 							ctx, cancel := context.WithTimeout(context.Background(), 6000*time.Second)
 							defer cancel()
-							//fmt.Fprintf(os.Stderr, "DEBUG: 3 a.recordsClient.CreateOrUpdate(ctx, %v, %v, %v, %v, %+v, nil)\n", *a.resourceGroup, zoneName, recordName, recordType, *rrset)
-							// fmt.Fprintf(os.Stderr, "DEBUG: 3 rrset: %+v\n", *rrset)
-							// fmt.Fprintf(os.Stderr, "DEBUG: 3 properties: %+v\n", rrset.Properties)
-							// fmt.Fprintf(os.Stderr, "DEBUG: 3 TargetResource: %+v\n", rrset.Properties.TargetResource)
-							// if rrset.Properties.TargetResource != nil {
-							// 	fmt.Fprintf(os.Stderr, "DEBUG: 3 TargetResourceID: %+v\n", *rrset.Properties.TargetResource.ID)
-							// }
 							_, err := a.recordsClient.CreateOrUpdate(ctx, *a.resourceGroup, zoneName, recordName, recordType, *rrset, nil)
 							if err != nil {
 								return err
@@ -347,14 +342,8 @@ func (a *azurednsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mod
 		return nil, err
 	}
 
-	// for i, j := range changes {
-	// 	fmt.Fprintf(os.Stderr, "DEBUG: CHANGE[%v] = %+v\n", i, j)
-	// }
-
 	for _, change := range changes {
 
-		//fmt.Fprintf(os.Stderr, "\n\nCHANGE=%v\n", change)
-
 		// Copy all param values to local variables to avoid overwrites
 		msgs := change.MsgsJoined
 		dcn := dc.Name
@@ -364,34 +353,14 @@ func (a *azurednsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mod
 		case diff2.REPORT:
 			corrections = append(corrections, &models.Correction{Msg: change.MsgsJoined})
 		case diff2.CHANGE, diff2.CREATE:
-
 			changeNew := change.New
-
-			// for i, j := range change.Old {
-			// 	fmt.Fprintf(os.Stderr, "DEBUG: OLD[%d] = %+v ttl=%d\n", i, j, j.TTL)
-			// }
-			// for i, j := range change.New {
-			// 	fmt.Fprintf(os.Stderr, "DEBUG: NEW[%d] = %+v ttl=%d\n", i, j, j.TTL)
-			// }
-			// fmt.Fprintf(os.Stderr, "DEBUG: CHANGE = \n%v\n", change)
-
 			corrections = append(corrections, &models.Correction{
 				Msg: msgs,
 				F: func() error {
 					return a.recordCreate(dcn, chaKey, changeNew)
 				},
 			})
-
 		case diff2.DELETE:
-
-			// for i, j := range change.Old {
-			// 	fmt.Fprintf(os.Stderr, "DEBUG: OLD[%d] = %+v\n", i, j)
-			// }
-			// for i, j := range change.New {
-			// 	fmt.Fprintf(os.Stderr, "DEBUG: NEW[%d] = %+v\n", i, j)
-			// }
-			//fmt.Fprintf(os.Stderr, "DEBUG: CHANGE = \n%v\n", change)
-
 			corrections = append(corrections, &models.Correction{
 				Msg: msgs,
 				F: func() error {
@@ -414,31 +383,16 @@ func (a *azurednsProvider) recordCreate(zoneName string, reckey models.RecordKey
 		return err
 	}
 
-	//	rrset, _, err := a.recordToNativeDiff2(reckey, recs)
-	//	if err != nil {
-	//		return err
-	//	}
-
 	var recordName string
 	var i int64
 	for _, r := range recs {
 		i = int64(r.TTL)
 		recordName = r.Name
-		//fmt.Fprintf(os.Stderr, "DEBUG: rn=%v ttl=%d\n", recordName, i)
 	}
 	rrset.Properties.TTL = &i
 
 	ctx, cancel := context.WithTimeout(context.Background(), 6000*time.Second)
 	defer cancel()
-	//fmt.Fprintf(os.Stderr, "DEBUG: a.recordsClient.CreateOrUpdate(%v, %v, %v, %v, %+v)\n", *a.resourceGroup, zoneName, recordName, azRecType, *rrset)
-	//fmt.Fprintf(os.Stderr, "DEBUG: rrset.Properties=%+v\n", *rrset.Properties)
-	//fmt.Fprintf(os.Stderr, "DEBUG: recordCreate a.recordsClient.CreateOrUpdate(ctx, %v, %v, %v, %v, %+v, nil)\n", *a.resourceGroup, zoneName, recordName, azRecType, *rrset)
-	//fmt.Fprintf(os.Stderr, "DEBUG: recordCreate rrset: %+v\n", *rrset)
-	//fmt.Fprintf(os.Stderr, "DEBUG: recordCreate properties: %+v\n", rrset.Properties)
-	//fmt.Fprintf(os.Stderr, "DEBUG: recordCreate TargetResource: %+v\n", rrset.Properties.TargetResource)
-	// if rrset.Properties.TargetResource != nil {
-	// 	fmt.Fprintf(os.Stderr, "DEBUG: recordCreate TargetResourceID: %+v\n", *rrset.Properties.TargetResource.ID)
-	// }
 	_, err = a.recordsClient.CreateOrUpdate(ctx, *a.resourceGroup, zoneName, recordName, azRecType, *rrset, nil)
 	return err
 }
@@ -446,12 +400,10 @@ func (a *azurednsProvider) recordCreate(zoneName string, reckey models.RecordKey
 func (a *azurednsProvider) recordDelete(zoneName string, reckey models.RecordKey, recs models.Records) error {
 
 	shortName := strings.TrimSuffix(reckey.NameFQDN, "."+zoneName)
-	//_, azRecType, err := a.recordToNative(reckey, recs)
 	if shortName == zoneName {
 		shortName = "@"
 	}
 
-	//azRecType, err := nativeToRecordType(to.StringPtr(*recordSet.Type))
 	azRecType, err := nativeToRecordTypeDiff2(to.StringPtr(reckey.Type))
 	if err != nil {
 		return nil
@@ -459,7 +411,6 @@ func (a *azurednsProvider) recordDelete(zoneName string, reckey models.RecordKey
 
 	ctx, cancel := context.WithTimeout(context.Background(), 6000*time.Second)
 	defer cancel()
-	//fmt.Fprintf(os.Stderr, "DEBUG: a.recordsClient.Delete(%v, %v, %v,  %v)\n", *a.resourceGroup, zoneName, shortName, azRecType)
 	_, err = a.recordsClient.Delete(ctx, *a.resourceGroup, zoneName, shortName, azRecType, nil)
 	return err
 }
@@ -524,7 +475,6 @@ func nativeToRecordTypeDiff2(recordType *string) (adns.RecordType, error) {
 
 func safeTarget(t *string) string {
 	if t == nil {
-		//panic("no TARGET")
 		return "foundnil"
 	}
 	return *t
@@ -802,34 +752,11 @@ func (a *azurednsProvider) recordToNativeDiff2(recordKey models.RecordKey, recor
 			}
 			recordSet.Properties.CaaRecords = append(recordSet.Properties.CaaRecords, &adns.CaaRecord{Value: to.StringPtr(rec.GetTargetField()), Tag: to.StringPtr(rec.CaaTag), Flags: to.Int32Ptr(int32(rec.CaaFlag))})
 		case "AZURE_ALIAS_A", "AZURE_ALIAS_AAAA", "AZURE_ALIAS_CNAME":
-			//case "AZURE_ALIAS":
-			// fmt.Fprintf(os.Stderr, "DEBUG: AZURE_ALIAS recordToNativeDiff2 rec=%+v\n", rec)
-			// fmt.Fprintf(os.Stderr, "DEBUG: AZURE_ALIAS rec.Type=%v\n", rec.Type)
-			// fmt.Fprintf(os.Stderr, "DEBUG: AZURE_ALIAS rec.TTL=%v\n", rec.TTL)
-			// fmt.Fprintf(os.Stderr, "DEBUG: AZURE_ALIAS rec.Label=%v\n", rec.GetLabel())
-			// fmt.Fprintf(os.Stderr, "DEBUG: AZURE_ALIAS rec.Target=%v\n", rec.GetTargetField())
-			// fmt.Fprintf(os.Stderr, "DEBUG: AZURE_ALIAS rec.AzureAlias=%v\n", rec.AzureAlias)
-
-			// OLD *recordSet.Type = rec.AzureAlias["type"]
-			// NEW:
 			aatype := rec.AzureAlias["type"]
 			recordSet.Type = &aatype
-			// if aatype == "CNAME" {
-			// 	fmt.Fprintf(os.Stderr, "DEBUG: AZURE_ALIAS Cname -> MS.n.dnszones.CNAME\n")
-			// 	aatype = "Microsoft.Network/dnszones/CNAME"
-			// 	recordSet.Properties.CnameRecord = adns.CnameRecord{Cname: to.StringPtr(rec.GetTargetField())}
-			// 	//				recordSet.Properties.CnameRecord.Cname
-			// 	//recordSet.Properties.CnameRecord = &adns.CnameRecord{Cname: to.StringPtr(rec.GetTargetField())}
-			// }
-
-			// OLD: recordSet.Properties.TargetResource = &adns.SubResource{ID: to.StringPtr(rec.GetTargetField())}
-			// NEW:
 			aatarg := to.StringPtr(rec.GetTargetField())
 			aasub := adns.SubResource{ID: aatarg}
 			recordSet.Properties.TargetResource = &aasub
-			// fmt.Fprintf(os.Stderr, "DEBUG: AZURE_ALIAS rrset.Type=%v\n", *recordSet.Type)
-			// fmt.Fprintf(os.Stderr, "DEBUG: AZURE_ALIAS rrset.Target=%v\n", *aatarg)
-			// fmt.Fprintf(os.Stderr, "DEBUG: AZURE_ALIAS rrset.TargetRes=%+v\n", aasub)
 
 		default:
 			return nil, adns.RecordTypeA, fmt.Errorf("recordToNativeDiff2 RTYPE %v UNIMPLEMENTED", recordKeyType) // ands.A is a placeholder
diff --git a/providers/bind/bindProvider.go b/providers/bind/bindProvider.go
index 5e73b22a..a2da38f7 100644
--- a/providers/bind/bindProvider.go
+++ b/providers/bind/bindProvider.go
@@ -27,7 +27,6 @@ import (
 	"github.com/StackExchange/dnscontrol/v3/pkg/diff2"
 	"github.com/StackExchange/dnscontrol/v3/pkg/prettyzone"
 	"github.com/StackExchange/dnscontrol/v3/pkg/printer"
-	"github.com/StackExchange/dnscontrol/v3/pkg/txtutil"
 	"github.com/StackExchange/dnscontrol/v3/providers"
 	"github.com/miekg/dns"
 )
@@ -202,29 +201,11 @@ func ParseZoneContents(content string, zoneName string, zonefileName string) (mo
 	return foundRecords, nil
 }
 
-// GetDomainCorrections returns a list of corrections to update a domain.
-func (c *bindProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	dc.Punycode()
-
-	comments := make([]string, 0, 5)
-	comments = append(comments,
-		fmt.Sprintf("generated with dnscontrol %s", time.Now().Format(time.RFC3339)),
-	)
-	if dc.AutoDNSSEC == "on" {
-		// This does nothing but reminds the user to add the correct
-		// auto-dnssecc zone statement to named.conf.
-		// While it is a no-op, it is useful for situations where a zone
-		// has multiple providers.
-		comments = append(comments, "Automatic DNSSEC signing requested")
-	}
-
-	c.zonefile = filepath.Join(c.directory,
-		makeFileName(c.filenameformat, dc.UniqueName, dc.Name, dc.Tag))
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (c *bindProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, foundRecords models.Records) ([]*models.Correction, error) {
 
-	foundRecords, err := c.GetZoneRecords(dc.Name)
-	if err != nil {
-		return nil, err
-	}
+	changes := false
+	var msg string
 
 	// Find the SOA records; use them to make or update the desired SOA.
 	var foundSoa *models.RecordConfig
@@ -250,13 +231,6 @@ func (c *bindProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.
 		c.skipNextSoaIncrease = true
 	}
 
-	// Normalize
-	models.PostProcessRecords(foundRecords)
-	txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
-
-	changes := false
-	var msg string
-
 	if !diff2.EnableDiff2 {
 
 		differ := diff.New(dc)
@@ -296,6 +270,7 @@ func (c *bindProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.
 	} else {
 
 		var msgs []string
+		var err error
 		msgs, changes, err = diff2.ByZone(foundRecords, dc, nil)
 		if err != nil {
 			return nil, err
@@ -309,6 +284,18 @@ func (c *bindProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.
 	//fmt.Printf("DEBUG: BIND changes=%v\n", changes)
 	if changes {
 
+		comments := make([]string, 0, 5)
+		comments = append(comments,
+			fmt.Sprintf("generated with dnscontrol %s", time.Now().Format(time.RFC3339)),
+		)
+		if dc.AutoDNSSEC == "on" {
+			// This does nothing but reminds the user to add the correct
+			// auto-dnssecc zone statement to named.conf.
+			// While it is a no-op, it is useful for situations where a zone
+			// has multiple providers.
+			comments = append(comments, "Automatic DNSSEC signing requested")
+		}
+
 		// We only change the serial number if there is a change.
 		if !c.skipNextSoaIncrease {
 			desiredSoa.SoaSerial = nextSerial
diff --git a/providers/cloudflare/cloudflareProvider.go b/providers/cloudflare/cloudflareProvider.go
index 39fade50..179c4e94 100644
--- a/providers/cloudflare/cloudflareProvider.go
+++ b/providers/cloudflare/cloudflareProvider.go
@@ -3,7 +3,6 @@ package cloudflare
 import (
 	"encoding/json"
 	"fmt"
-	"log"
 	"net"
 	"strings"
 
@@ -15,7 +14,6 @@ import (
 	"github.com/StackExchange/dnscontrol/v3/providers"
 	"github.com/cloudflare/cloudflare-go"
 	"github.com/fatih/color"
-	"github.com/miekg/dns/dnsutil"
 )
 
 /*
@@ -112,17 +110,19 @@ func (c *cloudflareProvider) ListZones() ([]string, error) {
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
 func (c *cloudflareProvider) GetZoneRecords(domain string) (models.Records, error) {
-	id, err := c.getDomainID(domain)
+
+	domainID, err := c.getDomainID(domain)
 	if err != nil {
 		return nil, err
 	}
-	records, err := c.getRecordsForDomain(id, domain)
+	records, err := c.getRecordsForDomain(domainID, domain)
 	if err != nil {
 		return nil, err
 	}
+
 	for _, rec := range records {
-		if rec.TTL == 1 {
-			rec.TTL = 0
+		if rec.TTL == 0 {
+			rec.TTL = 1
 		}
 		// Store the proxy status ("orange cloud") for use by get-zones:
 		m := getProxyMetadata(rec)
@@ -133,6 +133,37 @@ func (c *cloudflareProvider) GetZoneRecords(domain string) (models.Records, erro
 			rec.Metadata["cloudflare_proxy"] = p
 		}
 	}
+
+	// // FIXME(tlim) Why is this needed???
+	// // I don't know. Let's comment it out and see if anything breaks.
+	// for i := len(records) - 1; i >= 0; i-- {
+	// 	rec := records[i]
+	// 	// Delete ignore labels
+	// 	if labelMatches(dnsutil.TrimDomainName(rec.Original.(cloudflare.DNSRecord).Name, dc.Name), c.ignoredLabels) {
+	// 		printer.Debugf("ignored_label: %s\n", rec.Original.(cloudflare.DNSRecord).Name)
+	// 		records = append(records[:i], records[i+1:]...)
+	// 	}
+	// }
+
+	if c.manageRedirects {
+		prs, err := c.getPageRules(domainID, domain)
+		if err != nil {
+			return nil, err
+		}
+		records = append(records, prs...)
+	}
+
+	if c.manageWorkers {
+		wrs, err := c.getWorkerRoutes(domainID, domain)
+		if err != nil {
+			return nil, err
+		}
+		records = append(records, wrs...)
+	}
+
+	// Normalize
+	models.PostProcessRecords(records)
+
 	return records, nil
 }
 
@@ -149,52 +180,26 @@ func (c *cloudflareProvider) getDomainID(name string) (string, error) {
 	return id, nil
 }
 
-// GetDomainCorrections returns a list of corrections to update a domain.
-func (c *cloudflareProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	err := dc.Punycode()
-	if err != nil {
-		return nil, err
-	}
-
-	domainID, err := c.getDomainID(dc.Name)
-	if err != nil {
-		return nil, err
-	}
-	records, err := c.getRecordsForDomain(domainID, dc.Name)
-	if err != nil {
-		return nil, err
-	}
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (c *cloudflareProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, records models.Records) ([]*models.Correction, error) {
 
 	if err := c.preprocessConfig(dc); err != nil {
 		return nil, err
 	}
-	for i := len(records) - 1; i >= 0; i-- {
-		rec := records[i]
-		// Delete ignore labels
-		if labelMatches(dnsutil.TrimDomainName(rec.Original.(cloudflare.DNSRecord).Name, dc.Name), c.ignoredLabels) {
-			printer.Debugf("ignored_label: %s\n", rec.Original.(cloudflare.DNSRecord).Name)
-			records = append(records[:i], records[i+1:]...)
-		}
-	}
+	//	for i := len(records) - 1; i >= 0; i-- {
+	//		rec := records[i]
+	//		// Delete ignore labels
+	//		if labelMatches(dnsutil.TrimDomainName(rec.Original.(cloudflare.DNSRecord).Name, dc.Name), c.ignoredLabels) {
+	//			printer.Debugf("ignored_label: %s\n", rec.Original.(cloudflare.DNSRecord).Name)
+	//			records = append(records[:i], records[i+1:]...)
+	//		}
+	//	}
 
-	if c.manageRedirects {
-		prs, err := c.getPageRules(domainID, dc.Name)
-		//printer.Printf("GET PAGE RULES:\n")
-		//for i, p := range prs {
-		//	printer.Printf("%03d: %q\n", i, p.GetTargetField())
-		//}
-		if err != nil {
-			return nil, err
-		}
-		records = append(records, prs...)
-	}
+	checkNSModifications(dc)
 
-	if c.manageWorkers {
-		wrs, err := c.getWorkerRoutes(domainID, dc.Name)
-		if err != nil {
-			return nil, err
-		}
-		records = append(records, wrs...)
+	domainID, err := c.getDomainID(dc.Name)
+	if err != nil {
+		return nil, err
 	}
 
 	for _, rec := range dc.Records {
@@ -207,24 +212,13 @@ func (c *cloudflareProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*m
 		if rec.Metadata[metaProxy] != "off" {
 			rec.TTL = 1
 		}
-		if labelMatches(rec.GetLabel(), c.ignoredLabels) {
-			log.Fatalf("FATAL: dnsconfig contains label that matches ignored_labels: %#v is in %v)\n", rec.GetLabel(), c.ignoredLabels)
-		}
+		//		if labelMatches(rec.GetLabel(), c.ignoredLabels) {
+		//			log.Fatalf("FATAL: dnsconfig contains label that matches ignored_labels: %#v is in %v)\n", rec.GetLabel(), c.ignoredLabels)
+		//		}
 	}
 
 	checkNSModifications(dc)
 
-	// Normalize
-	models.PostProcessRecords(records)
-	//txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
-	// Don't split.
-	// Cloudflare's API only supports one TXT string of any non-zero length. No
-	// multiple strings.
-	// When serving the DNS record, it splits strings >255 octets into
-	// individual segments of 255 each. However that is hidden from the API.
-	// Therefore, whether the string is 1 octet or thousands, just store it as
-	// one string in the first element of .TxtStrings.
-
 	var corrections []*models.Correction
 	if !diff2.EnableDiff2 {
 
@@ -347,7 +341,7 @@ func (c *cloudflareProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*m
 			corrs = c.mkCreateCorrection(createRec, domainID, msg)
 			// DS records must always have a corresponding NS record.
 			// Therefore, we create NS records before any DS records.
-			addToFront = createRec.Type == "NS"
+			addToFront = (createRec.Type == "NS")
 		case diff2.CHANGE:
 			newrec := inst.New[0]
 			oldrec := inst.Old[0]
@@ -359,7 +353,7 @@ func (c *cloudflareProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*m
 			corrs = c.mkDeleteCorrection(deleteRecType, deleteRecOrig, domainID, msg)
 			// DS records must always have a corresponding NS record.
 			// Therefore, we remove DS records before any NS records.
-			addToFront = deleteRecType == "DS"
+			addToFront = (deleteRecType == "DS")
 		}
 
 		if addToFront {
@@ -387,13 +381,16 @@ func (c *cloudflareProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*m
 }
 
 func genComparable(rec *models.RecordConfig) string {
-	//fmt.Printf("DEBUG: genComparable called %v:%v meta=%+v\n", rec.Type, rec.GetLabel(), rec.Metadata)
 	if rec.Type == "A" || rec.Type == "AAAA" || rec.Type == "CNAME" {
 		proxy := rec.Metadata[metaProxy]
 		if proxy != "" {
-			//return "proxy=" + rec.Metadata[metaProxy]
+			if proxy == "on" {
+				proxy = "true"
+			}
+			if proxy == "off" {
+				proxy = "false"
+			}
 			return "proxy=" + proxy
-			//return fmt.Sprintf("proxy:%v=%s", rec.Type, proxy)
 		}
 	}
 	return ""
@@ -417,6 +414,7 @@ func (c *cloudflareProvider) mkCreateCorrection(newrec *models.RecordConfig, dom
 }
 
 func (c *cloudflareProvider) mkChangeCorrection(oldrec, newrec *models.RecordConfig, domainID string, msg string) []*models.Correction {
+
 	var idTxt string
 	switch oldrec.Type {
 	case "PAGE_RULE":
@@ -433,14 +431,14 @@ func (c *cloudflareProvider) mkChangeCorrection(oldrec, newrec *models.RecordCon
 		return []*models.Correction{{
 			Msg: msg,
 			F: func() error {
-				return c.updatePageRule(oldrec.Original.(cloudflare.PageRule).ID, domainID, newrec.GetTargetField())
+				return c.updatePageRule(idTxt, domainID, newrec.GetTargetField())
 			},
 		}}
 	case "WORKER_ROUTE":
 		return []*models.Correction{{
 			Msg: msg,
 			F: func() error {
-				return c.updateWorkerRoute(oldrec.Original.(cloudflare.WorkerRoute).ID, domainID, newrec.GetTargetField())
+				return c.updateWorkerRoute(idTxt, domainID, newrec.GetTargetField())
 			},
 		}}
 	default:
diff --git a/providers/cloudns/api.go b/providers/cloudns/api.go
index 4bc981f7..f5f5ac35 100644
--- a/providers/cloudns/api.go
+++ b/providers/cloudns/api.go
@@ -109,6 +109,9 @@ func (c *cloudnsProvider) fetchAvailableTTLValues(domain string) error {
 }
 
 func (c *cloudnsProvider) fetchDomainList() error {
+	// FIXME(tlim): This should return nil if c.domainIndex != nil.  Then all
+	// the callers won't have to check if c.domainIndex == nil before calling.
+
 	c.domainIndex = map[string]string{}
 	rowsPerPage := 100
 	page := 1
diff --git a/providers/cloudns/cloudnsProvider.go b/providers/cloudns/cloudnsProvider.go
index 7ee6c1b5..7b0f3786 100644
--- a/providers/cloudns/cloudnsProvider.go
+++ b/providers/cloudns/cloudnsProvider.go
@@ -71,14 +71,45 @@ func (c *cloudnsProvider) GetNameservers(domain string) ([]*models.Nameserver, e
 	return models.ToNameservers(c.nameserversNames)
 }
 
-// GetDomainCorrections returns the corrections for a domain.
-func (c *cloudnsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	dc, err := dc.Copy()
-	if err != nil {
-		return nil, err
-	}
-
-	dc.Punycode()
+// // GetDomainCorrections returns the corrections for a domain.
+// func (c *cloudnsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
+// 	dc, err := dc.Copy()
+// 	if err != nil {
+// 		return nil, err
+// 	}
+
+// 	dc.Punycode()
+
+// 	if c.domainIndex == nil {
+// 		if err := c.fetchDomainList(); err != nil {
+// 			return nil, err
+// 		}
+// 	}
+// 	_, ok := c.domainIndex[dc.Name]
+// 	if !ok {
+// 		return nil, fmt.Errorf("'%s' not a zone in ClouDNS account", dc.Name)
+// 	}
+
+// 	existingRecords, err := c.GetZoneRecords(dc.Name)
+// 	if err != nil {
+// 		return nil, err
+// 	}
+// 	// Normalize
+// 	models.PostProcessRecords(existingRecords)
+
+// 	// Get a list of available TTL values.
+// 	// The TTL list needs to be obtained for each domain, so get it first here.
+// 	c.fetchAvailableTTLValues(dc.Name)
+// 	// ClouDNS can only be specified from a specific TTL list, so change the TTL in advance.
+// 	for _, record := range dc.Records {
+// 		record.TTL = fixTTL(record.TTL)
+// 	}
+
+// 	return c.GetZoneRecordsCorrections(dc, existingRecords)
+// }
+
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (c *cloudnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
 
 	if c.domainIndex == nil {
 		if err := c.fetchDomainList(); err != nil {
@@ -90,13 +121,6 @@ func (c *cloudnsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
 		return nil, fmt.Errorf("'%s' not a zone in ClouDNS account", dc.Name)
 	}
 
-	existingRecords, err := c.GetZoneRecords(dc.Name)
-	if err != nil {
-		return nil, err
-	}
-	// Normalize
-	models.PostProcessRecords(existingRecords)
-
 	// Get a list of available TTL values.
 	// The TTL list needs to be obtained for each domain, so get it first here.
 	c.fetchAvailableTTLValues(dc.Name)
diff --git a/providers/cscglobal/dns.go b/providers/cscglobal/dns.go
index b9990da0..6a18b141 100644
--- a/providers/cscglobal/dns.go
+++ b/providers/cscglobal/dns.go
@@ -75,46 +75,8 @@ func (client *providerClient) GetNameservers(domain string) ([]*models.Nameserve
 	return models.ToNameservers(nss)
 }
 
-// GetDomainCorrections get the current and existing records,
-// post-process them, and generate corrections.
-// NB(tlim): This function should be exactly the same in all DNS providers.  Once
-// all providers do this, we can eliminate it and use a Go interface instead.
-func (client *providerClient) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	existing, err := client.GetZoneRecords(dc.Name)
-	if err != nil {
-		return nil, err
-	}
-	models.PostProcessRecords(existing)
-	//txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
-
-	clean := PrepFoundRecords(existing)
-	PrepDesiredRecords(dc)
-	return client.GenerateDomainCorrections(dc, clean)
-}
-
-// PrepFoundRecords munges any records to make them compatible with
-// this provider. Usually this is a no-op.
-func PrepFoundRecords(recs models.Records) models.Records {
-	// If there are records that need to be modified, removed, etc. we
-	// do it here.  Usually this is a no-op.
-	return recs
-}
-
-// PrepDesiredRecords munges any records to best suit this provider.
-func PrepDesiredRecords(dc *models.DomainConfig) {
-	// Sort through the dc.Records, eliminate any that can't be
-	// supported; modify any that need adjustments to work with the
-	// provider.  We try to do minimal changes otherwise it gets
-	// confusing.
-
-	dc.Punycode()
-}
-
-// GetDomainCorrections gets existing records, diffs them against existing, and returns corrections.
-func (client *providerClient) GenerateDomainCorrections(dc *models.DomainConfig, foundRecords models.Records) ([]*models.Correction, error) {
-
-	// Normalize
-	models.PostProcessRecords(foundRecords)
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (client *providerClient) GetZoneRecordsCorrections(dc *models.DomainConfig, foundRecords models.Records) ([]*models.Correction, error) {
 	//txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
 
 	var corrections []*models.Correction
diff --git a/providers/desec/desecProvider.go b/providers/desec/desecProvider.go
index 17d80968..7a946c60 100644
--- a/providers/desec/desecProvider.go
+++ b/providers/desec/desecProvider.go
@@ -74,28 +74,28 @@ func (c *desecProvider) GetNameservers(domain string) ([]*models.Nameserver, err
 	return models.ToNameservers(defaultNameServerNames)
 }
 
-func (c *desecProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	if dc.AutoDNSSEC == "off" {
-		printer.Printf("Notice: DNSSEC signing was not requested, but cannot be turned off. (deSEC always signs all records.)\n")
-	}
-
-	existing, err := c.GetZoneRecords(dc.Name)
-	if err != nil {
-		return nil, err
-	}
-	models.PostProcessRecords(existing)
-	clean := PrepFoundRecords(existing)
-	var minTTL uint32
-	c.mutex.Lock()
-	if ttl, ok := c.domainIndex[dc.Name]; !ok {
-		minTTL = 3600
-	} else {
-		minTTL = ttl
-	}
-	c.mutex.Unlock()
-	PrepDesiredRecords(dc, minTTL)
-	return c.GenerateDomainCorrections(dc, clean)
-}
+// func (c *desecProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
+// 	if dc.AutoDNSSEC == "off" {
+// 		printer.Printf("Notice: DNSSEC signing was not requested, but cannot be turned off. (deSEC always signs all records.)\n")
+// 	}
+
+// 	existing, err := c.GetZoneRecords(dc.Name)
+// 	if err != nil {
+// 		return nil, err
+// 	}
+// 	models.PostProcessRecords(existing)
+// 	clean := PrepFoundRecords(existing)
+// 	var minTTL uint32
+// 	c.mutex.Lock()
+// 	if ttl, ok := c.domainIndex[dc.Name]; !ok {
+// 		minTTL = 3600
+// 	} else {
+// 		minTTL = ttl
+// 	}
+// 	c.mutex.Unlock()
+// 	PrepDesiredRecords(dc, minTTL)
+// 	return c.GetZoneRecordsCorrections(dc, clean)
+// }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
 func (c *desecProvider) GetZoneRecords(domain string) (models.Records, error) {
@@ -110,6 +110,7 @@ func (c *desecProvider) GetZoneRecords(domain string) (models.Records, error) {
 	for _, rr := range records {
 		existingRecords = append(existingRecords, nativeToRecords(rr, domain)...)
 	}
+
 	return existingRecords, nil
 }
 
@@ -123,14 +124,6 @@ func (c *desecProvider) EnsureZoneExists(domain string) error {
 	return c.createDomain(domain)
 }
 
-// PrepFoundRecords munges any records to make them compatible with
-// this provider. Usually this is a no-op.
-func PrepFoundRecords(recs models.Records) models.Records {
-	// If there are records that need to be modified, removed, etc. we
-	// do it here.  Usually this is a no-op.
-	return recs
-}
-
 // PrepDesiredRecords munges any records to best suit this provider.
 func PrepDesiredRecords(dc *models.DomainConfig, minTTL uint32) {
 	// Sort through the dc.Records, eliminate any that can't be
@@ -138,8 +131,7 @@ func PrepDesiredRecords(dc *models.DomainConfig, minTTL uint32) {
 	// provider.  We try to do minimal changes otherwise it gets
 	// confusing.
 
-	dc.Punycode()
-	txtutil.SplitSingleLongTxt(dc.Records)
+	//dc.Punycode()
 	recordsToKeep := make([]*models.RecordConfig, 0, len(dc.Records))
 	for _, rec := range dc.Records {
 		if rec.Type == "ALIAS" {
@@ -158,12 +150,19 @@ func PrepDesiredRecords(dc *models.DomainConfig, minTTL uint32) {
 	dc.Records = recordsToKeep
 }
 
-// GenerateDomainCorrections takes the desired and existing records
-// and produces a Correction list.  The correction list is simply
-// a list of functions to call to actually make the desired
-// correction, and a message to output to the user when the change is
-// made.
-func (c *desecProvider) GenerateDomainCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, error) {
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (c *desecProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, error) {
+	txtutil.SplitSingleLongTxt(dc.Records)
+
+	var minTTL uint32
+	c.mutex.Lock()
+	if ttl, ok := c.domainIndex[dc.Name]; !ok {
+		minTTL = 3600
+	} else {
+		minTTL = ttl
+	}
+	c.mutex.Unlock()
+	PrepDesiredRecords(dc, minTTL)
 
 	var corrections []*models.Correction
 	var err error
diff --git a/providers/digitalocean/digitaloceanProvider.go b/providers/digitalocean/digitaloceanProvider.go
index 2c3eb2de..17ced775 100644
--- a/providers/digitalocean/digitaloceanProvider.go
+++ b/providers/digitalocean/digitaloceanProvider.go
@@ -133,20 +133,12 @@ func (api *digitaloceanProvider) GetZoneRecords(domain string) (models.Records,
 	return existingRecords, nil
 }
 
-// GetDomainCorrections returns a list of corretions for the  domain.
-func (api *digitaloceanProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	ctx := context.Background()
-	dc.Punycode()
-
-	existingRecords, err := api.GetZoneRecords(dc.Name)
-	if err != nil {
-		return nil, err
-	}
-
-	// Normalize
-	models.PostProcessRecords(existingRecords)
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (api *digitaloceanProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
 	txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
 
+	ctx := context.Background()
+
 	var corrections []*models.Correction
 	var differ diff.Differ
 	if !diff2.EnableDiff2 {
diff --git a/providers/dnsimple/dnsimpleProvider.go b/providers/dnsimple/dnsimpleProvider.go
index 1c532f51..4c9100b0 100644
--- a/providers/dnsimple/dnsimpleProvider.go
+++ b/providers/dnsimple/dnsimpleProvider.go
@@ -133,17 +133,16 @@ func (c *dnsimpleProvider) GetZoneRecords(domain string) (models.Records, error)
 		cleanedRecords = append(cleanedRecords, rec)
 	}
 
+	// Apex NS are immutable via API
+	cleanedRecords = removeApexNS(cleanedRecords)
+
 	return cleanedRecords, nil
 }
 
-// GetDomainCorrections returns corrections that update a domain.
-func (c *dnsimpleProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
+func (c *dnsimpleProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, error) {
 	var corrections []*models.Correction
 
-	err := dc.Punycode()
-	if err != nil {
-		return nil, err
-	}
+	removeOtherApexNS(dc)
 
 	dnssecFixes, err := c.getDNSSECCorrections(dc)
 	if err != nil {
@@ -151,25 +150,13 @@ func (c *dnsimpleProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mod
 	}
 	corrections = append(corrections, dnssecFixes...)
 
-	records, err := c.GetZoneRecords(dc.Name)
-	if err != nil {
-		return nil, err
-	}
-	// Apex NS are immutable via API
-	actual := removeApexNS(records)
-	removeOtherApexNS(dc)
-
-	// Normalize
-	models.PostProcessRecords(actual)
-
-	var create, del, modify diff.Changeset
 	var differ diff.Differ
 	if !diff2.EnableDiff2 {
 		differ = diff.New(dc)
 	} else {
 		differ = diff.NewCompat(dc)
 	}
-	_, create, del, modify, err = differ.IncrementalDiff(actual)
+	_, create, del, modify, err := differ.IncrementalDiff(actual)
 	if err != nil {
 		return nil, err
 	}
diff --git a/providers/dnsmadeeasy/dnsMadeEasyProvider.go b/providers/dnsmadeeasy/dnsMadeEasyProvider.go
index dcd5ec73..e191dbd9 100644
--- a/providers/dnsmadeeasy/dnsMadeEasyProvider.go
+++ b/providers/dnsmadeeasy/dnsMadeEasyProvider.go
@@ -63,14 +63,47 @@ func New(settings map[string]string, _ json.RawMessage) (providers.DNSServicePro
 	return api, nil
 }
 
-// GetDomainCorrections returns the corrections for a domain.
-func (api *dnsMadeEasyProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	dc, err := dc.Copy()
-	if err != nil {
-		return nil, err
-	}
+// // GetDomainCorrections returns the corrections for a domain.
+// func (api *dnsMadeEasyProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
+// 	dc, err := dc.Copy()
+// 	if err != nil {
+// 		return nil, err
+// 	}
+
+// 	err = dc.Punycode()
+// 	if err != nil {
+// 		return nil, err
+// 	}
+
+// 	for _, rec := range dc.Records {
+// 		if rec.Type == "ALIAS" {
+// 			// ALIAS is called ANAME on DNS Made Easy
+// 			rec.Type = "ANAME"
+// 		} else if rec.Type == "NS" {
+// 			// NS records have fixed TTL on DNS Made Easy and it cannot be changed
+// 			rec.TTL = fixedNameServerRecordTTL
+// 		}
+// 	}
+
+// 	domainName := dc.Name
+
+// 	// Get existing records
+// 	existingRecords, err := api.GetZoneRecords(domainName)
+// 	if err != nil {
+// 		return nil, err
+// 	}
+
+// 	// Normalize
+// 	models.PostProcessRecords(existingRecords)
+
+// 	return api.GetZoneRecordsCorrections(dc, existingRecords)
+// }
+
+func (api *dnsMadeEasyProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
+	txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
 
-	err = dc.Punycode()
+	domainName := dc.Name
+	domain, err := api.findDomain(domainName)
 	if err != nil {
 		return nil, err
 	}
@@ -85,32 +118,14 @@ func (api *dnsMadeEasyProvider) GetDomainCorrections(dc *models.DomainConfig) ([
 		}
 	}
 
-	domainName := dc.Name
-
-	domain, err := api.findDomain(domainName)
-	if err != nil {
-		return nil, err
-	}
-
-	// Get existing records
-	existingRecords, err := api.GetZoneRecords(domainName)
-	if err != nil {
-		return nil, err
-	}
-
-	// Normalize
-	models.PostProcessRecords(existingRecords)
-	txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
-
 	var corrections []*models.Correction
-	var create, del, modify diff.Changeset
 	var differ diff.Differ
 	if !diff2.EnableDiff2 {
 		differ = diff.New(dc)
 	} else {
 		differ = diff.NewCompat(dc)
 	}
-	_, create, del, modify, err = differ.IncrementalDiff(existingRecords)
+	_, create, del, modify, err := differ.IncrementalDiff(existingRecords)
 	if err != nil {
 		return nil, err
 	}
diff --git a/providers/domainnameshop/dns.go b/providers/domainnameshop/dns.go
index eb6d4b0e..0a6d8575 100644
--- a/providers/domainnameshop/dns.go
+++ b/providers/domainnameshop/dns.go
@@ -25,15 +25,8 @@ func (api *domainNameShopProvider) GetZoneRecords(domain string) (models.Records
 	return existingRecords, nil
 }
 
-func (api *domainNameShopProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	dc.Punycode()
-	existingRecords, err := api.GetZoneRecords(dc.Name)
-	if err != nil {
-		return nil, err
-	}
-
-	// Normalize
-	models.PostProcessRecords(existingRecords)
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (api *domainNameShopProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
 
 	// Merge TXT strings to one string
 	for _, rc := range dc.Records {
@@ -48,14 +41,13 @@ func (api *domainNameShopProvider) GetDomainCorrections(dc *models.DomainConfig)
 	}
 
 	var corrections []*models.Correction
-	var create, delete, modify diff.Changeset
 	var differ diff.Differ
 	if !diff2.EnableDiff2 {
 		differ = diff.New(dc)
 	} else {
 		differ = diff.NewCompat(dc)
 	}
-	_, create, delete, modify, err = differ.IncrementalDiff(existingRecords)
+	_, create, delete, modify, err := differ.IncrementalDiff(existingRecords)
 	if err != nil {
 		return nil, err
 	}
diff --git a/providers/exoscale/exoscaleProvider.go b/providers/exoscale/exoscaleProvider.go
index a939fae9..658ab6d9 100644
--- a/providers/exoscale/exoscaleProvider.go
+++ b/providers/exoscale/exoscaleProvider.go
@@ -88,22 +88,13 @@ func (c *exoscaleProvider) GetNameservers(domain string) ([]*models.Nameserver,
 }
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (c *exoscaleProvider) GetZoneRecords(domain string) (models.Records, error) {
-	return nil, fmt.Errorf("not implemented")
-	// This enables the get-zones subcommand.
-	// Implement this by extracting the code from GetDomainCorrections into
-	// a single function.  For most providers this should be relatively easy.
-}
-
-// GetDomainCorrections returns a list of corretions for the  domain.
-func (c *exoscaleProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	dc.Punycode()
+func (c *exoscaleProvider) GetZoneRecords(domainName string) (models.Records, error) {
+	//dc.Punycode()
 
-	domain, err := c.findDomainByName(dc.Name)
+	domain, err := c.findDomainByName(domainName)
 	if err != nil {
 		return nil, err
 	}
-
 	domainID := *domain.ID
 
 	ctx := context.Background()
@@ -165,7 +156,7 @@ func (c *exoscaleProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mod
 		if record.TTL != nil {
 			rc.TTL = uint32(*record.TTL)
 		}
-		rc.SetLabel(rname, dc.Name)
+		rc.SetLabel(rname, domainName)
 
 		switch rtype {
 		case "ALIAS", "URL":
@@ -178,7 +169,7 @@ func (c *exoscaleProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mod
 			}
 			err = rc.SetTargetMX(prio, rcontent)
 		default:
-			err = rc.PopulateFromString(rtype, rcontent, dc.Name)
+			err = rc.PopulateFromString(rtype, rcontent, domainName)
 		}
 		if err != nil {
 			return nil, fmt.Errorf("unparsable record received from exoscale: %w", err)
@@ -186,25 +177,33 @@ func (c *exoscaleProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mod
 
 		existingRecords = append(existingRecords, rc)
 	}
-	removeOtherNS(dc)
 
-	// Normalize
-	models.PostProcessRecords(existingRecords)
+	return existingRecords, nil
+}
+
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (c *exoscaleProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
+
+	removeOtherNS(dc)
+	domain, err := c.findDomainByName(dc.Name)
+	if err != nil {
+		return nil, err
+	}
+	domainID := *domain.ID
 
 	var corrections []*models.Correction
-	var create, delete, modify diff.Changeset
 	var differ diff.Differ
 	if !diff2.EnableDiff2 {
 		differ = diff.New(dc)
 	} else {
 		differ = diff.NewCompat(dc)
 	}
-	_, create, delete, modify, err = differ.IncrementalDiff(existingRecords)
+	_, create, toDelete, modify, err := differ.IncrementalDiff(existingRecords)
 	if err != nil {
 		return nil, err
 	}
 
-	for _, del := range delete {
+	for _, del := range toDelete {
 		record := del.Existing.Original.(*egoscale.DNSDomainRecord)
 		corrections = append(corrections, &models.Correction{
 			Msg: del.String(),
diff --git a/providers/gandiv5/gandi_v5Provider.go b/providers/gandiv5/gandi_v5Provider.go
index 9f56a1f9..b60d15b5 100644
--- a/providers/gandiv5/gandi_v5Provider.go
+++ b/providers/gandiv5/gandi_v5Provider.go
@@ -127,29 +127,6 @@ func newHelper(m map[string]string, metadata json.RawMessage) (*gandiv5Provider,
 // 	return zones, nil
 // }
 
-// NB(tal): To future-proof your code, all new providers should
-// implement GetDomainCorrections exactly as you see here
-// (byte-for-byte the same). In 3.0
-// we plan on using just the individual calls to GetZoneRecords,
-// PostProcessRecords, and so on.
-//
-// Currently every provider does things differently, which prevents
-// us from doing things like using GetZoneRecords() of a provider
-// to make convertzone work with all providers.
-
-// GetDomainCorrections get the current and existing records,
-// post-process them, and generate corrections.
-func (client *gandiv5Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	existing, err := client.GetZoneRecords(dc.Name)
-	if err != nil {
-		return nil, err
-	}
-	models.PostProcessRecords(existing)
-	clean := PrepFoundRecords(existing)
-	PrepDesiredRecords(dc)
-	return client.GenerateDomainCorrections(dc, clean)
-}
-
 // GetZoneRecords gathers the DNS records and converts them to
 // dnscontrol's format.
 func (client *gandiv5Provider) GetZoneRecords(domain string) (models.Records, error) {
@@ -178,14 +155,6 @@ func (client *gandiv5Provider) GetZoneRecords(domain string) (models.Records, er
 	return existingRecords, nil
 }
 
-// PrepFoundRecords munges any records to make them compatible with
-// this provider. Usually this is a no-op.
-func PrepFoundRecords(recs models.Records) models.Records {
-	// If there are records that need to be modified, removed, etc. we
-	// do it here.  Usually this is a no-op.
-	return recs
-}
-
 // PrepDesiredRecords munges any records to best suit this provider.
 func PrepDesiredRecords(dc *models.DomainConfig) {
 	// Sort through the dc.Records, eliminate any that can't be
@@ -193,8 +162,6 @@ func PrepDesiredRecords(dc *models.DomainConfig) {
 	// provider.  We try to do minimal changes otherwise it gets
 	// confusing.
 
-	dc.Punycode()
-
 	recordsToKeep := make([]*models.RecordConfig, 0, len(dc.Records))
 	for _, rec := range dc.Records {
 		if rec.Type == "ALIAS" && rec.Name != "@" {
@@ -224,16 +191,13 @@ func PrepDesiredRecords(dc *models.DomainConfig) {
 	dc.Records = recordsToKeep
 }
 
-// GenerateDomainCorrections takes the desired and existing records
-// and produces a Correction list.  The correction list is simply
-// a list of functions to call to actually make the desired
-// correction, and a message to output to the user when the change is
-// made.
-func (client *gandiv5Provider) GenerateDomainCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, error) {
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (client *gandiv5Provider) GetZoneRecordsCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, error) {
 	if client.debug {
 		debugRecords("GenDC input", existing)
 	}
 
+	PrepDesiredRecords(dc)
 	txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
 
 	var corrections []*models.Correction
@@ -368,7 +332,7 @@ func (client *gandiv5Provider) GenerateDomainCorrections(dc *models.DomainConfig
 		case diff2.CREATE:
 			// We have to create the label one rtype at a time.
 			// In other words, this is a ByRecordSet API for creation, even though
-			// this is a ByLabel() API for everything else.
+			// this is very ByLabel()-ish for everything else.
 			natives := recordsToNative(inst.New, dc.Name)
 			for _, n := range natives {
 				label := inst.Key.NameFQDN
diff --git a/providers/gcloud/gcloudProvider.go b/providers/gcloud/gcloudProvider.go
index 62c72533..4b524424 100644
--- a/providers/gcloud/gcloudProvider.go
+++ b/providers/gcloud/gcloudProvider.go
@@ -53,6 +53,9 @@ type gcloudProvider struct {
 	project       string
 	nameServerSet *string
 	zones         map[string]*gdns.ManagedZone
+	// diff1
+	oldRRsMap   map[string]map[key]*gdns.ResourceRecordSet
+	zoneNameMap map[string]string
 }
 
 type errNoExist struct {
@@ -105,6 +108,8 @@ func New(cfg map[string]string, metadata json.RawMessage) (providers.DNSServiceP
 		client:        dcli,
 		nameServerSet: nss,
 		project:       cfg["project_id"],
+		oldRRsMap:     map[string]map[key]*gdns.ResourceRecordSet{},
+		zoneNameMap:   map[string]string{},
 	}
 	return g, g.loadZoneInfo()
 }
@@ -168,14 +173,14 @@ func keyForRec(r *models.RecordConfig) key {
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
 func (g *gcloudProvider) GetZoneRecords(domain string) (models.Records, error) {
-	existingRecords, _, _, err := g.getZoneSets(domain)
+	existingRecords, err := g.getZoneSets(domain)
 	return existingRecords, err
 }
 
-func (g *gcloudProvider) getZoneSets(domain string) (models.Records, map[key]*gdns.ResourceRecordSet, string, error) {
+func (g *gcloudProvider) getZoneSets(domain string) (models.Records, error) {
 	rrs, zoneName, err := g.getRecords(domain)
 	if err != nil {
-		return nil, nil, "", err
+		return nil, err
 	}
 	// convert to dnscontrol RecordConfig format
 	existingRecords := []*models.RecordConfig{}
@@ -185,28 +190,33 @@ func (g *gcloudProvider) getZoneSets(domain string) (models.Records, map[key]*gd
 		for _, rec := range set.Rrdatas {
 			rt, err := nativeToRecord(set, rec, domain)
 			if err != nil {
-				return nil, nil, "", err
+				return nil, err
 			}
 
 			existingRecords = append(existingRecords, rt)
 		}
 	}
-	return existingRecords, oldRRs, zoneName, err
+
+	g.oldRRsMap[domain] = oldRRs
+	g.zoneNameMap[domain] = zoneName
+
+	return existingRecords, err
 }
 
-func (g *gcloudProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	if err := dc.Punycode(); err != nil {
-		return nil, fmt.Errorf("punycode error: %w", err)
-	}
-	existingRecords, oldRRs, zoneName, err := g.getZoneSets(dc.Name)
-	if err != nil {
-		return nil, fmt.Errorf("getzonesets error: %w", err)
-	}
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (g *gcloudProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
 
-	// Normalize
-	models.PostProcessRecords(existingRecords)
 	txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
 
+	oldRRs, ok := g.oldRRsMap[dc.Name]
+	if !ok {
+		return nil, fmt.Errorf("oldRRsMap: no zone named %q", dc.Name)
+	}
+	zoneName, ok := g.zoneNameMap[dc.Name]
+	if !ok {
+		return nil, fmt.Errorf("zoneNameMap: no zone named %q", dc.Name)
+	}
+
 	// first collect keys that have changed
 	var differ diff.Differ
 	if !diff2.EnableDiff2 {
diff --git a/providers/gcore/gcoreProvider.go b/providers/gcore/gcoreProvider.go
index 08343a35..760059f1 100644
--- a/providers/gcore/gcoreProvider.go
+++ b/providers/gcore/gcoreProvider.go
@@ -76,17 +76,6 @@ func (c *gcoreProvider) GetNameservers(domain string) ([]*models.Nameserver, err
 	return models.ToNameservers(defaultNameServerNames)
 }
 
-func (c *gcoreProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	existing, err := c.GetZoneRecords(dc.Name)
-	if err != nil {
-		return nil, err
-	}
-	models.PostProcessRecords(existing)
-	clean := PrepFoundRecords(existing)
-	PrepDesiredRecords(dc)
-	return c.GenerateDomainCorrections(dc, clean)
-}
-
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
 func (c *gcoreProvider) GetZoneRecords(domain string) (models.Records, error) {
 	zone, err := c.provider.Zone(c.ctx, domain)
@@ -132,19 +121,6 @@ func (c *gcoreProvider) EnsureZoneExists(domain string) error {
 	return err
 }
 
-// PrepFoundRecords munges any records to make them compatible with
-// this provider. Usually this is a no-op.
-func PrepFoundRecords(recs models.Records) models.Records {
-	// If there are records that need to be modified, removed, etc. we
-	// do it here.  Usually this is a no-op.
-	return recs
-}
-
-// PrepDesiredRecords munges any records to best suit this provider.
-func PrepDesiredRecords(dc *models.DomainConfig) {
-	dc.Punycode()
-}
-
 func generateChangeMsg(updates []string) string {
 	return strings.Join(updates, "\n")
 }
@@ -154,7 +130,7 @@ func generateChangeMsg(updates []string) string {
 // a list of functions to call to actually make the desired
 // correction, and a message to output to the user when the change is
 // made.
-func (c *gcoreProvider) GenerateDomainCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, error) {
+func (c *gcoreProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, error) {
 
 	// Make delete happen earlier than creates & updates.
 	var corrections []*models.Correction
diff --git a/providers/hedns/hednsProvider.go b/providers/hedns/hednsProvider.go
index e55ef137..021b04dc 100644
--- a/providers/hedns/hednsProvider.go
+++ b/providers/hedns/hednsProvider.go
@@ -178,18 +178,8 @@ func (c *hednsProvider) GetNameservers(_ string) ([]*models.Nameserver, error) {
 	return models.ToNameservers(defaultNameservers)
 }
 
-// GetDomainCorrections returns a list of corrections for the  domain.
-func (c *hednsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-
-	err := dc.Punycode()
-	if err != nil {
-		return nil, err
-	}
-
-	records, err := c.GetZoneRecords(dc.Name)
-	if err != nil {
-		return nil, err
-	}
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (c *hednsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, records models.Records) ([]*models.Correction, error) {
 
 	// Get the SOA record to get the ZoneID, then remove it from the list.
 	zoneID := uint64(0)
@@ -203,7 +193,6 @@ func (c *hednsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models
 	}
 
 	// Normalize
-	models.PostProcessRecords(prunedRecords)
 	txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
 
 	// Fallback to legacy mode if diff2 is not enabled, remove when diff1 is deprecated.
@@ -215,7 +204,6 @@ func (c *hednsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models
 
 func (c *hednsProvider) getDiff1DomainCorrections(dc *models.DomainConfig, zoneID uint64, records models.Records) ([]*models.Correction, error) {
 	var corrections []*models.Correction
-	var toCreate, toDelete, toModify diff.Changeset
 
 	differ := diff.New(dc)
 	_, toCreate, toDelete, toModify, err := differ.IncrementalDiff(records)
diff --git a/providers/hetzner/hetznerProvider.go b/providers/hetzner/hetznerProvider.go
index a4e3ec34..63d6c26d 100644
--- a/providers/hetzner/hetznerProvider.go
+++ b/providers/hetzner/hetznerProvider.go
@@ -69,38 +69,20 @@ func (api *hetznerProvider) EnsureZoneExists(domain string) error {
 	return api.createZone(domain)
 }
 
-// GetDomainCorrections returns the corrections for a domain.
-func (api *hetznerProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	dc, err := dc.Copy()
-	if err != nil {
-		return nil, err
-	}
-
-	err = dc.Punycode()
-	if err != nil {
-		return nil, err
-	}
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (api *hetznerProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
 	domain := dc.Name
 
-	// Get existing records
-	existingRecords, err := api.GetZoneRecords(domain)
-	if err != nil {
-		return nil, err
-	}
-
-	// Normalize
-	models.PostProcessRecords(existingRecords)
 	txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
 
 	var corrections []*models.Correction
-	var create, del, modify diff.Changeset
 	var differ diff.Differ
 	if !diff2.EnableDiff2 {
 		differ = diff.New(dc)
 	} else {
 		differ = diff.NewCompat(dc)
 	}
-	_, create, del, modify, err = differ.IncrementalDiff(existingRecords)
+	_, create, del, modify, err := differ.IncrementalDiff(existingRecords)
 	if err != nil {
 		return nil, err
 	}
diff --git a/providers/hexonet/records.go b/providers/hexonet/records.go
index 31dbd02a..6ca310ee 100644
--- a/providers/hexonet/records.go
+++ b/providers/hexonet/records.go
@@ -57,12 +57,8 @@ func (n *HXClient) GetZoneRecords(domain string) (models.Records, error) {
 
 }
 
-// GetDomainCorrections gathers correctios that would bring n to match dc.
-func (n *HXClient) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	err := dc.Punycode()
-	if err != nil {
-		return nil, err
-	}
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (n *HXClient) GetZoneRecordsCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, error) {
 
 	actual, err := n.GetZoneRecords(dc.Name)
 	if err != nil {
@@ -71,19 +67,16 @@ func (n *HXClient) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Corr
 
 	//checkNSModifications(dc)
 
-	// Normalize
-	models.PostProcessRecords(actual)
 	txtutil.SplitSingleLongTxt(dc.Records)
 
 	var corrections []*models.Correction
-	var create, del, mod diff.Changeset
 	var differ diff.Differ
 	if !diff2.EnableDiff2 {
 		differ = diff.New(dc)
 	} else {
 		differ = diff.NewCompat(dc)
 	}
-	_, create, del, mod, err = differ.IncrementalDiff(actual)
+	_, create, del, mod, err := differ.IncrementalDiff(actual)
 	if err != nil {
 		return nil, err
 	}
diff --git a/providers/hostingde/hostingdeProvider.go b/providers/hostingde/hostingdeProvider.go
index 72cf34ac..92ebe554 100644
--- a/providers/hostingde/hostingdeProvider.go
+++ b/providers/hostingde/hostingdeProvider.go
@@ -117,13 +117,9 @@ func soaToString(s soaValues) string {
 	return fmt.Sprintf("refresh=%d retry=%d expire=%d negativettl=%d ttl=%d", s.Refresh, s.Retry, s.Expire, s.NegativeTTL, s.TTL)
 }
 
-func (hp *hostingdeProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	err := dc.Punycode()
-	if err != nil {
-		return nil, err
-	}
-
-	zoneChanged := false
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (hp *hostingdeProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, records models.Records) ([]*models.Correction, error) {
+	var err error
 
 	// TTL must be between (inclusive) 1m and 1y (in fact, a little bit more)
 	for _, r := range dc.Records {
@@ -134,13 +130,14 @@ func (hp *hostingdeProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*m
 			r.TTL = 31556926
 		}
 	}
+
+	zoneChanged := false
+
 	zone, err := hp.getZone(dc.Name)
 	if err != nil {
 		return nil, err
 	}
 
-	records := hp.APIRecordsToStandardRecordsModel(dc.Name, zone.Records)
-
 	var create, del, mod diff.Changeset
 	if !diff2.EnableDiff2 {
 		_, create, del, mod, err = diff.New(dc).IncrementalDiff(records)
@@ -320,10 +317,10 @@ func firstNonZero(items ...uint32) uint32 {
 }
 
 func (hp *hostingdeProvider) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	err := dc.Punycode()
-	if err != nil {
-		return nil, err
-	}
+	// err := dc.Punycode()
+	// if err != nil {
+	// 	return nil, err
+	// }
 
 	found, err := hp.getNameservers(dc.Name)
 	if err != nil {
diff --git a/providers/inwx/inwxProvider.go b/providers/inwx/inwxProvider.go
index 80430569..401ee853 100644
--- a/providers/inwx/inwxProvider.go
+++ b/providers/inwx/inwxProvider.go
@@ -226,32 +226,24 @@ func checkRecords(records models.Records) error {
 	return nil
 }
 
-// GetDomainCorrections finds the currently existing records and returns the corrections required to update them.
-func (api *inwxAPI) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	dc.Punycode()
+// 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, error) {
 
-	foundRecords, err := api.GetZoneRecords(dc.Name)
-	if err != nil {
-		return nil, err
-	}
-
-	models.PostProcessRecords(foundRecords)
 	txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
 
-	err = checkRecords(dc.Records)
+	err := checkRecords(dc.Records)
 	if err != nil {
 		return nil, err
 	}
 
 	var corrections []*models.Correction
-	var create, del, mod diff.Changeset
 	var differ diff.Differ
 	if !diff2.EnableDiff2 {
 		differ = diff.New(dc)
 	} else {
 		differ = diff.NewCompat(dc)
 	}
-	_, create, del, mod, err = differ.IncrementalDiff(foundRecords)
+	_, create, del, mod, err := differ.IncrementalDiff(foundRecords)
 	if err != nil {
 		return nil, err
 	}
diff --git a/providers/linode/linodeProvider.go b/providers/linode/linodeProvider.go
index 98e8537c..ad5fd6ce 100644
--- a/providers/linode/linodeProvider.go
+++ b/providers/linode/linodeProvider.go
@@ -122,17 +122,19 @@ func (api *linodeProvider) GetZoneRecords(domain string) (models.Records, error)
 	return api.getRecordsForDomain(domainID, domain)
 }
 
-// GetDomainCorrections returns the corrections for a domain.
-func (api *linodeProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	dc, err := dc.Copy()
-	if err != nil {
-		return nil, err
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (api *linodeProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
+	// Linode doesn't allow selecting an arbitrary TTL, only a set of predefined values
+	// We need to make sure we don't change it every time if it is as close as it's going to get
+	// By experimentation, Linode always rounds up. 300 -> 300, 301 -> 3600.
+	// https://github.com/linode/manager/blob/edd99dc4e1be5ab8190f243c3dbf8b830716255e/src/domains/components/SelectDNSSeconds.js#L19
+	for _, record := range dc.Records {
+		record.TTL = fixTTL(record.TTL)
 	}
 
-	dc.Punycode()
-
+	var err error
 	if api.domainIndex == nil {
-		if err := api.fetchDomainList(); err != nil {
+		if err = api.fetchDomainList(); err != nil {
 			return nil, err
 		}
 	}
@@ -141,31 +143,14 @@ func (api *linodeProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mod
 		return nil, fmt.Errorf("'%s' not a zone in Linode account", dc.Name)
 	}
 
-	existingRecords, err := api.getRecordsForDomain(domainID, dc.Name)
-	if err != nil {
-		return nil, err
-	}
-
-	// Normalize
-	models.PostProcessRecords(existingRecords)
-
-	// Linode doesn't allow selecting an arbitrary TTL, only a set of predefined values
-	// We need to make sure we don't change it every time if it is as close as it's going to get
-	// By experimentation, Linode always rounds up. 300 -> 300, 301 -> 3600.
-	// https://github.com/linode/manager/blob/edd99dc4e1be5ab8190f243c3dbf8b830716255e/src/domains/components/SelectDNSSeconds.js#L19
-	for _, record := range dc.Records {
-		record.TTL = fixTTL(record.TTL)
-	}
-
 	var corrections []*models.Correction
-	var create, del, modify diff.Changeset
 	var differ diff.Differ
 	if !diff2.EnableDiff2 {
 		differ = diff.New(dc)
 	} else {
 		differ = diff.NewCompat(dc)
 	}
-	_, create, del, modify, err = differ.IncrementalDiff(existingRecords)
+	_, create, del, modify, err := differ.IncrementalDiff(existingRecords)
 	if err != nil {
 		return nil, err
 	}
diff --git a/providers/loopia/loopiaProvider.go b/providers/loopia/loopiaProvider.go
index d990178f..e3313841 100644
--- a/providers/loopia/loopiaProvider.go
+++ b/providers/loopia/loopiaProvider.go
@@ -156,29 +156,6 @@ func (c *APIClient) ListZones() ([]string, error) {
 	return zones, nil
 }
 
-// NB(tal): To future-proof your code, all new providers should
-// implement GetDomainCorrections exactly as you see here
-// (byte-for-byte the same). In 3.0
-// we plan on using just the individual calls to GetZoneRecords,
-// PostProcessRecords, and so on.
-//
-// Currently every provider does things differently, which prevents
-// us from doing things like using GetZoneRecords() of a provider
-// to make convertzone work with all providers.
-
-// GetDomainCorrections get the current and existing records,
-// post-process them, and generate corrections.
-func (c *APIClient) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	existing, err := c.GetZoneRecords(dc.Name)
-	if err != nil {
-		return nil, err
-	}
-	models.PostProcessRecords(existing)
-	clean := PrepFoundRecords(existing)
-	PrepDesiredRecords(dc)
-	return c.GenerateZoneRecordsCorrections(dc, clean)
-}
-
 // GetZoneRecords gathers the DNS records and converts them to
 // dnscontrol's format.
 func (c *APIClient) GetZoneRecords(domain string) (models.Records, error) {
@@ -235,11 +212,11 @@ func (c *APIClient) GetZoneRecords(domain string) (models.Records, error) {
 
 // PrepFoundRecords munges any records to make them compatible with
 // this provider. Usually this is a no-op.
-func PrepFoundRecords(recs models.Records) models.Records {
-	// If there are records that need to be modified, removed, etc. we
-	// do it here.  Usually this is a no-op.
-	return recs
-}
+//func PrepFoundRecords(recs models.Records) models.Records {
+// If there are records that need to be modified, removed, etc. we
+// do it here.  Usually this is a no-op.
+//return recs
+//}
 
 // PrepDesiredRecords munges any records to best suit this provider.
 func PrepDesiredRecords(dc *models.DomainConfig) {
@@ -248,8 +225,6 @@ func PrepDesiredRecords(dc *models.DomainConfig) {
 	// provider.  We try to do minimal changes otherwise it gets
 	// confusing.
 
-	dc.Punycode()
-
 	recordsToKeep := make([]*models.RecordConfig, 0, len(dc.Records))
 	for _, rec := range dc.Records {
 		if rec.Type == "ALIAS" {
@@ -266,9 +241,6 @@ func PrepDesiredRecords(dc *models.DomainConfig) {
 			printer.Warnf("Loopia does not support TTL > 68 years. Setting %s from %d to 2147483647\n", rec.GetLabelFQDN(), rec.TTL)
 			rec.TTL = 2147483647
 		}
-		// if rec.Type == "TXT" {
-		// 	rec.SetTarget("\"" + rec.GetTargetField() + "\"") // FIXME(systemcrash): Should do proper quoting.
-		// }
 		// if rec.Type == "NS" && rec.GetLabel() == "@" {
 		// 	if !strings.HasSuffix(rec.GetTargetField(), ".loopia.se.") {
 		// 		printer.Warnf("Loopia does not support changing apex NS records. Ignoring %s\n", rec.GetTargetField())
@@ -293,18 +265,15 @@ func gatherAffectedLabels(groups map[models.RecordKey][]string) (labels map[stri
 	return labels, msgs
 }
 
-// GenerateZoneRecordsCorrections takes the desired and existing records
-// and produces a Correction list.  The correction list is simply
-// a list of functions to call to actually make the desired
-// correction, and a message to output to the user when the change is
-// made.
-func (c *APIClient) GenerateZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (c *APIClient) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
 	if c.Debug {
 		debugRecords("GenerateZoneRecordsCorrections input:\n", existingRecords)
 	}
 
 	// Normalize
 	txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
+	PrepDesiredRecords(dc)
 
 	var corrections []*models.Correction
 	var keysToUpdate map[models.RecordKey][]string
diff --git a/providers/luadns/luadnsProvider.go b/providers/luadns/luadnsProvider.go
index 1c6a4004..46f91d8d 100644
--- a/providers/luadns/luadnsProvider.go
+++ b/providers/luadns/luadnsProvider.go
@@ -94,25 +94,15 @@ func (l *luadnsProvider) GetZoneRecords(domain string) (models.Records, error) {
 	return existingRecords, nil
 }
 
-// GetDomainCorrections returns a list of corrections to update a domain.
-func (l *luadnsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	err := dc.Punycode()
-	if err != nil {
-		return nil, err
-	}
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (l *luadnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, records models.Records) ([]*models.Correction, error) {
+
+	checkNS(dc)
+
 	domainID, err := l.getDomainID(dc.Name)
 	if err != nil {
 		return nil, err
 	}
-	records, err := l.GetZoneRecords(dc.Name)
-	if err != nil {
-		return nil, err
-	}
-
-	checkNS(dc)
-
-	// Normalize
-	models.PostProcessRecords(records)
 
 	var corrections []*models.Correction
 	var corrs []*models.Correction
diff --git a/providers/msdns/corrections.go b/providers/msdns/corrections.go
index d00f9ae8..b95ab93f 100644
--- a/providers/msdns/corrections.go
+++ b/providers/msdns/corrections.go
@@ -9,19 +9,17 @@ import (
 	"github.com/StackExchange/dnscontrol/v3/pkg/txtutil"
 )
 
-// GetDomainCorrections gets existing records, diffs them against existing, and returns corrections.
-func (client *msdnsProvider) GenerateDomainCorrections(dc *models.DomainConfig, foundRecords models.Records) ([]*models.Correction, error) {
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (client *msdnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, foundRecords models.Records) ([]*models.Correction, error) {
 
 	// Normalize
 	models.PostProcessRecords(foundRecords)
 	txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
 
 	var corrections []*models.Correction
-	var creates, dels, modifications diff.Changeset
-	var err error
 	if !diff2.EnableDiff2 {
 		differ := diff.New(dc)
-		_, creates, dels, modifications, err = differ.IncrementalDiff(foundRecords)
+		_, creates, dels, modifications, err := differ.IncrementalDiff(foundRecords)
 		if err != nil {
 			return nil, err
 		}
diff --git a/providers/msdns/msdnsProvider.go b/providers/msdns/msdnsProvider.go
index 482716ee..33ffa3da 100644
--- a/providers/msdns/msdnsProvider.go
+++ b/providers/msdns/msdnsProvider.go
@@ -6,7 +6,6 @@ import (
 
 	"github.com/StackExchange/dnscontrol/v3/models"
 	"github.com/StackExchange/dnscontrol/v3/pkg/printer"
-	"github.com/StackExchange/dnscontrol/v3/pkg/txtutil"
 	"github.com/StackExchange/dnscontrol/v3/providers"
 )
 
@@ -70,31 +69,6 @@ func newDNS(config map[string]string, metadata json.RawMessage) (providers.DNSSe
 
 // Section 3: Domain Service Provider (DSP) related functions
 
-// NB(tal): To future-proof your code, all new providers should
-// implement GetDomainCorrections exactly as you see here
-// (byte-for-byte the same). In 3.0
-// we plan on using just the individual calls to GetZoneRecords,
-// PostProcessRecords, and so on.
-//
-// Currently every provider does things differently, which prevents
-// us from doing things like using GetZoneRecords() of a provider
-// to make convertzone work with all providers.
-
-// GetDomainCorrections get the current and existing records,
-// post-process them, and generate corrections.
-func (client *msdnsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	existing, err := client.GetZoneRecords(dc.Name)
-	if err != nil {
-		return nil, err
-	}
-	models.PostProcessRecords(existing)
-	txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
-
-	clean := PrepFoundRecords(existing)
-	PrepDesiredRecords(dc)
-	return client.GenerateDomainCorrections(dc, clean)
-}
-
 // GetZoneRecords gathers the DNS records and converts them to
 // dnscontrol's format.
 func (client *msdnsProvider) GetZoneRecords(domain string) (models.Records, error) {
@@ -119,24 +93,6 @@ func (client *msdnsProvider) GetZoneRecords(domain string) (models.Records, erro
 	return existingRecords, nil
 }
 
-// PrepFoundRecords munges any records to make them compatible with
-// this provider. Usually this is a no-op.
-func PrepFoundRecords(recs models.Records) models.Records {
-	// If there are records that need to be modified, removed, etc. we
-	// do it here.  Usually this is a no-op.
-	return recs
-}
-
-// PrepDesiredRecords munges any records to best suit this provider.
-func PrepDesiredRecords(dc *models.DomainConfig) {
-	// Sort through the dc.Records, eliminate any that can't be
-	// supported; modify any that need adjustments to work with the
-	// provider.  We try to do minimal changes otherwise it gets
-	// confusing.
-
-	dc.Punycode()
-}
-
 // NB(tlim): If we want to implement a registrar, refer to
 // http://go.microsoft.com/fwlink/?LinkId=288158
 // (Get-DnsServerZoneDelegation) for hints about which PowerShell
diff --git a/providers/namecheap/auditrecords.go b/providers/namecheap/auditrecords.go
index 94030586..928afef6 100644
--- a/providers/namecheap/auditrecords.go
+++ b/providers/namecheap/auditrecords.go
@@ -1,10 +1,19 @@
 package namecheap
 
-import "github.com/StackExchange/dnscontrol/v3/models"
+import (
+	"github.com/StackExchange/dnscontrol/v3/models"
+	"github.com/StackExchange/dnscontrol/v3/pkg/rejectif"
+)
 
 // AuditRecords returns a list of errors corresponding to the records
 // that aren't supported by this provider.  If all records are
 // supported, an empty list is returned.
 func AuditRecords(records []*models.RecordConfig) []error {
-	return nil
+	a := rejectif.Auditor{}
+
+	a.Add("MX", rejectif.MxNull)
+
+	a.Add("TXT", rejectif.TxtHasDoubleQuotes)
+
+	return a.Audit(records)
 }
diff --git a/providers/namecheap/namecheapProvider.go b/providers/namecheap/namecheapProvider.go
index d0d9a28e..8bb6eddd 100644
--- a/providers/namecheap/namecheapProvider.go
+++ b/providers/namecheap/namecheapProvider.go
@@ -126,24 +126,113 @@ func (n *namecheapProvider) GetZoneRecords(domain string) (models.Records, error
 		return nil, err
 	}
 
+	// namecheap has this really annoying feature where they add some parking records if you have no records.
+	// This causes a few problems for our purposes, specifically the integration tests.
+	// lets detect that one case and pretend it is a no-op.
+	if len(records.Hosts) == 2 {
+		if records.Hosts[0].Type == "CNAME" &&
+			strings.Contains(records.Hosts[0].Address, "parkingpage") &&
+			records.Hosts[1].Type == "URL" {
+			// return an empty zone
+			return nil, nil
+		}
+	}
+
+	// Copying this from GetDomainCorrections.  This seems redundent
+	// with what toRecords() does.  Leaving it out.
+	// 	for _, r := range records.Hosts {
+	// 		if r.Type == "SOA" {
+	// 			continue
+	// 		}
+	// 		rec := &models.RecordConfig{
+	// 			Type:         r.Type,
+	// 			TTL:          uint32(r.TTL),
+	// 			MxPreference: uint16(r.MXPref),
+	// 			Original:     r,
+	// 		}
+	// 		rec.SetLabel(r.Name, dc.Name)
+	// 		switch rtype := r.Type; rtype { // #rtype_variations
+	// 		case "TXT":
+	// 			rec.SetTargetTXT(r.Address)
+	// 		case "CAA":
+	// 			rec.SetTargetCAAString(r.Address)
+	// 		default:
+	// 			rec.SetTarget(r.Address)
+	// 		}
+	// 		actual = append(actual, rec)
+	// 	}
+
 	return toRecords(records, domain)
 }
 
-// GetDomainCorrections returns the corrections for the domain.
-func (n *namecheapProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	dc.Punycode()
-	sld, tld := splitDomain(dc.Name)
-	var records *nc.DomainDNSGetHostsResult
-	var err error
-	doWithRetry(func() error {
-		records, err = n.client.DomainsDNSGetHosts(sld, tld)
-		return err
-	})
-	if err != nil {
-		return nil, err
-	}
-
-	var actual []*models.RecordConfig
+// // GetDomainCorrections returns the corrections for the domain.
+// func (n *namecheapProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
+// 	dc.Punycode()
+// 	sld, tld := splitDomain(dc.Name)
+// 	var records *nc.DomainDNSGetHostsResult
+// 	var err error
+// 	doWithRetry(func() error {
+// 		records, err = n.client.DomainsDNSGetHosts(sld, tld)
+// 		return err
+// 	})
+// 	if err != nil {
+// 		return nil, err
+// 	}
+
+// 	var actual []*models.RecordConfig
+
+// 	// namecheap does not allow setting @ NS with basic DNS
+// 	dc.Filter(func(r *models.RecordConfig) bool {
+// 		if r.Type == "NS" && r.GetLabel() == "@" {
+// 			if !strings.HasSuffix(r.GetTargetField(), "registrar-servers.com.") {
+// 				printer.Println("\n", r.GetTargetField(), "Namecheap does not support changing apex NS records. Skipping.")
+// 			}
+// 			return false
+// 		}
+// 		return true
+// 	})
+
+// 	// namecheap has this really annoying feature where they add some parking records if you have no records.
+// 	// This causes a few problems for our purposes, specifically the integration tests.
+// 	// lets detect that one case and pretend it is a no-op.
+// 	if len(dc.Records) == 0 && len(records.Hosts) == 2 {
+// 		if records.Hosts[0].Type == "CNAME" &&
+// 			strings.Contains(records.Hosts[0].Address, "parkingpage") &&
+// 			records.Hosts[1].Type == "URL" {
+// 			return nil, nil
+// 		}
+// 	}
+
+// 	for _, r := range records.Hosts {
+// 		if r.Type == "SOA" {
+// 			continue
+// 		}
+// 		rec := &models.RecordConfig{
+// 			Type:         r.Type,
+// 			TTL:          uint32(r.TTL),
+// 			MxPreference: uint16(r.MXPref),
+// 			Original:     r,
+// 		}
+// 		rec.SetLabel(r.Name, dc.Name)
+// 		switch rtype := r.Type; rtype { // #rtype_variations
+// 		case "TXT":
+// 			rec.SetTargetTXT(r.Address)
+// 		case "CAA":
+// 			rec.SetTargetCAAString(r.Address)
+// 		default:
+// 			rec.SetTarget(r.Address)
+// 		}
+// 		actual = append(actual, rec)
+// 	}
+
+// 	// Normalize
+// 	models.PostProcessRecords(actual)
+
+// 	return n.GetZoneRecordsCorrections(dc, actual)
+// }
+
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (n *namecheapProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, error) {
 
 	// namecheap does not allow setting @ NS with basic DNS
 	dc.Filter(func(r *models.RecordConfig) bool {
@@ -156,50 +245,13 @@ func (n *namecheapProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mo
 		return true
 	})
 
-	// namecheap has this really annoying feature where they add some parking records if you have no records.
-	// This causes a few problems for our purposes, specifically the integration tests.
-	// lets detect that one case and pretend it is a no-op.
-	if len(dc.Records) == 0 && len(records.Hosts) == 2 {
-		if records.Hosts[0].Type == "CNAME" &&
-			strings.Contains(records.Hosts[0].Address, "parkingpage") &&
-			records.Hosts[1].Type == "URL" {
-			return nil, nil
-		}
-	}
-
-	for _, r := range records.Hosts {
-		if r.Type == "SOA" {
-			continue
-		}
-		rec := &models.RecordConfig{
-			Type:         r.Type,
-			TTL:          uint32(r.TTL),
-			MxPreference: uint16(r.MXPref),
-			Original:     r,
-		}
-		rec.SetLabel(r.Name, dc.Name)
-		switch rtype := r.Type; rtype { // #rtype_variations
-		case "TXT":
-			rec.SetTargetTXT(r.Address)
-		case "CAA":
-			rec.SetTargetCAAString(r.Address)
-		default:
-			rec.SetTarget(r.Address)
-		}
-		actual = append(actual, rec)
-	}
-
-	// Normalize
-	models.PostProcessRecords(actual)
-
-	var create, delete, modify diff.Changeset
 	var differ diff.Differ
 	if !diff2.EnableDiff2 {
 		differ = diff.New(dc)
 	} else {
 		differ = diff.NewCompat(dc)
 	}
-	_, create, delete, modify, err = differ.IncrementalDiff(actual)
+	_, create, delete, modify, err := differ.IncrementalDiff(actual)
 	if err != nil {
 		return nil, err
 	}
@@ -245,7 +297,14 @@ func toRecords(result *nc.DomainDNSGetHostsResult, origin string) ([]*models.Rec
 			MxPreference: uint16(dnsHost.MXPref),
 			Name:         dnsHost.Name,
 		}
-		record.PopulateFromString(dnsHost.Type, dnsHost.Address, origin)
+		record.SetLabel(dnsHost.Name, origin)
+
+		switch dnsHost.Type {
+		case "MX":
+			record.SetTargetMX(uint16(dnsHost.MXPref), dnsHost.Address)
+		default:
+			record.PopulateFromString(dnsHost.Type, dnsHost.Address, origin)
+		}
 
 		records = append(records, &record)
 	}
diff --git a/providers/namedotcom/records.go b/providers/namedotcom/records.go
index 098b7ae7..e9446f9b 100644
--- a/providers/namedotcom/records.go
+++ b/providers/namedotcom/records.go
@@ -34,14 +34,10 @@ func (n *namedotcomProvider) GetZoneRecords(domain string) (models.Records, erro
 	return actual, nil
 }
 
-// GetDomainCorrections gathers correctios that would bring n to match dc.
-func (n *namedotcomProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	dc.Punycode()
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (n *namedotcomProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, error) {
 
-	actual, err := n.GetZoneRecords(dc.Name)
-	if err != nil {
-		return nil, err
-	}
+	checkNSModifications(dc)
 
 	for _, rec := range dc.Records {
 		if rec.Type == "ALIAS" {
@@ -49,11 +45,6 @@ func (n *namedotcomProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*m
 		}
 	}
 
-	checkNSModifications(dc)
-
-	// Normalize
-	models.PostProcessRecords(actual)
-
 	var corrections []*models.Correction
 	var differ diff.Differ
 	if !diff2.EnableDiff2 {
@@ -193,19 +184,6 @@ func (n *namedotcomProvider) createRecord(rc *models.RecordConfig, domain string
 	return err
 }
 
-// // makeTxt encodes TxtStrings for sending in the CREATE/MODIFY API:
-// func encodeTxt(txts []string) string {
-// 	ans := txts[0]
-
-// 	if len(txts) > 1 {
-// 		ans = ""
-// 		for _, t := range txts {
-// 			ans += `"` + strings.Replace(t, `"`, `\"`, -1) + `"`
-// 		}
-// 	}
-// 	return ans
-// }
-
 // finds a string surrounded by quotes that might contain an escaped quote character.
 var quotedStringRegexp = regexp.MustCompile(`"((?:[^"\\]|\\.)*)"`)
 
diff --git a/providers/netcup/netcupProvider.go b/providers/netcup/netcupProvider.go
index d9b707a9..c9085ece 100644
--- a/providers/netcup/netcupProvider.go
+++ b/providers/netcup/netcupProvider.go
@@ -53,6 +53,7 @@ func (api *netcupProvider) GetZoneRecords(domain string) (models.Records, error)
 	for i := range records {
 		existingRecords[i] = toRecordConfig(domain, &records[i])
 	}
+
 	return existingRecords, nil
 }
 
@@ -67,16 +68,13 @@ func (api *netcupProvider) GetNameservers(domain string) ([]*models.Nameserver,
 	})
 }
 
-// GetDomainCorrections returns the corrections for a domain.
-func (api *netcupProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	dc, err := dc.Copy()
-	if err != nil {
-		return nil, err
-	}
-
-	dc.Punycode()
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (api *netcupProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
 	domain := dc.Name
 
+	// no need for txtutil.SplitSingleLongTxt in function GetDomainCorrections
+	// txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
+
 	// Setting the TTL is not supported for netcup
 	for _, r := range dc.Records {
 		r.TTL = 0
@@ -91,26 +89,14 @@ func (api *netcupProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mod
 	}
 	dc.Records = newRecords
 
-	// Check existing set
-	existingRecords, err := api.GetZoneRecords(domain)
-	if err != nil {
-		return nil, err
-	}
-
-	// Normalize
-	models.PostProcessRecords(existingRecords)
-	// no need for txtutil.SplitSingleLongTxt in function GetDomainCorrections
-	// txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
-
 	var corrections []*models.Correction
-	var create, del, modify diff.Changeset
 	var differ diff.Differ
 	if !diff2.EnableDiff2 {
 		differ = diff.New(dc)
 	} else {
 		differ = diff.NewCompat(dc)
 	}
-	_, create, del, modify, err = differ.IncrementalDiff(existingRecords)
+	_, create, del, modify, err := differ.IncrementalDiff(existingRecords)
 	if err != nil {
 		return nil, err
 	}
diff --git a/providers/netlify/netlifyProvider.go b/providers/netlify/netlifyProvider.go
index 8a93a099..dc31418f 100644
--- a/providers/netlify/netlifyProvider.go
+++ b/providers/netlify/netlifyProvider.go
@@ -176,31 +176,19 @@ func removeOtherApexNS(dc *models.DomainConfig) {
 	dc.Records = newList
 }
 
-func (n *netlifyProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (n *netlifyProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, records models.Records) ([]*models.Correction, error) {
 
-	err := dc.Punycode()
-	if err != nil {
-		return nil, err
-	}
-
-	records, err := n.GetZoneRecords(dc.Name)
-	if err != nil {
-		return nil, err
-	}
-
-	// Normalize
-	models.PostProcessRecords(records)
 	removeOtherApexNS(dc)
 
 	var corrections []*models.Correction
-	var create, del, modify diff.Changeset
 	var differ diff.Differ
 	if !diff2.EnableDiff2 {
 		differ = diff.New(dc)
 	} else {
 		differ = diff.NewCompat(dc)
 	}
-	_, create, del, modify, err = differ.IncrementalDiff(records)
+	_, create, del, modify, err := differ.IncrementalDiff(records)
 	if err != nil {
 		return nil, err
 	}
diff --git a/providers/ns1/ns1Provider.go b/providers/ns1/ns1Provider.go
index 5e353c38..8760bd86 100644
--- a/providers/ns1/ns1Provider.go
+++ b/providers/ns1/ns1Provider.go
@@ -154,21 +154,10 @@ func (n *nsone) getDomainCorrectionsDNSSEC(domain, toggleDNSSEC string) *models.
 	return nil
 }
 
-func (n *nsone) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	dc.Punycode()
-	//dc.CombineMXs()
-
-	domain := dc.Name
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (n *nsone) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
 	corrections := []*models.Correction{}
-
-	// Get existing records
-	existingRecords, err := n.GetZoneRecords(domain)
-	if err != nil {
-		return nil, err
-	}
-
-	//  Normalize
-	models.PostProcessRecords(existingRecords)
+	domain := dc.Name
 
 	// add DNSSEC-related corrections
 	if dnssecCorrections := n.getDomainCorrectionsDNSSEC(domain, dc.AutoDNSSEC); dnssecCorrections != nil {
diff --git a/providers/oracle/oracleProvider.go b/providers/oracle/oracleProvider.go
index e905933c..63841044 100644
--- a/providers/oracle/oracleProvider.go
+++ b/providers/oracle/oracleProvider.go
@@ -202,25 +202,9 @@ func (o *oracleProvider) GetZoneRecords(zone string) (models.Records, error) {
 	return records, nil
 }
 
-func (o *oracleProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	dc, err := dc.Copy()
-	if err != nil {
-		return nil, err
-	}
-
-	err = dc.Punycode()
-	if err != nil {
-		return nil, err
-	}
-	domain := dc.Name
-
-	existingRecords, err := o.GetZoneRecords(domain)
-	if err != nil {
-		return nil, err
-	}
-
-	//  Normalize
-	models.PostProcessRecords(existingRecords)
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (o *oracleProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
+	var err error
 	txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
 
 	// Ensure we don't emit changes for attempted modification of built-in apex NSs
@@ -241,14 +225,13 @@ func (o *oracleProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*model
 		}
 	}
 
-	var create, dels, modify diff.Changeset
 	var differ diff.Differ
 	if !diff2.EnableDiff2 {
 		differ = diff.New(dc)
 	} else {
 		differ = diff.NewCompat(dc)
 	}
-	_, create, dels, modify, err = differ.IncrementalDiff(existingRecords)
+	_, create, dels, modify, err := differ.IncrementalDiff(existingRecords)
 	if err != nil {
 		return nil, err
 	}
@@ -294,7 +277,7 @@ func (o *oracleProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*model
 		return []*models.Correction{{
 			Msg: desc,
 			F: func() error {
-				return o.patch(createRecords, deleteRecords, domain)
+				return o.patch(createRecords, deleteRecords, dc.Name)
 			},
 		}}, nil
 	}
diff --git a/providers/ovh/ovhProvider.go b/providers/ovh/ovhProvider.go
index 7bdb4946..a2f75a1f 100644
--- a/providers/ovh/ovhProvider.go
+++ b/providers/ovh/ovhProvider.go
@@ -115,26 +115,15 @@ func (c *ovhProvider) GetZoneRecords(domain string) (models.Records, error) {
 			actual = append(actual, rec)
 		}
 	}
+
 	return actual, nil
 }
 
-func (c *ovhProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	var err error
-
-	err = dc.Punycode()
-	if err != nil {
-		return nil, err
-	}
-
-	actual, err := c.GetZoneRecords(dc.Name)
-	if err != nil {
-		return nil, err
-	}
-
-	// Normalize
-	models.PostProcessRecords(actual)
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (c *ovhProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, error) {
 
 	var corrections []*models.Correction
+	var err error
 	if !diff2.EnableDiff2 {
 		corrections, err = c.getDiff1DomainCorrections(dc, actual)
 	} else {
diff --git a/providers/packetframe/api.go b/providers/packetframe/api.go
index d237b9dd..626cac51 100644
--- a/providers/packetframe/api.go
+++ b/providers/packetframe/api.go
@@ -19,7 +19,7 @@ var defaultNameServerNames = []string{
 	"ns2.packetframe.com",
 }
 
-type zone struct {
+type zoneInfo struct {
 	ID         string   `json:"id"`
 	Zone       string   `json:"zone"`
 	Users      []string `json:"users"`
@@ -28,7 +28,7 @@ type zone struct {
 
 type domainResponse struct {
 	Data struct {
-		Zones []zone `json:"zones"`
+		Zones []zoneInfo `json:"zones"`
 	} `json:"data"`
 	Message string `json:"message"`
 	Success bool   `json:"success"`
@@ -58,7 +58,7 @@ type domainRecord struct {
 }
 
 func (api *packetframeProvider) fetchDomainList() error {
-	api.domainIndex = map[string]zone{}
+	api.domainIndex = map[string]zoneInfo{}
 	dr := &domainResponse{}
 	endpoint := "dns/zones"
 	if err := api.get(endpoint, dr); err != nil {
diff --git a/providers/packetframe/packetframeProvider.go b/providers/packetframe/packetframeProvider.go
index e0bf110a..e32a6c87 100644
--- a/providers/packetframe/packetframeProvider.go
+++ b/providers/packetframe/packetframeProvider.go
@@ -19,7 +19,7 @@ type packetframeProvider struct {
 	client      *http.Client
 	baseURL     *url.URL
 	token       string
-	domainIndex map[string]zone
+	domainIndex map[string]zoneInfo
 }
 
 // newPacketframe creates the provider.
@@ -60,19 +60,28 @@ func (api *packetframeProvider) GetNameservers(domain string) ([]*models.Nameser
 	return models.ToNameservers(defaultNameServerNames)
 }
 
-// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (api *packetframeProvider) GetZoneRecords(domain string) (models.Records, error) {
-
+func (api *packetframeProvider) getZone(domain string) (*zoneInfo, error) {
 	if api.domainIndex == nil {
 		if err := api.fetchDomainList(); err != nil {
 			return nil, err
 		}
 	}
-	zone, ok := api.domainIndex[domain+"."]
+	z, ok := api.domainIndex[domain+"."]
 	if !ok {
 		return nil, fmt.Errorf("%q not a zone in Packetframe account", domain)
 	}
 
+	return &z, nil
+}
+
+// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
+func (api *packetframeProvider) GetZoneRecords(domain string) (models.Records, error) {
+
+	zone, err := api.getZone(domain)
+	if err != nil {
+		return nil, fmt.Errorf("no such zone %q in Packetframe account", domain)
+	}
+
 	records, err := api.getRecords(zone.ID)
 	if err != nil {
 		return nil, fmt.Errorf("could not load records for domain %q", domain)
@@ -91,48 +100,21 @@ func (api *packetframeProvider) GetZoneRecords(domain string) (models.Records, e
 	return existingRecords, nil
 }
 
-// GetDomainCorrections returns the corrections for a domain.
-func (api *packetframeProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	dc, err := dc.Copy()
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (api *packetframeProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
+	zone, err := api.getZone(dc.Name)
 	if err != nil {
-		return nil, err
-	}
-
-	dc.Punycode()
-
-	if api.domainIndex == nil {
-		if err := api.fetchDomainList(); err != nil {
-			return nil, err
-		}
-	}
-	zone, ok := api.domainIndex[dc.Name+"."]
-	if !ok {
 		return nil, fmt.Errorf("no such zone %q in Packetframe account", dc.Name)
 	}
 
-	records, err := api.getRecords(zone.ID)
-	if err != nil {
-		return nil, fmt.Errorf("could not load records for domain %q", dc.Name)
-	}
-
-	existingRecords := make([]*models.RecordConfig, len(records))
-
-	for i := range records {
-		existingRecords[i] = toRc(dc, &records[i])
-	}
-
-	// Normalize
-	models.PostProcessRecords(existingRecords)
-
 	var corrections []*models.Correction
-	var create, dels, modify diff.Changeset
 	var differ diff.Differ
 	if !diff2.EnableDiff2 {
 		differ = diff.New(dc)
 	} else {
 		differ = diff.NewCompat(dc)
 	}
-	_, create, dels, modify, err = differ.IncrementalDiff(existingRecords)
+	_, create, dels, modify, err := differ.IncrementalDiff(existingRecords)
 	if err != nil {
 		return nil, err
 	}
diff --git a/providers/porkbun/porkbunProvider.go b/providers/porkbun/porkbunProvider.go
index 0346d77c..7011edb5 100644
--- a/providers/porkbun/porkbunProvider.go
+++ b/providers/porkbun/porkbunProvider.go
@@ -75,26 +75,12 @@ func (c *porkbunProvider) GetNameservers(domain string) ([]*models.Nameserver, e
 	return models.ToNameservers(defaultNS)
 }
 
-// GetDomainCorrections returns the corrections for a domain.
-func (c *porkbunProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	dc, err := dc.Copy()
-	if err != nil {
-		return nil, err
-	}
-
-	dc.Punycode()
-
-	existingRecords, err := c.GetZoneRecords(dc.Name)
-	if err != nil {
-		return nil, err
-	}
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (c *porkbunProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
 
 	// Block changes to NS records for base domain
 	checkNSModifications(dc)
 
-	// Normalize
-	models.PostProcessRecords(existingRecords)
-
 	// Make sure TTL larger than the minimum TTL
 	for _, record := range dc.Records {
 		record.TTL = fixTTL(record.TTL)
diff --git a/providers/powerdns/dns.go b/providers/powerdns/dns.go
index 546c92f5..fe50e718 100644
--- a/providers/powerdns/dns.go
+++ b/providers/powerdns/dns.go
@@ -2,11 +2,12 @@ package powerdns
 
 import (
 	"context"
+	"net/http"
+
 	"github.com/StackExchange/dnscontrol/v3/models"
 	"github.com/StackExchange/dnscontrol/v3/pkg/diff2"
 	"github.com/mittwald/go-powerdns/apis/zones"
 	"github.com/mittwald/go-powerdns/pdnshttp"
-	"net/http"
 )
 
 // GetNameservers returns the nameservers for a domain.
@@ -44,19 +45,10 @@ func (dsp *powerdnsProvider) GetZoneRecords(domain string) (models.Records, erro
 	return curRecords, nil
 }
 
-// GetDomainCorrections returns a list of corrections to update a domain.
-func (dsp *powerdnsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	// get current zone records
-	existing, err := dsp.GetZoneRecords(dc.Name)
-	if err != nil {
-		return nil, err
-	}
-
-	// post-process records
-	if err := dc.Punycode(); err != nil {
-		return nil, err
-	}
-	models.PostProcessRecords(existing)
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (dsp *powerdnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, error) {
+	// create record diff by group
+	var err error
 
 	var corrections []*models.Correction
 	if !diff2.EnableDiff2 {
diff --git a/providers/providers.go b/providers/providers.go
index eaf11d5f..d8e8a47c 100644
--- a/providers/providers.go
+++ b/providers/providers.go
@@ -158,10 +158,12 @@ func (n None) GetNameservers(string) ([]*models.Nameserver, error) {
 
 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
 func (n None) GetZoneRecords(domain string) (models.Records, error) {
-	return nil, fmt.Errorf("not implemented")
-	// This enables the get-zones subcommand.
-	// Implement this by extracting the code from GetDomainCorrections into
-	// a single function.  For most providers this should be relatively easy.
+	return nil, nil
+}
+
+// GetZoneRecordsCorrections gets the records of a zone and returns them in RecordConfig format.
+func (n None) GetZoneRecordsCorrections(dc *models.DomainConfig, records models.Records) ([]*models.Correction, error) {
+	return nil, nil
 }
 
 // GetDomainCorrections returns corrections to update a domain.
diff --git a/providers/route53/route53Provider.go b/providers/route53/route53Provider.go
index 044834d5..e07ecb8c 100644
--- a/providers/route53/route53Provider.go
+++ b/providers/route53/route53Provider.go
@@ -134,6 +134,8 @@ func (r *route53Provider) ListZones() ([]string, error) {
 }
 
 func (r *route53Provider) getZones() error {
+	// TODO(tlim) This should memoize itself.
+
 	if r.zonesByDomain != nil {
 		return nil
 	}
@@ -223,6 +225,8 @@ func (r *route53Provider) GetZoneRecords(domain string) (models.Records, error)
 }
 
 func (r *route53Provider) getZone(dc *models.DomainConfig) (r53Types.HostedZone, error) {
+	// TODO(tlim) This should memoize itself.
+
 	if err := r.getZones(); err != nil {
 		return r53Types.HostedZone{}, err
 	}
@@ -260,19 +264,15 @@ func (r *route53Provider) getZoneRecords(zone r53Types.HostedZone) (models.Recor
 	return existingRecords, nil
 }
 
-func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	dc.Punycode()
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (r *route53Provider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
+	txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
 
 	zone, err := r.getZone(dc)
 	if err != nil {
 		return nil, err
 	}
 
-	existingRecords, err := r.getZoneRecords(zone)
-	if err != nil {
-		return nil, err
-	}
-
 	// update zone_id to current zone.id if not specified by the user
 	for _, want := range dc.Records {
 		if want.Type == "R53_ALIAS" && want.R53Alias["zone_id"] == "" {
@@ -280,13 +280,14 @@ func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
 		}
 	}
 
-	// Normalize
-	models.PostProcessRecords(existingRecords)
-	txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
-
 	var corrections []*models.Correction
 	if !diff2.EnableDiff2 {
 
+		zone, err := r.getZone(dc)
+		if err != nil {
+			return nil, err
+		}
+
 		// diff
 		differ := diff.New(dc, getAliasMap)
 		namesToUpdate, err := differ.ChangedGroups(existingRecords)
diff --git a/providers/rwth/dns.go b/providers/rwth/dns.go
index 8bde39d1..82583956 100644
--- a/providers/rwth/dns.go
+++ b/providers/rwth/dns.go
@@ -30,37 +30,19 @@ func (api *rwthProvider) GetNameservers(domain string) ([]*models.Nameserver, er
 	return models.ToNameservers(RWTHDefaultNs)
 }
 
-// GetDomainCorrections returns a list of corretions to execute.
-func (api *rwthProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	dc, err := dc.Copy()
-	if err != nil {
-		return nil, err
-	}
-
-	err = dc.Punycode()
-	if err != nil {
-		return nil, err
-	}
-	domain := dc.Name
-
-	// Get existing records
-	existingRecords, err := api.GetZoneRecords(domain)
-	if err != nil {
-		return nil, err
-	}
-	// Normalize
-	models.PostProcessRecords(existingRecords)
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (api *rwthProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) {
 	txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
+	domain := dc.Name
 
 	var corrections []*models.Correction
-	var create, del, modify diff.Changeset
 	var differ diff.Differ
 	if !diff2.EnableDiff2 {
 		differ = diff.New(dc)
 	} else {
 		differ = diff.NewCompat(dc)
 	}
-	_, create, del, modify, err = differ.IncrementalDiff(existingRecords)
+	_, create, del, modify, err := differ.IncrementalDiff(existingRecords)
 	if err != nil {
 		return nil, err
 	}
diff --git a/providers/softlayer/softlayerProvider.go b/providers/softlayer/softlayerProvider.go
index 2d23ca71..adcbb5b6 100644
--- a/providers/softlayer/softlayerProvider.go
+++ b/providers/softlayer/softlayerProvider.go
@@ -59,25 +59,25 @@ func (s *softlayerProvider) GetNameservers(domain string) ([]*models.Nameserver,
 	return models.ToNameservers([]string{"ns1.softlayer.com", "ns2.softlayer.com"})
 }
 
-// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
-func (s *softlayerProvider) GetZoneRecords(domain string) (models.Records, error) {
-	return nil, fmt.Errorf("not implemented")
-	// This enables the get-zones subcommand.
-	// Implement this by extracting the code from GetDomainCorrections into
-	// a single function.  For most providers this should be relatively easy.
-}
-
-// GetDomainCorrections returns corrections to update a domain.
-func (s *softlayerProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-
-	domain, err := s.getDomain(&dc.Name)
-
+// GetZoneRecords gets all the records for domainName and converts
+// them to model.RecordConfig.
+func (s *softlayerProvider) GetZoneRecords(domainName string) (models.Records, error) {
+	domain, err := s.getDomain(&domainName)
 	if err != nil {
 		return nil, err
 	}
 
 	actual, err := s.getExistingRecords(domain)
+	if err != nil {
+		return nil, err
+	}
+
+	return actual, nil
+}
 
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (s *softlayerProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, error) {
+	domain, err := s.getDomain(&dc.Name)
 	if err != nil {
 		return nil, err
 	}
@@ -120,6 +120,8 @@ func (s *softlayerProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mo
 }
 
 func (s *softlayerProvider) getDomain(name *string) (*datatypes.Dns_Domain, error) {
+	// FIXME(tlim) Memoize this
+
 	domains, err := services.GetAccountService(s.Session).
 		Filter(filter.Path("domains.name").Eq(name).Build()).
 		Mask("resourceRecords").
@@ -138,7 +140,7 @@ func (s *softlayerProvider) getDomain(name *string) (*datatypes.Dns_Domain, erro
 	return &domains[0], nil
 }
 
-func (s *softlayerProvider) getExistingRecords(domain *datatypes.Dns_Domain) ([]*models.RecordConfig, error) {
+func (s *softlayerProvider) getExistingRecords(domain *datatypes.Dns_Domain) (models.Records, error) {
 	actual := []*models.RecordConfig{}
 
 	for _, record := range domain.ResourceRecords {
@@ -190,9 +192,6 @@ func (s *softlayerProvider) getExistingRecords(domain *datatypes.Dns_Domain) ([]
 		actual = append(actual, recConfig)
 	}
 
-	// Normalize
-	models.PostProcessRecords(actual)
-
 	return actual, nil
 }
 
diff --git a/providers/transip/transipProvider.go b/providers/transip/transipProvider.go
index c742a9ce..9fd130ef 100644
--- a/providers/transip/transipProvider.go
+++ b/providers/transip/transipProvider.go
@@ -94,25 +94,15 @@ func (n *transipProvider) ListZones() ([]string, error) {
 	return domains, nil
 }
 
-func (n *transipProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	curRecords, err := n.GetZoneRecords(dc.Name)
-	if err != nil {
-		return nil, err
-	}
-
-	if err := dc.Punycode(); err != nil {
-		return nil, err
-	}
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (n *transipProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, curRecords models.Records) ([]*models.Correction, error) {
 
 	removeOtherNS(dc)
 
-	models.PostProcessRecords(curRecords)
-
 	if !diff2.EnableDiff2 {
 		corrections, err := n.getCorrectionsUsingOldDiff(dc, curRecords)
 		return corrections, err
 	}
-
 	corrections, err := n.getCorrectionsUsingDiff2(dc, curRecords)
 	return corrections, err
 }
diff --git a/providers/vultr/vultrProvider.go b/providers/vultr/vultrProvider.go
index 63dacd28..aebf4fec 100644
--- a/providers/vultr/vultrProvider.go
+++ b/providers/vultr/vultrProvider.go
@@ -109,9 +109,8 @@ func (api *vultrProvider) GetZoneRecords(domain string) (models.Records, error)
 	return curRecords, nil
 }
 
-// GetDomainCorrections gets the corrections for a DomainConfig.
-func (api *vultrProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
-	dc.Punycode()
+// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
+func (api *vultrProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, curRecords models.Records) ([]*models.Correction, error) {
 
 	for _, rec := range dc.Records {
 		switch rec.Type { // #rtype_variations
@@ -127,23 +126,16 @@ func (api *vultrProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
 		}
 	}
 
-	curRecords, err := api.GetZoneRecords(dc.Name)
-	if err != nil {
-		return nil, err
-	}
-
-	models.PostProcessRecords(curRecords)
-
 	var corrections []*models.Correction
 	if !diff2.EnableDiff2 {
 		differ := diff.New(dc)
-		_, create, delete, modify, err := differ.IncrementalDiff(curRecords)
+		_, create, toDelete, modify, err := differ.IncrementalDiff(curRecords)
 
 		if err != nil {
 			return nil, err
 		}
 
-		for _, mod := range delete {
+		for _, mod := range toDelete {
 			id := mod.Existing.Original.(govultr.DomainRecord).ID
 			corrections = append(corrections, &models.Correction{
 				Msg: fmt.Sprintf("%s; Vultr RecordID: %v", mod.String(), id),


Test patch:
diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go
index 4aa55a54..416ce68f 100644
--- a/integrationTest/integration_test.go
+++ b/integrationTest/integration_test.go
@@ -15,6 +15,7 @@ import (
 	"github.com/StackExchange/dnscontrol/v3/pkg/diff2"
 	"github.com/StackExchange/dnscontrol/v3/pkg/nameservers"
 	"github.com/StackExchange/dnscontrol/v3/pkg/normalize"
+	"github.com/StackExchange/dnscontrol/v3/pkg/zonerecs"
 	"github.com/StackExchange/dnscontrol/v3/providers"
 	_ "github.com/StackExchange/dnscontrol/v3/providers/_all"
 	"github.com/StackExchange/dnscontrol/v3/providers/cloudflare"
@@ -200,6 +201,12 @@ func makeChanges(t *testing.T, prv providers.DNSServiceProvider, dc *models.Doma
 			//}
 			dom.Records = append(dom.Records, &rc)
 		}
+		if *providerToRun == "AXFRDDNS" {
+			// Bind will refuse a DDNS update when the resulting zone
+			// contains a NS record without an associated address
+			// records (A or AAAA)
+			dom.Records = append(dom.Records, a("ns."+domainName+".", "9.8.7.6"))
+		}
 		dom.IgnoredNames = tst.IgnoredNames
 		dom.IgnoredTargets = tst.IgnoredTargets
 		models.PostProcessRecords(dom.Records)
@@ -211,7 +218,7 @@ func makeChanges(t *testing.T, prv providers.DNSServiceProvider, dc *models.Doma
 		}
 
 		// get and run corrections for first time
-		corrections, err := prv.GetDomainCorrections(dom)
+		corrections, err := zonerecs.CorrectZoneRecords(prv, dom)
 		if err != nil {
 			t.Fatal(fmt.Errorf("runTests: %w", err))
 		}
@@ -234,7 +241,7 @@ func makeChanges(t *testing.T, prv providers.DNSServiceProvider, dc *models.Doma
 		}
 
 		// run a second time and expect zero corrections
-		corrections, err = prv.GetDomainCorrections(dom2)
+		corrections, err = zonerecs.CorrectZoneRecords(prv, dom2)
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -328,7 +335,8 @@ func TestDualProviders(t *testing.T) {
 	// clear everything
 	run := func() {
 		dom, _ := dc.Copy()
-		cs, err := p.GetDomainCorrections(dom)
+
+		cs, err := zonerecs.CorrectZoneRecords(p, dom)
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -350,7 +358,7 @@ func TestDualProviders(t *testing.T) {
 	run()
 	// run again to make sure no corrections
 	t.Log("Running again to ensure stability")
-	cs, err := p.GetDomainCorrections(dc)
+	cs, err := zonerecs.CorrectZoneRecords(p, dc)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/integrationTest/providers.json b/integrationTest/providers.json
index 3ad0dae1..5bc22885 100644
--- a/integrationTest/providers.json
+++ b/integrationTest/providers.json
@@ -22,7 +22,8 @@
     "master": "$AXFRDDNS_MASTER",
     "nameservers": "ns.example.com",
     "transfer-key": "$AXFRDDNS_TRANSFER_KEY",
-    "update-key": "$AXFRDDNS_UPDATE_KEY"
+    "update-key": "$AXFRDDNS_UPDATE_KEY",
+    "buggy-cname": "$AXFRDDNS_BUGGY_CNAME"
   },
   "AZURE_DNS": {
     "ClientID": "$AZURE_DNS_CLIENT_ID",