Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add wildcard hostname support #769

Merged
merged 3 commits into from
Jun 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/gateway-api-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Fields:
* `gatewayClassName` - supported.
* `listeners`
* `name` - supported.
* `hostname` - partially supported. Wildcard hostnames like `*.example.com` are not yet supported.
* `hostname` - supported.
* `port` - supported.
* `protocol` - partially supported. Allowed values: `HTTP`, `HTTPS`.
* `tls`
Expand Down Expand Up @@ -101,7 +101,7 @@ Fields:
Fields:
* `spec`
* `parentRefs` - partially supported. Port not supported.
* `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.
* `hostnames` - supported.
* `rules`
* `matches`
* `path` - partially supported. Only `PathPrefix` and `Exact` types.
Expand Down
23 changes: 23 additions & 0 deletions examples/cafe-example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,26 @@ curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT
Server address: 10.12.0.19:80
Server name: tea-7cd44fcb4d-xfw2x
```

## 5. Using different hostnames

Traffic is allowed to `cafe.example.com` because the Gateway listener's hostname allows `*.example.com`. You can
change an HTTPRoute's hostname to something that matches this wildcard and still pass traffic.

For example, run the following command to open your editor and change the HTTPRoute's hostname to `foo.example.com`.

```
kubectl -n default edit httproute tea
```

Once changed, update the `curl` command above for the `tea` service to use the new hostname. Traffic should still pass successfully.

Likewise, if you change the Gateway listener's hostname to something else, you can prevent the HTTPRoute's traffic from passing successfully.

For example, run the following to open your editor and change the Gateway listener's hostname to `bar.example.com`:

```
kubectl -n default edit gateway gateway
```

Once changed, try running the same `curl` requests as above. They should be denied with a `404 Not Found`.
1 change: 1 addition & 0 deletions examples/cafe-example/gateway.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ spec:
- name: http
port: 80
protocol: HTTP
hostname: "*.example.com"
17 changes: 2 additions & 15 deletions internal/state/dataplane/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -567,20 +567,7 @@ func convertPathType(pathType v1beta1.PathMatchType) PathType {
}
}

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

return len(host1Str) >= len(host2Str)
return graph.GetMoreSpecificHostname(host1Str, host2Str) == host1Str
}
24 changes: 19 additions & 5 deletions internal/state/dataplane/configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1996,8 +1996,6 @@ func TestConvertPathType(t *testing.T) {
}

func TestHostnameMoreSpecific(t *testing.T) {
g := NewGomegaWithT(t)

tests := []struct {
host1 *v1beta1.Hostname
host2 *v1beta1.Hostname
Expand Down Expand Up @@ -2029,15 +2027,31 @@ func TestHostnameMoreSpecific(t *testing.T) {
msg: "host1 has value; host2 empty",
},
{
host1: helpers.GetPointer(v1beta1.Hostname("example.com")),
host1: helpers.GetPointer(v1beta1.Hostname("")),
host2: helpers.GetPointer(v1beta1.Hostname("example.com")),
host1Wins: false,
msg: "host2 has value; host1 empty",
},
{
host1: helpers.GetPointer(v1beta1.Hostname("foo.example.com")),
host2: helpers.GetPointer(v1beta1.Hostname("*.example.com")),
host1Wins: true,
msg: "host1 more specific than host2",
},
{
host1: helpers.GetPointer(v1beta1.Hostname("*.example.com")),
host2: helpers.GetPointer(v1beta1.Hostname("foo.example.com")),
host1Wins: false,
msg: "host2 longer than host1",
msg: "host2 more specific than host1",
},
}

for _, tc := range tests {
g.Expect(listenerHostnameMoreSpecific(tc.host1, tc.host2)).To(Equal(tc.host1Wins), tc.msg)
t.Run(tc.msg, func(t *testing.T) {
g := NewGomegaWithT(t)

g.Expect(listenerHostnameMoreSpecific(tc.host1, tc.host2)).To(Equal(tc.host1Wins))
sjberman marked this conversation as resolved.
Show resolved Hide resolved
})
}
}

