diff --git a/docs/_functions/domain/ALIAS.md b/docs/_functions/domain/ALIAS.md new file mode 100644 index 0000000000..0776d0a2bf --- /dev/null +++ b/docs/_functions/domain/ALIAS.md @@ -0,0 +1,25 @@ +--- +name: ALIAS +parameters: + - name + - target + - modifiers... +--- + +ALIAS is a virtual record type that points a record at another record. It is analagous 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 (`@`) + +Different providers handle ALIAS records differently, and many do not support it at all. Attempting to use ALIAS records with a DNS provider type that does not support them will result in an error. + +The name should be the relative label for the domain. + +Target should be a string representing the target. If it is a single label we will assume it is a relative name on the current domain. If it contains *any* dots, it should be a fully qualified domain name, ending with a `.`. + +{% include startExample.html %} +{% highlight js %} + +D("example.com", REGISTRAR, DnsProvider("CLOUDFLARE"), + ALIAS("@", "google.com."), // example.com -> google.com +); + +{%endhighlight%} +{% include endExample.html %} \ No newline at end of file diff --git a/docs/alias.md b/docs/alias.md new file mode 100644 index 0000000000..a26b935aea --- /dev/null +++ b/docs/alias.md @@ -0,0 +1,25 @@ +--- +layout: default +--- + +# ALIAS records + +ALIAS records are not widely standardized across DNS providers. Some (Route 53, DNSimple) have a native ALIAS record type. Others (Cloudflare) implement transparent CNAME flattening. + +DNSControl adds an ALIAS record type, and leaves it up to the provider implementation to handle it. + +A few notes: + +1. A provider must "opt-in" to supporting ALIAS records. When registering a provider, you specify which capabilities you support. Here is an example of how the + cloudflare provider declares its support for aliases: + +``` +func init() { + providers.RegisterDomainServiceProviderType("CLOUDFLAREAPI", newCloudflare, providers.CanUseAlias) +} +``` + +2. If you try to use ALIAS records, **all** dns providers for the domain must support ALIAS records. We do not want to serve inconsistent records across providers. +3. CNAMEs at `@` are disallowed, but ALIAS is allowed. +4. Cloudflare does not have a native ALIAS type, but CNAMEs behave similarly. The Cloudflare provider "rewrites" ALIAS records to CNAME as it sees them. Other providers may not need this step. + diff --git a/docs/index.md b/docs/index.md index 90e8640201..2142252856 100644 --- a/docs/index.md +++ b/docs/index.md @@ -23,3 +23,4 @@ Dnscontrol is a platform for seamlessly managing your dns configuration across a - [Why CNAME/MX/NS targets require a trailing "dot"]({{site.github.url}}/why-the-dot) - [Writing Providers]({{site.github.url}}/writing-providers) +- [ALIAS records in dnscontrol]({{site.github.url}}/alias) diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index f90539f3c9..6428abf942 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -97,6 +97,10 @@ func runTests(t *testing.T, prv providers.DNSServiceProvider, domainName string, break } t.Run(fmt.Sprintf("%d: %s", i, tst.Desc), func(t *testing.T) { + if tst.SkipUnless != 0 && !providers.ProviderHasCabability(*providerToRun, tst.SkipUnless) { + t.Log("Skipping because provider does not support test features") + return + } skipVal := false if knownFailures[i] { t.Log("SKIPPING VALIDATION FOR KNOWN FAILURE CASE") @@ -186,8 +190,9 @@ func TestDualProviders(t *testing.T) { } type TestCase struct { - Desc string - Records []*rec + Desc string + Records []*rec + SkipUnless providers.Capability } type rec models.RecordConfig @@ -200,6 +205,10 @@ func cname(name, target string) *rec { return makeRec(name, target, "CNAME") } +func alias(name, target string) *rec { + return makeRec(name, target, "ALIAS") +} + func ns(name, target string) *rec { return makeRec(name, target, "NS") } @@ -231,6 +240,11 @@ func tc(desc string, recs ...*rec) *TestCase { } } +func (tc *TestCase) IfHasCapability(c providers.Capability) *TestCase { + tc.SkipUnless = c + return tc +} + //ALWAYS ADD TO BOTTOM OF LIST. Order and indexes matter. var tests = []*TestCase{ // A @@ -274,5 +288,12 @@ var tests = []*TestCase{ tc("Change to other name", mx("@", 5, "foo2.com."), mx("mail", 15, "foo3.com.")), tc("Change Priority", mx("@", 7, "foo2.com."), mx("mail", 15, "foo3.com.")), - tc("IDN pre-punycoded", cname("xn--o-0gab", "xn--o-0gab.xn--o-0gab.")), + //ALIAS + tc("EMPTY"), + tc("ALIAS at root", alias("@", "foo.com.")).IfHasCapability(providers.CanUseAlias), + tc("change it", alias("@", "foo2.com.")).IfHasCapability(providers.CanUseAlias), + tc("ALIAS at subdomain", alias("test", "foo.com.")).IfHasCapability(providers.CanUseAlias), + + //TODO: in validation, check that everything is given in unicode. This case hurts too much. + //tc("IDN pre-punycoded", cname("xn--o-0gab", "xn--o-0gab.xn--o-0gab.")), } diff --git a/integrationTest/providers.json b/integrationTest/providers.json index 448ea5938c..d55008b0dd 100644 --- a/integrationTest/providers.json +++ b/integrationTest/providers.json @@ -1,7 +1,17 @@ { + "ACTIVEDIRECTORY_PS": { + "knownFailures": "17,18,19,25,26,27,28,29,30", + "domain": "$AD_DOMAIN", + "ADServer": "$AD_SERVER" + }, "BIND": { "domain": "example.com" }, + "CLOUDFLAREAPI":{ + "domain": "$CF_DOMAIN", + "apiuser": "$CF_USER", + "apikey": "$CF_KEY" + }, "DNSIMPLE": { //16/17: no ns records managable. Not even for subdomains. "knownFailures": "17,18", @@ -9,7 +19,7 @@ "token": "$DNSIMPLE_TOKEN", "baseurl": "https://api.sandbox.dnsimple.com" }, - "GANDI":{ + "GANDI": { //5: gandi does not accept ttls less than 300 "knownFailures": "5", "domain": "$GANDI_DOMAIN", @@ -25,10 +35,5 @@ "domain": "$R53_DOMAIN", "KeyId": "$R53_KEY_ID", "SecretKey": "$R53_KEY" - }, - "ACTIVEDIRECTORY_PS":{ - "knownFailures": "17,18,19,25,26,27,28,29,30", - "domain": "$AD_DOMAIN", - "ADServer": "$AD_SERVER" } } \ No newline at end of file diff --git a/integrationTest/zones/example.com.zone b/integrationTest/zones/example.com.zone index 48ae293bad..0837d01d52 100644 --- a/integrationTest/zones/example.com.zone +++ b/integrationTest/zones/example.com.zone @@ -1,4 +1,2 @@ $TTL 300 -@ IN SOA DEFAULT_NOT_SET. DEFAULT_NOT_SET. 2017032394 3600 600 604800 1440 - IN MX 7 foo2.com. -mail IN MX 15 foo3.com. +@ IN SOA DEFAULT_NOT_SET. DEFAULT_NOT_SET. 2017041717 3600 600 604800 1440 diff --git a/js/helpers.js b/js/helpers.js index f17b6c4886..a87711fbbf 100644 --- a/js/helpers.js +++ b/js/helpers.js @@ -123,6 +123,15 @@ function AAAA(name, ip) { } } +// ALIAS(name,target, recordModifiers...) +function ALIAS(name, target) { + var mods = getModifiers(arguments,2) + return function(d) { + addRecord(d,"ALIAS",name,target,mods) + } +} + + // CNAME(name,target, recordModifiers...) function CNAME(name, target) { var mods = getModifiers(arguments,2) diff --git a/js/js_test.go b/js/js_test.go index 61a90f7be1..f6241520da 100644 --- a/js/js_test.go +++ b/js/js_test.go @@ -30,39 +30,40 @@ func TestParsedFiles(t *testing.T) { if filepath.Ext(f.Name()) != ".js" || !unicode.IsNumber(rune(f.Name()[0])) { continue } - t.Log(f.Name(), "------") - content, err := ioutil.ReadFile(filepath.Join(testDir, f.Name())) - if err != nil { - t.Fatal(err) - } - conf, err := ExecuteJavascript(string(content), true) - if err != nil { - t.Fatal(err) - } - actualJson, err := json.MarshalIndent(conf, "", " ") - if err != nil { - t.Fatal(err) - } - expectedFile := filepath.Join(testDir, f.Name()[:len(f.Name())-3]+".json") - expectedData, err := ioutil.ReadFile(expectedFile) - if err != nil { - t.Fatal(err) - } - conf = &models.DNSConfig{} - err = json.Unmarshal(expectedData, conf) - if err != nil { - t.Fatal(err) - } - expectedJson, err := json.MarshalIndent(conf, "", " ") - if err != nil { - t.Fatal(err) - } - if string(expectedJson) != string(actualJson) { - t.Error("Expected and actual json don't match") - t.Log("Expected:", string(expectedJson)) - t.Log("Actual:", string(actualJson)) - t.FailNow() - } + t.Run(f.Name(), func(t *testing.T) { + content, err := ioutil.ReadFile(filepath.Join(testDir, f.Name())) + if err != nil { + t.Fatal(err) + } + conf, err := ExecuteJavascript(string(content), true) + if err != nil { + t.Fatal(err) + } + actualJSON, err := json.MarshalIndent(conf, "", " ") + if err != nil { + t.Fatal(err) + } + expectedFile := filepath.Join(testDir, f.Name()[:len(f.Name())-3]+".json") + expectedData, err := ioutil.ReadFile(expectedFile) + if err != nil { + t.Fatal(err) + } + conf = &models.DNSConfig{} + //unmarshal and remarshal to not require manual formatting + err = json.Unmarshal(expectedData, conf) + if err != nil { + t.Fatal(err) + } + expectedJSON, err := json.MarshalIndent(conf, "", " ") + if err != nil { + t.Fatal(err) + } + if string(expectedJSON) != string(actualJSON) { + t.Error("Expected and actual json don't match") + t.Log("Expected:", string(expectedJSON)) + t.Log("Actual:", string(actualJSON)) + } + }) } } diff --git a/js/parse_tests/010-alias.js b/js/parse_tests/010-alias.js new file mode 100644 index 0000000000..e9a27ab4fd --- /dev/null +++ b/js/parse_tests/010-alias.js @@ -0,0 +1,3 @@ +D("foo.com","none", + ALIAS("@","foo.com.") +); \ No newline at end of file diff --git a/js/parse_tests/010-alias.json b/js/parse_tests/010-alias.json new file mode 100644 index 0000000000..6b76fa2977 --- /dev/null +++ b/js/parse_tests/010-alias.json @@ -0,0 +1,19 @@ +{ + "registrars": [], + "dns_providers": [], + "domains": [ + { + "name": "foo.com", + "registrar": "none", + "dnsProviders": {}, + "records": [ + { + "type": "ALIAS", + "name": "@", + "target": "foo.com." + } + ], + "keepunknown": false + } + ] + } \ No newline at end of file diff --git a/js/static.go b/js/static.go index 53e7961e7f..e3ca35c6d9 100644 --- a/js/static.go +++ b/js/static.go @@ -190,48 +190,48 @@ var _escData = map[string]*_escFile{ "/helpers.js": { local: "js/helpers.js", - size: 7563, + size: 7758, modtime: 0, compressed: ` -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 `, }, diff --git a/normalize/validate.go b/normalize/validate.go index 81f7aa76e4..e6c675dcf2 100644 --- a/normalize/validate.go +++ b/normalize/validate.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/StackExchange/dnscontrol/models" + "github.com/StackExchange/dnscontrol/providers" "github.com/StackExchange/dnscontrol/transform" "github.com/miekg/dns" "github.com/miekg/dns/dnsutil" @@ -52,6 +53,7 @@ func validateRecordTypes(rec *models.RecordConfig, domain string) error { "MX": true, "TXT": true, "NS": true, + "ALIAS": false, } if _, ok := validTypes[rec.Type]; !ok { @@ -112,6 +114,9 @@ func checkTargets(rec *models.RecordConfig, domain string) (errs []error) { check(checkIPv6(target)) case "CNAME": check(checkTarget(target)) + if label == "@" { + check(fmt.Errorf("cannot create CNAME record for bare domain")) + } case "MX": check(checkTarget(target)) case "NS": @@ -119,6 +124,8 @@ func checkTargets(rec *models.RecordConfig, domain string) (errs []error) { if label == "@" { check(fmt.Errorf("cannot create NS record for bare domain. Use NAMESERVER instead")) } + case "ALIAS": + check(checkTarget(target)) case "TXT", "IMPORT_TRANSFORM": default: errs = append(errs, fmt.Errorf("Unimplemented record type (%v) domain=%v name=%v", @@ -260,9 +267,20 @@ func NormalizeAndValidateConfig(config *models.DNSConfig) (errs []error) { errs = append(errs, err) } } + + //Check that CNAMES don't have to co-exist with any other records for _, d := range config.Domains { errs = append(errs, checkCNAMEs(d)...) } + + //Check that if any aliases are used in a domain, every provider for that domain supports them + for _, d := range config.Domains { + err := checkALIASes(d, config.DNSProviders) + if err != nil { + errs = append(errs, nil) + } + } + return errs } @@ -284,6 +302,30 @@ func checkCNAMEs(dc *models.DomainConfig) (errs []error) { return } +func checkALIASes(dc *models.DomainConfig, pList []*models.DNSProviderConfig) error { + hasAlias := false + for _, r := range dc.Records { + if r.Type == "ALIAS" { + hasAlias = true + break + } + } + if !hasAlias { + return nil + } + for pName := range dc.DNSProviders { + for _, p := range pList { + if p.Name == pName { + if !providers.ProviderHasCabability(p.Type, providers.CanUseAlias) { + return fmt.Errorf("Domain %s uses ALIAS records, but DNS provider type %s does not support them", dc.Name, p.Type) + } + break + } + } + } + return nil +} + func applyRecordTransforms(domain *models.DomainConfig) error { for _, rec := range domain.Records { if rec.Type != "A" { diff --git a/providers/cloudflare/cloudflareProvider.go b/providers/cloudflare/cloudflareProvider.go index 9fcb3178f2..4969bc0729 100644 --- a/providers/cloudflare/cloudflareProvider.go +++ b/providers/cloudflare/cloudflareProvider.go @@ -81,7 +81,6 @@ func (c *CloudflareApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models if err != nil { return nil, err } - //for _, rec := range records { for i := len(records) - 1; i >= 0; i-- { rec := records[i] // Delete ignore labels @@ -91,9 +90,11 @@ func (c *CloudflareApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models } } for _, rec := range dc.Records { + if rec.Type == "ALIAS" { + rec.Type = "CNAME" + } if labelMatches(rec.Name, c.ignoredLabels) { log.Fatalf("FATAL: dnsconfig contains label that matches ignored_labels: %#v is in %v)\n", rec.Name, c.ignoredLabels) - // Since we log.Fatalf, we don't need to be clean here. } } checkNSModifications(dc) @@ -166,9 +167,15 @@ func (c *CloudflareApi) preprocessConfig(dc *models.DomainConfig) error { // A and CNAMEs: Validate. If null, set to default. // else: Make sure it wasn't set. Set to default. for _, rec := range dc.Records { + if rec.Metadata == nil { + rec.Metadata = map[string]string{} + } if rec.TTL == 0 || rec.TTL == 300 { rec.TTL = 1 } + if rec.TTL != 1 && rec.TTL < 120 { + rec.TTL = 120 + } if rec.Type != "A" && rec.Type != "CNAME" && rec.Type != "AAAA" { if rec.Metadata[metaProxy] != "" { return fmt.Errorf("cloudflare_proxy set on %v record: %#v cloudflare_proxy=%#v", rec.Type, rec.Name, rec.Metadata[metaProxy]) @@ -243,7 +250,7 @@ func newCloudflare(m map[string]string, metadata json.RawMessage) (providers.DNS } func init() { - providers.RegisterDomainServiceProviderType("CLOUDFLAREAPI", newCloudflare) + providers.RegisterDomainServiceProviderType("CLOUDFLAREAPI", newCloudflare, providers.CanUseAlias) } // Used on the "existing" records. diff --git a/providers/providers.go b/providers/providers.go index 201e5ba953..36e79066d5 100644 --- a/providers/providers.go +++ b/providers/providers.go @@ -34,6 +34,21 @@ var registrarTypes = map[string]RegistrarInitializer{} type DspInitializer func(map[string]string, json.RawMessage) (DNSServiceProvider, error) var dspTypes = map[string]DspInitializer{} +var dspCapabilities = map[string]Capability{} + +//Capability is a bitmasked set of "features" that a provider supports. Only use constants from this package. +type Capability uint32 + +const ( + // CanUseAlias indicates the provider support ALIAS records (or flattened CNAMES). Up to the provider to translate them to the appropriate record type. + CanUseAlias Capability = 1 << iota + // CanUsePTR indicates the provider can handle PTR records + CanUsePTR +) + +func ProviderHasCabability(pType string, cap Capability) bool { + return dspCapabilities[pType]&cap != 0 +} //RegisterRegistrarType adds a registrar type to the registry by providing a suitable initialization function. func RegisterRegistrarType(name string, init RegistrarInitializer) { @@ -44,11 +59,16 @@ func RegisterRegistrarType(name string, init RegistrarInitializer) { } //RegisterDomainServiceProviderType adds a dsp to the registry with the given initialization function. -func RegisterDomainServiceProviderType(name string, init DspInitializer) { +func RegisterDomainServiceProviderType(name string, init DspInitializer, caps ...Capability) { if _, ok := dspTypes[name]; ok { log.Fatalf("Cannot register registrar type %s multiple times", name) } + var abilities Capability + for _, c := range caps { + abilities |= c + } dspTypes[name] = init + dspCapabilities[name] = abilities } func createRegistrar(rType string, config map[string]string) (Registrar, error) {