Skip to content

Commit b22d2b7

Browse files
authored
Add wildcard hostname support (#769)
Until now, users were only able to use hostnames without wildcards, limiting their options. We now support wildcard hostnames for HTTPRoutes and Gateway listeners.
1 parent 920fb90 commit b22d2b7

10 files changed

+157
-35
lines changed

docs/gateway-api-compatibility.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ Fields:
5757
* `gatewayClassName` - supported.
5858
* `listeners`
5959
* `name` - supported.
60-
* `hostname` - partially supported. Wildcard hostnames like `*.example.com` are not yet supported.
60+
* `hostname` - supported.
6161
* `port` - supported.
6262
* `protocol` - partially supported. Allowed values: `HTTP`, `HTTPS`.
6363
* `tls`
@@ -101,7 +101,7 @@ Fields:
101101
Fields:
102102
* `spec`
103103
* `parentRefs` - partially supported. Port not supported.
104-
* `hostnames` - partially supported. Wildcard binding is not supported: a hostname like `example.com` will not bind to a listener with the hostname `*.example.com`. However, `example.com` will bind to a listener with the empty hostname.
104+
* `hostnames` - supported.
105105
* `rules`
106106
* `matches`
107107
* `path` - partially supported. Only `PathPrefix` and `Exact` types.

examples/cafe-example/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,26 @@ curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT
7070
Server address: 10.12.0.19:80
7171
Server name: tea-7cd44fcb4d-xfw2x
7272
```
73+
74+
## 5. Using different hostnames
75+
76+
Traffic is allowed to `cafe.example.com` because the Gateway listener's hostname allows `*.example.com`. You can
77+
change an HTTPRoute's hostname to something that matches this wildcard and still pass traffic.
78+
79+
For example, run the following command to open your editor and change the HTTPRoute's hostname to `foo.example.com`.
80+
81+
```
82+
kubectl -n default edit httproute tea
83+
```
84+
85+
Once changed, update the `curl` command above for the `tea` service to use the new hostname. Traffic should still pass successfully.
86+
87+
Likewise, if you change the Gateway listener's hostname to something else, you can prevent the HTTPRoute's traffic from passing successfully.
88+
89+
For example, run the following to open your editor and change the Gateway listener's hostname to `bar.example.com`:
90+
91+
```
92+
kubectl -n default edit gateway gateway
93+
```
94+
95+
Once changed, try running the same `curl` requests as above. They should be denied with a `404 Not Found`.

examples/cafe-example/gateway.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ spec:
1010
- name: http
1111
port: 80
1212
protocol: HTTP
13+
hostname: "*.example.com"

internal/state/dataplane/configuration.go

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -567,20 +567,7 @@ func convertPathType(pathType v1beta1.PathMatchType) PathType {
567567
}
568568
}
569569

570-
// listenerHostnameMoreSpecific returns true if host1 is more specific than host2 (using length).
571-
//
572-
// Since the only caller of this function specifies listener hostnames that are both
573-
// bound to the same route hostname, this function assumes that host1 and host2 match, either
574-
// exactly or as a substring.
575-
//
576-
// For example:
577-
// - foo.example.com and "" (host1 wins)
578-
// Non-example:
579-
// - foo.example.com and bar.example.com (should not be given to this function)
580-
//
581-
// As we add regex support, we should put in the proper
582-
// validation and error handling for this function to ensure that the hostnames are actually matching,
583-
// to avoid the unintended inputs above for the invalid case.
570+
// listenerHostnameMoreSpecific returns true if host1 is more specific than host2.
584571
func listenerHostnameMoreSpecific(host1, host2 *v1beta1.Hostname) bool {
585572
var host1Str, host2Str string
586573
if host1 != nil {
@@ -591,5 +578,5 @@ func listenerHostnameMoreSpecific(host1, host2 *v1beta1.Hostname) bool {
591578
host2Str = string(*host2)
592579
}
593580

594-
return len(host1Str) >= len(host2Str)
581+
return graph.GetMoreSpecificHostname(host1Str, host2Str) == host1Str
595582
}

internal/state/dataplane/configuration_test.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1996,8 +1996,6 @@ func TestConvertPathType(t *testing.T) {
19961996
}
19971997

19981998
func TestHostnameMoreSpecific(t *testing.T) {
1999-
g := NewGomegaWithT(t)
2000-
20011999
tests := []struct {
20022000
host1 *v1beta1.Hostname
20032001
host2 *v1beta1.Hostname
@@ -2029,15 +2027,31 @@ func TestHostnameMoreSpecific(t *testing.T) {
20292027
msg: "host1 has value; host2 empty",
20302028
},
20312029
{
2032-
host1: helpers.GetPointer(v1beta1.Hostname("example.com")),
2030+
host1: helpers.GetPointer(v1beta1.Hostname("")),
2031+
host2: helpers.GetPointer(v1beta1.Hostname("example.com")),
2032+
host1Wins: false,
2033+
msg: "host2 has value; host1 empty",
2034+
},
2035+
{
2036+
host1: helpers.GetPointer(v1beta1.Hostname("foo.example.com")),
2037+
host2: helpers.GetPointer(v1beta1.Hostname("*.example.com")),
2038+
host1Wins: true,
2039+
msg: "host1 more specific than host2",
2040+
},
2041+
{
2042+
host1: helpers.GetPointer(v1beta1.Hostname("*.example.com")),
20332043
host2: helpers.GetPointer(v1beta1.Hostname("foo.example.com")),
20342044
host1Wins: false,
2035-
msg: "host2 longer than host1",
2045+
msg: "host2 more specific than host1",
20362046
},
20372047
}
20382048

20392049
for _, tc := range tests {
2040-
g.Expect(listenerHostnameMoreSpecific(tc.host1, tc.host2)).To(Equal(tc.host1Wins), tc.msg)
2050+
t.Run(tc.msg, func(t *testing.T) {
2051+
g := NewGomegaWithT(t)
2052+
2053+
g.Expect(listenerHostnameMoreSpecific(tc.host1, tc.host2)).To(Equal(tc.host1Wins))
2054+
})
20412055
}
20422056
}
20432057

internal/state/graph/gateway_listener_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ func TestValidateListenerHostname(t *testing.T) {
222222
},
223223
{
224224
hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("*.example.com")),
225-
expectErr: true,
225+
expectErr: false,
226226
name: "wildcard hostname",
227227
},
228228
{

internal/state/graph/httproute.go

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package graph
33
import (
44
"errors"
55
"fmt"
6+
"strings"
67

78
apiv1 "k8s.io/api/core/v1"
89
"k8s.io/apimachinery/pkg/labels"
@@ -401,24 +402,79 @@ func findAcceptedHostnames(listenerHostname *v1beta1.Hostname, routeHostnames []
401402
return []string{hostname}
402403
}
403404

404-
match := func(h v1beta1.Hostname) bool {
405-
if hostname == "" {
406-
return true
407-
}
408-
return string(h) == hostname
409-
}
410-
411405
var result []string
412406

413407
for _, h := range routeHostnames {
414-
if match(h) {
415-
result = append(result, string(h))
408+
routeHost := string(h)
409+
if match(hostname, routeHost) {
410+
result = append(result, GetMoreSpecificHostname(hostname, routeHost))
416411
}
417412
}
418413

419414
return result
420415
}
421416

417+
func match(listenerHost, routeHost string) bool {
418+
if listenerHost == "" {
419+
return true
420+
}
421+
422+
if routeHost == listenerHost {
423+
return true
424+
}
425+
426+
wildcardMatch := func(host1, host2 string) bool {
427+
return strings.HasPrefix(host1, "*.") && strings.HasSuffix(host2, strings.TrimPrefix(host1, "*"))
428+
}
429+
430+
// check if listenerHost is a wildcard and routeHost matches
431+
if wildcardMatch(listenerHost, routeHost) {
432+
return true
433+
}
434+
435+
// check if routeHost is a wildcard and listener matchess
436+
return wildcardMatch(routeHost, listenerHost)
437+
}
438+
439+
// GetMoreSpecificHostname returns the more specific hostname between the two inputs.
440+
//
441+
// This function assumes that the two hostnames match each other, either:
442+
// - Exactly
443+
// - One as a substring of the other
444+
func GetMoreSpecificHostname(hostname1, hostname2 string) string {
445+
if hostname1 == hostname2 {
446+
return hostname1
447+
}
448+
if hostname1 == "" {
449+
return hostname2
450+
}
451+
if hostname2 == "" {
452+
return hostname1
453+
}
454+
455+
// Compare if wildcards are present
456+
if strings.HasPrefix(hostname1, "*.") {
457+
if strings.HasPrefix(hostname2, "*.") {
458+
subdomains1 := strings.Split(hostname1, ".")
459+
subdomains2 := strings.Split(hostname2, ".")
460+
461+
// Compare number of subdomains
462+
if len(subdomains1) > len(subdomains2) {
463+
return hostname1
464+
}
465+
466+
return hostname2
467+
}
468+
469+
return hostname2
470+
}
471+
if strings.HasPrefix(hostname2, "*.") {
472+
return hostname1
473+
}
474+
475+
return ""
476+
}
477+
422478
func routeAllowedByListener(
423479
listener *Listener,
424480
routeNS,

internal/state/graph/httproute_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1217,6 +1217,7 @@ func TestBindRouteToListeners(t *testing.T) {
12171217
func TestFindAcceptedHostnames(t *testing.T) {
12181218
var listenerHostnameFoo v1beta1.Hostname = "foo.example.com"
12191219
var listenerHostnameCafe v1beta1.Hostname = "cafe.example.com"
1220+
var listenerHostnameWildcard v1beta1.Hostname = "*.example.com"
12201221
routeHostnames := []v1beta1.Hostname{"foo.example.com", "bar.example.com"}
12211222

12221223
tests := []struct {
@@ -1255,6 +1256,36 @@ func TestFindAcceptedHostnames(t *testing.T) {
12551256
expected: []string{wildcardHostname},
12561257
msg: "both listener and route have empty hostnames",
12571258
},
1259+
{
1260+
listenerHostname: &listenerHostnameWildcard,
1261+
routeHostnames: routeHostnames,
1262+
expected: []string{"foo.example.com", "bar.example.com"},
1263+
msg: "listener wildcard hostname",
1264+
},
1265+
{
1266+
listenerHostname: &listenerHostnameFoo,
1267+
routeHostnames: []v1beta1.Hostname{"*.example.com"},
1268+
expected: []string{"foo.example.com"},
1269+
msg: "route wildcard hostname; specific listener hostname",
1270+
},
1271+
{
1272+
listenerHostname: &listenerHostnameWildcard,
1273+
routeHostnames: nil,
1274+
expected: []string{"*.example.com"},
1275+
msg: "listener wildcard hostname; nil route hostname",
1276+
},
1277+
{
1278+
listenerHostname: nil,
1279+
routeHostnames: []v1beta1.Hostname{"*.example.com"},
1280+
expected: []string{"*.example.com"},
1281+
msg: "route wildcard hostname; nil listener hostname",
1282+
},
1283+
{
1284+
listenerHostname: &listenerHostnameWildcard,
1285+
routeHostnames: []v1beta1.Hostname{"*.bar.example.com"},
1286+
expected: []string{"*.bar.example.com"},
1287+
msg: "route and listener wildcard hostnames",
1288+
},
12581289
}
12591290

12601291
for _, test := range tests {

internal/state/graph/validation.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,13 @@ func validateHostname(hostname string) error {
1313
return errors.New("cannot be empty string")
1414
}
1515

16-
if strings.Contains(hostname, "*") {
17-
return errors.New("wildcards are not supported")
16+
if strings.HasPrefix(hostname, "*.") {
17+
msgs := validation.IsWildcardDNS1123Subdomain(hostname)
18+
if len(msgs) > 0 {
19+
combined := strings.Join(msgs, ",")
20+
return errors.New(combined)
21+
}
22+
return nil
1823
}
1924

2025
msgs := validation.IsDNS1123Subdomain(hostname)

internal/state/graph/validation_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,19 @@ func TestValidateHostname(t *testing.T) {
2424
},
2525
{
2626
hostname: "*.example.com",
27-
expectErr: true,
27+
expectErr: false,
2828
name: "wildcard hostname",
2929
},
3030
{
3131
hostname: "example$com",
3232
expectErr: true,
3333
name: "invalid hostname",
3434
},
35+
{
36+
hostname: "*.example.*.com",
37+
expectErr: true,
38+
name: "invalid wildcard hostname",
39+
},
3540
}
3641

3742
for _, test := range tests {

0 commit comments

Comments
 (0)