Skip to content

Commit edb8b16

Browse files
committed
feat: Plan normalizeDNSName convert Unicode to ASCII
1 parent e64e536 commit edb8b16

File tree

4 files changed

+179
-10
lines changed

4 files changed

+179
-10
lines changed

endpoint/domain_filter.go

+18-4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ import (
2323
"regexp"
2424
"sort"
2525
"strings"
26+
27+
log "github.com/sirupsen/logrus"
28+
"golang.org/x/net/idna"
2629
)
2730

2831
type MatchAllDomainFilters []DomainFilterInterface
@@ -69,7 +72,7 @@ type domainFilterSerde struct {
6972
func prepareFilters(filters []string) []string {
7073
var fs []string
7174
for _, filter := range filters {
72-
if domain := strings.ToLower(strings.TrimSuffix(strings.TrimSpace(filter), ".")); domain != "" {
75+
if domain := normalizeDomain(strings.TrimSpace(filter)); domain != "" {
7376
fs = append(fs, domain)
7477
}
7578
}
@@ -109,7 +112,7 @@ func matchFilter(filters []string, domain string, emptyval bool) bool {
109112
return emptyval
110113
}
111114

112-
strippedDomain := strings.ToLower(strings.TrimSuffix(domain, "."))
115+
strippedDomain := normalizeDomain(domain)
113116
for _, filter := range filters {
114117
if filter == "" {
115118
continue
@@ -133,7 +136,7 @@ func matchFilter(filters []string, domain string, emptyval bool) bool {
133136
// only regex regular expression matches the domain
134137
// Otherwise, if either negativeRegex matches or regex does not match the domain, it returns false
135138
func matchRegex(regex *regexp.Regexp, negativeRegex *regexp.Regexp, domain string) bool {
136-
strippedDomain := strings.ToLower(strings.TrimSuffix(domain, "."))
139+
strippedDomain := normalizeDomain(domain)
137140

138141
if negativeRegex != nil && negativeRegex.String() != "" {
139142
return !negativeRegex.MatchString(strippedDomain)
@@ -214,7 +217,7 @@ func (df DomainFilter) MatchParent(domain string) bool {
214217
return true
215218
}
216219

217-
strippedDomain := strings.ToLower(strings.TrimSuffix(domain, "."))
220+
strippedDomain := normalizeDomain(domain)
218221
for _, filter := range df.Filters {
219222
if filter == "" || strings.HasPrefix(filter, ".") {
220223
// We don't check parents if the filter is prefixed with "."
@@ -226,3 +229,14 @@ func (df DomainFilter) MatchParent(domain string) bool {
226229
}
227230
return false
228231
}
232+
233+
// normalizeDomain converts a domain to a canonical form, so that we can filter on it
234+
// it: trim "." suffix, get Unicode version of domain complient with Section 5 of RFC 5891
235+
func normalizeDomain(domain string) string {
236+
s, err := idna.Lookup.ToUnicode(strings.TrimSuffix(domain, "."))
237+
if err != nil {
238+
log.Warnf(`Got error while parsing domain %s: %v`, domain, err)
239+
return domain
240+
}
241+
return s
242+
}

endpoint/domain_filter_test.go

+135-4
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,26 @@ var domainFilterTests = []domainFilterTest{
248248
"exclude": {".api.example.org"},
249249
},
250250
},
251+
{
252+
[]string{"æøå.org"},
253+
[]string{"api.æøå.org"},
254+
[]string{"foo.api.æøå.org", "api.æøå.org"},
255+
false,
256+
map[string][]string{
257+
"include": {"æøå.org"},
258+
"exclude": {"api.æøå.org"},
259+
},
260+
},
261+
{
262+
[]string{" æøå.org. "},
263+
[]string{" .api.æøå.org "},
264+
[]string{"foo.api.æøå.org", "bar.baz.api.æøå.org."},
265+
false,
266+
map[string][]string{
267+
"include": {"æøå.org"},
268+
"exclude": {".api.æøå.org"},
269+
},
270+
},
251271
{
252272
[]string{"example.org."},
253273
[]string{"api.example.org"},
@@ -298,6 +318,16 @@ var domainFilterTests = []domainFilterTest{
298318
"exclude": {"foo-bar.example.org"},
299319
},
300320
},
321+
{
322+
[]string{"sTOnks📈.ORG", "API.xn--StonkS-u354e.ORG"},
323+
[]string{"Foo-Bar.stoNks📈.Org"},
324+
[]string{"FoOoo.Api.Stonks📈.Org"},
325+
true,
326+
map[string][]string{
327+
"include": {"api.stonks📈.org", "stonks📈.org"},
328+
"exclude": {"foo-bar.stonks📈.org"},
329+
},
330+
},
301331
{
302332
[]string{"eXaMPle.ORG", "API.example.ORG"},
303333
[]string{"api.example.org"},
@@ -349,8 +379,27 @@ var regexDomainFilterTests = []regexDomainFilterTest{
349379
},
350380
},
351381
{
352-
regexp.MustCompile(`(?:foo|bar)\.org$`),
353-
regexp.MustCompile(`^example\.(?:foo|bar)\.org$`),
382+
regexp.MustCompile("(?:😍|🤩)\\.org$"),
383+
regexp.MustCompile(""),
384+
[]string{"😍.org", "xn--r28h.org", "🤩.org", "example.😍.org", "example.🤩.org", "a.example.xn--r28h.org", "a.example.🤩.org"},
385+
true,
386+
map[string]string{
387+
"regexInclude": "(?:😍|🤩)\\.org$",
388+
},
389+
},
390+
{
391+
regexp.MustCompile("(?:😍|🤩)\\.org$"),
392+
regexp.MustCompile("^example\\.(?:😍|🤩)\\.org$"),
393+
[]string{"example.😍.org", "example.🤩.org"},
394+
false,
395+
map[string]string{
396+
"regexInclude": "(?:😍|🤩)\\.org$",
397+
"regexExclude": "^example\\.(?:😍|🤩)\\.org$",
398+
},
399+
},
400+
{
401+
regexp.MustCompile("(?:foo|bar)\\.org$"),
402+
regexp.MustCompile("^example\\.(?:foo|bar)\\.org$"),
354403
[]string{"foo.org", "bar.org", "a.example.foo.org", "a.example.bar.org"},
355404
true,
356405
map[string]string{
@@ -480,8 +529,8 @@ func TestPrepareFiltersStripsWhitespaceAndDotSuffix(t *testing.T) {
480529
nil,
481530
},
482531
{
483-
[]string{" foo ", " bar. ", "baz."},
484-
[]string{"foo", "bar", "baz"},
532+
[]string{" foo ", " bar. ", "baz.", "xn--bar-zna"},
533+
[]string{"foo", "bar", "baz", "øbar"},
485534
},
486535
{
487536
[]string{"foo.bar", " foo.bar. ", " foo.bar.baz ", " foo.bar.baz. "},
@@ -715,6 +764,24 @@ func TestDomainFilterMatchParent(t *testing.T) {
715764
"include": {"a.example.com", "b.example.com"},
716765
},
717766
},
767+
{
768+
[]string{"a.xn--c1yn36f.æøå.", "b.點看.xn--5cab8c", "c.點看.æøå"},
769+
[]string{},
770+
[]string{"xn--c1yn36f.xn--5cab8c"},
771+
true,
772+
map[string][]string{
773+
"include": {"a.點看.æøå", "b.點看.æøå", "c.點看.æøå"},
774+
},
775+
},
776+
{
777+
[]string{"punycode.xn--c1yn36f.local", "å.點看.local.", "ø.點看.local"},
778+
[]string{},
779+
[]string{"點看.local"},
780+
true,
781+
map[string][]string{
782+
"include": {"punycode.點看.local", "å.點看.local", "ø.點看.local"},
783+
},
784+
},
718785
{
719786
[]string{"a.example.com"},
720787
[]string{},
@@ -818,3 +885,67 @@ func TestSimpleDomainFilterWithExclusion(t *testing.T) {
818885
})
819886
}
820887
}
888+
889+
func TestDomainFilterNormalizeDomain(t *testing.T) {
890+
records := []struct {
891+
dnsName string
892+
expect string
893+
}{
894+
{
895+
"3AAAA.FOO.BAR.COM",
896+
"3aaaa.foo.bar.com",
897+
},
898+
{
899+
"example.foo.com.",
900+
"example.foo.com",
901+
},
902+
{
903+
"example123.foo.com",
904+
"example123.foo.com",
905+
},
906+
{
907+
"foo.com.",
908+
"foo.com",
909+
},
910+
{
911+
"foo123.COM",
912+
"foo123.com",
913+
},
914+
{
915+
"my-exaMple3.FOO.BAR.COM",
916+
"my-example3.foo.bar.com",
917+
},
918+
{
919+
"my-example1214.FOO-1235.BAR-foo.COM",
920+
"my-example1214.foo-1235.bar-foo.com",
921+
},
922+
{
923+
"my-example-my-example-1214.FOO-1235.BAR-foo.COM",
924+
"my-example-my-example-1214.foo-1235.bar-foo.com",
925+
},
926+
{
927+
"xn--c1yn36f.org.",
928+
"點看.org",
929+
},
930+
{
931+
"xn--nordic--w1a.xn--xn--kItty-pd34d-hn01b3542b.com",
932+
"nordic-ø.xn--kitty-點看pd34d.com",
933+
},
934+
{
935+
"xn--nordic--w1a.xn--kItty-pd34d.com",
936+
"nordic-ø.kitty😸.com",
937+
},
938+
{
939+
"nordic-ø.kitty😸.COM",
940+
"nordic-ø.kitty😸.com",
941+
},
942+
{
943+
"xn--nordic--w1a.kiTTy😸.com.",
944+
"nordic-ø.kitty😸.com",
945+
},
946+
}
947+
for _, r := range records {
948+
gotName := normalizeDomain(r.dnsName)
949+
assert.Equal(t, r.expect, gotName)
950+
}
951+
}

plan/plan.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222

2323
"github.com/google/go-cmp/cmp"
2424
log "github.com/sirupsen/logrus"
25+
"golang.org/x/net/idna"
2526

2627
"sigs.k8s.io/external-dns/endpoint"
2728
)
@@ -337,9 +338,12 @@ func filterRecordsForPlan(records []*endpoint.Endpoint, domainFilter endpoint.Ma
337338
}
338339

339340
// normalizeDNSName converts a DNS name to a canonical form, so that we can use string equality
340-
// it: removes space, converts to lower case, ensures there is a trailing dot
341+
// it: removes space, get ASCII version of dnsName complient with Section 5 of RFC 5891, ensures there is a trailing dot
341342
func normalizeDNSName(dnsName string) string {
342-
s := strings.TrimSpace(strings.ToLower(dnsName))
343+
s, err := idna.Lookup.ToASCII(strings.TrimSpace(dnsName))
344+
if err != nil {
345+
log.Warnf(`Got error while parsing DNSName %s: %v`, dnsName, err)
346+
}
343347
if !strings.HasSuffix(s, ".") {
344348
s += "."
345349
}

plan/plan_test.go

+20
Original file line numberDiff line numberDiff line change
@@ -1018,6 +1018,26 @@ func TestNormalizeDNSName(t *testing.T) {
10181018
"my-example-my-example-1214.FOO-1235.BAR-foo.COM",
10191019
"my-example-my-example-1214.foo-1235.bar-foo.com.",
10201020
},
1021+
{
1022+
"點看.org.",
1023+
"xn--c1yn36f.org.",
1024+
},
1025+
{
1026+
"nordic-ø.xn--kitty-點看pd34d.com",
1027+
"xn--nordic--w1a.xn--xn--kitty-pd34d-hn01b3542b.com.",
1028+
},
1029+
{
1030+
"nordic-ø.kitty😸.com.",
1031+
"xn--nordic--w1a.xn--kitty-pd34d.com.",
1032+
},
1033+
{
1034+
" nordic-ø.kitty😸.COM",
1035+
"xn--nordic--w1a.xn--kitty-pd34d.com.",
1036+
},
1037+
{
1038+
"xn--nordic--w1a.kitty😸.com.",
1039+
"xn--nordic--w1a.xn--kitty-pd34d.com.",
1040+
},
10211041
}
10221042
for _, r := range records {
10231043
gotName := normalizeDNSName(r.dnsName)

0 commit comments

Comments
 (0)