Skip to content

Commit 170192d

Browse files
committed
Add wildcard hostname support
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 170192d

File tree

10 files changed

+103
-23
lines changed

10 files changed

+103
-23
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: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"sort"
7+
"strings"
78

89
"k8s.io/apimachinery/pkg/types"
910
"sigs.k8s.io/gateway-api/apis/v1beta1"
@@ -567,20 +568,9 @@ func convertPathType(pathType v1beta1.PathMatchType) PathType {
567568
}
568569
}
569570

570-
// listenerHostnameMoreSpecific returns true if host1 is more specific than host2 (using length).
571+
// listenerHostnameMoreSpecific returns true if host1 is more specific than host2.
571572
//
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.
573+
// This function assumes that host1 and host2 match, either exactly or as a substring.
584574
func listenerHostnameMoreSpecific(host1, host2 *v1beta1.Hostname) bool {
585575
var host1Str, host2Str string
586576
if host1 != nil {
@@ -591,5 +581,15 @@ func listenerHostnameMoreSpecific(host1, host2 *v1beta1.Hostname) bool {
591581
host2Str = string(*host2)
592582
}
593583

584+
host1Segments := len(strings.Split(host1Str, "."))
585+
host2Segments := len(strings.Split(host2Str, "."))
586+
if host1Segments > host2Segments {
587+
return true
588+
}
589+
590+
if host2Segments > host1Segments {
591+
return false
592+
}
593+
594594
return len(host1Str) >= len(host2Str)
595595
}

internal/state/dataplane/configuration_test.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2029,15 +2029,29 @@ func TestHostnameMoreSpecific(t *testing.T) {
20292029
msg: "host1 has value; host2 empty",
20302030
},
20312031
{
2032-
host1: helpers.GetPointer(v1beta1.Hostname("example.com")),
2032+
host1: helpers.GetPointer(v1beta1.Hostname("foo.bar.example.com")),
20332033
host2: helpers.GetPointer(v1beta1.Hostname("foo.example.com")),
2034+
host1Wins: true,
2035+
msg: "host1 has more segments than host2",
2036+
},
2037+
{
2038+
host1: helpers.GetPointer(v1beta1.Hostname("somelongname.example.com")),
2039+
host2: helpers.GetPointer(v1beta1.Hostname("foo.bar.example.com")),
2040+
host1Wins: false,
2041+
msg: "host2 has more segments than host1",
2042+
},
2043+
{
2044+
host1: helpers.GetPointer(v1beta1.Hostname("example.com")),
2045+
host2: helpers.GetPointer(v1beta1.Hostname("longerexample.com")),
20342046
host1Wins: false,
20352047
msg: "host2 longer than host1",
20362048
},
20372049
}
20382050

20392051
for _, tc := range tests {
2040-
g.Expect(listenerHostnameMoreSpecific(tc.host1, tc.host2)).To(Equal(tc.host1Wins), tc.msg)
2052+
t.Run(tc.msg, func(t *testing.T) {
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: 14 additions & 2 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"
@@ -405,20 +406,31 @@ func findAcceptedHostnames(listenerHostname *v1beta1.Hostname, routeHostnames []
405406
if hostname == "" {
406407
return true
407408
}
408-
return string(h) == hostname
409+
410+
routeHost := string(h)
411+
return routeHost == hostname || wildcardMatch(hostname, routeHost) || wildcardMatch(routeHost, hostname)
409412
}
410413

411414
var result []string
412415

413416
for _, h := range routeHostnames {
414417
if match(h) {
415-
result = append(result, string(h))
418+
if len(hostname) > len(h) {
419+
result = append(result, hostname)
420+
} else {
421+
result = append(result, string(h))
422+
}
416423
}
417424
}
418425

419426
return result
420427
}
421428

429+
// wildcardMatch checks if host1 is a wildcard host, and if so, checks if host2 is a match for that wildcard.
430+
func wildcardMatch(host1, host2 string) bool {
431+
return strings.HasPrefix(host1, "*.") && strings.HasSuffix(host2, strings.TrimPrefix(host1, "*"))
432+
}
433+
422434
func routeAllowedByListener(
423435
listener *Listener,
424436
routeNS,

internal/state/graph/httproute_test.go

Lines changed: 25 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,30 @@ 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+
},
12581283
}
12591284

12601285
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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func TestValidateHostname(t *testing.T) {
2424
},
2525
{
2626
hostname: "*.example.com",
27-
expectErr: true,
27+
expectErr: false,
2828
name: "wildcard hostname",
2929
},
3030
{

0 commit comments

Comments
 (0)