mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 09:36:58 +02:00
gateway-api: make wildcards suffix matchers (e.g. match multiple labels)
This commit is contained in:
parent
5da9393b58
commit
a50a4f9aba
@ -19,6 +19,7 @@ package source
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
@ -486,39 +487,89 @@ func gwProtocolMatches(a, b v1.ProtocolType) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// gwMatchingHost returns the most-specific overlapping host and a bool indicating if one was found.
|
// gwMatchingHost returns the most-specific overlapping host and a bool indicating if one was found.
|
||||||
// For example, if one host is "*.foo.com" and the other is "bar.foo.com", "bar.foo.com" will be returned.
|
// Hostnames that are prefixed with a wildcard label (`*.`) are interpreted as a suffix match.
|
||||||
// An empty string matches anything.
|
// That means that "*.example.com" would match both "test.example.com" and "foo.test.example.com",
|
||||||
func gwMatchingHost(gwHost, rtHost string) (string, bool) {
|
// but not "example.com". An empty string matches anything.
|
||||||
gwHost = toLowerCaseASCII(gwHost) // TODO: trim "." suffix?
|
func gwMatchingHost(a, b string) (string, bool) {
|
||||||
rtHost = toLowerCaseASCII(rtHost) // TODO: trim "." suffix?
|
var ok bool
|
||||||
|
if a, ok = gwHost(a); !ok {
|
||||||
if gwHost == "" {
|
return "", false
|
||||||
return rtHost, true
|
|
||||||
}
|
}
|
||||||
if rtHost == "" {
|
if b, ok = gwHost(b); !ok {
|
||||||
return gwHost, true
|
|
||||||
}
|
|
||||||
|
|
||||||
gwParts := strings.Split(gwHost, ".")
|
|
||||||
rtParts := strings.Split(rtHost, ".")
|
|
||||||
if len(gwParts) != len(rtParts) {
|
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
host := rtHost
|
if a == "" {
|
||||||
for i, gwPart := range gwParts {
|
return b, true
|
||||||
switch rtPart := rtParts[i]; {
|
}
|
||||||
case rtPart == gwPart:
|
if b == "" || a == b {
|
||||||
// continue
|
return a, true
|
||||||
case i == 0 && gwPart == "*":
|
}
|
||||||
// continue
|
if na, nb := len(a), len(b); nb < na || (na == nb && strings.HasPrefix(b, "*.")) {
|
||||||
case i == 0 && rtPart == "*":
|
a, b = b, a
|
||||||
host = gwHost // gwHost is more specific
|
}
|
||||||
default:
|
if strings.HasPrefix(a, "*.") && strings.HasSuffix(b, a[1:]) {
|
||||||
return "", false
|
return b, true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// gwHost returns the canonical host and a value indicating if it's valid.
|
||||||
|
func gwHost(host string) (string, bool) {
|
||||||
|
if host == "" {
|
||||||
|
return "", true
|
||||||
|
}
|
||||||
|
if isIPAddr(host) || !isDNS1123Domain(strings.TrimPrefix(host, "*.")) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return toLowerCaseASCII(host), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// isIPAddr returns whether s in an IP address.
|
||||||
|
func isIPAddr(s string) bool {
|
||||||
|
_, err := netip.ParseAddr(s)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isDNS1123Domain returns whether s is a valid domain name according to RFC 1123.
|
||||||
|
func isDNS1123Domain(s string) bool {
|
||||||
|
if n := len(s); n == 0 || n > 255 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for lbl, rest := "", s; rest != ""; {
|
||||||
|
if lbl, rest, _ = strings.Cut(rest, "."); !isDNS1123Label(lbl) {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return host, true
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// isDNS1123Label returns whether s is a valid domain label according to RFC 1123.
|
||||||
|
func isDNS1123Label(s string) bool {
|
||||||
|
n := len(s)
|
||||||
|
if n == 0 || n > 63 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !isAlphaNum(s[0]) || !isAlphaNum(s[n-1]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i, k := 1, n-1; i < k; i++ {
|
||||||
|
if b := s[i]; b != '-' && !isAlphaNum(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAlphaNum(b byte) bool {
|
||||||
|
switch {
|
||||||
|
case 'a' <= b && b <= 'z',
|
||||||
|
'A' <= b && b <= 'Z',
|
||||||
|
'0' <= b && b <= '9':
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func strVal(ptr *string, def string) string {
|
func strVal(ptr *string, def string) string {
|
||||||
|
191
source/gateway_test.go
Normal file
191
source/gateway_test.go
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGatewayMatchingHost(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
a, b string
|
||||||
|
host string
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "ipv4-rejected",
|
||||||
|
a: "1.2.3.4",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "ipv6-rejected",
|
||||||
|
a: "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "empty-matches-empty",
|
||||||
|
ok: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "empty-matches-nonempty",
|
||||||
|
a: "example.net",
|
||||||
|
host: "example.net",
|
||||||
|
ok: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "simple-match",
|
||||||
|
a: "example.net",
|
||||||
|
b: "example.net",
|
||||||
|
host: "example.net",
|
||||||
|
ok: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "wildcard-matches-longer",
|
||||||
|
a: "*.example.net",
|
||||||
|
b: "test.example.net",
|
||||||
|
host: "test.example.net",
|
||||||
|
ok: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "wildcard-matches-equal-length",
|
||||||
|
a: "*.example.net",
|
||||||
|
b: "a.example.net",
|
||||||
|
host: "a.example.net",
|
||||||
|
ok: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "wildcard-matches-multiple-subdomains",
|
||||||
|
a: "*.example.net",
|
||||||
|
b: "foo.bar.test.example.net",
|
||||||
|
host: "foo.bar.test.example.net",
|
||||||
|
ok: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "wildcard-doesnt-match-parent",
|
||||||
|
a: "*.example.net",
|
||||||
|
b: "example.net",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "wildcard-must-be-complete-label",
|
||||||
|
a: "*example.net",
|
||||||
|
b: "test.example.net",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
if host, ok := gwMatchingHost(tt.a, tt.b); host != tt.host || ok != tt.ok {
|
||||||
|
t.Errorf(
|
||||||
|
"gwMatchingHost(%q, %q); got: %q, %v; want: %q, %v",
|
||||||
|
tt.a, tt.b, host, ok, tt.host, tt.ok,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
tt.a, tt.b = tt.b, tt.a
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsDNS1123Domain(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
in string
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "empty",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "label-too-long",
|
||||||
|
in: strings.Repeat("x", 64) + ".example.net",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "domain-too-long",
|
||||||
|
in: strings.Repeat("testing.", 256/(len("testing."))) + "example.net",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "hostname",
|
||||||
|
in: "example",
|
||||||
|
ok: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "domain",
|
||||||
|
in: "example.net",
|
||||||
|
ok: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "subdomain",
|
||||||
|
in: "test.example.net",
|
||||||
|
ok: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "dashes",
|
||||||
|
in: "test-with-dash.example.net",
|
||||||
|
ok: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "dash-prefix",
|
||||||
|
in: "-dash-prefix.example.net",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "dash-suffix",
|
||||||
|
in: "dash-suffix-.example.net",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "underscore",
|
||||||
|
in: "under_score.example.net",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "plus",
|
||||||
|
in: "pl+us.example.net",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "brackets",
|
||||||
|
in: "bra[k]ets.example.net",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "parens",
|
||||||
|
in: "pa[re]ns.example.net",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "wild",
|
||||||
|
in: "*.example.net",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
|
if ok := isDNS1123Domain(tt.in); ok != tt.ok {
|
||||||
|
t.Errorf("isDNS1123Domain(%q); got: %v; want: %v", tt.in, ok, tt.ok)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user