From 48c99f7065fac6689487d552ce871adfe79058fe Mon Sep 17 00:00:00 2001 From: Eli Heady Date: Tue, 25 Feb 2025 12:27:24 -0500 Subject: [PATCH] Improve tagged domain handling in support of Split Horizon feature (#3444) Co-authored-by: Tom Limoncelli --- commands/commands.go | 28 ++- commands/commands_test.go | 64 +++++++ commands/ppreviewPush.go | 6 +- commands/ppreviewPush_test.go | 180 ++++++++++++++++++ commands/types/dnscontrol.d.ts | 9 +- .../top-level-functions/D.md | 9 +- documentation/preview-push.md | 17 +- models/domain.go | 4 + models/domain_test.go | 68 +++++++ pkg/js/helpers.js | 30 ++- pkg/js/js_test.go | 14 +- pkg/js/parse_tests/037-splithor.js | 34 ++++ pkg/js/parse_tests/037-splithor.json | 137 +++++++++++++ .../037-splithor/empty.example.net.zone | 3 + .../037-splithor/example-b.net.zone | 3 + .../037-splithor/example.com!inside.zone | 3 + .../037-splithor/example.com!outside.zone | 2 + .../parse_tests/037-splithor/example.com.zone | 3 + .../037-splithor/example.net!inside.zone | 4 + .../037-splithor/example.net!outside.zone | 4 + .../parse_tests/037-splithor/example.net.zone | 3 + pkg/normalize/validate.go | 2 + 22 files changed, 607 insertions(+), 20 deletions(-) create mode 100644 commands/ppreviewPush_test.go create mode 100644 models/domain_test.go create mode 100644 pkg/js/parse_tests/037-splithor/empty.example.net.zone create mode 100644 pkg/js/parse_tests/037-splithor/example-b.net.zone create mode 100644 pkg/js/parse_tests/037-splithor/example.com!inside.zone create mode 100644 pkg/js/parse_tests/037-splithor/example.com!outside.zone create mode 100644 pkg/js/parse_tests/037-splithor/example.com.zone create mode 100644 pkg/js/parse_tests/037-splithor/example.net!inside.zone create mode 100644 pkg/js/parse_tests/037-splithor/example.net!outside.zone create mode 100644 pkg/js/parse_tests/037-splithor/example.net.zone diff --git a/commands/commands.go b/commands/commands.go index fdf6cc00d..ad1b75185 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -304,13 +304,37 @@ func (args *FilterArgs) flags() []cli.Flag { } } +// domainInList takes a domain and a list of domains and returns true if the +// domain is in the list, accounting for wildcards and tags. func domainInList(domain string, list []string) bool { for _, item := range list { + if item == domain { + return true + } if strings.HasPrefix(item, "*") && strings.HasSuffix(domain, item[1:]) { return true } - if item == domain { - return true + filterDom, filterTag, isFilterTagged := strings.Cut(item, "!") + splitDom, domainTag, isDomainTagged := strings.Cut(domain, "!") + if splitDom == filterDom { + if isDomainTagged { + if filterTag == "*" { + return true + } + if domainTag == "" && !isFilterTagged { + // domain example.com! == filter example.com + return true + } + if isFilterTagged && domainTag == filterTag { + return true + } + } + if isFilterTagged { + if filterTag == "" && !isDomainTagged { + // filter example.com! == domain example.com + return true + } + } } } return false diff --git a/commands/commands_test.go b/commands/commands_test.go index 046889734..73f974bbd 100644 --- a/commands/commands_test.go +++ b/commands/commands_test.go @@ -52,6 +52,70 @@ func Test_domainInList(t *testing.T) { }, want: false, }, + { + name: "tagged", + args: args{ + domain: "foo.com!bar", + list: []string{"foo.com"}, + }, + want: false, + }, + { + name: "taggedWildcard", + args: args{ + domain: "foo.com!bar", + list: []string{"foo.com!*"}, + }, + want: true, + }, + { + name: "taggedWildcardMatchesEmpty", + args: args{ + domain: "foo.com!", + list: []string{"foo.com!*"}, + }, + want: true, + }, + { + name: "taggedWildcardNotMatchUntagged", + args: args{ + domain: "foo.com", + list: []string{"foo.com!*"}, + }, + want: false, + }, + { + name: "taggedEmtpy", + args: args{ + domain: "foo.com", + list: []string{"foo.com!"}, + }, + want: true, + }, + { + name: "domainTaggedEmtpy", + args: args{ + domain: "foo.com!", + list: []string{"foo.com"}, + }, + want: true, + }, + { + name: "filterTaggedNoMatch", + args: args{ + domain: "foo.com", + list: []string{"foo.com!foo"}, + }, + want: false, + }, + { + name: "domainTaggedNoMatch", + args: args{ + domain: "foo.com!foo", + list: []string{"foo.com"}, + }, + want: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/commands/ppreviewPush.go b/commands/ppreviewPush.go index c015cacd6..cf546a30b 100644 --- a/commands/ppreviewPush.go +++ b/commands/ppreviewPush.go @@ -346,6 +346,10 @@ func prun(args PPreviewArgs, push bool, interactive bool, out printer.CLI, repor // return r //} +// whichZonesToProcess takes a list of DomainConfigs and a filter string and +// returns a list of DomainConfigs whose metadata[DomainUniqueName] matched the +// filter. The filter string is a comma-separated list of domain names. If the +// filter string is empty or "all", all domains are returned. func whichZonesToProcess(domains []*models.DomainConfig, filter string) []*models.DomainConfig { if filter == "" || filter == "all" { return domains @@ -354,7 +358,7 @@ func whichZonesToProcess(domains []*models.DomainConfig, filter string) []*model permitList := strings.Split(filter, ",") var picked []*models.DomainConfig for _, domain := range domains { - if domainInList(domain.Name, permitList) { + if domainInList(domain.GetUniqueName(), permitList) { picked = append(picked, domain) } } diff --git a/commands/ppreviewPush_test.go b/commands/ppreviewPush_test.go new file mode 100644 index 000000000..735749f9b --- /dev/null +++ b/commands/ppreviewPush_test.go @@ -0,0 +1,180 @@ +package commands + +import ( + "testing" + + "github.com/StackExchange/dnscontrol/v4/models" +) + +func Test_whichZonesToProcess(t *testing.T) { + + dcNoTag := &models.DomainConfig{Name: "example.com"} + dcNoTag2 := &models.DomainConfig{Name: "example.net"} + dcTaggedEmpty := &models.DomainConfig{Name: "example.com!"} + dcTaggedGeorge := &models.DomainConfig{Name: "example.com!george"} + dcTaggedJohn := &models.DomainConfig{Name: "example.com!john"} + + allDC := []*models.DomainConfig{ + dcNoTag, + dcNoTag2, + dcTaggedGeorge, + dcTaggedJohn, + dcTaggedEmpty, + } + + for _, dc := range allDC { + dc.UpdateSplitHorizonNames() + } + + type args struct { + dc []*models.DomainConfig + filter string + } + + tests := []struct { + name string + why string + args args + want []*models.DomainConfig + }{ + { + name: "testAllFilter", + why: "Should return all domain configs", + args: args{ + dc: allDC, + filter: "all", + }, + want: allDC, + }, + { + name: "testNoFilter", + why: "Should return all domain configs", + args: args{ + dc: allDC, + filter: "", + }, + want: allDC, + }, + { + name: "testFilterTagged", + why: "Should return one tagged domain", + args: args{ + dc: allDC, + filter: "example.com!george", + }, + want: []*models.DomainConfig{dcTaggedGeorge}, + }, + { + name: "testMultiFilterTagged", + why: "Should return two tagged domains", + args: args{ + dc: allDC, + filter: "example.com!george,example.com!john", + }, + want: []*models.DomainConfig{dcTaggedGeorge, dcTaggedJohn}, + }, + { + name: "testMultiFilterTaggedNoMatch", + why: "Should return nothing", + args: args{ + dc: allDC, + filter: "example.com!ringo", + }, + want: []*models.DomainConfig{}, + }, + { + name: "testMultiFilterTaggedWildcard", + why: "Should return all matching tagged domains", + args: args{ + dc: allDC, + filter: "example.com!*", + }, + want: []*models.DomainConfig{dcTaggedGeorge, dcTaggedJohn}, + }, + { + name: "testFilterNoTag", + why: "Should return untagged and empty tagged domain", + args: args{ + dc: allDC, + filter: "example.com", + }, + want: []*models.DomainConfig{dcNoTag, dcTaggedEmpty}, + }, + { + name: "testFilterEmptyTag", + why: "Should return untagged and empty tagged domain", + args: args{ + dc: allDC, + filter: "example.com!", + }, + want: []*models.DomainConfig{dcNoTag, dcTaggedEmpty}, + }, + { + name: "testFilterEmptyTagAndNoTag", + why: "Should return untagged and empty tagged domain", + args: args{ + dc: allDC, + filter: "example.com!,example.com", + }, + want: []*models.DomainConfig{dcNoTag, dcTaggedEmpty}, + }, + { + name: "testFilterNoTagTagged", + why: "Should return the tagged and untagged domains", + args: args{ + dc: allDC, + filter: "example.com!george,example.com", + }, + want: []*models.DomainConfig{dcTaggedGeorge, dcNoTag, dcTaggedEmpty}, + }, + { + name: "testFilterDuplicates2", + why: "Should return one untagged domain", + args: args{ + dc: allDC, + filter: "example.net,example.net", + }, + want: []*models.DomainConfig{dcNoTag2}, + }, + { + name: "testFilterNoTagNoMatch", + why: "Should return nothing", + args: args{ + dc: []*models.DomainConfig{dcTaggedGeorge, dcTaggedJohn}, + filter: "example.com", + }, + want: []*models.DomainConfig{}, + }, + { + name: "testFilterTaggedNoMatch", + why: "Should return nothing", + args: args{ + dc: []*models.DomainConfig{dcNoTag}, + filter: "example.com!george", + }, + want: []*models.DomainConfig{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := whichZonesToProcess(tt.args.dc, tt.args.filter) + if len(got) != len(tt.want) { + t.Errorf("whichZonesToProcess() %s: %s", tt.name, tt.why) + for i := range got { + t.Errorf("got[%d]: %s", i, got[i].GetUniqueName()) + } + for i := range tt.want { + t.Errorf("want[%d]: %s", i, tt.want[i].GetUniqueName()) + } + return + } + for i := range got { + if got[i].Name != tt.want[i].Name { + t.Errorf("whichZonesToProcess() %s: %s", tt.name, tt.why) + return + } + } + }) + } +} diff --git a/commands/types/dnscontrol.d.ts b/commands/types/dnscontrol.d.ts index 90926fb32..9ef2305f7 100644 --- a/commands/types/dnscontrol.d.ts +++ b/commands/types/dnscontrol.d.ts @@ -690,11 +690,12 @@ declare function CNAME(name: string, target: string, ...modifiers: RecordModifie * six months? You get the idea. * * DNSControl command line flag `--domains` matches the full name (with the "!"). If you - * define domains `example.com!george` and `example.com!john` then: + * define domains `example.com!john`, `example.com!paul`, and `example.com!george` then: * - * * `--domains=example.com` will not match either domain. - * * `--domains='example.com!george'` will match only match the first. - * * `--domains='example.com!george,example.com!john'` will match both. + * * `--domains=example.com` will not match any of the three. + * * `--domains='example.com!george'` will only match george. + * * `--domains='example.com!george,example.com!john'` will match george and john. + * * `--domains='example.com!*'` will match all three. * * NOTE: The quotes are required if your shell treats `!` as a special * character, which is probably does. If you see an error that mentions diff --git a/documentation/language-reference/top-level-functions/D.md b/documentation/language-reference/top-level-functions/D.md index 948a9a7fa..886336671 100644 --- a/documentation/language-reference/top-level-functions/D.md +++ b/documentation/language-reference/top-level-functions/D.md @@ -86,11 +86,12 @@ may have noticed this mistake, but will your coworkers? Will you in six months? You get the idea. DNSControl command line flag `--domains` matches the full name (with the "!"). If you -define domains `example.com!george` and `example.com!john` then: +define domains `example.com!john`, `example.com!paul`, and `example.com!george` then: -* `--domains=example.com` will not match either domain. -* `--domains='example.com!george'` will match only match the first. -* `--domains='example.com!george,example.com!john'` will match both. +* `--domains=example.com` will not match any of the three. +* `--domains='example.com!george'` will only match george. +* `--domains='example.com!george,example.com!john'` will match george and john. +* `--domains='example.com!*'` will match all three. {% hint style="info" %} **NOTE**: The quotes are required if your shell treats `!` as a special diff --git a/documentation/preview-push.md b/documentation/preview-push.md index d29cc539b..7cd28fd66 100644 --- a/documentation/preview-push.md +++ b/documentation/preview-push.md @@ -49,10 +49,19 @@ OPTIONS: * `--domains value` * Specifies a comma-separated list of domains to include. - Typically all domains are included in `preview`/`push`. Wildcards are not - permitted except `*` at the start of the entry. For example, `--domains - example.com,*.in-addr.arpa` would include `example.com` plus all reverse lookup - domains. + Example: `--domains example.com,myexample.net` + * Domains may include a wildcard at the beginning. + For example, `--domains example.com,*.in-addr.arpa` would include + `example.com` plus all IPv4 reverse lookup domains. + * Matching includes tags. If the domains are `example.com!foo` and + `example.com!bar`, then `--domains example.com!foo` would match the first + one, and `--domains example.com` will not match either. + * A wildcard tag is permitted and indicates all configured tags of that domain + should be selected. Example: `--domains=example.com!*` would match + `example.com!foo` and `example.com!bar` but not `example.com`. + * If `--domains` is not specified, the default is all domains. + * NOTE: An empty tag is considered equivalent to the untagged domain. + For example, `--domains=example.com!` will match `example.com` and `example.com!` * `--v foo=bar` * Sets the variable `foo` to the value `bar` prior to diff --git a/models/domain.go b/models/domain.go index 48b415711..f8909f4f6 100644 --- a/models/domain.go +++ b/models/domain.go @@ -81,6 +81,10 @@ func (dc *DomainConfig) UpdateSplitHorizonNames() { name = l[0] tag = l[1] } + if tag == "" { + // ensure empty tagged domain is treated as untagged + unique = name + } } dc.Name = name diff --git a/models/domain_test.go b/models/domain_test.go new file mode 100644 index 000000000..140b8edff --- /dev/null +++ b/models/domain_test.go @@ -0,0 +1,68 @@ +package models + +import ( + "testing" +) + +func Test_UpdateSplitHorizonNames(t *testing.T) { + tests := []struct { + name string + dc *DomainConfig + expected *DomainConfig + }{ + { + name: "testNoTag", + dc: &DomainConfig{ + Name: "example.com", + }, + expected: &DomainConfig{ + Name: "example.com", + Metadata: map[string]string{ + DomainUniqueName: "example.com", + DomainTag: "", + }, + }, + }, + { + name: "testEmptyTag", + dc: &DomainConfig{ + Name: "example.com!", + }, + expected: &DomainConfig{ + Name: "example.com", + Metadata: map[string]string{ + DomainUniqueName: "example.com", + DomainTag: "", + }, + }, + }, + { + name: "testWithTag", + dc: &DomainConfig{ + Name: "example.com!john", + }, + expected: &DomainConfig{ + Name: "example.com", + Metadata: map[string]string{ + DomainUniqueName: "example.com!john", + DomainTag: "john", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.dc.UpdateSplitHorizonNames() + if tt.dc.Name != tt.expected.Name { + t.Errorf("expected name %s, got %s", tt.expected.Name, tt.dc.Name) + } + if tt.dc.Metadata[DomainUniqueName] != tt.expected.Metadata[DomainUniqueName] { + t.Errorf("expected unique name %s, got %s", tt.expected.Metadata[DomainUniqueName], tt.dc.Metadata[DomainUniqueName]) + } + if tt.dc.Metadata[DomainTag] != tt.expected.Metadata[DomainTag] { + t.Errorf("expected tag %s, got %s", tt.expected.Metadata[DomainTag], tt.dc.Metadata[DomainTag]) + } + }) + } +} diff --git a/pkg/js/helpers.js b/pkg/js/helpers.js index c5ea9f07b..5447e77dc 100644 --- a/pkg/js/helpers.js +++ b/pkg/js/helpers.js @@ -148,8 +148,20 @@ function D(name, registrar) { var m = arguments[i]; processDargs(m, domain); } + + // handle the empty tag ("example.com!" -> "example.com") + // replace name with result of removing the empty tag if it exists + // keep track so we can explain the situation in the error message + var withoutEmptyTag = _removeEmptyTag(name); + name = withoutEmptyTag[0]; + var tagWasRemoved = withoutEmptyTag[1]; + if (conf.domain_names.indexOf(name) !== -1) { - throw name + ' is declared more than once'; + var message = name + ' is declared more than once'; + if (tagWasRemoved) { + message += ' (check empty tags)'; + } + throw message; } conf.domains.push(domain); conf.domain_names.push(name); @@ -188,15 +200,31 @@ function D_EXTEND(name) { conf.domains[domain.id] = domain.obj; // let's overwrite the object. } +// _removeEmptyTag(domain): Remove empty tag. +function _removeEmptyTag(name) { + var tagWasRemoved = false; + if (name.slice(-1) === '!') { + name = name.slice(0, name.length - 1); + tagWasRemoved = true; + } + return [name, tagWasRemoved]; +} + // _getDomainObject(name): This implements the domain matching // algorithm used by D_EXTEND(). Candidate matches are an exact match // of the domain's name, or if name is a proper subdomain of the // domain's name. The longest match is returned. function _getDomainObject(name) { + var nameTrimmedTag = _removeEmptyTag(name); + name = nameTrimmedTag[0]; var domain = null; var domainLen = 0; for (var i = 0; i < conf.domains.length; i++) { var thisName = conf.domains[i]['name']; + // check for empty tag + var thisNameTrimmedTag = _removeEmptyTag(thisName); + thisName = thisNameTrimmedTag[0]; + var desiredSuffix = '.' + thisName; var foundSuffix = name.substr(-desiredSuffix.length); // If this is an exact match or the suffix matches... diff --git a/pkg/js/js_test.go b/pkg/js/js_test.go index 6982251d8..eb38858be 100644 --- a/pkg/js/js_test.go +++ b/pkg/js/js_test.go @@ -49,9 +49,9 @@ func TestParsedFiles(t *testing.T) { if err != nil { t.Fatal(err) } - // for _, dc := range conf.Domains { - // normalize.UpdateNameSplitHorizon(dc) - // } + for _, dc := range conf.Domains { + dc.UpdateSplitHorizonNames() + } errs := normalize.ValidateAndNormalizeConfig(conf) if len(errs) != 0 { @@ -114,7 +114,13 @@ func TestParsedFiles(t *testing.T) { var dCount int for _, dc := range conf.Domains { - zoneFile := filepath.Join(testDir, testName, dc.Name+".zone") + var zoneFile string + dc.UpdateSplitHorizonNames() + if dc.Metadata[models.DomainTag] != "" { + zoneFile = filepath.Join(testDir, testName, dc.GetUniqueName()+".zone") + } else { + zoneFile = filepath.Join(testDir, testName, dc.Name+".zone") + } expectedZone, err := os.ReadFile(zoneFile) if err != nil { continue diff --git a/pkg/js/parse_tests/037-splithor.js b/pkg/js/parse_tests/037-splithor.js index e3b99bf69..4544c3c82 100644 --- a/pkg/js/parse_tests/037-splithor.js +++ b/pkg/js/parse_tests/037-splithor.js @@ -22,3 +22,37 @@ D_EXTEND("example.com", D_EXTEND("example.com!inside", A("main", "11.11.11.11"), ); + +D("example.net", REG, DnsProvider(DNS_OUTSIDE), + A("www", "203.0.113.1"), +); + +D_EXTEND("example.net!", + A("main", "203.0.113.12"), +); + +D("example.net!inside", REG, DnsProvider(DNS_INSIDE), + INCLUDE("example.net!"), + A("main", "192.0.2.1"), +); + +D("example.net!outside", REG, DnsProvider(DNS_OUTSIDE), + INCLUDE("example.net"), + A("main", "203.0.113.1"), +); + +D("empty.example.net", REG, DnsProvider(DNS_OUTSIDE), + A("www", "203.0.113.2"), +); + +D_EXTEND("empty.example.net!", + A("main", "203.0.113.22"), +); + +D("example-b.net!", REG, DnsProvider(DNS_OUTSIDE), + A("www", "203.0.113.1"), +); + +D_EXTEND("example-b.net", + A("main", "203.0.113.12"), +); diff --git a/pkg/js/parse_tests/037-splithor.json b/pkg/js/parse_tests/037-splithor.json index fb2f06a64..cf1e11e01 100644 --- a/pkg/js/parse_tests/037-splithor.json +++ b/pkg/js/parse_tests/037-splithor.json @@ -82,6 +82,143 @@ } ], "registrar": "Third-Party" + }, + { + "dnsProviders": { + "bind": -1 + }, + "meta": { + "dnscontrol_tag": "", + "dnscontrol_uniquename": "example.net" + }, + "name": "example.net", + "records": [ + { + "name": "main", + "target": "203.0.113.12", + "ttl": 300, + "type": "A" + }, + { + "name": "www", + "target": "203.0.113.1", + "ttl": 300, + "type": "A" + } + ], + "registrar": "Third-Party" + }, + { + "dnsProviders": { + "Cloudflare": -1 + }, + "meta": { + "dnscontrol_tag": "inside", + "dnscontrol_uniquename": "example.net!inside" + }, + "name": "example.net", + "records": [ + { + "name": "main", + "target": "192.0.2.1", + "ttl": 300, + "type": "A" + }, + { + "name": "main", + "target": "203.0.113.12", + "ttl": 300, + "type": "A" + }, + { + "name": "www", + "target": "203.0.113.1", + "ttl": 300, + "type": "A" + } + ], + "registrar": "Third-Party" + }, + { + "dnsProviders": { + "bind": -1 + }, + "meta": { + "dnscontrol_tag": "outside", + "dnscontrol_uniquename": "example.net!outside" + }, + "name": "example.net", + "records": [ + { + "name": "main", + "target": "203.0.113.1", + "ttl": 300, + "type": "A" + }, + { + "name": "main", + "target": "203.0.113.12", + "ttl": 300, + "type": "A" + }, + { + "name": "www", + "target": "203.0.113.1", + "ttl": 300, + "type": "A" + } + ], + "registrar": "Third-Party" + }, + { + "dnsProviders": { + "bind": -1 + }, + "meta": { + "dnscontrol_tag": "", + "dnscontrol_uniquename": "empty.example.net" + }, + "name": "empty.example.net", + "records": [ + { + "name": "main", + "target": "203.0.113.22", + "ttl": 300, + "type": "A" + }, + { + "name": "www", + "target": "203.0.113.2", + "ttl": 300, + "type": "A" + } + ], + "registrar": "Third-Party" + }, + { + "dnsProviders": { + "bind": -1 + }, + "meta": { + "dnscontrol_tag": "", + "dnscontrol_uniquename": "example-b.net" + }, + "name": "example-b.net", + "records": [ + { + "name": "main", + "target": "203.0.113.12", + "ttl": 300, + "type": "A" + }, + { + "name": "www", + "target": "203.0.113.1", + "ttl": 300, + "type": "A" + } + ], + "registrar": "Third-Party" } ], "registrars": [ diff --git a/pkg/js/parse_tests/037-splithor/empty.example.net.zone b/pkg/js/parse_tests/037-splithor/empty.example.net.zone new file mode 100644 index 000000000..700e08c48 --- /dev/null +++ b/pkg/js/parse_tests/037-splithor/empty.example.net.zone @@ -0,0 +1,3 @@ +$TTL 300 +main IN A 203.0.113.22 +www IN A 203.0.113.2 diff --git a/pkg/js/parse_tests/037-splithor/example-b.net.zone b/pkg/js/parse_tests/037-splithor/example-b.net.zone new file mode 100644 index 000000000..17f2e8625 --- /dev/null +++ b/pkg/js/parse_tests/037-splithor/example-b.net.zone @@ -0,0 +1,3 @@ +$TTL 300 +main IN A 203.0.113.12 +www IN A 203.0.113.1 diff --git a/pkg/js/parse_tests/037-splithor/example.com!inside.zone b/pkg/js/parse_tests/037-splithor/example.com!inside.zone new file mode 100644 index 000000000..b9e3aeeae --- /dev/null +++ b/pkg/js/parse_tests/037-splithor/example.com!inside.zone @@ -0,0 +1,3 @@ +$TTL 300 +main IN A 1.1.1.1 + IN A 11.11.11.11 diff --git a/pkg/js/parse_tests/037-splithor/example.com!outside.zone b/pkg/js/parse_tests/037-splithor/example.com!outside.zone new file mode 100644 index 000000000..c893af8b5 --- /dev/null +++ b/pkg/js/parse_tests/037-splithor/example.com!outside.zone @@ -0,0 +1,2 @@ +$TTL 300 +main IN A 8.8.8.8 diff --git a/pkg/js/parse_tests/037-splithor/example.com.zone b/pkg/js/parse_tests/037-splithor/example.com.zone new file mode 100644 index 000000000..f1549f660 --- /dev/null +++ b/pkg/js/parse_tests/037-splithor/example.com.zone @@ -0,0 +1,3 @@ +$TTL 300 +main IN A 3.3.3.3 +www IN A 33.33.33.33 diff --git a/pkg/js/parse_tests/037-splithor/example.net!inside.zone b/pkg/js/parse_tests/037-splithor/example.net!inside.zone new file mode 100644 index 000000000..64e1ded6e --- /dev/null +++ b/pkg/js/parse_tests/037-splithor/example.net!inside.zone @@ -0,0 +1,4 @@ +$TTL 300 +main IN A 192.0.2.1 + IN A 203.0.113.12 +www IN A 203.0.113.1 diff --git a/pkg/js/parse_tests/037-splithor/example.net!outside.zone b/pkg/js/parse_tests/037-splithor/example.net!outside.zone new file mode 100644 index 000000000..37b24aa21 --- /dev/null +++ b/pkg/js/parse_tests/037-splithor/example.net!outside.zone @@ -0,0 +1,4 @@ +$TTL 300 +main IN A 203.0.113.1 + IN A 203.0.113.12 +www IN A 203.0.113.1 diff --git a/pkg/js/parse_tests/037-splithor/example.net.zone b/pkg/js/parse_tests/037-splithor/example.net.zone new file mode 100644 index 000000000..17f2e8625 --- /dev/null +++ b/pkg/js/parse_tests/037-splithor/example.net.zone @@ -0,0 +1,3 @@ +$TTL 300 +main IN A 203.0.113.12 +www IN A 203.0.113.1 diff --git a/pkg/normalize/validate.go b/pkg/normalize/validate.go index 1945a7900..159e73f88 100644 --- a/pkg/normalize/validate.go +++ b/pkg/normalize/validate.go @@ -564,6 +564,8 @@ func processSplitHorizonDomains(config *models.DNSConfig) error { seen := map[string]bool{} for _, d := range config.Domains { uniquename := d.GetUniqueName() + // empty tag == untagged ("example.com!" -> "example.com") + uniquename = strings.TrimSuffix(uniquename, "!") if seen[uniquename] { return fmt.Errorf("duplicate domain name: %q", uniquename) }