diff --git a/changelogs/unreleased/7306-sunjayBhatia-small.md b/changelogs/unreleased/7306-sunjayBhatia-small.md new file mode 100644 index 00000000000..05ec262262c --- /dev/null +++ b/changelogs/unreleased/7306-sunjayBhatia-small.md @@ -0,0 +1 @@ +For a Gateway HTTPS Listeners with unspecified (catchall/wildcard) hostnames, we add a wildcard filter chain. This is to ensure we get a routing failure HTTP response (404) when a request is received that is for an "unknown" fqdn, rather than a connection error because Envoy does not have a filter chain to match. diff --git a/internal/dag/builder_test.go b/internal/dag/builder_test.go index 6e594e11b2d..b62cbe95e94 100644 --- a/internal/dag/builder_test.go +++ b/internal/dag/builder_test.go @@ -1633,6 +1633,12 @@ func TestDAGInsertGatewayAPI(t *testing.T) { &Listener{ Name: "https-443", SecureVirtualHosts: securevirtualhosts( + &SecureVirtualHost{ + VirtualHost: VirtualHost{ + Name: "*", + }, + Secret: secret(sec1), + }, &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "test.projectcontour.io", @@ -1682,6 +1688,12 @@ func TestDAGInsertGatewayAPI(t *testing.T) { &Listener{ Name: "https-443", SecureVirtualHosts: securevirtualhosts( + &SecureVirtualHost{ + VirtualHost: VirtualHost{ + Name: "*", + }, + Secret: secret(sec1), + }, &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "test.projectcontour.io", @@ -1791,6 +1803,12 @@ func TestDAGInsertGatewayAPI(t *testing.T) { &Listener{ Name: "https-443", SecureVirtualHosts: securevirtualhosts( + &SecureVirtualHost{ + VirtualHost: VirtualHost{ + Name: "*", + }, + Secret: secret(sec2), + }, &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "test.projectcontour.io", @@ -1841,6 +1859,12 @@ func TestDAGInsertGatewayAPI(t *testing.T) { &Listener{ Name: "https-443", SecureVirtualHosts: securevirtualhosts( + &SecureVirtualHost{ + VirtualHost: VirtualHost{ + Name: "*", + }, + Secret: secret(sec2), + }, &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "test.projectcontour.io", @@ -2112,6 +2136,12 @@ func TestDAGInsertGatewayAPI(t *testing.T) { &Listener{ Name: "https-443", SecureVirtualHosts: securevirtualhosts( + &SecureVirtualHost{ + VirtualHost: VirtualHost{ + Name: "*", + }, + Secret: secret(sec1), + }, &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "test.projectcontour.io", @@ -5027,6 +5057,12 @@ func TestDAGInsertGatewayAPI(t *testing.T) { &Listener{ Name: "https-443", SecureVirtualHosts: securevirtualhosts( + &SecureVirtualHost{ + VirtualHost: VirtualHost{ + Name: "*", + }, + Secret: secret(sec1), + }, &SecureVirtualHost{ VirtualHost: VirtualHost{ Name: "test.projectcontour.io", diff --git a/internal/dag/dag.go b/internal/dag/dag.go index 0e75755abfd..1de21496392 100644 --- a/internal/dag/dag.go +++ b/internal/dag/dag.go @@ -904,8 +904,9 @@ type AuthorizationServerBufferSettings struct { func (s *SecureVirtualHost) Valid() bool { // A SecureVirtualHost is valid if either // 1. it has a secret and at least one route. + // 1. it has a secret and hostname '*' to ensure TLS is terminated for a wildcard/catchall Listener // 2. it has a tcpproxy, because the tcpproxy backend may negotiate TLS itself. - return (s.Secret != nil && len(s.Routes) > 0) || s.TCPProxy != nil + return (s.Secret != nil && (len(s.Routes) > 0 || s.Name == "*")) || s.TCPProxy != nil } // A Listener represents a TCP socket that accepts diff --git a/internal/dag/dag_test.go b/internal/dag/dag_test.go index 4aa7555f283..9a703a7362f 100644 --- a/internal/dag/dag_test.go +++ b/internal/dag/dag_test.go @@ -72,6 +72,14 @@ func TestSecureVirtualHostValid(t *testing.T) { TCPProxy: new(TCPProxy), } assert.True(t, vh.Valid()) + + vh = SecureVirtualHost{ + VirtualHost: VirtualHost{ + Name: "*", + }, + Secret: new(Secret), + } + assert.True(t, vh.Valid()) } func TestPeerValidationContext(t *testing.T) { diff --git a/internal/dag/gatewayapi_processor.go b/internal/dag/gatewayapi_processor.go index e8dd659ed5b..d58e93b0622 100644 --- a/internal/dag/gatewayapi_processor.go +++ b/internal/dag/gatewayapi_processor.go @@ -593,6 +593,17 @@ func (p *GatewayAPIProcessor) computeListener( // routes to be bound to this listener since it can't serve TLS traffic. return info } + + // We have a valid Listener with a valid TLS Secret and empty + // (catchall) hostname. + // Always add a wildcard secure virtualhost to ensure requests that do + // not match any attached routes get a 404 response (rather than a + // connection error because Envoy does not have a filter chain match). + if ptr.Deref(listener.Hostname, "") == "" { + svh := p.dag.EnsureSecureVirtualHost(validateListenersResult.ListenerNames[string(listener.Name)], "*") + svh.Secret = listenerSecret + + } case gatewayapi_v1.TLSProtocolType: // The TLS protocol is used for TCP traffic encrypted with TLS. // Gateway API allows TLS to be either terminated at the proxy diff --git a/internal/featuretests/v3/httproute_test.go b/internal/featuretests/v3/httproute_test.go index e87c4771588..ac397dfbc69 100644 --- a/internal/featuretests/v3/httproute_test.go +++ b/internal/featuretests/v3/httproute_test.go @@ -173,6 +173,9 @@ func TestGateway_TLS(t *testing.T) { filterchaintls("test.projectcontour.io", sec1, httpsFilterForGateway("https-443", "test.projectcontour.io"), nil, "h2", "http/1.1"), + filterchaintls("*", sec1, + httpsFilterForGateway("https-443", "*"), + nil, "h2", "http/1.1"), ), SocketOptions: envoy_v3.NewSocketOptions().TCPKeepalive().Build(), }, diff --git a/internal/featuretests/v3/listeners_test.go b/internal/featuretests/v3/listeners_test.go index dc6c0a620f1..2fe5512005c 100644 --- a/internal/featuretests/v3/listeners_test.go +++ b/internal/featuretests/v3/listeners_test.go @@ -1501,6 +1501,9 @@ func TestGatewayListenersSetAddress(t *testing.T) { filterchaintls("test.projectcontour.io", tlssecret, httpsFilterForGateway("https-443", "test.projectcontour.io"), nil, "h2", "http/1.1"), + filterchaintls("*", tlssecret, + httpsFilterForGateway("https-443", "*"), + nil, "h2", "http/1.1"), ), SocketOptions: envoy_v3.NewSocketOptions().TCPKeepalive().Build(), }, diff --git a/test/conformance/gatewayapi/gateway_conformance_test.go b/test/conformance/gatewayapi/gateway_conformance_test.go index 0c76a8a6834..439f18cd75f 100644 --- a/test/conformance/gatewayapi/gateway_conformance_test.go +++ b/test/conformance/gatewayapi/gateway_conformance_test.go @@ -91,13 +91,6 @@ func TestGatewayConformance(t *testing.T) { // created. // See: https://github.com/kubernetes-sigs/gateway-api/issues/2592 tests.HTTPRouteInvalidParentRefSectionNameNotMatchingPort.ShortName, - - // This test currently fails since we do not program any filter chain - // for a Gateway Listener that has no attached routes. The test - // includes a TLS Listener with no hostname specified and the test - // sends a request for an unknown (to Contour/Envoy) host which fails - // instead of returning a 404. - tests.HTTPRouteHTTPSListener.ShortName, }, ExemptFeatures: sets.New( features.SupportMesh,