From d7f40ed680fc3e6f263d81388b68554bda788fcb Mon Sep 17 00:00:00 2001 From: taybinakh <51935048+jamaltebi@users.noreply.github.com> Date: Fri, 22 Jan 2021 18:54:39 +0100 Subject: [PATCH] Implement DS record support for ClouDNS (#1018) * Add PTR support for ClouDNS * Implement PTR Support for CLouDNS * implemnent DS Record for ClouDNS * implement DS record for clouDNS * pull request review * note that SshFpAlgorithm and DsAlgorithm both use json field algorithm * primitive rate limit and fix order of NS/DS-entries * codefixes Co-authored-by: IT-Sumpfling Co-authored-by: bentaybi jamal Co-authored-by: Tom Limoncelli --- docs/_providers/cloudns.md | 6 +++- integrationTest/integration_test.go | 48 ++++++++++++++++++++++++++++ providers/cloudns/api.go | 10 +++++- providers/cloudns/cloudnsProvider.go | 39 ++++++++++++++++++++-- 4 files changed, 99 insertions(+), 4 deletions(-) diff --git a/docs/_providers/cloudns.md b/docs/_providers/cloudns.md index 3aade3e53..f817aae79 100644 --- a/docs/_providers/cloudns.md +++ b/docs/_providers/cloudns.md @@ -23,7 +23,11 @@ Current version of provider doesn't support `sub-auth-user`. ## Records -ClouDNS does not supprt DS Record. +ClouDNS does support DS Record on subdomains (not the apex domain itself). + +ClouDNS requires NS records exist for any DS records. No other records for +the same label may exist (A, MX, TXT, etc.). If DNSControl is adding NS and +DS records in the same update, the NS records will be inserted first. ## Metadata This provider does not recognize any special metadata fields unique to ClouDNS. diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index 85e288cdd..cf2f8cf44 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -240,6 +240,7 @@ func runTests(t *testing.T, prv providers.DNSServiceProvider, domainName string, } // Run the tests. + for _, tst := range group.tests { makeChanges(t, prv, dc, tst, fmt.Sprintf("%02d:%s", gIdx, group.Desc), true, origConfig) if t.Failed() { @@ -980,6 +981,7 @@ func makeTests(t *testing.T) []*TestGroup { testgroup("DS (children only)", requires(providers.CanUseDSForChildren), + not("CLOUDNS"), // Use a valid digest value here, because GCLOUD (which implements this capability) verifies // the value passed in is a valid digest. RFC 4034, s5.1.4 specifies SHA1 as the only digest // algo at present, i.e. only hexadecimal values currently usable. @@ -997,6 +999,52 @@ func makeTests(t *testing.T) []*TestGroup { ), ), + testgroup("DS (children only) CLOUDNS", + requires(providers.CanUseDSForChildren), + only("CLOUDNS"), + // Use a valid digest value here, because GCLOUD (which implements this capability) verifies + // the value passed in is a valid digest. RFC 4034, s5.1.4 specifies SHA1 as the only digest + // algo at present, i.e. only hexadecimal values currently usable. + // Cloudns requires NS Record before creating DS Record. + tc("create DS", + // we test that provider correctly handles creating NS first by reversing the entries here + ds("child", 35632, 13, 1, "1E07663FF507A40874B8605463DD41DE482079D6"), + ns("child", "ns101.cloudns.net."), + ), + tc("modify field 1", + ds("child", 2075, 13, 1, "2706D12E256C8FDD9BFB45EFB25FE537E21A82F6"), + ns("child", "ns101.cloudns.net."), + ), + tc("modify field 3", + ds("child", 2075, 13, 2, "3F7A1EAC8C813A0BEBD0C3B8AAB387E31945EA0CD5E1D84A2E8E27674566C156"), + ns("child", "ns101.cloudns.net."), + ), + tc("modify field 2+3", + ds("child", 2159, 1, 4, "F50BEFEA333EE2901D72D31A08E1A3CD3F7E943FF4B38CF7C8AD92807F5302F76FB0B419182C0F47FFC71CBCB6EF4BD4"), + ns("child", "ns101.cloudns.net."), + ), + tc("modify field 2", + ds("child", 63909, 3, 4, "EEC7FA02E6788DA889B2CE41D43D92F948AB126EDCF83B7037E73CE9531C8E7E45653ABBAA76C2D6E42F98316EDE599B"), + ns("child", "ns101.cloudns.net."), + ), + //tc("modify field 2", ds("child", 65535, 254, 4, "0123456789ABCDEF")), + tc("delete 1, create 1", + ds("another-child", 35632, 13, 4, "F5F32ABCA6B01AA7A9963012F90B7C8523A1D946185A3AD70B67F3C9F18E7312FA9DD6AB2F7D8382F789213DB173D429"), + ns("another-child", "ns101.cloudns.net."), + ), + tc("add 2 more DS", + ds("another-child", 35632, 13, 4, "F5F32ABCA6B01AA7A9963012F90B7C8523A1D946185A3AD70B67F3C9F18E7312FA9DD6AB2F7D8382F789213DB173D429"), + ds("another-child", 2159, 1, 4, "F50BEFEA333EE2901D72D31A08E1A3CD3F7E943FF4B38CF7C8AD92807F5302F76FB0B419182C0F47FFC71CBCB6EF4BD4"), + ds("another-child", 63909, 3, 4, "EEC7FA02E6788DA889B2CE41D43D92F948AB126EDCF83B7037E73CE9531C8E7E45653ABBAA76C2D6E42F98316EDE599B"), + ns("another-child", "ns101.cloudns.net."), + ), + // in CLouDNS we must delete DS Record before deleting NS record + // should no longer be necessary, provider should handle order correctly + //tc("delete all DS", + // ns("another-child", "ns101.cloudns.net."), + //), + ), + // // Pseudo rtypes: // diff --git a/providers/cloudns/api.go b/providers/cloudns/api.go index e375d0d3c..3f2f3e386 100644 --- a/providers/cloudns/api.go +++ b/providers/cloudns/api.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "net/http" "strconv" + "time" ) // Api layer for CloDNS @@ -62,6 +63,10 @@ type domainRecord struct { TlsaMatchingType string `json:"tlsa_matching_type,omitempty"` SshfpAlgorithm string `json:"algorithm,omitempty"` SshfpFingerprint string `json:"fp_type,omitempty"` + DsKeyTag string `json:"key_tag,omitempty"` + DsAlgorithm string `json:"dsalgorithm,omitempty"` + DsDigestType string `json:"digest_type,omitempty"` + DsDigest string `json:"dsdigest,omitempty"` } type recordResponse map[string]domainRecord @@ -143,7 +148,7 @@ func (c *cloudnsProvider) createDomain(domain string) error { func (c *cloudnsProvider) createRecord(domainID string, rec requestParams) error { rec["domain-name"] = domainID - if _, err := c.get("/dns/add-record.json", rec); err != nil { + if _, err := c.get("/dns/add-record.json", rec); err != nil { // here we add record return fmt.Errorf("failed create record (ClouDNS): %s", err) } return nil @@ -204,6 +209,9 @@ func (c *cloudnsProvider) get(endpoint string, params requestParams) ([]byte, er req.URL.RawQuery = q.Encode() + // ClouDNS has a rate limit (not documented) of 10 request/second + // so we do a very primitive rate-limiting here - delay every request for 100ms - so max. 10 requests/second ... + time.Sleep(100 * time.Millisecond) resp, err := client.Do(req) if err != nil { return []byte{}, err diff --git a/providers/cloudns/cloudnsProvider.go b/providers/cloudns/cloudnsProvider.go index c2469af7a..4e088101f 100644 --- a/providers/cloudns/cloudnsProvider.go +++ b/providers/cloudns/cloudnsProvider.go @@ -48,6 +48,8 @@ var features = providers.DocumentationNotes{ providers.CanUseTLSA: providers.Can(), providers.CanUsePTR: providers.Can(), providers.CanGetZones: providers.Can(), + providers.CanUseDSForChildren: providers.Can(), + //providers.CanUseDS: providers.Can(), } func init() { @@ -111,9 +113,17 @@ func (c *cloudnsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode return c.deleteRecord(domainID, id) }, } - corrections = append(corrections, corr) + // at ClouDNS, we MUST have a NS for a DS + // So, when deleting, we must delete the DS first, otherwise deleting the NS throws an error + if m.Existing.Type == "DS" { + // type DS is prepended - so executed first + corrections = append([]*models.Correction{corr}, corrections...) + } else { + corrections = append(corrections, corr) + } } + var createCorrections []*models.Correction for _, m := range create { req, err := toReq(m.Desired) if err != nil { @@ -126,8 +136,17 @@ func (c *cloudnsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode return c.createRecord(domainID, req) }, } - corrections = append(corrections, corr) + // at ClouDNS, we MUST have a NS for a DS + // So, when creating, we must create the NS first, otherwise creating the DS throws an error + if m.Desired.Type == "NS" { + // type NS is prepended - so executed first + createCorrections = append([]*models.Correction{corr}, createCorrections...) + } else { + createCorrections = append(createCorrections, corr) + } } + corrections = append(corrections, createCorrections...) + for _, m := range modify { id := m.Existing.Original.(*domainRecord).ID req, err := toReq(m.Desired) @@ -172,6 +191,7 @@ func (c *cloudnsProvider) EnsureDomainExists(domain string) error { return c.createDomain(domain) } +//parses the ClouDNS format into our standard RecordConfig func toRc(domain string, r *domainRecord) *models.RecordConfig { ttl, _ := strconv.ParseUint(r.TTL, 10, 32) @@ -214,6 +234,15 @@ func toRc(domain string, r *domainRecord) *models.RecordConfig { sshfpFingerprint, _ := strconv.ParseUint(r.SshfpFingerprint, 10, 32) rc.SshfpFingerprint = uint8(sshfpFingerprint) rc.SetTarget(r.Target) + case "DS": + dsKeyTag, _ := strconv.ParseUint(r.DsKeyTag, 10, 32) + rc.DsKeyTag = uint16(dsKeyTag) + dsAlgorithm, _ := strconv.ParseUint(r.SshfpAlgorithm, 10, 32) // SshFpAlgorithm and DsAlgorithm both use json field "algorithm" + rc.DsAlgorithm = uint8(dsAlgorithm) + dsDigestType, _ := strconv.ParseUint(r.DsDigestType, 10, 32) + rc.DsDigestType = uint8(dsDigestType) + rc.DsDigest = r.Target + rc.SetTarget(r.Target) default: rc.SetTarget(r.Target) } @@ -221,6 +250,7 @@ func toRc(domain string, r *domainRecord) *models.RecordConfig { return rc } +//toReq takes a RecordConfig and turns it into the native format used by the API. func toReq(rc *models.RecordConfig) (requestParams, error) { req := requestParams{ "record-type": rc.Type, @@ -254,6 +284,11 @@ func toReq(rc *models.RecordConfig) (requestParams, error) { case "SSHFP": req["algorithm"] = strconv.Itoa(int(rc.SshfpAlgorithm)) req["fptype"] = strconv.Itoa(int(rc.SshfpFingerprint)) + case "DS": + req["key-tag"] = strconv.Itoa(int(rc.DsKeyTag)) + req["algorithm"] = strconv.Itoa(int(rc.DsAlgorithm)) + req["digest-type"] = strconv.Itoa(int(rc.DsDigestType)) + req["record"] = rc.DsDigest default: return nil, fmt.Errorf("ClouDNS.toReq rtype %q unimplemented", rc.Type) }