Repository: StackExchange/dnscontrol
Pull request #2417 (2023-06-02 08:01:13)

NEW FEATURE: Add dhcid DNS resource type
I just stumbled over the missing type dhcid and added it as a first draft. Prop. not that relevant but caused some mini issues migrating existing zones. 

fritterhoff: I'm not sure if my current draft is fine for a first integration? In theory some further checks could be added but I assume, this RR type is a real corner case in dnscontrol?
tlimoncelli: (Sorry for the long delay. I was on vacation)

This looks good.  Please rebase and I'll merge.  (I see it is marked "draft"... is there more you'd like to do?)
fritterhoff: Hi @tlimoncelli,
it is marked as draft since I was unsure if I updated all files and fulfilled all requirements ;-)  
fritterhoff: @tlimoncelli fixed all incorrect ordered entries. 
fritterhoff: Ping :) 
tlimoncelli: Looks good so far!    

I went through `documentation/adding-new-rtypes.md` and saw a few things were missing.

1. There are a few places where a case statement needs DHCID added.  Search for `#rtype_variations`:

commands/getZones.go
models/domain.go
models/t_parse.go
models/target.go GetTargetDebug()
pkg/normalize/validate.go

2.  It needs an integration test (integrationTest/integration_test.go).  Look at `testgroup("PTR",` as an example.

3.  It needs documentation.  Look at functions/domain/PTR.md as an example.  (documentation/styleguide-doc.md explains how to add something to the TOC)

Thanks,
Tom

