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 <it-sumpfling@maxit-con.de>
Co-authored-by: bentaybi jamal <jamal@pfalzcloud.de>
Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
This commit is contained in:
taybinakh 2021-01-22 18:54:39 +01:00 committed by GitHub
parent 20a726df27
commit d7f40ed680
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 99 additions and 4 deletions

View file

@ -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.

View file

@ -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:
//

View file

@ -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

View file

@ -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)
}