fix: produce valid listener names for wildcard and unusual DNS names
A DNS name like *.home.hrajfrisbee.cz put "*" into the listener name; the API server rejected the patch, which blocked every certificate on that Gateway. - lowercase the DNS part, spell "*" as "wildcard", map other characters outside [a-z0-9] to "-" - names over 253 chars are cut and get a hash suffix instead of a blind cut - names that were valid before are unchanged Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -132,16 +134,38 @@ func listenerMap(listeners []gwapiv1.Listener) map[string]listenerKey {
|
||||
return m
|
||||
}
|
||||
|
||||
// maxSectionNameLen is the Gateway API limit for listener names.
|
||||
const maxSectionNameLen = 253
|
||||
|
||||
// listenerNameFor generates a deterministic listener name from cert name and DNS name.
|
||||
// Truncates to stay within Gateway API's 253-char limit for section names.
|
||||
// The result is a valid Gateway API SectionName: names that would exceed the limit
|
||||
// are cut and get a hash of the full name appended, so distinct long names stay distinct.
|
||||
func listenerNameFor(prefix, certName, dnsName string) string {
|
||||
// Replace dots with dashes for DNS names, combine with cert name.
|
||||
sanitized := strings.ReplaceAll(dnsName, ".", "-")
|
||||
name := fmt.Sprintf("%s%s-%s", prefix, certName, sanitized)
|
||||
if len(name) > 253 {
|
||||
name = name[:253]
|
||||
name := strings.TrimRight(fmt.Sprintf("%s%s-%s", prefix, certName, sanitizeDNSName(dnsName)), "-.")
|
||||
if len(name) <= maxSectionNameLen {
|
||||
return name
|
||||
}
|
||||
return name
|
||||
sum := sha256.Sum256([]byte(name))
|
||||
suffix := "-" + hex.EncodeToString(sum[:4])
|
||||
return strings.TrimRight(name[:maxSectionNameLen-len(suffix)], "-.") + suffix
|
||||
}
|
||||
|
||||
// sanitizeDNSName maps a certificate DNS name onto the characters allowed in a
|
||||
// listener name. A wildcard label is spelled out so "*.example.com" and
|
||||
// "example.com" stay distinct.
|
||||
func sanitizeDNSName(dnsName string) string {
|
||||
var b strings.Builder
|
||||
for _, r := range strings.ToLower(dnsName) {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z', r >= '0' && r <= '9':
|
||||
b.WriteRune(r)
|
||||
case r == '*':
|
||||
b.WriteString("wildcard")
|
||||
default:
|
||||
b.WriteByte('-')
|
||||
}
|
||||
}
|
||||
return strings.Trim(b.String(), "-")
|
||||
}
|
||||
|
||||
func ptrTo[T any](v T) *T {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -272,7 +274,20 @@ func TestListenersEqual_managedFields(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenerNameFor_sanitizesDots(t *testing.T) {
|
||||
// sectionNamePattern is the Gateway API SectionName validation rule.
|
||||
var sectionNamePattern = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`)
|
||||
|
||||
func assertValidSectionName(t *testing.T, name string) {
|
||||
t.Helper()
|
||||
if len(name) > 253 {
|
||||
t.Errorf("name is %d chars, limit is 253", len(name))
|
||||
}
|
||||
if !sectionNamePattern.MatchString(name) {
|
||||
t.Errorf("name %q is not a valid SectionName", name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenerNameFor_validSectionName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
@@ -284,14 +299,57 @@ func TestListenerNameFor_sanitizesDots(t *testing.T) {
|
||||
{name: "dots become dashes", certName: "web", dnsName: "app.example.com", want: "auto-web-app-example-com"},
|
||||
{name: "single label", certName: "web", dnsName: "localhost", want: "auto-web-localhost"},
|
||||
{name: "existing dashes are kept", certName: "my-cert", dnsName: "my-app.example.com", want: "auto-my-cert-my-app-example-com"},
|
||||
{
|
||||
name: "wildcard label is spelled out",
|
||||
certName: "wildcard-home-hrajfrisbee",
|
||||
dnsName: "*.home.hrajfrisbee.cz",
|
||||
want: "auto-wildcard-home-hrajfrisbee-wildcard-home-hrajfrisbee-cz",
|
||||
},
|
||||
{name: "uppercase is lowered", certName: "web", dnsName: "App.Example.COM", want: "auto-web-app-example-com"},
|
||||
{name: "other characters become dashes", certName: "web", dnsName: "my_app.example.com", want: "auto-web-my-app-example-com"},
|
||||
{name: "trailing dot is dropped", certName: "web", dnsName: "app.example.com.", want: "auto-web-app-example-com"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := listenerNameFor(testPrefix, tc.certName, tc.dnsName); got != tc.want {
|
||||
got := listenerNameFor(testPrefix, tc.certName, tc.dnsName)
|
||||
if got != tc.want {
|
||||
t.Errorf("listenerNameFor() = %q, want %q", got, tc.want)
|
||||
}
|
||||
assertValidSectionName(t, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenerNameFor_longNames(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// 62 dashes right where the name is cut, so a blind cut would end in "-".
|
||||
shared := strings.Repeat("a", 200) + strings.Repeat("-", 62)
|
||||
first := listenerNameFor(testPrefix, "web", shared+"x.example.com")
|
||||
second := listenerNameFor(testPrefix, "web", shared+"y.example.com")
|
||||
|
||||
assertValidSectionName(t, first)
|
||||
assertValidSectionName(t, second)
|
||||
if first == second {
|
||||
t.Errorf("two long DNS names sharing a prefix got the same listener name %q", first)
|
||||
}
|
||||
if again := listenerNameFor(testPrefix, "web", shared+"x.example.com"); again != first {
|
||||
t.Errorf("name is not deterministic: %q then %q", first, again)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildListenersFromCertificates_wildcard(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := BuildListenersFromCertificates(
|
||||
[]cmv1.Certificate{cert("default", "wild", "wild-tls", "*.example.com")}, testPrefix)
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("got %d listeners, want 1", len(got))
|
||||
}
|
||||
assertValidSectionName(t, string(got[0].Name))
|
||||
if got[0].Hostname == nil || *got[0].Hostname != "*.example.com" {
|
||||
t.Errorf("hostname = %v, want the wildcard kept as *.example.com", got[0].Hostname)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user