From 87a5c4b3391a88ae3d5cd8813378bcc952d1857d Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sat, 30 May 2020 10:40:21 -0400 Subject: [PATCH] New RTYPE: DS records now supported! (#753) Thanks to @haraldkoch for starting this, @McNetic for picking it up. * Added DS record type * Added DS for cloudflare provider with tests * Removed DS validation, fixed parse test * Added generated files * Added dnsimple ds record * Regenerated documentation matrix * rebased and regenerated * Updated integration tests * Rebase and regenerate * Enable DS record type for provider desec * Added DS record type * Added DS for cloudflare provider with tests * Removed DS validation, fixed parse test * Added generated files * Added dnsimple ds record * Regenerated documentation matrix * rebased and regenerated * Updated integration tests * Rebase and regenerate * Enable DS record type for provider desec * Rebase and fixes Co-authored-by: Robert Koch Co-authored-by: Nicolai Ehemann --- build/generate/featureMatrix.go | 2 + docs/_functions/domain/DS.md | 30 ++++ docs/_includes/matrix.html | 82 +++++++++ go.mod | 1 + integrationTest/integration_test.go | 21 +++ integrationTest/testing.txt | 22 +++ models/dnsrr.go | 2 + models/domain.go | 2 +- models/record.go | 11 +- models/t_ds.go | 54 ++++++ models/t_parse.go | 2 + models/target.go | 2 + pkg/js/helpers.js | 19 +++ pkg/js/parse_tests/027-ds.js | 4 + pkg/js/parse_tests/027-ds.json | 31 ++++ pkg/js/static.go | 189 +++++++++++---------- pkg/normalize/validate.go | 3 +- providers/bind/bindProvider.go | 1 + providers/capabilities.go | 3 + providers/capability_string.go | 33 ++-- providers/cloudflare/cloudflareProvider.go | 6 +- providers/cloudflare/rest.go | 17 ++ providers/desec/desecProvider.go | 1 + providers/dnsimple/dnsimpleProvider.go | 7 + vendor/modules.txt | 1 + 25 files changed, 432 insertions(+), 114 deletions(-) create mode 100644 docs/_functions/domain/DS.md create mode 100644 integrationTest/testing.txt create mode 100644 models/t_ds.go create mode 100644 pkg/js/parse_tests/027-ds.js create mode 100644 pkg/js/parse_tests/027-ds.json 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 @@ + + + Registrar @@ -258,6 +265,9 @@ + + + @@ -316,6 +326,9 @@ + + + @@ -356,6 +369,9 @@ + + + @@ -419,6 +435,9 @@ + + + @@ -485,6 +504,9 @@ + + + @@ -525,6 +547,7 @@ + SRV @@ -597,6 +620,9 @@ + + + SSHFP @@ -642,6 +668,9 @@ + + + @@ -696,6 +725,9 @@ + + + @@ -744,6 +776,7 @@ + @@ -776,6 +809,7 @@ + @@ -811,6 +845,44 @@ + + + + DS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dual host @@ -872,6 +944,7 @@ + @@ -949,6 +1022,9 @@ + + + @@ -1033,6 +1109,9 @@ + + + get-zones @@ -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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 -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