Skip to content

Commit 1b2f5d4

Browse files
tlimoncellidas7pad
andauthored
BUGFIX: IDN support is broken for domain names (#3845)
# Issue Fixes #3842 CC @das7pad # Resolution Convert domain.Name to IDN earlier in the pipeline. Hack the --domains processing to convert everything to IDN. * Domain names are now stored 3 ways: The original input from dnsconfig.js, canonical IDN format (`xn--...`), and Unicode format. All are downcased. Providers that haven't been updated will receive the IDN format instead of the original input format. This might break some providers but only for users with unicode in their D("domain.tld"). PLEASE TEST YOUR PROVIDER. * BIND filename formatting options have been added to access the new formats. # Breaking changes * BIND zonefiles may change. The default used the name input in the D() statement. It now defaults to the IDN name + "!tag" if there is a tag. * Providers that are not IDN-aware may break (hopefully only if they weren't processing IDN already) --------- Co-authored-by: Jakob Ackermann <[email protected]>
1 parent 9aad292 commit 1b2f5d4

File tree

83 files changed

+621
-468
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

83 files changed

+621
-468
lines changed

commands/commands.go

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -303,39 +303,3 @@ func (args *FilterArgs) flags() []cli.Flag {
303303
},
304304
}
305305
}
306-
307-
// domainInList takes a domain and a list of domains and returns true if the
308-
// domain is in the list, accounting for wildcards and tags.
309-
func domainInList(domain string, list []string) bool {
310-
for _, item := range list {
311-
if item == domain {
312-
return true
313-
}
314-
if strings.HasPrefix(item, "*") && strings.HasSuffix(domain, item[1:]) {
315-
return true
316-
}
317-
filterDom, filterTag, isFilterTagged := strings.Cut(item, "!")
318-
splitDom, domainTag, isDomainTagged := strings.Cut(domain, "!")
319-
if splitDom == filterDom {
320-
if isDomainTagged {
321-
if filterTag == "*" {
322-
return true
323-
}
324-
if domainTag == "" && !isFilterTagged {
325-
// domain example.com! == filter example.com
326-
return true
327-
}
328-
if isFilterTagged && domainTag == filterTag {
329-
return true
330-
}
331-
}
332-
if isFilterTagged {
333-
if filterTag == "" && !isDomainTagged {
334-
// filter example.com! == domain example.com
335-
return true
336-
}
337-
}
338-
}
339-
}
340-
return false
341-
}

commands/commands_test.go

Lines changed: 0 additions & 127 deletions
This file was deleted.

