diff --git a/commands/types/dnscontrol.d.ts b/commands/types/dnscontrol.d.ts index c92391f07..6e3047892 100644 --- a/commands/types/dnscontrol.d.ts +++ b/commands/types/dnscontrol.d.ts @@ -266,6 +266,38 @@ declare function ADGUARDHOME_A_PASSTHROUGH(source: string, destination: string): */ declare function AKAMAICDN(name: string, target: string, ...modifiers: RecordModifier[]): DomainModifier; +/** + * `AKAMAITLC` is a proprietary Top-Level CNAME (TLC) record type specific to Akamai Edge DNS. + * It allows CNAME-like functionality at the zone apex (`@`) of a domain where regular CNAME records + * are not permitted. + * + * The difference between `AKAMAITLC` and `CNAME` is that `AKAMAITLC` records are resolved by Akamai Edge DNS + * servers instead of the client's resolver. This is similar to how `AKAMAICDN` records work, except that `AKAMAITLC` + * records can be pointed to any domain, not just Akamai properties. If you are pointing to an Akamai property, + * you should use `AKAMAICDN` instead. + * + * Important restrictions: + * - Can only be used at the zone apex (`@`) + * - Limited to one `AKAMAITLC` record per zone + * - Cannot coexist with an `AKAMAICDN` record at the apex + * + * The `answer_type` parameter controls which record types are returned when clients resolve the target: + * - `DUAL`: Returns both IPv4 (`A`) and IPv6 (`AAAA`) records + * - `A`: Returns only IPv4 records + * - `AAAA`: Returns only IPv6 records + * + * ## Example + * ```javascript + * D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER), + * // Redirect example.com to google.com, returning both A and AAAA records + * AKAMAITLC("@", "DUAL", "google.com."), + * ); + * ``` + * + * @see https://docs.dnscontrol.org/language-reference/domain-modifiers/service-provider-specific/akamai-edge-dns/akamaitlc + */ +declare function AKAMAITLC(name: string, answer_type: "DUAL" | "A" | "AAAA", target: string, ...modifiers: RecordModifier[]): DomainModifier; + /** * ALIAS is a virtual record type that points a record at another record. It is analogous to a CNAME, but is usually resolved at request-time and served as an A record. Unlike CNAMEs, ALIAS records can be used at the zone apex (`@`) * diff --git a/documentation/SUMMARY.md b/documentation/SUMMARY.md index d3877753d..4fdc07d67 100644 --- a/documentation/SUMMARY.md +++ b/documentation/SUMMARY.md @@ -86,6 +86,7 @@ * [ADGUARDHOME_AAAA_PASSTHROUGH](language-reference/domain-modifiers/ADGUARDHOME_AAAA_PASSTHROUGH.md) * Akamai Edge Dns * [AKAMAICDN](language-reference/domain-modifiers/AKAMAICDN.md) + * [AKAMAITLC](language-reference/domain-modifiers/AKAMAITLC.md) * Amazon Route 53 * [R53_ALIAS](language-reference/domain-modifiers/R53_ALIAS.md) * Azure DNS diff --git a/documentation/language-reference/domain-modifiers/AKAMAITLC.md b/documentation/language-reference/domain-modifiers/AKAMAITLC.md new file mode 100644 index 000000000..83e6d9e83 --- /dev/null +++ b/documentation/language-reference/domain-modifiers/AKAMAITLC.md @@ -0,0 +1,43 @@ +--- +name: AKAMAITLC +parameters: + - name + - answer_type + - target + - modifiers... +provider: AKAMAIEDGEDNS +parameter_types: + name: string + answer_type: '"DUAL" | "A" | "AAAA"' + target: string + "modifiers...": RecordModifier[] +--- + +`AKAMAITLC` is a proprietary Top-Level CNAME (TLC) record type specific to Akamai Edge DNS. +It allows CNAME-like functionality at the zone apex (`@`) of a domain where regular CNAME records +are not permitted. + +The difference between `AKAMAITLC` and `CNAME` is that `AKAMAITLC` records are resolved by Akamai Edge DNS +servers instead of the client's resolver. This is similar to how `AKAMAICDN` records work, except that `AKAMAITLC` +records can be pointed to any domain, not just Akamai properties. If you are pointing to an Akamai property, +you should use `AKAMAICDN` instead. + +Important restrictions: +- Can only be used at the zone apex (`@`) +- Limited to one `AKAMAITLC` record per zone +- Cannot coexist with an `AKAMAICDN` record at the apex + +The `answer_type` parameter controls which record types are returned when clients resolve the target: +- `DUAL`: Returns both IPv4 (`A`) and IPv6 (`AAAA`) records +- `A`: Returns only IPv4 records +- `AAAA`: Returns only IPv6 records + +## Example +{% code title="dnsconfig.js" %} +```javascript +D("example.com", REG_MY_PROVIDER, DnsProvider(DSP_MY_PROVIDER), + // Redirect example.com to google.com, returning both A and AAAA records + AKAMAITLC("@", "DUAL", "google.com."), +); +``` +{% endcode %} diff --git a/documentation/provider/akamaiedgedns.md b/documentation/provider/akamaiedgedns.md index c611e04bf..97dd4536a 100644 --- a/documentation/provider/akamaiedgedns.md +++ b/documentation/provider/akamaiedgedns.md @@ -35,9 +35,35 @@ Example: ``` {% endcode %} +## Limitations + +### Records + +#### AKAMAICDN + +The AKAMAICDN target must be an Edge Hostname preconfigured in your Akamai account. + +The AKAMAICDN record must have a TTL of 20 seconds. + +The AKAMAICDN record may only be used at the zone apex (`@`) if an AKAMAITLC record hasn't been used. + +#### AKAMAITLC + +The AKAMAITLC record can only be used at the zone apex (`@`). + +The AKAMAITLC record can only be used once per zone. + +#### ALIAS +Akamai Edge DNS does directly support `ALIAS` records. This provider will convert `ALIAS` records used at the +zone apex (`@`) to `AKAMAITLC` records, and any other names to `CNAME` records. + +### Secondary zones + +This provider only supports creating primary zones in Akamai. If a secondary zone has been manually created, only `AKAMAICDN` and `AKAMAITLC` records can be managed, as all other records are read-only. + ## Usage -A new zone created by DNSControl: +A new primary zone created by DNSControl: ```shell dnscontrol create-domains @@ -70,11 +96,9 @@ var DSP_AKAMAIEDGEDNS = NewDnsProvider("akamaiedgedns"); D("example.com", REG_NONE, DnsProvider(DSP_AKAMAIEDGEDNS), NAMESERVER_TTL(86400), AUTODNSSEC_ON, - AKAMAICDN("@", "www.preconfigured.edgesuite.net", TTL(20)), + AKAMAICDN("@", "preconfigured.edgesuite.net", TTL(20)), + AKAMAICDN("www", "www.preconfigured.edgesuite.net", TTL(20)), A("foo", "1.2.3.4"), ); ``` {% endcode %} - -AKAMAICDN is a proprietary record type that is used to configure [Zone Apex Mapping](https://www.akamai.com/blog/security/edge-dns--zone-apex-mapping---dnssec). -The AKAMAICDN target must be preconfigured in the Akamai network. diff --git a/documentation/provider/index.md b/documentation/provider/index.md index b07c7ddd6..911144f12 100644 --- a/documentation/provider/index.md +++ b/documentation/provider/index.md @@ -147,7 +147,7 @@ Jump to a table: | Provider name | [`ALIAS`](../language-reference/domain-modifiers/ALIAS.md) | [`DNAME`](../language-reference/domain-modifiers/DNAME.md) | [`LOC`](../language-reference/domain-modifiers/LOC.md) | [`PTR`](../language-reference/domain-modifiers/PTR.md) | [`SOA`](../language-reference/domain-modifiers/SOA.md) | | ------------- | ---------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------ | | [`ADGUARDHOME`](adguardhome.md) | ✅ | ❔ | ❔ | ❔ | ❔ | -| [`AKAMAIEDGEDNS`](akamaiedgedns.md) | ❌ | ❔ | ✅ | ✅ | ❌ | +| [`AKAMAIEDGEDNS`](akamaiedgedns.md) | ✅ | ❔ | ✅ | ✅ | ❌ | | [`AUTODNS`](autodns.md) | ✅ | ❔ | ❔ | ✅ | ❔ | | [`AXFRDDNS`](axfrddns.md) | ❌ | ✅ | ✅ | ✅ | ❌ | | [`AZURE_DNS`](azure_dns.md) | ❌ | ❔ | ❌ | ✅ | ❔ | diff --git a/models/domain.go b/models/domain.go index 364cbc340..f978b8868 100644 --- a/models/domain.go +++ b/models/domain.go @@ -129,7 +129,7 @@ func (dc *DomainConfig) Punycode() error { // Set the target: switch rec.Type { // #rtype_variations - case "ALIAS", "MX", "NS", "CNAME", "DNAME", "PTR", "SRV", "URL", "URL301", "FRAME", "R53_ALIAS", "AKAMAICDN", "CLOUDNS_WR", "PORKBUN_URLFWD", "BUNNY_DNS_RDR": + case "ALIAS", "MX", "NS", "CNAME", "DNAME", "PTR", "SRV", "URL", "URL301", "FRAME", "R53_ALIAS", "AKAMAICDN", "AKAMAITLC", "CLOUDNS_WR", "PORKBUN_URLFWD", "BUNNY_DNS_RDR": // These rtypes are hostnames, therefore need to be converted (unlike, for example, an AAAA record) t, err := idna.ToASCII(rec.GetTargetField()) if err != nil { diff --git a/models/record.go b/models/record.go index afb12a519..467b37491 100644 --- a/models/record.go +++ b/models/record.go @@ -147,6 +147,7 @@ type RecordConfig struct { TlsaMatchingType uint8 `json:"tlsamatchingtype,omitempty"` R53Alias map[string]string `json:"r53_alias,omitempty"` AzureAlias map[string]string `json:"azure_alias,omitempty"` + AnswerType string `json:"answer_type,omitempty"` UnknownTypeName string `json:"unknown_type_name,omitempty"` // Cloudflare-specific fields: @@ -252,6 +253,7 @@ func (rc *RecordConfig) UnmarshalJSON(b []byte) error { TlsaMatchingType uint8 `json:"tlsamatchingtype,omitempty"` R53Alias map[string]string `json:"r53_alias,omitempty"` AzureAlias map[string]string `json:"azure_alias,omitempty"` + AnswerType string `json:"answer_type,omitempty"` UnknownTypeName string `json:"unknown_type_name,omitempty"` EnsureAbsent bool `json:"ensure_absent,omitempty"` // Override NO_PURGE and delete this record @@ -647,7 +649,7 @@ func Downcase(recs []*RecordConfig) { r.Name = strings.ToLower(r.Name) r.NameFQDN = strings.ToLower(r.NameFQDN) switch r.Type { // #rtype_variations - case "AKAMAICDN", "ALIAS", "AAAA", "ANAME", "CNAME", "DNAME", "DS", "DNSKEY", "MX", "NS", "NAPTR", "OPENPGPKEY", "SMIMEA", "PTR", "SRV", "TLSA", "AZURE_ALIAS": + case "AKAMAICDN", "AKAMAITLC", "ALIAS", "AAAA", "ANAME", "CNAME", "DNAME", "DS", "DNSKEY", "MX", "NS", "NAPTR", "OPENPGPKEY", "SMIMEA", "PTR", "SRV", "TLSA", "AZURE_ALIAS": // Target is case insensitive. Downcase it. r.target = strings.ToLower(r.target) // BUGFIX(tlim): isn't ALIAS in the wrong case statement? @@ -675,7 +677,7 @@ func CanonicalizeTargets(recs []*RecordConfig, origin string) { case "ALIAS", "ANAME", "CNAME", "DNAME", "DS", "DNSKEY", "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", "AKAMAICDN", "CAA", "DHCID", "CLOUDFLAREAPI_SINGLE_REDIRECT", "CF_REDIRECT", "CF_TEMP_REDIRECT", "CF_WORKER_ROUTE", "HTTPS", "IMPORT_TRANSFORM", "LOC", "OPENPGPKEY", "SMIMEA", "SSHFP", "SVCB", "TLSA", "TXT", "ADGUARDHOME_A_PASSTHROUGH", "ADGUARDHOME_AAAA_PASSTHROUGH": + case "A", "AKAMAICDN", "AKAMAITLC", "CAA", "DHCID", "CLOUDFLAREAPI_SINGLE_REDIRECT", "CF_REDIRECT", "CF_TEMP_REDIRECT", "CF_WORKER_ROUTE", "HTTPS", "IMPORT_TRANSFORM", "LOC", "OPENPGPKEY", "SMIMEA", "SSHFP", "SVCB", "TLSA", "TXT", "ADGUARDHOME_A_PASSTHROUGH", "ADGUARDHOME_AAAA_PASSTHROUGH": // Do nothing. case "SOA": if r.target != "DEFAULT_NOT_SET." { diff --git a/models/t_parse.go b/models/t_parse.go index dfc072987..9f8544f45 100644 --- a/models/t_parse.go +++ b/models/t_parse.go @@ -72,7 +72,7 @@ func (rc *RecordConfig) PopulateFromStringFunc(rtype, contents, origin string, t return fmt.Errorf("invalid IP in AAAA record: %s", contents) } return rc.SetTargetIP(ip) // Reformat to canonical form. - case "AKAMAICDN", "ALIAS", "ANAME", "CNAME", "NS", "PTR": + case "AKAMAICDN", "AKAMAITLC", "ALIAS", "ANAME", "CNAME", "NS", "PTR": return rc.SetTarget(contents) case "CAA": return rc.SetTargetCAAString(contents) @@ -181,7 +181,7 @@ func (rc *RecordConfig) PopulateFromString(rtype, contents, origin string) error return fmt.Errorf("invalid IP in AAAA record: %s", contents) } return rc.SetTargetIP(ip) // Reformat to canonical form. - case "AKAMAICDN", "ALIAS", "ANAME", "CNAME", "NS", "PTR": + case "AKAMAICDN", "AKAMAITLC", "ALIAS", "ANAME", "CNAME", "NS", "PTR": return rc.SetTarget(contents) case "CAA": return rc.SetTargetCAAString(contents) diff --git a/models/target.go b/models/target.go index 438cc8180..922329d4d 100644 --- a/models/target.go +++ b/models/target.go @@ -57,6 +57,8 @@ func (rc *RecordConfig) GetTargetCombined() string { case "AZURE_ALIAS": // Differentiate between multiple AZURE_ALIASs on the same label. return fmt.Sprintf("%s atype=%s", rc.target, rc.AzureAlias["type"]) + case "AKAMAITLC": + return fmt.Sprintf("%s %s", rc.AnswerType, rc.target) default: // Just return the target. return rc.target diff --git a/pkg/js/helpers.js b/pkg/js/helpers.js index 97896366f..7c00479bf 100644 --- a/pkg/js/helpers.js +++ b/pkg/js/helpers.js @@ -331,6 +331,20 @@ var AAAA = recordBuilder('AAAA'); // AKAMAICDN(name, target, recordModifiers...) var AKAMAICDN = recordBuilder('AKAMAICDN'); +// AKAMAITLC(name, answer_type, target, recordModifiers...) +var AKAMAITLC = recordBuilder('AKAMAITLC', { + args: [ + ['name', _.isString], + ['answer_type', function(value) { return _.isString(value) && ['DUAL', 'A', 'AAAA'].indexOf(value) !== -1; }], + ['target', _.isString], + ], + transform: function (record, args, modifier) { + record.name = args.name; + record.answer_type = args.answer_type; + record.target = args.target; + }, +}); + // ALIAS(name,target, recordModifiers...) var ALIAS = recordBuilder('ALIAS'); diff --git a/pkg/normalize/validate.go b/pkg/normalize/validate.go index 8112e2e7f..a787f4dfa 100644 --- a/pkg/normalize/validate.go +++ b/pkg/normalize/validate.go @@ -743,6 +743,7 @@ var providerCapabilityChecks = []pairTypeCapability{ // If a zone uses rType X, the provider must support capability Y. // {"X", providers.Y}, capabilityCheck("AKAMAICDN", providers.CanUseAKAMAICDN), + capabilityCheck("AKAMAITLC", providers.CanUseAKAMAITLC), capabilityCheck("ALIAS", providers.CanUseAlias), capabilityCheck("AUTODNSSEC", providers.CanAutoDNSSEC), capabilityCheck("AZURE_ALIAS", providers.CanUseAzureAlias), diff --git a/providers/akamaiedgedns/akamaiEdgeDnsProvider.go b/providers/akamaiedgedns/akamaiEdgeDnsProvider.go index 90447daaf..60c7f4aa5 100644 --- a/providers/akamaiedgedns/akamaiEdgeDnsProvider.go +++ b/providers/akamaiedgedns/akamaiEdgeDnsProvider.go @@ -28,7 +28,8 @@ var features = providers.DocumentationNotes{ providers.CanGetZones: providers.Can(), providers.CanOnlyDiff1Features: providers.Can(), providers.CanUseAKAMAICDN: providers.Can(), - providers.CanUseAlias: providers.Cannot(), + providers.CanUseAKAMAITLC: providers.Can(), + providers.CanUseAlias: providers.Can("Akamai Edge DNS does not directly support ALIAS. Apex record will be converted to AKAMAITLC, any others to CNAME."), providers.CanUseCAA: providers.Can(), providers.CanUseDS: providers.Cannot(), providers.CanUseDSForChildren: providers.Can(), @@ -58,6 +59,7 @@ func init() { } providers.RegisterDomainServiceProviderType(providerName, fns, features) providers.RegisterCustomRecordType("AKAMAICDN", providerName, "") + providers.RegisterCustomRecordType("AKAMAITLC", providerName, "") providers.RegisterMaintainer(providerName, providerMaintainer) } @@ -109,6 +111,11 @@ func (a *edgeDNSProvider) EnsureZoneExists(domain string, metadata map[string]st // GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records. func (a *edgeDNSProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) { + + if err := a.preprocessConfig(dc); err != nil { + return nil, 0, err + } + keysToUpdate, toReport, actualChangeCount, err := diff.NewCompat(dc).ChangedGroups(existingRecords) if err != nil { return nil, 0, err @@ -235,3 +242,19 @@ func (a *edgeDNSProvider) ListZones() ([]string, error) { } return zones, nil } + +func (a *edgeDNSProvider) preprocessConfig(dc *models.DomainConfig) error { + for _, rec := range dc.Records { + // Convert ALIAS records to the Akamai equivalents. AKAMAITLC is only valid + // at the apex, so any other ALIAS must be converted to CNAME. + if rec.Type == "ALIAS" { + if rec.Name == "@" { + rec.ChangeType("AKAMAITLC", dc.Name) + rec.AnswerType = "DUAL" + } else { + rec.ChangeType("CNAME", dc.Name) + } + } + } + return nil +} diff --git a/providers/akamaiedgedns/akamaiEdgeDnsService.go b/providers/akamaiedgedns/akamaiEdgeDnsService.go index 377dea5fd..bf996d2a5 100644 --- a/providers/akamaiedgedns/akamaiEdgeDnsService.go +++ b/providers/akamaiedgedns/akamaiEdgeDnsService.go @@ -11,11 +11,11 @@ https://github.com/akamai/AkamaiOPEN-edgegrid-golang import ( "errors" "fmt" - "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/printer" dnsv2 "github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v2" "github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid" + "strings" ) // initialize initializes the "Akamai OPEN EdgeGrid" library @@ -293,6 +293,14 @@ func getRecords(zonename string) ([]*models.RecordConfig, error) { TTL: uint32(akattl), } rc.SetLabelFromFQDN(akaname, zonename) + if akatype == "AKAMAITLC" { + part := strings.Fields(r) + if len(part) != 2 { + return nil, fmt.Errorf("AKAMAITLC value does not contain 2 fields: (%#v)", r) + } + rc.AnswerType = part[0] + r = part[1] + } err = rc.PopulateFromString(akatype, r, zonename) if err != nil { return nil, err diff --git a/providers/capabilities.go b/providers/capabilities.go index 7f2908bc0..299762dff 100644 --- a/providers/capabilities.go +++ b/providers/capabilities.go @@ -111,6 +111,9 @@ const ( // DocOfficiallySupported means it is actively used and maintained by stack exchange DocOfficiallySupported + + // CanUseAKAMAITLC indicates the provider supports the specific AKAMAITLC records that only the Akamai EdgeDns provider supports + CanUseAKAMAITLC ) var providerCapabilities = map[string]map[Capability]bool{} diff --git a/providers/capability_string.go b/providers/capability_string.go index 2a50cbfb4..a51692f7c 100644 --- a/providers/capability_string.go +++ b/providers/capability_string.go @@ -36,11 +36,12 @@ func _() { _ = x[DocCreateDomains-25] _ = x[DocDualHost-26] _ = x[DocOfficiallySupported-27] + _ = x[CanUseAKAMAITLC-28] } -const _Capability_name = "CanAutoDNSSECCanConcurCanGetZonesCanOnlyDiff1FeaturesCanUseAKAMAICDNCanUseAliasCanUseAzureAliasCanUseCAACanUseDHCIDCanUseDNAMECanUseDSCanUseDSForChildrenCanUseHTTPSCanUseLOCCanUseNAPTRCanUsePTRCanUseRoute53AliasCanUseSMIMEACanUseSOACanUseSRVCanUseSSHFPCanUseSVCBCanUseTLSACanUseDNSKEYCanUseOPENPGPKEYDocCreateDomainsDocDualHostDocOfficiallySupported" +const _Capability_name = "CanAutoDNSSECCanConcurCanGetZonesCanOnlyDiff1FeaturesCanUseAKAMAICDNCanUseAliasCanUseAzureAliasCanUseCAACanUseDHCIDCanUseDNAMECanUseDSCanUseDSForChildrenCanUseHTTPSCanUseLOCCanUseNAPTRCanUsePTRCanUseRoute53AliasCanUseSMIMEACanUseSOACanUseSRVCanUseSSHFPCanUseSVCBCanUseTLSACanUseDNSKEYCanUseOPENPGPKEYDocCreateDomainsDocDualHostDocOfficiallySupportedCanUseAKAMAITLC" -var _Capability_index = [...]uint16{0, 13, 22, 33, 53, 68, 79, 95, 104, 115, 126, 134, 153, 164, 173, 184, 193, 211, 223, 232, 241, 252, 262, 272, 284, 300, 316, 327, 349} +var _Capability_index = [...]uint16{0, 13, 22, 33, 53, 68, 79, 95, 104, 115, 126, 134, 153, 164, 173, 184, 193, 211, 223, 232, 241, 252, 262, 272, 284, 300, 316, 327, 349, 364} func (i Capability) String() string { idx := int(i) - 0