Repository: kubernetes-sigs/external-dns

Issue:
Disallowed rune U+005F (underscore) warning in logs
<!-- Please use this template while reporting a bug and provide as much info as possible. Not doing so may result in your bug not being addressed in a timely manner. Thanks!

-->

**What happened**:

Updated from version 0.15.1 to 0.18.0.

There are existing CNAME records in route53 that start with underscores. These are used for AWS Certificate Manager domain validations. They cause this warning in the external-dns logs:

```
time="2025-06-27T17:20:31Z" level=warning msg="Got error while parsing DNSName _something.something.com: idna: disallowed rune U+005F"
```

**What you expected to happen**:

No log warnings for these domains.

**How to reproduce it (as minimally and precisely as possible)**:

Create a CNAME starting with underscore in route53 in a zone that external-dns can see. Start external-dns.

CLI arguments:

```
--source=service
--source=ingress
--domain-filter=something.com
--provider=aws
--policy=upsert-only
--aws-zone-type=public
--registry=txt
--txt-owner-id=external-dns
```

**Anything else we need to know?**:

I believe the warning comes from this line:

https://github.com/kubernetes-sigs/external-dns/blob/master/plan/plan.go#L345

**Environment**:

- External-DNS version (use `external-dns --version`): 0.18.0
- DNS provider: route53
- Others:


Discussion:
@fredleger: @szuecs i see there is a fix commited but does not seems to be released in a stable version. Is there any ETA for it ? and for the release of the patched helm chart ?@Hokwang: https://github.com/kubernetes-sigs/external-dns/issues/5774@arani007: Hi all is this fix is merged with [#5774](https://github.com/kubernetes-sigs/external-dns/issues/5774) or still pending release in the next version?@axeal: > Hi all is this fix is merged with [#5774](https://github.com/kubernetes-sigs/external-dns/issues/5774) or still pending release in the next version?

The fix is available in [v0.19.0](https://github.com/kubernetes-sigs/external-dns/releases/tag/v0.19.0)

Solution patch:
diff --git a/plan/plan.go b/plan/plan.go
index 699dcdc5..eb91bb4e 100644
--- a/plan/plan.go
+++ b/plan/plan.go
@@ -339,10 +339,16 @@ func filterRecordsForPlan(records []*endpoint.Endpoint, domainFilter endpoint.Ma
 	return filtered
 }
 
+var idnaProfile = idna.New(
+	idna.MapForLookup(),
+	idna.Transitional(true),
+	idna.StrictDomainName(false),
+)
+
 // normalizeDNSName converts a DNS name to a canonical form, so that we can use string equality
 // it: removes space, get ASCII version of dnsName complient with Section 5 of RFC 5891, ensures there is a trailing dot
 func normalizeDNSName(dnsName string) string {
-	s, err := idna.Lookup.ToASCII(strings.TrimSpace(dnsName))
+	s, err := idnaProfile.ToASCII(strings.TrimSpace(dnsName))
 	if err != nil {
 		log.Warnf(`Got error while parsing DNSName %s: %v`, dnsName, err)
 	}



Test patch:
diff --git a/plan/plan_test.go b/plan/plan_test.go
index 5dde2c9c..c4278087 100644
--- a/plan/plan_test.go
+++ b/plan/plan_test.go
@@ -1028,7 +1028,7 @@ func validateEntries(t *testing.T, entries, expected []*endpoint.Endpoint) {
 	}
 }
 
-func TestNormalizeDNSName(t *testing.T) {
+func TestNormalizeDNSName(tt *testing.T) {
 	records := []struct {
 		dnsName string
 		expect  string
@@ -1061,6 +1061,18 @@ func TestNormalizeDNSName(t *testing.T) {
 			"foo.com.",
 			"foo.com.",
 		},
+		{
+			"_foo.com.",
+			"_foo.com.",
+		},
+		{
+			"\u005Ffoo.com.",
+			"_foo.com.",
+		},
+		{
+			".foo.com.",
+			".foo.com.",
+		},
 		{
 			"foo123.COM",
 			"foo123.com.",
@@ -1099,8 +1111,10 @@ func TestNormalizeDNSName(t *testing.T) {
 		},
 	}
 	for _, r := range records {
-		gotName := normalizeDNSName(r.dnsName)
-		assert.Equal(t, r.expect, gotName)
+		tt.Run(r.dnsName, func(t *testing.T) {
+			gotName := normalizeDNSName(r.dnsName)
+			assert.Equal(t, r.expect, gotName)
+		})
 	}
 }