diff --git a/build/generate/featureMatrix.go b/build/generate/featureMatrix.go
index f68111de1..1c0ecd843 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 000000000..1deaef55d
--- /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 356c479ef..1b5bdb86a 100644
--- a/docs/_includes/matrix.html
+++ b/docs/_includes/matrix.html
@@ -28,6 +28,7 @@
OCTODNS |
OPENSRS |
OVH |
+ POWERDNS |
ROUTE53 |
SOFTLAYER |
VULTR |
@@ -102,6 +103,9 @@
|
+
+
+ |
|
@@ -189,6 +193,9 @@
|
+
+
+ |
@@ -258,6 +265,9 @@
|
+
+
+ |
|
@@ -316,6 +326,9 @@
|
+
+
+ |
|
@@ -356,6 +369,9 @@
|
|
|
+
+
+ |
|
|
|
@@ -419,6 +435,9 @@
|
+
+
+ |
|
@@ -485,6 +504,9 @@
|
|
+
+
+ |
|
@@ -525,6 +547,7 @@
| |
|
|
+ |
@@ -597,6 +620,9 @@
|
+
+
+ |
@@ -642,6 +668,9 @@
|
+
+
+ |
|
|
@@ -696,6 +725,9 @@
|
|
+
+
+ |
|
|
@@ -744,6 +776,7 @@
| |
|
|
+ |
|
@@ -776,6 +809,7 @@
|
|
|
+ |
|
@@ -811,6 +845,44 @@
|
|
|
+ |
+
+
+
+ |
+ |
+ |
+
+
+ |
+
+
+ |
+ |
+
+
+ |
+ |
+
+
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
@@ -872,6 +944,7 @@
|
+ |
|
@@ -949,6 +1022,9 @@
|
+
+
+ |
|
@@ -1033,6 +1109,9 @@
|
+
+
+ |
@@ -1099,6 +1178,9 @@
|
+
+
+ |
|
diff --git a/go.mod b/go.mod
index 30a3116dd..78a008912 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/integrationTest/integration_test.go b/integrationTest/integration_test.go
index 63bdc83e5..dd13c3eca 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 000000000..bd4f72d8a
--- /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/models/dnsrr.go b/models/dnsrr.go
index e99ea40c1..7475fc931 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 ad240733b..76896fafa 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 b6affe1c3..b99cc125b 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 000000000..244b1a7bf
--- /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 916a8debf..a0fd53d35 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 f443a5960..2561ed2fd 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 2050a8f33..11550b65e 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/parse_tests/027-ds.js b/pkg/js/parse_tests/027-ds.js
new file mode 100644
index 000000000..4eccc76a8
--- /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 000000000..ba7ba59d6
--- /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"
+ }
+ ]
+ }
+ ]
+}
diff --git a/pkg/js/static.go b/pkg/js/static.go
index ce4795023..d43553c3d 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 1d82da5c3..31c56e0d6 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 5e79efeb2..70a47d5b4 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 58fa6ef17..86cf7cf36 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 e0a4c8db2..bca3c663e 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 aa0f67e42..abe307fab 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 37225d500..09b243b6d 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 b5eaf97f5..cc35fa204 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 76f9e7cee..50fc28a51 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 a1df79e43..77c0f610d 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