commands/getZones.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,10 @@ func GetZone(args GetZoneArgs) error {
200200
// fetch all of the records
201201
zoneRecs := make([]models.Records, len(zones))
202202
for i, zone := range zones {
203-
recs, err := provider.GetZoneRecords(zone, nil)
203+
recs, err := provider.GetZoneRecords(zone,
204+
map[string]string{
205+
models.DomainUniqueName: zone,
206+
})
204207
if err != nil {
205208
return fmt.Errorf("failed GetZone gzr: %w", err)
206209
}

commands/gz_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func testFormat(t *testing.T, domain, format string) {
6363
// Read the expected result
6464
want, err := os.ReadFile(expectedFilename)
6565
if err != nil {
66-
log.Fatal(fmt.Errorf("can't read expected %q: %w", outfile.Name(), err))
66+
log.Fatal(fmt.Errorf("can't read expected %q: %w", expectedFilename, err))
6767
}
6868

6969
if w, g := string(want), string(got); w != g {

commands/ppreviewPush.go

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/StackExchange/dnscontrol/v4/models"
2121
"github.com/StackExchange/dnscontrol/v4/pkg/bindserial"
2222
"github.com/StackExchange/dnscontrol/v4/pkg/credsfile"
23+
"github.com/StackExchange/dnscontrol/v4/pkg/domaintags"
2324
"github.com/StackExchange/dnscontrol/v4/pkg/nameservers"
2425
"github.com/StackExchange/dnscontrol/v4/pkg/normalize"
2526
"github.com/StackExchange/dnscontrol/v4/pkg/notifications"
@@ -288,7 +289,7 @@ func prun(args PPreviewArgs, push bool, interactive bool, out printer.CLI, repor
288289
continue // Do not emit noise when zone exists
289290
}
290291
if !started {
291-
out.StartDomain(zone.GetUniqueName())
292+
out.StartDomain(zone)
292293
started = true
293294
}
294295
skip := skipProvider(provider.Name, providersToProcess)
@@ -351,7 +352,7 @@ func prun(args PPreviewArgs, push bool, interactive bool, out printer.CLI, repor
351352
// Now we know what to do, print or do the tasks.
352353
out.PrintfIf(fullMode, "PHASE 3: CORRECTIONS\n")
353354
for _, zone := range zonesToProcess {
354-
out.StartDomain(zone.GetUniqueName())
355+
out.StartDomain(zone)
355356

356357
// Process DNS provider changes:
357358
providersToProcess := whichProvidersToProcess(zone.DNSProviderInstances, args.Providers)
@@ -400,29 +401,16 @@ func prun(args PPreviewArgs, push bool, interactive bool, out printer.CLI, repor
400401
return nil
401402
}
402403

403-
// func countActions(corrections []*models.Correction) int {
404-
// r := 0
405-
// for _, c := range corrections {
406-
// if c.F != nil {
407-
// r++
408-
// }
409-
// }
410-
// return r
411-
//}
412-
413404
// whichZonesToProcess takes a list of DomainConfigs and a filter string and
414-
// returns a list of DomainConfigs whose metadata[DomainUniqueName] matched the
405+
// returns a list of DomainConfigs whose Domain.UniqueName matched the
415406
// filter. The filter string is a comma-separated list of domain names. If the
416407
// filter string is empty or "all", all domains are returned.
417408
func whichZonesToProcess(domains []*models.DomainConfig, filter string) []*models.DomainConfig {
418-
if filter == "" || filter == "all" {
419-
return domains
420-
}
409+
fh := domaintags.CompilePermitList(filter)
421410

422-
permitList := strings.Split(filter, ",")
423411
var picked []*models.DomainConfig
424412
for _, domain := range domains {
425-
if domainInList(domain.GetUniqueName(), permitList) {
413+
if fh.Permitted(domain.GetUniqueName()) {
426414
picked = append(picked, domain)
427415
}
428416
}

commands/ppreviewPush_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func Test_whichZonesToProcess(t *testing.T) {
2323
}
2424

2525
for _, dc := range allDC {
26-
dc.UpdateSplitHorizonNames()
26+
dc.PostProcess()
2727
}
2828

2929
type args struct {

documentation/provider/bind.md

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ Example:
2222
{
2323
"bind": {
2424
"TYPE": "BIND",
25-
"directory": "myzones"
25+
"directory": "myzones",
26+
"filenameformat": "%U.zone"
2627
}
2728
}
2829
```
@@ -89,10 +90,13 @@ file name is the name as specified in the `D()` function plus ".zone".
8990

9091
The filenameformat is a string with a few printf-like `%` verbs:
9192

92-
* `%U` the domain name as specified in `D()`
93-
* `%D` the domain name without any split horizon tag (the "example.com" part of "example.com!tag")
94-
* `%T` the split horizon tag, or "" (the "tag" part of "example.com!tag")
95-
* `%?x` this returns `x` if the split horizon tag is non-null, otherwise nothing. `x` can be any printable.
93+
* The domain name without tag (the `example.com` part of `example.com!tag`):
94+
* `%D` as specified in `D()` (no IDN conversion, but downcased)
95+
* `%I` converted to IDN/Punycode (`xn--...`) and downcased.
96+
* `%N` converted to Unicode (downcased first)
97+
* `%T` the split horizon tag, or "" (the `tag` part of `example.com!tag`)
98+
* `%?x` this returns `x` if the split horizon tag is non-null, otherwise nothing. `x` can be any printable but is usually `!`.
99+
* `%U` short for "%I%?!%T". This is the universal, canonical, name for the domain used for comparisons within DNSControl. This is best for filenames which is why it is used in the default.
96100
* `%%` `%`
97101
* ordinary characters (not `%`) are copied unchanged to the output stream
98102
* FYI: format strings must not end with an incomplete `%` or `%?`
@@ -101,19 +105,17 @@ Typical values:
101105

102106
* `%U.zone` (The default)
103107
* `example.com.zone` or `example.com!tag.zone`
104-
* `%T%*U%D.zone` (optional tag and `_` + domain + `.zone`)
108+
* `%T%?_%I.zone` (optional tag and `_` + domain + `.zone`)
105109
* `tag_example.com.zone` or `example.com.zone`
106110
* `db_%T%?_%D`
107111
* `db_inside_example.com` or `db_example.com`
108-
* `db_%D`
109-
* `db_example.com`
110-
111-
The last example will generate the same name for both
112-
`D("example.com!inside")` and `D("example.com!outside")`. This
113-
assumes two BIND providers are configured in `creds.json`, each with
114-
a different `directory` setting. Otherwise `dnscontrol` will write
115-
both domains to the same file, flapping between the two back and
116-
forth.
112+
113+
{% hint style="warning" %}
114+
**Warning** DNSControl will not warn you if two zones generate the same
115+
filename. Instead, each will write to the same place. The content would end up
116+
flapping back and forth between the two. The best way to prevent this is to
117+
always include the tag (`%T`) or use `%U` which includes the tag.
118+
{% endhint %}
117119

118120
(new in v4.2.0) `dnscontrol push` will create subdirectories along the path to
119121
the filename. This includes both the portion of the path created by the

integrationTest/helpers_integration_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func getDomainConfigWithNameservers(t *testing.T, prv providers.DNSServiceProvid
4040
dc := &models.DomainConfig{
4141
Name: domainName,
4242
}
43-
dc.UpdateSplitHorizonNames()
43+
dc.PostProcess()
4444

4545
// fix up nameservers
4646
ns, err := prv.GetNameservers(domainName)
@@ -148,6 +148,8 @@ func makeChanges(t *testing.T, prv providers.DNSServiceProvider, dc *models.Doma
148148
return
149149
}
150150

151+
//fmt.Printf("DEBUG: Running test %q: Names %q %q %q\n", desc, dom.Name, dom.NameRaw, dom.NameUnicode)
152+
151153
// get and run corrections for first time
152154
_, corrections, actualChangeCount, err := zonerecs.CorrectZoneRecords(prv, dom)
153155
if err != nil {

models/dns.go

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -105,17 +105,10 @@ type Correction struct {
105105
Msg string
106106
}
107107

108-
// DomainContainingFQDN finds the best domain from the dns config for the given record fqdn.
109-
// It will chose the domain whose name is the longest suffix match for the fqdn.
110-
func (config *DNSConfig) DomainContainingFQDN(fqdn string) *DomainConfig {
111-
fqdn = strings.TrimSuffix(fqdn, ".")
112-
longestLength := 0
113-
var d *DomainConfig
114-
for _, dom := range config.Domains {
115-
if (dom.Name == fqdn || strings.HasSuffix(fqdn, "."+dom.Name)) && len(dom.Name) > longestLength {
116-
longestLength = len(dom.Name)
117-
d = dom
118-
}
108+
// PostProcess performs and post-processing required after running dnsconfig.js and loading the result.
109+
func (config *DNSConfig) PostProcess() error {
110+
for _, domain := range config.Domains {
111+
domain.PostProcess()
119112
}
120-
return d
113+
return nil
121114
}

0 commit comments

Comments
 (0)