Expand Down
2 changes: 1 addition & 1 deletion internal/state/graph/gateway_listener_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ func TestValidateListenerHostname(t *testing.T) {
},
{
hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("*.example.com")),
expectErr: true,
expectErr: false,
name: "wildcard hostname",
},
{
Expand Down
74 changes: 65 additions & 9 deletions internal/state/graph/httproute.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package graph
import (
"errors"
"fmt"
"strings"

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

match := func(h v1beta1.Hostname) bool {
if hostname == "" {
return true
}
return string(h) == hostname
}

var result []string

for _, h := range routeHostnames {
if match(h) {
result = append(result, string(h))
routeHost := string(h)
if match(hostname, routeHost) {
result = append(result, GetMoreSpecificHostname(hostname, routeHost))
}
}

return result
}

func match(listenerHost, routeHost string) bool {
if listenerHost == "" {
return true
}

if routeHost == listenerHost {
return true
}

wildcardMatch := func(host1, host2 string) bool {
return strings.HasPrefix(host1, "*.") && strings.HasSuffix(host2, strings.TrimPrefix(host1, "*"))
}

// check if listenerHost is a wildcard and routeHost matches
if wildcardMatch(listenerHost, routeHost) {
return true
}

// check if routeHost is a wildcard and listener matchess
return wildcardMatch(routeHost, listenerHost)
}

// GetMoreSpecificHostname returns the more specific hostname between the two inputs.
//
// This function assumes that the two hostnames match each other, either:
// - Exactly
// - One as a substring of the other
func GetMoreSpecificHostname(hostname1, hostname2 string) string {
if hostname1 == hostname2 {
return hostname1
}
if hostname1 == "" {
return hostname2
}
if hostname2 == "" {
return hostname1
}

// Compare if wildcards are present
if strings.HasPrefix(hostname1, "*.") {
if strings.HasPrefix(hostname2, "*.") {
subdomains1 := strings.Split(hostname1, ".")
subdomains2 := strings.Split(hostname2, ".")

// Compare number of subdomains
if len(subdomains1) > len(subdomains2) {
return hostname1
}

return hostname2
kate-osborn marked this conversation as resolved.
Show resolved Hide resolved
}

return hostname2
}
if strings.HasPrefix(hostname2, "*.") {
return hostname1
}

return ""
}

func routeAllowedByListener(
listener *Listener,
routeNS,
Expand Down
31 changes: 31 additions & 0 deletions internal/state/graph/httproute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1217,6 +1217,7 @@ func TestBindRouteToListeners(t *testing.T) {
func TestFindAcceptedHostnames(t *testing.T) {
var listenerHostnameFoo v1beta1.Hostname = "foo.example.com"
var listenerHostnameCafe v1beta1.Hostname = "cafe.example.com"
var listenerHostnameWildcard v1beta1.Hostname = "*.example.com"
routeHostnames := []v1beta1.Hostname{"foo.example.com", "bar.example.com"}

tests := []struct {
Expand Down Expand Up @@ -1255,6 +1256,36 @@ func TestFindAcceptedHostnames(t *testing.T) {
expected: []string{wildcardHostname},
msg: "both listener and route have empty hostnames",
},
{
listenerHostname: &listenerHostnameWildcard,
routeHostnames: routeHostnames,
expected: []string{"foo.example.com", "bar.example.com"},
msg: "listener wildcard hostname",
},
{
listenerHostname: &listenerHostnameFoo,
routeHostnames: []v1beta1.Hostname{"*.example.com"},
expected: []string{"foo.example.com"},
msg: "route wildcard hostname; specific listener hostname",
},
{
listenerHostname: &listenerHostnameWildcard,
routeHostnames: nil,
expected: []string{"*.example.com"},
msg: "listener wildcard hostname; nil route hostname",
},
{
listenerHostname: nil,
routeHostnames: []v1beta1.Hostname{"*.example.com"},
expected: []string{"*.example.com"},
msg: "route wildcard hostname; nil listener hostname",
sjberman marked this conversation as resolved.
Show resolved Hide resolved
},
{
listenerHostname: &listenerHostnameWildcard,
routeHostnames: []v1beta1.Hostname{"*.bar.example.com"},
expected: []string{"*.bar.example.com"},
msg: "route and listener wildcard hostnames",
},
}

for _, test := range tests {
Expand Down
9 changes: 7 additions & 2 deletions internal/state/graph/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@ func validateHostname(hostname string) error {
return errors.New("cannot be empty string")
}

if strings.Contains(hostname, "*") {
return errors.New("wildcards are not supported")
if strings.HasPrefix(hostname, "*.") {
msgs := validation.IsWildcardDNS1123Subdomain(hostname)
if len(msgs) > 0 {
combined := strings.Join(msgs, ",")
return errors.New(combined)
}
return nil
}

msgs := validation.IsDNS1123Subdomain(hostname)
Expand Down
7 changes: 6 additions & 1 deletion internal/state/graph/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,19 @@ func TestValidateHostname(t *testing.T) {
},
{
hostname: "*.example.com",
expectErr: true,
expectErr: false,
sjberman marked this conversation as resolved.
Show resolved Hide resolved
name: "wildcard hostname",
},
{
hostname: "example$com",
expectErr: true,
name: "invalid hostname",
},
{
hostname: "*.example.*.com",
expectErr: true,
name: "invalid wildcard hostname",
},
}

for _, test := range tests {
Expand Down