Repository: StackExchange/dnscontrol
Pull request #753 (2020-05-30 14:39:41)

New RTYPE: DS records now supported!
Thanks to @haraldkoch for starting this, @McNetic for picking it up.  We now support DS records!

Fixes https://github.com/StackExchange/dnscontrol/issues/75


Patch:
diff --git a/build/generate/featureMatrix.go b/build/generate/featureMatrix.go
index f68111de..1c0ecd84 100644
--- a/build/generate/featureMatrix.go
+++ b/build/generate/featureMatrix.go
@@ -40,6 +40,7 @@ func generateFeatureMatrix() error {
 			{"TXTMulti", "Provider can manage TXT records with multiple strings"},
 			{"R53_ALIAS", "Provider supports Route 53 limited ALIAS"},
 			{"AZURE_ALIAS", "Provider supports Azure DNS limited ALIAS"},
+			{"DS", "Provider supports adding DS records"},
 
 			{"dual host", "This provider is recommended for use in 'dual hosting' scenarios. Usually this means the provider allows full control over the apex NS records"},
 			{"create-domains", "This means the provider can automatically create domains that do not currently exist on your account. The 'dnscontrol create-domains' command will initialize any missing domains"},
@@ -87,6 +88,7 @@ func generateFeatureMatrix() error {
 		setCap("TLSA", providers.CanUseTLSA)
 		setCap("TXTMulti", providers.CanUseTXTMulti)
 		setCap("get-zones", providers.CanGetZones)
+		setCap("DS", providers.CanUseDS)
 		setDoc("dual host", providers.DocDualHost, false)
 		setDoc("create-domains", providers.DocCreateDomains, true)
 
diff --git a/docs/_functions/domain/DS.md b/docs/_functions/domain/DS.md
new file mode 100644
index 00000000..1deaef55
--- /dev/null
+++ b/docs/_functions/domain/DS.md
@@ -0,0 +1,30 @@
+---
+name: DS
+parameters:
+  - name
+  - keytag
+  - algorithm
+  - digesttype
+  - digest
+  - modifiers...
+---
+
+DS adds a DS record to the domain.
+
+Key Tag should be a number.
+
+Algorithm should be a number.
+
+Digest Type must be a number.
+
+Digest must be a string.
+
+{% include startExample.html %}
+{% highlight js %}
+
+D("example.com", REGISTRAR, DnsProvider(R53),
+  DS("example.com", 2371, 13, 2, "ABCDEF")
+);
+
+{%endhighlight%}
+{% include endExample.html %}
\ No newline at end of file
diff --git a/docs/_includes/matrix.html b/docs/_includes/matrix.html
index 356c479e..1b5bdb86 100644
--- a/docs/_includes/matrix.html
+++ b/docs/_includes/matrix.html
@@ -28,6 +28,7 @@
 	<th class="rotate"><div><span>OCTODNS</span></div></th>
 	<th class="rotate"><div><span>OPENSRS</span></div></th>
 	<th class="rotate"><div><span>OVH</span></div></th>
+	<th class="rotate"><div><span>POWERDNS</span></div></th>
 	<th class="rotate"><div><span>ROUTE53</span></div></th>
 	<th class="rotate"><div><span>SOFTLAYER</span></div></th>
 	<th class="rotate"><div><span>VULTR</span></div></th>
@@ -102,6 +103,9 @@
 		<td class="danger">
 			<i class="fa fa-times text-danger" aria-hidden="true"></i>
 		</td>
+		<td class="danger">
+			<i class="fa fa-times text-danger" aria-hidden="true"></i>
+		</td>
 		<td class="success">
 			<i class="fa fa-check text-success" aria-hidden="true"></i>
 		</td>
@@ -189,6 +193,9 @@
 		<td class="success">
 			<i class="fa fa-check text-success" aria-hidden="true"></i>
 		</td>
+		<td class="success">
+			<i class="fa fa-check text-success" aria-hidden="true"></i>
+		</td>
 		</tr>
 	<tr>
 		<th class="row-header" style="text-decoration: underline;" data-toggle="tooltip" data-container="body" data-placement="top" title="The provider has registrar capabilities to set nameservers for zones">Registrar</th>
@@ -258,6 +265,9 @@
 		<td class="success">
 			<i class="fa fa-check text-success" aria-hidden="true"></i>
 		</td>
+		<td class="danger">
+			<i class="fa fa-times text-danger" aria-hidden="true"></i>
+		</td>
 		<td class="success">
 			<i class="fa fa-check text-success" aria-hidden="true"></i>
 		</td>
@@ -316,6 +326,9 @@
 		<td class="danger">
 			<i class="fa fa-times text-danger" aria-hidden="true"></i>
 		</td>
+		<td class="success">
+			<i class="fa fa-check text-success" aria-hidden="true"></i>
+		</td>
 		<td class="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="R53 does not provide a generic ALIAS functionality. Use R53_ALIAS instead.">
 			<i class="fa has-tooltip fa-times text-danger" aria-hidden="true"></i>
 		</td>
@@ -356,6 +369,9 @@
 		<td><i class="fa fa-minus dim"></i></td>
 		<td><i class="fa fa-minus dim"></i></td>
 		<td><i class="fa fa-minus dim"></i></td>
+		<td class="info" data-toggle="tooltip" data-container="body" data-placement="top" title="Need support in library first">
+			<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
+		</td>
 		<td><i class="fa fa-minus dim"></i></td>
 		<td><i class="fa fa-minus dim"></i></td>
 		<td><i class="fa fa-minus dim"></i></td>
@@ -419,6 +435,9 @@
 		<td class="success">
 			<i class="fa fa-check text-success" aria-hidden="true"></i>
 		</td>
+		<td class="success">
+			<i class="fa fa-check text-success" aria-hidden="true"></i>
+		</td>
 		<td><i class="fa fa-minus dim"></i></td>
 		<td class="success">
 			<i class="fa fa-check text-success" aria-hidden="true"></i>
@@ -485,6 +504,9 @@
 		<td class="success">
 			<i class="fa fa-check text-success" aria-hidden="true"></i>
 		</td>
+		<td class="success">
+			<i class="fa fa-check text-success" aria-hidden="true"></i>
+		</td>
 		<td><i class="fa fa-minus dim"></i></td>
 		<td class="danger">
 			<i class="fa fa-times text-danger" aria-hidden="true"></i>
@@ -525,6 +547,7 @@
 		<td><i class="fa fa-minus dim"></i></td>
 		<td><i class="fa fa-minus dim"></i></td>
 		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
 		</tr>
 	<tr>
 		<th class="row-header" style="text-decoration: underline;" data-toggle="tooltip" data-container="body" data-placement="top" title="Driver has explicitly implemented SRV record management">SRV</th>
@@ -597,6 +620,9 @@
 		<td class="success">
 			<i class="fa fa-check text-success" aria-hidden="true"></i>
 		</td>
+		<td class="success">
+			<i class="fa fa-check text-success" aria-hidden="true"></i>
+		</td>
 		</tr>
 	<tr>
 		<th class="row-header" style="text-decoration: underline;" data-toggle="tooltip" data-container="body" data-placement="top" title="Provider can manage SSHFP records">SSHFP</th>
@@ -642,6 +668,9 @@
 		<td class="success">
 			<i class="fa fa-check text-success" aria-hidden="true"></i>
 		</td>
+		<td class="success">
+			<i class="fa fa-check text-success" aria-hidden="true"></i>
+		</td>
 		<td><i class="fa fa-minus dim"></i></td>
 		<td><i class="fa fa-minus dim"></i></td>
 		<td class="success">
@@ -696,6 +725,9 @@
 		<td class="success">
 			<i class="fa fa-check text-success" aria-hidden="true"></i>
 		</td>
+		<td class="success">
+			<i class="fa fa-check text-success" aria-hidden="true"></i>
+		</td>
 		<td><i class="fa fa-minus dim"></i></td>
 		<td><i class="fa fa-minus dim"></i></td>
 		<td class="danger">
@@ -744,6 +776,7 @@
 		<td><i class="fa fa-minus dim"></i></td>
 		<td><i class="fa fa-minus dim"></i></td>
 		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
 		<td class="success">
 			<i class="fa fa-check text-success" aria-hidden="true"></i>
 		</td>
@@ -776,6 +809,7 @@
 		<td><i class="fa fa-minus dim"></i></td>
 		<td><i class="fa fa-minus dim"></i></td>
 		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
 		<td class="success">
 			<i class="fa fa-check text-success" aria-hidden="true"></i>
 		</td>
@@ -811,6 +845,44 @@
 		<td><i class="fa fa-minus dim"></i></td>
 		<td><i class="fa fa-minus dim"></i></td>
 		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		</tr>
+	<tr>
+		<th class="row-header" style="text-decoration: underline;" data-toggle="tooltip" data-container="body" data-placement="top" title="Provider supports adding DS records">DS</th>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td class="success">
+			<i class="fa fa-check text-success" aria-hidden="true"></i>
+		</td>
+		<td class="success">
+			<i class="fa fa-check text-success" aria-hidden="true"></i>
+		</td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td class="success">
+			<i class="fa fa-check text-success" aria-hidden="true"></i>
+		</td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td class="success">
+			<i class="fa fa-check text-success" aria-hidden="true"></i>
+		</td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
+		<td><i class="fa fa-minus dim"></i></td>
 		</tr>
 	<tr>
 		<th class="row-header" style="text-decoration: underline;" data-toggle="tooltip" data-container="body" data-placement="top" title="This provider is recommended for use in &#39;dual hosting&#39; scenarios. Usually this means the provider allows full control over the apex NS records">dual host</th>
@@ -872,6 +944,7 @@
 		<td class="success">
 			<i class="fa fa-check text-success" aria-hidden="true"></i>
 		</td>
+		<td><i class="fa fa-minus dim"></i></td>
 		<td class="success">
 			<i class="fa fa-check text-success" aria-hidden="true"></i>
 		</td>
@@ -949,6 +1022,9 @@
 		<td class="success">
 			<i class="fa fa-check text-success" aria-hidden="true"></i>
 		</td>
+		<td class="success">
+			<i class="fa fa-check text-success" aria-hidden="true"></i>
+		</td>
 		<td class="danger">
 			<i class="fa fa-times text-danger" aria-hidden="true"></i>
 		</td>
@@ -1033,6 +1109,9 @@
 		<td class="success">
 			<i class="fa fa-check text-success" aria-hidden="true"></i>
 		</td>
+		<td class="success">
+			<i class="fa fa-check text-success" aria-hidden="true"></i>
+		</td>
 		</tr>
 	<tr>
 		<th class="row-header" style="text-decoration: underline;" data-toggle="tooltip" data-container="body" data-placement="top" title="indicates the dnscontrol get-zones subcommand is implemented.">get-zones</th>
@@ -1099,6 +1178,9 @@
 		<td class="success">
 			<i class="fa fa-check text-success" aria-hidden="true"></i>
 		</td>
+		<td class="success">
+			<i class="fa fa-check text-success" aria-hidden="true"></i>
+		</td>
 		<td class="info">
 			<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
 		</td>
diff --git a/go.mod b/go.mod
index 30a3116d..78a00891 100644
--- a/go.mod
+++ b/go.mod
@@ -34,6 +34,7 @@ require (
 	github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04
 	github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014
 	github.com/philhug/opensrs-go v0.0.0-20171126225031-9dfa7433020d
+	github.com/pkg/errors v0.9.1
 	github.com/renier/xmlrpc v0.0.0-20170708154548-ce4a1a486c03 // indirect
 	github.com/robertkrimen/otto v0.0.0-20191219234010-c382bd3c16ff
 	github.com/sergi/go-diff v1.1.0 // indirect
diff --git a/models/dnsrr.go b/models/dnsrr.go
index e99ea40c..7475fc93 100644
--- a/models/dnsrr.go
+++ b/models/dnsrr.go
@@ -71,6 +71,8 @@ func RRtoRC(rr dns.RR, origin string) RecordConfig {
 		panicInvalid(rc.SetTargetCAA(v.Flag, v.Tag, v.Value))
 	case *dns.CNAME:
 		panicInvalid(rc.SetTarget(v.Target))
+	case *dns.DS:
+		panicInvalid(rc.SetTargetDS(v.KeyTag, v.Algorithm, v.DigestType, v.Digest))
 	case *dns.MX:
 		panicInvalid(rc.SetTargetMX(v.Preference, v.Mx))
 	case *dns.NS:
diff --git a/models/domain.go b/models/domain.go
index ad240733..76896faf 100644
--- a/models/domain.go
+++ b/models/domain.go
@@ -79,7 +79,7 @@ func (dc *DomainConfig) Punycode() error {
 			if err != nil {
 				return err
 			}
-		case "A", "AAAA", "CAA", "NAPTR", "SOA", "SSHFP", "TXT", "TLSA", "AZURE_ALIAS":
+		case "A", "AAAA", "CAA", "DS", "NAPTR", "SOA", "SSHFP", "TXT", "TLSA", "AZURE_ALIAS":
 			// Nothing to do.
 		default:
 			msg := fmt.Sprintf("Punycode rtype %v unimplemented", rec.Type)
diff --git a/models/record.go b/models/record.go
index b6affe1c..b99cc125 100644
--- a/models/record.go
+++ b/models/record.go
@@ -77,6 +77,10 @@ type RecordConfig struct {
 	SrvPort          uint16            `json:"srvport,omitempty"`
 	CaaTag           string            `json:"caatag,omitempty"`
 	CaaFlag          uint8             `json:"caaflag,omitempty"`
+	DsKeyTag         uint16            `json:"dskeytag,omitempty"`
+	DsAlgorithm      uint8             `json:"dsalgorithm,omitempty"`
+	DsDigestType     uint8             `json:"dsdigesttype,omitempty"`
+	DsDigest         string            `json:"dsdigest,omitempty"`
 	NaptrOrder       uint16            `json:"naptrorder,omitempty"`
 	NaptrPreference  uint16            `json:"naptrpreference,omitempty"`
 	NaptrFlags       string            `json:"naptrflags,omitempty"`
@@ -242,6 +246,11 @@ func (rc *RecordConfig) ToRR() dns.RR {
 		rr.(*dns.AAAA).AAAA = rc.GetTargetIP()
 	case dns.TypeCNAME:
 		rr.(*dns.CNAME).Target = rc.GetTargetField()
+	case dns.TypeDS:
+		rr.(*dns.DS).Algorithm = rc.DsAlgorithm
+		rr.(*dns.DS).DigestType = rc.DsDigestType
+		rr.(*dns.DS).Digest = rc.DsDigest
+		rr.(*dns.DS).KeyTag = rc.DsKeyTag
 	case dns.TypePTR:
 		rr.(*dns.PTR).Ptr = rc.GetTargetField()
 	case dns.TypeNAPTR:
@@ -388,7 +397,7 @@ func downcase(recs []*RecordConfig) {
 		r.Name = strings.ToLower(r.Name)
 		r.NameFQDN = strings.ToLower(r.NameFQDN)
 		switch r.Type { // #rtype_variations
-		case "ANAME", "CNAME", "MX", "NS", "PTR", "NAPTR", "SRV":
+		case "ANAME", "CNAME", "DS", "MX", "NS", "PTR", "NAPTR", "SRV":
 			// These record types have a target that is case insensitive, so we downcase it.
 			r.Target = strings.ToLower(r.Target)
 		case "A", "AAAA", "ALIAS", "CAA", "IMPORT_TRANSFORM", "TLSA", "TXT", "SSHFP", "CF_REDIRECT", "CF_TEMP_REDIRECT":
diff --git a/models/t_ds.go b/models/t_ds.go
new file mode 100644
index 00000000..244b1a7b
--- /dev/null
+++ b/models/t_ds.go
@@ -0,0 +1,54 @@
+package models
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/pkg/errors"
+)
+
+// SetTargetDS sets the DS fields.
+func (rc *RecordConfig) SetTargetDS(keytag uint16, algorithm, digesttype uint8, digest string) error {
+	rc.DsKeyTag = keytag
+	rc.DsAlgorithm = algorithm
+	rc.DsDigestType = digesttype
+	rc.DsDigest = digest
+
+	if rc.Type == "" {
+		rc.Type = "DS"
+	}
+	if rc.Type != "DS" {
+		panic("assertion failed: SetTargetDS called when .Type is not DS")
+	}
+
+	return nil
+}
+
+// SetTargetDSStrings is like SetTargetDS but accepts strings.
+func (rc *RecordConfig) SetTargetDSStrings(keytag, algorithm, digesttype, digest string) error {
+	u16keytag, err := strconv.ParseUint(keytag, 10, 16)
+	if err != nil {
+		return errors.Wrap(err, "DS KeyTag can't fit in 16 bits")
+	}
+	u8algorithm, err := strconv.ParseUint(algorithm, 10, 8)
+	if err != nil {
+		return errors.Wrap(err, "DS Algorithm can't fit in 8 bits")
+	}
+	u8digesttype, err := strconv.ParseUint(digesttype, 10, 8)
+	if err != nil {
+		return errors.Wrap(err, "DS DigestType can't fit in 8 bits")
+	}
+
+	return rc.SetTargetDS(uint16(u16keytag), uint8(u8algorithm), uint8(u8digesttype), digest)
+}
+
+// SetTargetDSString is like SetTargetDS but accepts one big string.
+func (rc *RecordConfig) SetTargetDSString(s string) error {
+	part := strings.Fields(s)
+	if len(part) != 4 {
+		return errors.Errorf("DS value does not contain 5 fields: (%#v)", s)
+	}
+	fmt.Println(part)
+	return rc.SetTargetDSStrings(part[0], part[1], part[2], part[3])
+}
diff --git a/models/t_parse.go b/models/t_parse.go
index 916a8deb..a0fd53d3 100644
--- a/models/t_parse.go
+++ b/models/t_parse.go
@@ -37,6 +37,8 @@ func (r *RecordConfig) PopulateFromString(rtype, contents, origin string) error
 		return r.SetTarget(contents)
 	case "CAA":
 		return r.SetTargetCAAString(contents)
+	case "DS":
+		return r.SetTargetDSString(contents)
 	case "MX":
 		return r.SetTargetMXString(contents)
 	case "NAPTR":
diff --git a/models/target.go b/models/target.go
index f443a596..2561ed2f 100644
--- a/models/target.go
+++ b/models/target.go
@@ -88,6 +88,8 @@ func (rc *RecordConfig) GetTargetDebug() string {
 	switch rc.Type { // #rtype_variations
 	case "A", "AAAA", "CNAME", "NS", "PTR", "TXT":
 		// Nothing special.
+	case "DS":
+		content += fmt.Sprintf(" ds_algorithm=%d ds_keytag=%d ds_digesttype=%d ds_digest=%s", rc.DsAlgorithm, rc.DsKeyTag, rc.DsDigestType, rc.DsDigest)
 	case "NAPTR":
 		content += fmt.Sprintf(" naptrorder=%d naptrpreference=%d naptrflags=%s naptrservice=%s naptrregexp=%s", rc.NaptrOrder, rc.NaptrPreference, rc.NaptrFlags, rc.NaptrService, rc.NaptrRegexp)
 	case "MX":
diff --git a/pkg/js/helpers.js b/pkg/js/helpers.js
index 2050a8f3..11550b65 100644
--- a/pkg/js/helpers.js
+++ b/pkg/js/helpers.js
@@ -260,6 +260,25 @@ var CAA = recordBuilder('CAA', {
 // CNAME(name,target, recordModifiers...)
 var CNAME = recordBuilder('CNAME');
 
+// DS(name, keytag, algorithm, digestype, digest)
+var DS = recordBuilder("DS", {
+    args: [
+        ['name', _.isString],
+        ['keytag', _.isNumber],
+        ['algorithm', _.isNumber],
+        ['digesttype', _.isNumber],
+        ['digest', _.isString]
+    ],
+    transform: function(record, args, modifiers) {
+        record.name = args.name;
+        record.dskeytag = args.keytag;
+        record.dsalgorithm = args.algorithm;
+        record.dsdigesttype = args.digesttype;
+        record.dsdigest = args.digest;
+        record.target = args.target;
+    },
+});
+
 // PTR(name,target, recordModifiers...)
 var PTR = recordBuilder('PTR');
 
diff --git a/pkg/js/static.go b/pkg/js/static.go
index ce479502..d43553c3 100644
--- a/pkg/js/static.go
+++ b/pkg/js/static.go
@@ -212,105 +212,106 @@ var _escData = map[string]*_escFile{
 	"/helpers.js": {
 		name:    "helpers.js",
 		local:   "pkg/js/helpers.js",
-		size:    22817,
+		size:    23384,
 		modtime: 0,
 		compressed: `
-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
+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
-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
+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
-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
+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
-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
-Ef83AAD//yJmQYUhWQAA
+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
 `,
 	},
 }
diff --git a/pkg/normalize/validate.go b/pkg/normalize/validate.go
index 1d82da5c..31c56e0d 100644
--- a/pkg/normalize/validate.go
+++ b/pkg/normalize/validate.go
@@ -53,6 +53,7 @@ func validateRecordTypes(rec *models.RecordConfig, domain string, pTypes []strin
 		"AAAA":             true,
 		"CNAME":            true,
 		"CAA":              true,
+		"DS":               true,
 		"TLSA":             true,
 		"IMPORT_TRANSFORM": false,
 		"MX":               true,
@@ -185,7 +186,7 @@ func checkTargets(rec *models.RecordConfig, domain string) (errs []error) {
 		check(checkTarget(target))
 	case "SRV":
 		check(checkTarget(target))
-	case "TXT", "IMPORT_TRANSFORM", "CAA", "SSHFP", "TLSA":
+	case "TXT", "IMPORT_TRANSFORM", "CAA", "SSHFP", "TLSA", "DS":
 	default:
 		if rec.Metadata["orig_custom_type"] != "" {
 			// it is a valid custom type. We perform no validation on target
diff --git a/providers/bind/bindProvider.go b/providers/bind/bindProvider.go
index 5e79efeb..70a47d5b 100644
--- a/providers/bind/bindProvider.go
+++ b/providers/bind/bindProvider.go
@@ -33,6 +33,7 @@ import (
 
 var features = providers.DocumentationNotes{
 	providers.CanUseCAA:              providers.Can(),
+	providers.CanUseDS:               providers.Can(),
 	providers.CanUsePTR:              providers.Can(),
 	providers.CanUseNAPTR:            providers.Can(),
 	providers.CanUseSRV:              providers.Can(),
diff --git a/providers/capabilities.go b/providers/capabilities.go
index 58fa6ef1..86cf7cf3 100644
--- a/providers/capabilities.go
+++ b/providers/capabilities.go
@@ -18,6 +18,9 @@ const (
 	// CanUseCAA indicates the provider can handle CAA records
 	CanUseCAA
 
+	// CanUseDs indicates that the provider can handle DS record types
+	CanUseDS
+
 	// CanUsePTR indicates the provider can handle PTR records
 	CanUsePTR
 
diff --git a/providers/capability_string.go b/providers/capability_string.go
index e0a4c8db..bca3c663 100644
--- a/providers/capability_string.go
+++ b/providers/capability_string.go
@@ -10,25 +10,26 @@ func _() {
 	var x [1]struct{}
 	_ = x[CanUseAlias-0]
 	_ = x[CanUseCAA-1]
-	_ = x[CanUsePTR-2]
-	_ = x[CanUseNAPTR-3]
-	_ = x[CanUseSRV-4]
-	_ = x[CanUseSSHFP-5]
-	_ = x[CanUseTLSA-6]
-	_ = x[CanUseTXTMulti-7]
-	_ = x[CanAutoDNSSEC-8]
-	_ = x[CantUseNOPURGE-9]
-	_ = x[DocOfficiallySupported-10]
-	_ = x[DocDualHost-11]
-	_ = x[DocCreateDomains-12]
-	_ = x[CanUseRoute53Alias-13]
-	_ = x[CanGetZones-14]
-	_ = x[CanUseAzureAlias-15]
+	_ = x[CanUseDS-2]
+	_ = x[CanUsePTR-3]
+	_ = x[CanUseNAPTR-4]
+	_ = x[CanUseSRV-5]
+	_ = x[CanUseSSHFP-6]
+	_ = x[CanUseTLSA-7]
+	_ = x[CanUseTXTMulti-8]
+	_ = x[CanAutoDNSSEC-9]
+	_ = x[CantUseNOPURGE-10]
+	_ = x[DocOfficiallySupported-11]
+	_ = x[DocDualHost-12]
+	_ = x[DocCreateDomains-13]
+	_ = x[CanUseRoute53Alias-14]
+	_ = x[CanGetZones-15]
+	_ = x[CanUseAzureAlias-16]
 }
 
-const _Capability_name = "CanUseAliasCanUseCAACanUsePTRCanUseNAPTRCanUseSRVCanUseSSHFPCanUseTLSACanUseTXTMultiCanAutoDNSSECCantUseNOPURGEDocOfficiallySupportedDocDualHostDocCreateDomainsCanUseRoute53AliasCanGetZonesCanUseAzureAlias"
+const _Capability_name = "CanUseAliasCanUseCAACanUseDSCanUsePTRCanUseNAPTRCanUseSRVCanUseSSHFPCanUseTLSACanUseTXTMultiCanAutoDNSSECCantUseNOPURGEDocOfficiallySupportedDocDualHostDocCreateDomainsCanUseRoute53AliasCanGetZonesCanUseAzureAlias"
 
-var _Capability_index = [...]uint8{0, 11, 20, 29, 40, 49, 60, 70, 84, 97, 111, 133, 144, 160, 178, 189, 205}
+var _Capability_index = [...]uint8{0, 11, 20, 28, 37, 48, 57, 68, 78, 92, 105, 119, 141, 152, 168, 186, 197, 213}
 
 func (i Capability) String() string {
 	if i >= Capability(len(_Capability_index)-1) {
diff --git a/providers/cloudflare/cloudflareProvider.go b/providers/cloudflare/cloudflareProvider.go
index aa0f67e4..abe307fa 100644
--- a/providers/cloudflare/cloudflareProvider.go
+++ b/providers/cloudflare/cloudflareProvider.go
@@ -44,6 +44,7 @@ var features = providers.DocumentationNotes{
 	providers.CanUseSRV:              providers.Can(),
 	providers.CanUseTLSA:             providers.Can(),
 	providers.CanUseSSHFP:            providers.Can(),
+	providers.CanUseDS:               providers.Can(),
 	providers.DocCreateDomains:       providers.Can(),
 	providers.DocDualHost:            providers.Cannot("Cloudflare will not work well in situations where it is not the only DNS server"),
 	providers.DocOfficiallySupported: providers.Can(),
@@ -473,9 +474,12 @@ type cfRecData struct {
 	Selector      uint8    `json:"selector"`      // TLSA
 	Matching_Type uint8    `json:"matching_type"` // TLSA
 	Certificate   string   `json:"certificate"`   // TLSA
-	Algorithm     uint8    `json:"algorithm"`     // SSHFP
+	Algorithm     uint8    `json:"algorithm"`     // SSHFP/DS
 	Hash_Type     uint8    `json:"type"`          // SSHFP
 	Fingerprint   string   `json:"fingerprint"`   // SSHFP
+	KeyTag        uint16   `json:"key_tag"`       // DS
+	DigestType    uint8    `json:"digest_type"`   // DS
+	Digest        string   `json:"digest"`        // DS
 }
 
 // cfTarget is a SRV target. A null target is represented by an empty string, but
diff --git a/providers/cloudflare/rest.go b/providers/cloudflare/rest.go
index 37225d50..09b243b6 100644
--- a/providers/cloudflare/rest.go
+++ b/providers/cloudflare/rest.go
@@ -127,6 +127,15 @@ func (c *CloudflareApi) createZone(domainName string) (string, error) {
 	return id, err
 }
 
+func cfDSData(rec *models.RecordConfig) *cfRecData {
+	return &cfRecData{
+		KeyTag:     rec.DsKeyTag,
+		Algorithm:  rec.DsAlgorithm,
+		DigestType: rec.DsDigestType,
+		Digest:     rec.DsDigest,
+	}
+}
+
 func cfSrvData(rec *models.RecordConfig) *cfRecData {
 	serverParts := strings.Split(rec.GetLabelFQDN(), ".")
 	c := &cfRecData{
@@ -184,6 +193,9 @@ func (c *CloudflareApi) createRec(rec *models.RecordConfig, domainID string) []*
 	if rec.Type == "MX" {
 		prio = fmt.Sprintf(" %d ", rec.MxPreference)
 	}
+	if rec.Type == "DS" {
+		content = fmt.Sprintf("%d %d %d %s", rec.DsKeyTag, rec.DsAlgorithm, rec.DsDigestType, rec.DsDigest)
+	}
 	arr := []*models.Correction{{
 		Msg: fmt.Sprintf("CREATE record: %s %s %d%s %s", rec.GetLabel(), rec.Type, rec.TTL, prio, content),
 		F: func() error {
@@ -208,6 +220,8 @@ func (c *CloudflareApi) createRec(rec *models.RecordConfig, domainID string) []*
 			} else if rec.Type == "SSHFP" {
 				cf.Data = cfSshfpData(rec)
 				cf.Name = rec.GetLabelFQDN()
+			} else if rec.Type == "DS" {
+				cf.Data = cfDSData(rec)
 			}
 			endpoint := fmt.Sprintf(recordsURL, domainID)
 			buf := &bytes.Buffer{}
@@ -270,6 +284,9 @@ func (c *CloudflareApi) modifyRecord(domainID, recID string, proxied bool, rec *
 	} else if rec.Type == "SSHFP" {
 		r.Data = cfSshfpData(rec)
 		r.Name = rec.GetLabelFQDN()
+	} else if rec.Type == "DS" {
+		r.Data = cfDSData(rec)
+		r.Content = ""
 	}
 	endpoint := fmt.Sprintf(singleRecordURL, domainID, recID)
 	buf := &bytes.Buffer{}
diff --git a/providers/desec/desecProvider.go b/providers/desec/desecProvider.go
index b5eaf97f..cc35fa20 100644
--- a/providers/desec/desecProvider.go
+++ b/providers/desec/desecProvider.go
@@ -40,6 +40,7 @@ var features = providers.DocumentationNotes{
 	providers.DocCreateDomains:       providers.Can(),
 	providers.CanUseAlias:            providers.Cannot(),
 	providers.CanUseSRV:              providers.Can(),
+	providers.CanUseDS:               providers.Can(),
 	providers.CanUseSSHFP:            providers.Can(),
 	providers.CanUseCAA:              providers.Can(),
 	providers.CanUseTLSA:             providers.Can(),
diff --git a/providers/dnsimple/dnsimpleProvider.go b/providers/dnsimple/dnsimpleProvider.go
index 76f9e7ce..50fc28a5 100644
--- a/providers/dnsimple/dnsimpleProvider.go
+++ b/providers/dnsimple/dnsimpleProvider.go
@@ -20,6 +20,7 @@ var features = providers.DocumentationNotes{
 	providers.CanUseAlias:            providers.Can(),
 	providers.CanUseCAA:              providers.Can(),
 	providers.CanUseNAPTR:            providers.Can(),
+	providers.CanUseDS:               providers.Can(),
 	providers.CanUsePTR:              providers.Can(),
 	providers.CanUseSSHFP:            providers.Can(),
 	providers.CanUseSRV:              providers.Can(),
@@ -94,6 +95,10 @@ func (client *DnsimpleApi) GetZoneRecords(domain string) (models.Records, error)
 			if err := rec.SetTarget(r.Content); err != nil {
 				panic(fmt.Errorf("unparsable record received from dnsimple: %w", err))
 			}
+		case "DS":
+			if err := rec.SetTargetDSString(r.Content); err != nil {
+				panic(fmt.Errorf("unparsable record received from dnsimple: %w", err))
+			}
 		case "MX":
 			if err := rec.SetTargetMX(uint16(r.Priority), r.Content); err != nil {
 				panic(fmt.Errorf("unparsable record received from dnsimple: %w", err))
@@ -563,6 +568,8 @@ func getTargetRecordContent(rc *models.RecordConfig) string {
 		return rc.GetTargetCombined()
 	case "SSHFP":
 		return fmt.Sprintf("%d %d %s", rc.SshfpAlgorithm, rc.SshfpFingerprint, rc.GetTargetField())
+	case "DS":
+		return fmt.Sprintf("%d %d %d %s", rc.DsKeyTag, rc.DsAlgorithm, rc.DsDigestType, rc.DsDigest)
 	case "SRV":
 		return fmt.Sprintf("%d %d %s", rc.SrvWeight, rc.SrvPort, rc.GetTargetField())
 	case "TXT":
diff --git a/vendor/modules.txt b/vendor/modules.txt
index a1df79e4..77c0f610 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -235,6 +235,7 @@ github.com/philhug/opensrs-go/opensrs
 github.com/pierrec/lz4
 github.com/pierrec/lz4/internal/xxh32
 # github.com/pkg/errors v0.9.1
+## explicit
 github.com/pkg/errors
 # github.com/pmezard/go-difflib v1.0.0
 github.com/pmezard/go-difflib/difflib


Test patch:
diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go
index 63bdc83e..dd13c3ec 100644
--- a/integrationTest/integration_test.go
+++ b/integrationTest/integration_test.go
@@ -373,6 +373,15 @@ func naptr(name string, order uint16, preference uint16, flags string, service s
 	return r
 }
 
+func ds(name string, keyTag uint16, algorithm, digestType uint8, digest string) *rec {
+	r := makeRec(name, "", "DS")
+	r.DsKeyTag = keyTag
+	r.DsAlgorithm = algorithm
+	r.DsDigestType = digestType
+	r.DsDigest = digest
+	return r
+}
+
 func srv(name string, priority, weight, port uint16, target string) *rec {
 	r := makeRec(name, target, "SRV")
 	r.SrvPriority = priority
@@ -829,6 +838,18 @@ func makeTests(t *testing.T) []*TestGroup {
 				txtmulti("foo3", []string{strings.Repeat("X", 255), strings.Repeat("Y", 255), strings.Repeat("Z", 255)})),
 		),
 
+		testgroup("DS",
+			requires(providers.canUseDS),
+			tc("create DS", ds("@", 1, 13, 1, "ADIGEST")),
+			tc("modify field 1", ds("@", 65535, 13, 1, "ADIGEST")),
+			tc("modify field 3", ds("@", 65535, 13, 2, "ADIGEST")),
+			tc("modify field 2+3", ds("@", 65535, 1, 4, "ADIGEST")),
+			tc("modify field 2", ds("@", 65535, 3, 4, "ADIGEST")),
+			tc("modify field 2", ds("@", 65535, 254, 4, "ADIGEST")),
+			tc("delete 1, create 1", ds("foo", 2, 13, 4, "ADIGEST")),
+			tc("add 2 more DS", ds("foo", 2, 13, 4, "ADIGEST"), ds("@", 65535, 5, 4, "ADIGEST"), ds("@", 65535, 253, 4, "ADIGEST")),
+		),
+
 		//
 		// Pseudo rtypes:
 		//
diff --git a/integrationTest/testing.txt b/integrationTest/testing.txt
new file mode 100644
index 00000000..bd4f72d8
--- /dev/null
+++ b/integrationTest/testing.txt
@@ -0,0 +1,22 @@
+
+
+go test -v -verbose -provider BIND
+go test -v -verbose -provider AZURE_DNS
+go test -v -verbose -provider ROUTE53
+go test -v -verbose -provider GCLOUD
+go test -v -verbose -provider DIGITALOCEAN
+go test -v -verbose -provider GANDI_V5
+
+-run TestDNSProviders -start 5 -end 6
+
+DONE   go test -v -provider BIND
+DONE   go test -v -provider AZURE_DNS
+DONE   go test -v -provider ROUTE53
+DONE   go test -v -provider GCLOUD
+REPORTED go test -v -provider NAMEDOTCOM
+REPORTED go test -v -provider CLOUDFLAREAPI
+DONE   go test -v -provider DIGITALOCEAN
+DONE   go test -v -provider GANDI_V5
+
+go test -v -verbose -provider NAMEDOTCOM -run TestDNSProviders -start 5 -end 6
+go test -v -verbose -provider CLOUDFLAREAPI -run TestDNSProviders -start 5 -end 6
diff --git a/pkg/js/parse_tests/027-ds.js b/pkg/js/parse_tests/027-ds.js
new file mode 100644
index 00000000..4eccc76a
--- /dev/null
+++ b/pkg/js/parse_tests/027-ds.js
@@ -0,0 +1,4 @@
+D("foo.com","none",
+    DS("@", 1000, 13, 2, "AABBCCDDEEFF"),
+    DS("@", 1, 1, 1, "FFFF")
+);
diff --git a/pkg/js/parse_tests/027-ds.json b/pkg/js/parse_tests/027-ds.json
new file mode 100644
index 00000000..ba7ba59d
--- /dev/null
+++ b/pkg/js/parse_tests/027-ds.json
@@ -0,0 +1,31 @@
+{
+  "registrars": [],
+  "dns_providers": [],
+  "domains": [
+    {
+      "name": "foo.com",
+      "registrar": "none",
+      "dnsProviders": {},
+      "records": [
+        {
+          "type": "DS",
+          "name": "@",
+          "target": "",
+          "dskeytag": 1000,
+          "dsalgorithm": 13,
+          "dsdigesttype": 2,
+          "dsdigest": "AABBCCDDEEFF"
+        },
+        {
+          "type": "DS",
+          "name": "@",
+          "target": "",
+          "dskeytag": 1,
+          "dsalgorithm": 1,
+          "dsdigesttype": 1,
+          "dsdigest": "FFFF"
+        }
+      ]
+    }
+  ]
+}