diff --git a/internal/gatewayapi/listener.go b/internal/gatewayapi/listener.go index c088de59b8..25332e70e3 100644 --- a/internal/gatewayapi/listener.go +++ b/internal/gatewayapi/listener.go @@ -384,6 +384,10 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR resource // refers to the Listener TLS. TLS: irTLSConfigsForTCPListener(&listener.tls), } + // TLS listeners support hostname-based (SNI) matching; TCP does not. + if listener.Protocol == gwapiv1.TLSProtocolType && listener.Hostname != nil { + irListener.Hostnames = append(irListener.Hostnames, string(*listener.Hostname)) + } xdsIR[irKey].TCP = append(xdsIR[irKey].TCP, irListener) case gwapiv1.UDPProtocolType: irListener := &ir.UDPListener{ diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-tls-terminate-and-passthrough.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-tls-terminate-and-passthrough.out.yaml index 5f5b7ae8e7..6bca320cfd 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-tls-terminate-and-passthrough.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-tls-terminate-and-passthrough.out.yaml @@ -249,6 +249,8 @@ xdsIR: tcp: - address: 0.0.0.0 externalPort: 90 + hostnames: + - foo.bar.com metadata: kind: Gateway name: gateway-1 diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-tls-terminate-and-tls-fingerprint.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-tls-terminate-and-tls-fingerprint.out.yaml index 64fe4897a6..60cb19da4e 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-tls-terminate-and-tls-fingerprint.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-tls-terminate-and-tls-fingerprint.out.yaml @@ -288,6 +288,8 @@ xdsIR: tcp: - address: 0.0.0.0 externalPort: 90 + hostnames: + - foo.bar.com metadata: kind: Gateway name: gateway-1 diff --git a/internal/gatewayapi/testdata/tcproute-attaching-to-gateway-with-listener-tls-terminate.out.yaml b/internal/gatewayapi/testdata/tcproute-attaching-to-gateway-with-listener-tls-terminate.out.yaml index 1950d01a59..4984412a3d 100644 --- a/internal/gatewayapi/testdata/tcproute-attaching-to-gateway-with-listener-tls-terminate.out.yaml +++ b/internal/gatewayapi/testdata/tcproute-attaching-to-gateway-with-listener-tls-terminate.out.yaml @@ -245,6 +245,8 @@ xdsIR: privateKey: '[redacted]' - address: 0.0.0.0 externalPort: 90 + hostnames: + - foo.bar.com metadata: kind: Gateway name: gateway-1 diff --git a/internal/gatewayapi/testdata/tlsroute-attaching-to-gateway.out.yaml b/internal/gatewayapi/testdata/tlsroute-attaching-to-gateway.out.yaml index 485933ce9e..a87ca2bfe2 100644 --- a/internal/gatewayapi/testdata/tlsroute-attaching-to-gateway.out.yaml +++ b/internal/gatewayapi/testdata/tlsroute-attaching-to-gateway.out.yaml @@ -123,6 +123,8 @@ xdsIR: tcp: - address: 0.0.0.0 externalPort: 90 + hostnames: + - foo.com metadata: kind: Gateway name: gateway-1 diff --git a/internal/gatewayapi/testdata/tlsroute-hostname-intersection.out.yaml b/internal/gatewayapi/testdata/tlsroute-hostname-intersection.out.yaml index 4b71d583fd..4cac37015f 100644 --- a/internal/gatewayapi/testdata/tlsroute-hostname-intersection.out.yaml +++ b/internal/gatewayapi/testdata/tlsroute-hostname-intersection.out.yaml @@ -247,6 +247,8 @@ xdsIR: tcp: - address: 0.0.0.0 externalPort: 90 + hostnames: + - '*.example.com' metadata: kind: Gateway name: gateway-1 @@ -344,6 +346,8 @@ xdsIR: tcp: - address: 0.0.0.0 externalPort: 90 + hostnames: + - '*.com' metadata: kind: Gateway name: gateway-2 diff --git a/internal/gatewayapi/testdata/tlsroute-invalid-no-matching-listener.out.yaml b/internal/gatewayapi/testdata/tlsroute-invalid-no-matching-listener.out.yaml index fd30c28ccc..6e0e04f784 100644 --- a/internal/gatewayapi/testdata/tlsroute-invalid-no-matching-listener.out.yaml +++ b/internal/gatewayapi/testdata/tlsroute-invalid-no-matching-listener.out.yaml @@ -563,6 +563,8 @@ xdsIR: tcp: - address: 0.0.0.0 externalPort: 443 + hostnames: + - www.example.com metadata: kind: Gateway name: gateway-tls-exact-hostname @@ -603,6 +605,8 @@ xdsIR: tcp: - address: 0.0.0.0 externalPort: 443 + hostnames: + - '*.example.com' metadata: kind: Gateway name: gateway-tls-wildcard-hostname diff --git a/internal/gatewayapi/testdata/tlsroute-invalid-reference-grant.out.yaml b/internal/gatewayapi/testdata/tlsroute-invalid-reference-grant.out.yaml index e2adbb9512..2300aa4549 100644 --- a/internal/gatewayapi/testdata/tlsroute-invalid-reference-grant.out.yaml +++ b/internal/gatewayapi/testdata/tlsroute-invalid-reference-grant.out.yaml @@ -126,6 +126,8 @@ xdsIR: tcp: - address: 0.0.0.0 externalPort: 443 + hostnames: + - '*.example.com' metadata: kind: Gateway name: gateway-tlsroute-referencegrant diff --git a/internal/gatewayapi/testdata/tlsroute-with-backend.out.yaml b/internal/gatewayapi/testdata/tlsroute-with-backend.out.yaml index 1ad3c66933..3265a861c1 100644 --- a/internal/gatewayapi/testdata/tlsroute-with-backend.out.yaml +++ b/internal/gatewayapi/testdata/tlsroute-with-backend.out.yaml @@ -142,6 +142,8 @@ xdsIR: tcp: - address: 0.0.0.0 externalPort: 90 + hostnames: + - foo.com metadata: kind: Gateway name: gateway-1 diff --git a/internal/gatewayapi/testdata/tlsroute-with-backendref-in-other-namespace-allowed-by-refgrant.out.yaml b/internal/gatewayapi/testdata/tlsroute-with-backendref-in-other-namespace-allowed-by-refgrant.out.yaml index e13998c5c5..abccf88da6 100644 --- a/internal/gatewayapi/testdata/tlsroute-with-backendref-in-other-namespace-allowed-by-refgrant.out.yaml +++ b/internal/gatewayapi/testdata/tlsroute-with-backendref-in-other-namespace-allowed-by-refgrant.out.yaml @@ -124,6 +124,8 @@ xdsIR: tcp: - address: 0.0.0.0 externalPort: 90 + hostnames: + - foo.com metadata: kind: Gateway name: gateway-1 diff --git a/internal/gatewayapi/testdata/tlsroute-with-tls-terminate-hostname-match.out.yaml b/internal/gatewayapi/testdata/tlsroute-with-tls-terminate-hostname-match.out.yaml index c29b380ec7..be908ba692 100644 --- a/internal/gatewayapi/testdata/tlsroute-with-tls-terminate-hostname-match.out.yaml +++ b/internal/gatewayapi/testdata/tlsroute-with-tls-terminate-hostname-match.out.yaml @@ -133,6 +133,8 @@ xdsIR: tcp: - address: 0.0.0.0 externalPort: 90 + hostnames: + - '*.example.com' metadata: kind: Gateway name: gateway-1 diff --git a/internal/gatewayapi/testdata/tlsroute-with-tls-terminate-invalid-hostname.out.yaml b/internal/gatewayapi/testdata/tlsroute-with-tls-terminate-invalid-hostname.out.yaml index ff076452c0..28de4e6b19 100644 --- a/internal/gatewayapi/testdata/tlsroute-with-tls-terminate-invalid-hostname.out.yaml +++ b/internal/gatewayapi/testdata/tlsroute-with-tls-terminate-invalid-hostname.out.yaml @@ -134,6 +134,8 @@ xdsIR: tcp: - address: 0.0.0.0 externalPort: 90 + hostnames: + - foo.example.com metadata: kind: Gateway name: gateway-1 diff --git a/internal/gatewayapi/testdata/tlsroute-with-tls-terminate-multiple-routes.out.yaml b/internal/gatewayapi/testdata/tlsroute-with-tls-terminate-multiple-routes.out.yaml index d710d69984..5c4fbdc1a9 100644 --- a/internal/gatewayapi/testdata/tlsroute-with-tls-terminate-multiple-routes.out.yaml +++ b/internal/gatewayapi/testdata/tlsroute-with-tls-terminate-multiple-routes.out.yaml @@ -201,6 +201,8 @@ xdsIR: tcp: - address: 0.0.0.0 externalPort: 90 + hostnames: + - '*.example.com' metadata: kind: Gateway name: gateway-1 diff --git a/internal/ir/xds.go b/internal/ir/xds.go index 5987fb8706..cb120199d8 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -2382,6 +2382,8 @@ func (s StringMatch) Validate() error { // +k8s:deepcopy-gen=true type TCPListener struct { CoreListenerDetails `json:",inline" yaml:",inline"` + // Hostnames (SNI/server name) with which the service can be expected to be accessed by clients. + Hostnames []string `json:"hostnames,omitempty" yaml:"hostnames,omitempty"` // TLS holds information for configuring TLS on a listener. TLS *TLSConfig `json:"tls,omitempty" yaml:"tls,omitempty"` // TCPKeepalive configuration for the listener diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index b9650fb876..1b60fa4648 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -4546,6 +4546,11 @@ func (in *TCPKeepalive) DeepCopy() *TCPKeepalive { func (in *TCPListener) DeepCopyInto(out *TCPListener) { *out = *in in.CoreListenerDetails.DeepCopyInto(&out.CoreListenerDetails) + if in.Hostnames != nil { + in, out := &in.Hostnames, &out.Hostnames + *out = make([]string, len(*in)) + copy(*out, *in) + } if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(TLSConfig) diff --git a/internal/xds/translator/testdata/in/xds-ir/https-and-tls-listener-no-routes-same-port.yaml b/internal/xds/translator/testdata/in/xds-ir/https-and-tls-listener-no-routes-same-port.yaml new file mode 100644 index 0000000000..f1ff544127 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/https-and-tls-listener-no-routes-same-port.yaml @@ -0,0 +1,38 @@ +http: +- name: "https-listener" + address: "0.0.0.0" + port: 10443 + hostnames: + - "*" + path: + mergeSlashes: true + escapedSlashesAction: UnescapeAndRedirect + tls: + alpnProtocols: + - h2 + - http/1.1 + certificates: + - name: secret-1 + certificate: [99, 101, 114, 116, 45, 100, 97, 116, 97] + privateKey: [107, 101, 121, 45, 100, 97, 116, 97] + routes: + - name: "first-route" + hostname: "*" + destination: + name: "first-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 + name: "first-route-dest/backend/0" +tcp: +- name: "tls-terminate-no-routes" + address: "0.0.0.0" + port: 10443 + hostnames: + - "repro.example.com" + tls: + certificates: + - name: secret-1 + certificate: [99, 101, 114, 116, 45, 100, 97, 116, 97] + privateKey: [107, 101, 121, 45, 100, 97, 116, 97] diff --git a/internal/xds/translator/testdata/out/xds-ir/https-and-tls-listener-no-routes-same-port.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/https-and-tls-listener-no-routes-same-port.clusters.yaml new file mode 100644 index 0000000000..c7e8a08281 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/https-and-tls-listener-no-routes-same-port.clusters.yaml @@ -0,0 +1,25 @@ +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: first-route-dest + ignoreHealthOnHostRemoval: true + loadBalancingPolicy: + policies: + - typedExtensionConfig: + name: envoy.load_balancing_policies.least_request + typedConfig: + '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest + localityLbConfig: + localityWeightedLbConfig: {} + name: first-route-dest + perConnectionBufferLimitBytes: 32768 + type: EDS +- name: EmptyCluster + type: STATIC diff --git a/internal/xds/translator/testdata/out/xds-ir/https-and-tls-listener-no-routes-same-port.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/https-and-tls-listener-no-routes-same-port.endpoints.yaml new file mode 100644 index 0000000000..3b3f2d0907 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/https-and-tls-listener-no-routes-same-port.endpoints.yaml @@ -0,0 +1,12 @@ +- clusterName: first-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: first-route-dest/backend/0 diff --git a/internal/xds/translator/testdata/out/xds-ir/https-and-tls-listener-no-routes-same-port.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/https-and-tls-listener-no-routes-same-port.listeners.yaml new file mode 100644 index 0000000000..5ed42bee02 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/https-and-tls-listener-no-routes-same-port.listeners.yaml @@ -0,0 +1,64 @@ +- address: + socketAddress: + address: 0.0.0.0 + portValue: 10443 + filterChains: + - filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + commonHttpProtocolOptions: + headersWithUnderscoresAction: REJECT_REQUEST + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 + maxConcurrentStreams: 100 + httpFilters: + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + suppressEnvoyHeaders: true + mergeSlashes: true + normalizePath: true + pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: https-listener + serverHeaderTransformation: PASS_THROUGH + statPrefix: https-10443 + useRemoteAddress: true + name: https-listener + transportSocket: + name: envoy.transport_sockets.tls + typedConfig: + '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext + commonTlsContext: + alpnProtocols: + - h2 + - http/1.1 + tlsCertificateSdsSecretConfigs: + - name: secret-1 + sdsConfig: + ads: {} + resourceApiVersion: V3 + disableStatefulSessionResumption: true + disableStatelessSessionResumption: true + - filterChainMatch: + serverNames: + - repro.example.com + filters: + - name: envoy.filters.network.tcp_proxy + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy + cluster: EmptyCluster + statPrefix: tls-passthrough-10443 + name: EmptyCluster/tls-terminate-no-routes + listenerFilters: + - name: envoy.filters.listener.tls_inspector + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector + maxConnectionsToAcceptPerSocketEvent: 1 + name: https-listener + perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/https-and-tls-listener-no-routes-same-port.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/https-and-tls-listener-no-routes-same-port.routes.yaml new file mode 100644 index 0000000000..bffd7ebfe3 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/https-and-tls-listener-no-routes-same-port.routes.yaml @@ -0,0 +1,14 @@ +- ignorePortInHostMatching: true + name: https-listener + virtualHosts: + - domains: + - '*' + name: https-listener/* + routes: + - match: + prefix: / + name: first-route + route: + cluster: first-route-dest + upgradeConfigs: + - upgradeType: websocket diff --git a/internal/xds/translator/testdata/out/xds-ir/https-and-tls-listener-no-routes-same-port.secrets.yaml b/internal/xds/translator/testdata/out/xds-ir/https-and-tls-listener-no-routes-same-port.secrets.yaml new file mode 100644 index 0000000000..32fd695927 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/https-and-tls-listener-no-routes-same-port.secrets.yaml @@ -0,0 +1,6 @@ +- name: secret-1 + tlsCertificate: + certificateChain: + inlineBytes: Y2VydC1kYXRh + privateKey: + inlineBytes: a2V5LWRhdGE= diff --git a/internal/xds/translator/translator.go b/internal/xds/translator/translator.go index ec4272e49d..3ef53f6092 100644 --- a/internal/xds/translator/translator.go +++ b/internal/xds/translator/translator.go @@ -869,9 +869,35 @@ func (t *Translator) processTCPListenerXdsTranslation( } } - // Only add the filter chain once per xDS listener; multiple IR listeners may share - // the same address/port and map to the same xDS listener. - if !emptyFilterChainAdded[xdsListener.Name] { + if len(tcpListener.Hostnames) > 0 { + // TLS listener with a specific hostname: give the empty filter chain a unique SNI + // match so it does not collide with other wildcard filter chains on the same xDS + // listener (e.g. an HTTPS listener with hostname "*"). + emptyTCPRoute := &ir.TCPRoute{ + Name: emptyClusterName + "/" + tcpListener.Name, + Destination: &ir.RouteDestination{ + Name: emptyClusterName, + }, + TLS: &ir.TLS{ + TLSInspectorConfig: &ir.TLSInspectorConfig{ + SNIs: tcpListener.Hostnames, + }, + }, + } + if err := t.addXdsTCPFilterChain( + xdsListener, + emptyTCPRoute, + emptyClusterName, + accesslog, + tcpListener.Timeout, + tcpListener.Connection, + tcpListener.TLS, + ); err != nil { + errs = errors.Join(errs, err) + } + } else if !emptyFilterChainAdded[xdsListener.Name] { + // No hostname: add one shared wildcard empty filter chain per xDS listener. + // Multiple IR listeners may share the same address/port and map to the same xDS listener. if err := t.addXdsTCPFilterChain( xdsListener, sharedEmptyTCPRoute, diff --git a/release-notes/current/bug_fixes/9342-tls-listener-no-routes-duplicate-filter-chain.md b/release-notes/current/bug_fixes/9342-tls-listener-no-routes-duplicate-filter-chain.md new file mode 100644 index 0000000000..7069d273ad --- /dev/null +++ b/release-notes/current/bug_fixes/9342-tls-listener-no-routes-duplicate-filter-chain.md @@ -0,0 +1 @@ +Fixed a duplicate filter chain matcher error when a TLS listener with a specific hostname and no routes shares a port with an HTTPS wildcard listener, which caused Envoy to reject the xDS config update.