Fix hostname alignment checks for HTTPRoutes (#16300)

* Fix hostname alignment checks for HTTPRoutes
This commit is contained in:
Andrew Stucki 2023-02-17 13:18:11 -05:00 committed by GitHub
parent a958fb0376
commit 3a5981ab98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 291 additions and 13 deletions

View File

@ -17,6 +17,7 @@ type GatewayChainSynthesizer struct {
trustDomain string trustDomain string
suffix string suffix string
gateway *structs.APIGatewayConfigEntry gateway *structs.APIGatewayConfigEntry
hostname string
matchesByHostname map[string][]hostnameMatch matchesByHostname map[string][]hostnameMatch
tcpRoutes []structs.TCPRouteConfigEntry tcpRoutes []structs.TCPRouteConfigEntry
} }
@ -44,17 +45,17 @@ func (l *GatewayChainSynthesizer) AddTCPRoute(route structs.TCPRouteConfigEntry)
l.tcpRoutes = append(l.tcpRoutes, route) l.tcpRoutes = append(l.tcpRoutes, route)
} }
// SetHostname sets the base hostname for a listener that this is being synthesized for
func (l *GatewayChainSynthesizer) SetHostname(hostname string) {
l.hostname = hostname
}
// AddHTTPRoute takes a new route and flattens its rule matches out per hostname. // AddHTTPRoute takes a new route and flattens its rule matches out per hostname.
// This is required since a single route can specify multiple hostnames, and a // This is required since a single route can specify multiple hostnames, and a
// single hostname can be specified in multiple routes. Routing for a given // single hostname can be specified in multiple routes. Routing for a given
// hostname must behave based on the aggregate of all rules that apply to it. // hostname must behave based on the aggregate of all rules that apply to it.
func (l *GatewayChainSynthesizer) AddHTTPRoute(route structs.HTTPRouteConfigEntry) { func (l *GatewayChainSynthesizer) AddHTTPRoute(route structs.HTTPRouteConfigEntry) {
hostnames := route.Hostnames hostnames := route.FilteredHostnames(l.hostname)
if len(route.Hostnames) == 0 {
// add a wildcard if there are no explicit hostnames set
hostnames = append(hostnames, "*")
}
for _, host := range hostnames { for _, host := range hostnames {
matches, ok := l.matchesByHostname[host] matches, ok := l.matchesByHostname[host]
if !ok { if !ok {

View File

@ -459,6 +459,7 @@ func TestGatewayChainSynthesizer_AddHTTPRoute(t *testing.T) {
gatewayChainSynthesizer := NewGatewayChainSynthesizer(datacenter, "domain", "suffix", gateway) gatewayChainSynthesizer := NewGatewayChainSynthesizer(datacenter, "domain", "suffix", gateway)
gatewayChainSynthesizer.SetHostname("*")
gatewayChainSynthesizer.AddHTTPRoute(tc.route) gatewayChainSynthesizer.AddHTTPRoute(tc.route)
require.Equal(t, tc.expectedMatchesByHostname, gatewayChainSynthesizer.matchesByHostname) require.Equal(t, tc.expectedMatchesByHostname, gatewayChainSynthesizer.matchesByHostname)
@ -621,6 +622,8 @@ func TestGatewayChainSynthesizer_Synthesize(t *testing.T) {
for name, tc := range cases { for name, tc := range cases {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
tc.synthesizer.SetHostname("*")
for _, tcpRoute := range tc.tcpRoutes { for _, tcpRoute := range tc.tcpRoutes {
tc.synthesizer.AddTCPRoute(*tcpRoute) tc.synthesizer.AddTCPRoute(*tcpRoute)
} }

View File

@ -701,6 +701,14 @@ func (g *gatewayMeta) bindRoute(listener *structs.APIGatewayListener, bound *str
return false, nil return false, nil
} }
if route, ok := route.(*structs.HTTPRouteConfigEntry); ok {
// check our hostnames
hostnames := route.FilteredHostnames(listener.GetHostname())
if len(hostnames) == 0 {
return false, fmt.Errorf("failed to bind route to gateway %s: listener %s is does not have any hostnames that match the route", route.GetName(), g.Gateway.Name)
}
}
if listener.Protocol == route.GetProtocol() && bound.BindRoute(structs.ResourceReference{ if listener.Protocol == route.GetProtocol() && bound.BindRoute(structs.ResourceReference{
Kind: route.GetKind(), Kind: route.GetKind(),
Name: route.GetName(), Name: route.GetName(),

View File

@ -774,7 +774,7 @@ func (c *configSnapshotAPIGateway) ToIngress(datacenter string) (configSnapshotI
} }
// Create a synthesized discovery chain for each service. // Create a synthesized discovery chain for each service.
services, upstreams, compiled, err := c.synthesizeChains(datacenter, listener.Protocol, listener.Port, listener.Name, boundListener) services, upstreams, compiled, err := c.synthesizeChains(datacenter, listener, boundListener)
if err != nil { if err != nil {
return configSnapshotIngressGateway{}, err return configSnapshotIngressGateway{}, err
} }
@ -836,7 +836,7 @@ func (c *configSnapshotAPIGateway) ToIngress(datacenter string) (configSnapshotI
}, nil }, nil
} }
func (c *configSnapshotAPIGateway) synthesizeChains(datacenter string, protocol structs.APIGatewayListenerProtocol, port int, name string, boundListener structs.BoundAPIGatewayListener) ([]structs.IngressService, structs.Upstreams, []*structs.CompiledDiscoveryChain, error) { func (c *configSnapshotAPIGateway) synthesizeChains(datacenter string, listener structs.APIGatewayListener, boundListener structs.BoundAPIGatewayListener) ([]structs.IngressService, structs.Upstreams, []*structs.CompiledDiscoveryChain, error) {
chains := []*structs.CompiledDiscoveryChain{} chains := []*structs.CompiledDiscoveryChain{}
trustDomain := "" trustDomain := ""
@ -852,12 +852,13 @@ DOMAIN_LOOP:
} }
} }
synthesizer := discoverychain.NewGatewayChainSynthesizer(datacenter, trustDomain, name, c.GatewayConfig) synthesizer := discoverychain.NewGatewayChainSynthesizer(datacenter, trustDomain, listener.Name, c.GatewayConfig)
synthesizer.SetHostname(listener.GetHostname())
for _, routeRef := range boundListener.Routes { for _, routeRef := range boundListener.Routes {
switch routeRef.Kind { switch routeRef.Kind {
case structs.HTTPRoute: case structs.HTTPRoute:
route, ok := c.HTTPRoutes.Get(routeRef) route, ok := c.HTTPRoutes.Get(routeRef)
if !ok || protocol != structs.ListenerProtocolHTTP { if !ok || listener.Protocol != structs.ListenerProtocolHTTP {
continue continue
} }
synthesizer.AddHTTPRoute(*route) synthesizer.AddHTTPRoute(*route)
@ -869,7 +870,7 @@ DOMAIN_LOOP:
} }
case structs.TCPRoute: case structs.TCPRoute:
route, ok := c.TCPRoutes.Get(routeRef) route, ok := c.TCPRoutes.Get(routeRef)
if !ok || protocol != structs.ListenerProtocolTCP { if !ok || listener.Protocol != structs.ListenerProtocolTCP {
continue continue
} }
synthesizer.AddTCPRoute(*route) synthesizer.AddTCPRoute(*route)
@ -901,9 +902,9 @@ DOMAIN_LOOP:
DestinationNamespace: service.NamespaceOrDefault(), DestinationNamespace: service.NamespaceOrDefault(),
DestinationPartition: service.PartitionOrDefault(), DestinationPartition: service.PartitionOrDefault(),
IngressHosts: service.Hosts, IngressHosts: service.Hosts,
LocalBindPort: port, LocalBindPort: listener.Port,
Config: map[string]interface{}{ Config: map[string]interface{}{
"protocol": string(protocol), "protocol": string(listener.Protocol),
}, },
}) })
} }

View File

@ -897,6 +897,13 @@ type APIGatewayListener struct {
TLS APIGatewayTLSConfiguration TLS APIGatewayTLSConfiguration
} }
func (l APIGatewayListener) GetHostname() string {
if l.Hostname != "" {
return l.Hostname
}
return "*"
}
// APIGatewayTLSConfiguration specifies the configuration of a listeners // APIGatewayTLSConfiguration specifies the configuration of a listeners
// TLS settings. // TLS settings.
type APIGatewayTLSConfiguration struct { type APIGatewayTLSConfiguration struct {

View File

@ -2,6 +2,7 @@ package structs
import ( import (
"fmt" "fmt"
"strings"
"github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/acl"
) )
@ -121,6 +122,31 @@ func (e *HTTPRouteConfigEntry) GetRaftIndex() *RaftIndex {
return &e.RaftIndex return &e.RaftIndex
} }
func (e *HTTPRouteConfigEntry) FilteredHostnames(listenerHostname string) []string {
if len(e.Hostnames) == 0 {
// we have no hostnames specified here, so treat it like a wildcard
return []string{listenerHostname}
}
wildcardHostname := strings.ContainsRune(listenerHostname, '*') || listenerHostname == "*"
listenerHostname = strings.TrimPrefix(strings.TrimPrefix(listenerHostname, "*"), ".")
hostnames := []string{}
for _, hostname := range e.Hostnames {
if wildcardHostname {
if strings.HasSuffix(hostname, listenerHostname) {
hostnames = append(hostnames, hostname)
}
continue
}
if hostname == listenerHostname {
hostnames = append(hostnames, hostname)
}
}
return hostnames
}
// HTTPMatch specifies the criteria that should be // HTTPMatch specifies the criteria that should be
// used in determining whether or not a request should // used in determining whether or not a request should
// be routed to a given set of services. // be routed to a given set of services.

View File

@ -0,0 +1,3 @@
#!/bin/bash
snapshot_envoy_admin localhost:20000 api-gateway primary || true

View File

@ -0,0 +1,4 @@
services {
name = "api-gateway"
kind = "api-gateway"
}

View File

@ -0,0 +1,156 @@
#!/bin/bash
set -euo pipefail
upsert_config_entry primary '
kind = "api-gateway"
name = "api-gateway"
listeners = [
{
name = "listener-one"
port = 9999
protocol = "http"
hostname = "*.consul.example"
},
{
name = "listener-two"
port = 9998
protocol = "http"
hostname = "foo.bar.baz"
},
{
name = "listener-three"
port = 9997
protocol = "http"
hostname = "*.consul.example"
},
{
name = "listener-four"
port = 9996
protocol = "http"
hostname = "*.consul.example"
},
{
name = "listener-five"
port = 9995
protocol = "http"
hostname = "foo.bar.baz"
}
]
'
upsert_config_entry primary '
Kind = "proxy-defaults"
Name = "global"
Config {
protocol = "http"
}
'
upsert_config_entry primary '
kind = "http-route"
name = "api-gateway-route-one"
hostnames = ["test.consul.example"]
rules = [
{
services = [
{
name = "s1"
}
]
}
]
parents = [
{
name = "api-gateway"
sectionName = "listener-one"
},
]
'
upsert_config_entry primary '
kind = "http-route"
name = "api-gateway-route-two"
hostnames = ["foo.bar.baz"]
rules = [
{
services = [
{
name = "s1"
}
]
}
]
parents = [
{
name = "api-gateway"
sectionName = "listener-two"
},
]
'
upsert_config_entry primary '
kind = "http-route"
name = "api-gateway-route-three"
hostnames = ["foo.bar.baz"]
rules = [
{
services = [
{
name = "s1"
}
]
}
]
parents = [
{
name = "api-gateway"
sectionName = "listener-three"
},
]
'
upsert_config_entry primary '
kind = "http-route"
name = "api-gateway-route-four"
rules = [
{
services = [
{
name = "s1"
}
]
}
]
parents = [
{
name = "api-gateway"
sectionName = "listener-four"
},
]
'
upsert_config_entry primary '
kind = "http-route"
name = "api-gateway-route-five"
rules = [
{
services = [
{
name = "s1"
}
]
}
]
parents = [
{
name = "api-gateway"
sectionName = "listener-five"
},
]
'
register_services primary
gen_envoy_bootstrap api-gateway 20000 primary true
gen_envoy_bootstrap s1 19000

View File

@ -0,0 +1,3 @@
#!/bin/bash
export REQUIRED_SERVICES="$DEFAULT_REQUIRED_SERVICES api-gateway-primary"

View File

@ -0,0 +1,66 @@
#!/usr/bin/env bats
load helpers
@test "api gateway proxy admin is up on :20000" {
retry_default curl -f -s localhost:20000/stats -o /dev/null
}
@test "api gateway should have be accepted and not conflicted" {
assert_config_entry_status Accepted True Accepted primary api-gateway api-gateway
assert_config_entry_status Conflicted False NoConflict primary api-gateway api-gateway
}
@test "api gateway should be bound to route one" {
assert_config_entry_status Bound True Bound primary http-route api-gateway-route-one
assert_upstream_has_endpoints_in_status 127.0.0.1:20000 s1 HEALTHY 1
}
@test "api gateway should be bound to route two" {
assert_config_entry_status Bound True Bound primary http-route api-gateway-route-two
}
@test "api gateway should be unbound to route three" {
assert_config_entry_status Bound False FailedToBind primary http-route api-gateway-route-three
}
@test "api gateway should be bound to route four" {
assert_config_entry_status Bound True Bound primary http-route api-gateway-route-four
}
@test "api gateway should be bound to route five" {
assert_config_entry_status Bound True Bound primary http-route api-gateway-route-five
}
@test "api gateway should be able to connect to s1 via route one with the proper host" {
run retry_long curl -H "Host: test.consul.example" -s -f -d hello localhost:9999
[ "$status" -eq 0 ]
[[ "$output" == *"hello"* ]]
}
@test "api gateway should not be able to connect to s1 via route one with a mismatched host" {
run retry_default sh -c "curl -H \"Host: foo.consul.example\" -sI -o /dev/null -w \"%{http_code}\" localhost:9999 | grep 404"
[ "$status" -eq 0 ]
[[ "$output" == "404" ]]
}
@test "api gateway should be able to connect to s1 via route two with the proper host" {
run retry_long curl -H "Host: foo.bar.baz" -s -f -d hello localhost:9998
[ "$status" -eq 0 ]
[[ "$output" == *"hello"* ]]
}
@test "api gateway should be able to connect to s1 via route four with any subdomain of the listener host" {
run retry_long curl -H "Host: test.consul.example" -s -f -d hello localhost:9996
[ "$status" -eq 0 ]
[[ "$output" == *"hello"* ]]
run retry_long curl -H "Host: foo.consul.example" -s -f -d hello localhost:9996
[ "$status" -eq 0 ]
[[ "$output" == *"hello"* ]]
}
@test "api gateway should be able to connect to s1 via route five with the proper host" {
run retry_long curl -H "Host: foo.bar.baz" -s -f -d hello localhost:9995
[ "$status" -eq 0 ]
[[ "$output" == *"hello"* ]]
}