Patch:
diff --git a/build/generate/featureMatrix.go b/build/generate/featureMatrix.go
index 06c0178f..e204728e 100644
--- a/build/generate/featureMatrix.go
+++ b/build/generate/featureMatrix.go
@@ -87,6 +87,7 @@ func matrixData() *FeatureMatrix {
 		DomainModifierSshfp  = "[`SSHFP`](functions/domain/SSHFP.md)"
 		DomainModifierTlsa   = "[`TLSA`](functions/domain/TLSA.md)"
 		DomainModifierDs     = "[`DS`](functions/domain/DS.md)"
+		DomainModifierDhcid  = "[`DHCID`](functions/domain/DHCID.md)"
 		DualHost             = "dual host"
 		CreateDomains        = "create-domains"
 		NoPurge              = "[`NO_PURGE`](functions/domain/NO_PURGE.md)"
@@ -110,6 +111,7 @@ func matrixData() *FeatureMatrix {
 			DomainModifierSshfp,
 			DomainModifierTlsa,
 			DomainModifierDs,
+			DomainModifierDhcid,
 			DualHost,
 			CreateDomains,
 			NoPurge,
@@ -180,6 +182,10 @@ func matrixData() *FeatureMatrix {
 			DomainModifierCaa,
 			providers.CanUseCAA,
 		)
+		setCapability(
+			DomainModifierDhcid,
+			providers.CanUseDHCID,
+		)
 		setCapability(
 			DomainModifierDs,
 			providers.CanUseDS,
diff --git a/models/dnsrr.go b/models/dnsrr.go
index 2361c7f3..1ef0f821 100644
--- a/models/dnsrr.go
+++ b/models/dnsrr.go
@@ -74,6 +74,8 @@ func RRtoRC(rr dns.RR, origin string) (RecordConfig, error) {
 		err = rc.SetTargetCAA(v.Flag, v.Tag, v.Value)
 	case *dns.CNAME:
 		err = rc.SetTarget(v.Target)
+	case *dns.DHCID:
+		err = rc.SetTarget(v.Digest)
 	case *dns.DS:
 		err = rc.SetTargetDS(v.KeyTag, v.Algorithm, v.DigestType, v.Digest)
 	case *dns.LOC:
diff --git a/models/domain.go b/models/domain.go
index 63c8a8a2..b7640745 100644
--- a/models/domain.go
+++ b/models/domain.go
@@ -135,7 +135,7 @@ func (dc *DomainConfig) Punycode() error {
 			rec.SetTarget(t)
 		case "CF_REDIRECT", "CF_TEMP_REDIRECT", "CF_WORKER_ROUTE":
 			rec.SetTarget(rec.GetTargetField())
-		case "A", "AAAA", "CAA", "DS", "LOC", "NAPTR", "SOA", "SSHFP", "TXT", "TLSA", "AZURE_ALIAS":
+		case "A", "AAAA", "CAA", "DHCPID", "DS", "LOC", "NAPTR", "SOA", "SSHFP", "TXT", "TLSA", "AZURE_ALIAS":
 			// Nothing to do.
 		default:
 			return fmt.Errorf("Punycode rtype %v unimplemented", rec.Type)
diff --git a/models/record.go b/models/record.go
index 9c958a80..894c49da 100644
--- a/models/record.go
+++ b/models/record.go
@@ -390,6 +390,8 @@ func (rc *RecordConfig) ToRR() dns.RR {
 		rr.(*dns.CAA).Value = rc.GetTargetField()
 	case dns.TypeCNAME:
 		rr.(*dns.CNAME).Target = rc.GetTargetField()
+	case dns.TypeDHCID:
+		rr.(*dns.DHCID).Digest = rc.GetTargetField()
 	case dns.TypeDS:
 		rr.(*dns.DS).Algorithm = rc.DsAlgorithm
 		rr.(*dns.DS).DigestType = rc.DsDigestType
@@ -593,7 +595,7 @@ func Downcase(recs []*RecordConfig) {
 			// Target is case insensitive. Downcase it.
 			r.target = strings.ToLower(r.target)
 			// BUGFIX(tlim): isn't ALIAS in the wrong case statement?
-		case "A", "ALIAS", "CAA", "CF_REDIRECT", "CF_TEMP_REDIRECT", "CF_WORKER_ROUTE", "IMPORT_TRANSFORM", "LOC", "SSHFP", "TXT":
+		case "A", "ALIAS", "CAA", "CF_REDIRECT", "CF_TEMP_REDIRECT", "CF_WORKER_ROUTE", "DHCID", "IMPORT_TRANSFORM", "LOC", "SSHFP", "TXT":
 			// Do nothing. (IP address or case sensitive target)
 		case "SOA":
 			if r.target != "DEFAULT_NOT_SET." {
@@ -617,7 +619,7 @@ func CanonicalizeTargets(recs []*RecordConfig, origin string) {
 		case "AKAMAICDN", "ANAME", "CNAME", "DS", "MX", "NS", "NAPTR", "PTR", "SRV":
 			// Target is a hostname that might be a shortname. Turn it into a FQDN.
 			r.target = dnsutil.AddOrigin(r.target, originFQDN)
-		case "A", "ALIAS", "CAA", "CF_REDIRECT", "CF_TEMP_REDIRECT", "CF_WORKER_ROUTE", "IMPORT_TRANSFORM", "LOC", "SSHFP", "TLSA", "TXT":
+		case "A", "ALIAS", "CAA", "DHCID", "CF_REDIRECT", "CF_TEMP_REDIRECT", "CF_WORKER_ROUTE", "IMPORT_TRANSFORM", "LOC", "SSHFP", "TLSA", "TXT":
 			// Do nothing.
 		case "SOA":
 			if r.target != "DEFAULT_NOT_SET." {
diff --git a/models/t_parse.go b/models/t_parse.go
index 374526e9..1cbfeea6 100644
--- a/models/t_parse.go
+++ b/models/t_parse.go
@@ -59,6 +59,8 @@ func (rc *RecordConfig) PopulateFromString(rtype, contents, origin string) error
 		return rc.SetTargetCAAString(contents)
 	case "DS":
 		return rc.SetTargetDSString(contents)
+	case "DHCID":
+		return rc.SetTarget(contents)
 	case "LOC":
 		return rc.SetTargetLOCString(origin, contents)
 	case "MX":
diff --git a/pkg/js/helpers.js b/pkg/js/helpers.js
index 6581cfaf..10dfa1f6 100644
--- a/pkg/js/helpers.js
+++ b/pkg/js/helpers.js
@@ -407,6 +407,9 @@ var DS = recordBuilder('DS', {
     },
 });
 
+// DHCID(name,target, recordModifiers...)
+var DHCID = recordBuilder('DHCID');
+
 // PTR(name,target, recordModifiers...)
 var PTR = recordBuilder('PTR');
 
diff --git a/pkg/normalize/validate.go b/pkg/normalize/validate.go
index 0a52cad1..3b30cc34 100644
--- a/pkg/normalize/validate.go
+++ b/pkg/normalize/validate.go
@@ -61,6 +61,7 @@ func validateRecordTypes(rec *models.RecordConfig, domain string, pTypes []strin
 		"ALIAS":            false,
 		"CAA":              true,
 		"CNAME":            true,
+		"DHCID":            true,
 		"DS":               true,
 		"IMPORT_TRANSFORM": false,
 		"LOC":              true,
@@ -221,7 +222,7 @@ func checkTargets(rec *models.RecordConfig, domain string) (errs []error) {
 		}
 	case "SRV":
 		check(checkTarget(target))
-	case "TXT", "IMPORT_TRANSFORM", "CAA", "SSHFP", "TLSA", "DS":
+	case "CAA", "DHCID", "DS", "IMPORT_TRANSFORM", "SSHFP", "TLSA", "TXT":
 	default:
 		if rec.Metadata["orig_custom_type"] != "" {
 			// it is a valid custom type. We perform no validation on target
@@ -688,6 +689,7 @@ var providerCapabilityChecks = []pairTypeCapability{
 	capabilityCheck("AUTODNSSEC", providers.CanAutoDNSSEC),
 	capabilityCheck("AZURE_ALIAS", providers.CanUseAzureAlias),
 	capabilityCheck("CAA", providers.CanUseCAA),
+	capabilityCheck("DHCID", providers.CanUseDHCID),
 	capabilityCheck("LOC", providers.CanUseLOC),
 	capabilityCheck("NAPTR", providers.CanUseNAPTR),
 	capabilityCheck("PTR", providers.CanUsePTR),
diff --git a/providers/axfrddns/axfrddnsProvider.go b/providers/axfrddns/axfrddnsProvider.go
index d455fc20..d670b775 100644
--- a/providers/axfrddns/axfrddnsProvider.go
+++ b/providers/axfrddns/axfrddnsProvider.go
@@ -49,6 +49,7 @@ var features = providers.DocumentationNotes{
 	providers.CanUseSRV:              providers.Can(),
 	providers.CanUseSSHFP:            providers.Can(),
 	providers.CanUseTLSA:             providers.Can(),
+	providers.CanUseDHCID:            providers.Can(),
 	providers.CantUseNOPURGE:         providers.Cannot(),
 	providers.DocCreateDomains:       providers.Cannot(),
 	providers.DocDualHost:            providers.Cannot(),
diff --git a/providers/bind/bindProvider.go b/providers/bind/bindProvider.go
index fdb74662..444ce6d7 100644
--- a/providers/bind/bindProvider.go
+++ b/providers/bind/bindProvider.go
@@ -36,6 +36,7 @@ var features = providers.DocumentationNotes{
 	providers.CanAutoDNSSEC:          providers.Can("Just writes out a comment indicating DNSSEC was requested"),
 	providers.CanGetZones:            providers.Can(),
 	providers.CanUseCAA:              providers.Can(),
+	providers.CanUseDHCID:            providers.Can(),
 	providers.CanUseDS:               providers.Can(),
 	providers.CanUseLOC:              providers.Can(),
 	providers.CanUseNAPTR:            providers.Can(),
diff --git a/providers/capabilities.go b/providers/capabilities.go
index 3335c8f5..5af2817c 100644
--- a/providers/capabilities.go
+++ b/providers/capabilities.go
@@ -32,6 +32,9 @@ const (
 	// CanUseCAA indicates the provider can handle CAA records
 	CanUseCAA
 
+	// CanUseDHCID indicates the provider can handle DHCID records
+	CanUseDHCID
+
 	// CanUseDS indicates that the provider can handle DS record types. This
 	// implies CanUseDSForChildren without specifying the latter explicitly.
 	CanUseDS
diff --git a/providers/capability_string.go b/providers/capability_string.go
index fb35841a..1363be8d 100644
--- a/providers/capability_string.go
+++ b/providers/capability_string.go
@@ -14,25 +14,26 @@ func _() {
 	_ = x[CanUseAlias-3]
 	_ = x[CanUseAzureAlias-4]
 	_ = x[CanUseCAA-5]
-	_ = x[CanUseDS-6]
-	_ = x[CanUseDSForChildren-7]
-	_ = x[CanUseLOC-8]
-	_ = x[CanUseNAPTR-9]
-	_ = x[CanUsePTR-10]
-	_ = x[CanUseRoute53Alias-11]
-	_ = x[CanUseSOA-12]
-	_ = x[CanUseSRV-13]
-	_ = x[CanUseSSHFP-14]
-	_ = x[CanUseTLSA-15]
-	_ = x[CantUseNOPURGE-16]
-	_ = x[DocCreateDomains-17]
-	_ = x[DocDualHost-18]
-	_ = x[DocOfficiallySupported-19]
+	_ = x[CanUseDHCID-6]
+	_ = x[CanUseDS-7]
+	_ = x[CanUseDSForChildren-8]
+	_ = x[CanUseLOC-9]
+	_ = x[CanUseNAPTR-10]
+	_ = x[CanUsePTR-11]
+	_ = x[CanUseRoute53Alias-12]
+	_ = x[CanUseSOA-13]
+	_ = x[CanUseSRV-14]
+	_ = x[CanUseSSHFP-15]
+	_ = x[CanUseTLSA-16]
+	_ = x[CantUseNOPURGE-17]
+	_ = x[DocCreateDomains-18]
+	_ = x[DocDualHost-19]
+	_ = x[DocOfficiallySupported-20]
 }
 
-const _Capability_name = "CanAutoDNSSECCanGetZonesCanUseAKAMAICDNCanUseAliasCanUseAzureAliasCanUseCAACanUseDSCanUseDSForChildrenCanUseLOCCanUseNAPTRCanUsePTRCanUseRoute53AliasCanUseSOACanUseSRVCanUseSSHFPCanUseTLSACantUseNOPURGEDocCreateDomainsDocDualHostDocOfficiallySupported"
+const _Capability_name = "CanAutoDNSSECCanGetZonesCanUseAKAMAICDNCanUseAliasCanUseAzureAliasCanUseCAACanUseDHCIDCanUseDSCanUseDSForChildrenCanUseLOCCanUseNAPTRCanUsePTRCanUseRoute53AliasCanUseSOACanUseSRVCanUseSSHFPCanUseTLSACantUseNOPURGEDocCreateDomainsDocDualHostDocOfficiallySupported"
 
-var _Capability_index = [...]uint8{0, 13, 24, 39, 50, 66, 75, 83, 102, 111, 122, 131, 149, 158, 167, 178, 188, 202, 218, 229, 251}
+var _Capability_index = [...]uint16{0, 13, 24, 39, 50, 66, 75, 86, 94, 113, 122, 133, 142, 160, 169, 178, 189, 199, 213, 229, 240, 262}
 
 func (i Capability) String() string {
 	if i >= Capability(len(_Capability_index)-1) {


Test patch:
diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go
index 66dadb8f..36689888 100644
--- a/integrationTest/integration_test.go
+++ b/integrationTest/integration_test.go
@@ -536,6 +536,10 @@ func cname(name, target string) *models.RecordConfig {
 	return makeRec(name, target, "CNAME")
 }
 
+func dhcid(name, target string) *models.RecordConfig {
+	return makeRec(name, target, "DHCID")
+}
+
 func ds(name string, keyTag uint16, algorithm, digestType uint8, digest string) *models.RecordConfig {
 	r := makeRec(name, "", "DS")
 	r.SetTargetDS(keyTag, algorithm, digestType, digest)
@@ -1554,6 +1558,11 @@ func makeTests(t *testing.T) []*TestGroup {
 			//	ns("another-child", "ns101.cloudns.net."),
 			//),
 		),
+		testgroup("PTR",
+			requires(providers.CanUsePTR),
+			tc("Create DHCPID record", dhcid("test", "AAIBY2/AuCccgoJbsaxcQc9TUapptP69lOjxfNuVAA2kjEA=")),
+			tc("Modify DHCPID record", dhcid("test", "Test/AuCccgoJbsaxcQc9TUapptP69lOjxfNuVAA2kjEA=")),
+		),
 
 		//// Vendor-specific record types
 
diff --git a/pkg/js/parse_tests/046-DHCID.js b/pkg/js/parse_tests/046-DHCID.js
new file mode 100644
index 00000000..8847c54f
--- /dev/null
+++ b/pkg/js/parse_tests/046-DHCID.js
@@ -0,0 +1,3 @@
+D("foo.com","none",
+    DHCID("@", "Test")
+);
diff --git a/pkg/js/parse_tests/046-DHCID.json b/pkg/js/parse_tests/046-DHCID.json
new file mode 100644
index 00000000..a6481828
--- /dev/null
+++ b/pkg/js/parse_tests/046-DHCID.json
@@ -0,0 +1,18 @@
+{
+  "registrars": [],
+  "dns_providers": [],
+  "domains": [
+    {
+      "name": "foo.com",
+      "registrar": "none",
+      "dnsProviders": {},
+      "records": [
+        {
+          "type": "DHCID",
+          "name": "@",
+          "target": "Test"
+        }
+      ]
+    }
+  ]
+}
diff --git a/pkg/prettyzone/prettyzone_test.go b/pkg/prettyzone/prettyzone_test.go
index 85e2f3c9..88f93454 100644
--- a/pkg/prettyzone/prettyzone_test.go
+++ b/pkg/prettyzone/prettyzone_test.go
@@ -272,6 +272,7 @@ func TestWriteZoneFileEach(t *testing.T) {
 	d = append(d, mustNewRR(`_443._tcp.bosun.org. 300 IN TLSA  3 1 1 abcdef0`)) // Label must be _port._proto
 	d = append(d, mustNewRR(`sub.bosun.org.       300 IN NS    bosun.org.`))    // Must be a label with no other records.
 	d = append(d, mustNewRR(`x.bosun.org.         300 IN CNAME bosun.org.`))    // Must be a label with no other records.
+	d = append(d, mustNewRR(`bosun.org.           300 IN DHCID   AAIBY2/AuCccgoJbsaxcQc9TUapptP69lOjxfNuVAA2kjEA=`))
 	buf := &bytes.Buffer{}
 	WriteZoneFileRR(buf, d, "bosun.org")
 	if buf.String() != testdataZFEach {
@@ -289,6 +290,7 @@ var testdataZFEach = `$TTL 300
                  IN SRV   10 10 9999 foo.com.
                  IN TXT   "my text"
                  IN CAA   0 issue "letsencrypt.org"
+                 IN DHCID AAIBY2/AuCccgoJbsaxcQc9TUapptP69lOjxfNuVAA2kjEA=
 4.5              IN PTR   y.bosun.org.
 _443._tcp        IN TLSA  3 1 1 abcdef0
 sub              IN NS    bosun.org.