From be3ba067ee6b0f0cbccd23d539dbedb3a85a85e5 Mon Sep 17 00:00:00 2001 From: Steve Kriss Date: Fri, 21 Jul 2023 12:41:16 -0600 Subject: [PATCH] Gateway API: support TLS termination with TLSRoute/TCPRoute (#5481) Adds support for TLS termination with the TLS listener protocol. Envoy is configured to terminate TLS and then to proxy TCP traffic to the backend. Closes #5461. Signed-off-by: Steve Kriss --- changelogs/unreleased/5481-skriss-minor.md | 22 ++ internal/dag/builder_test.go | 5 +- internal/dag/gatewayapi_processor.go | 42 +-- internal/dag/status_test.go | 298 +++++++++++++++------ internal/featuretests/v3/tcproute_test.go | 103 +++++++ internal/featuretests/v3/tlsroute_test.go | 135 +++++++++- internal/xdscache/v3/listener.go | 3 +- 7 files changed, 498 insertions(+), 110 deletions(-) create mode 100644 changelogs/unreleased/5481-skriss-minor.md diff --git a/changelogs/unreleased/5481-skriss-minor.md b/changelogs/unreleased/5481-skriss-minor.md new file mode 100644 index 00000000000..9e54682604f --- /dev/null +++ b/changelogs/unreleased/5481-skriss-minor.md @@ -0,0 +1,22 @@ +## Gateway API: Support TLS termination with TLSRoute and TCPRoute + +Contour now supports using TLSRoute and TCPRoute in combination with TLS termination. +To use this feature, create a Gateway with a Listener like the following: + +```yaml +- name: tls-listener + protocol: TLS + port: 5000 + tls: + mode: Terminate + certificateRefs: + - name: tls-cert-secret + allowedRoutes: + namespaces: + from: All +--- +``` + +It is then possible to attach either 1+ TLSRoutes, or a single TCPRoute, to this Listener. +If using TLSRoute, traffic can be routed to a different backend based on SNI. +If using TCPRoute, all traffic is forwarded to the backend referenced in the route. \ No newline at end of file diff --git a/internal/dag/builder_test.go b/internal/dag/builder_test.go index 64ad2797631..78de3300116 100644 --- a/internal/dag/builder_test.go +++ b/internal/dag/builder_test.go @@ -921,7 +921,7 @@ func TestDAGInsertGatewayAPI(t *testing.T) { }, want: listeners(), }, - "TLS Listener with TLS.Mode=Terminate is invalid": { + "TLS Listener with TLS.Mode=Terminate is invalid if certificateRef is not specified": { gatewayclass: validClass, gateway: &gatewayapi_v1beta1.Gateway{ ObjectMeta: metav1.ObjectMeta{ @@ -935,9 +935,6 @@ func TestDAGInsertGatewayAPI(t *testing.T) { Protocol: gatewayapi_v1beta1.TLSProtocolType, TLS: &gatewayapi_v1beta1.GatewayTLSConfig{ Mode: ref.To(gatewayapi_v1beta1.TLSModeTerminate), - CertificateRefs: []gatewayapi_v1beta1.SecretObjectReference{ - gatewayapi.CertificateRef(sec1.Name, sec1.Namespace), - }, }, AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ diff --git a/internal/dag/gatewayapi_processor.go b/internal/dag/gatewayapi_processor.go index 5b1a0296e9f..8ad2094987c 100644 --- a/internal/dag/gatewayapi_processor.go +++ b/internal/dag/gatewayapi_processor.go @@ -544,25 +544,27 @@ func (p *GatewayAPIProcessor) computeListener( case gatewayapi_v1beta1.TLSProtocolType: // The TLS protocol is used for TCP traffic encrypted with TLS. // Gateway API allows TLS to be either terminated at the proxy - // or passed through to the backend, but the former requires using - // TCPRoute to route traffic since the underlying protocol is TCP - // not HTTP, which Contour doesn't support. Therefore, we only - // support "Passthrough" with the TLS protocol, which requires - // the use of TLSRoute to route to backends since the traffic is - // still encrypted. - + // or passed through to the backend. if listener.TLS == nil { addInvalidListenerCondition(fmt.Sprintf("Listener.TLS is required when protocol is %q.", listener.Protocol)) return false, nil } - if listener.TLS.Mode == nil || *listener.TLS.Mode != gatewayapi_v1beta1.TLSModePassthrough { - addInvalidListenerCondition(fmt.Sprintf("Listener.TLS.Mode must be %q when protocol is %q.", gatewayapi_v1beta1.TLSModePassthrough, listener.Protocol)) - return false, nil - } - - if len(listener.TLS.CertificateRefs) != 0 { - addInvalidListenerCondition(fmt.Sprintf("Listener.TLS.CertificateRefs cannot be defined when Listener.TLS.Mode is %q.", gatewayapi_v1beta1.TLSModePassthrough)) + switch { + case listener.TLS.Mode == nil || *listener.TLS.Mode == gatewayapi_v1beta1.TLSModeTerminate: + // Resolve the TLS secret. + if listenerSecret = p.resolveListenerSecret(listener.TLS.CertificateRefs, string(listener.Name), gwAccessor); listenerSecret == nil { + // If TLS was configured on the Listener, but the secret ref is invalid, don't allow any + // routes to be bound to this listener since it can't serve TLS traffic. + return false, nil + } + case *listener.TLS.Mode == gatewayapi_v1beta1.TLSModePassthrough: + if len(listener.TLS.CertificateRefs) != 0 { + addInvalidListenerCondition(fmt.Sprintf("Listener.TLS.CertificateRefs cannot be defined when Listener.TLS.Mode is %q.", gatewayapi_v1beta1.TLSModePassthrough)) + return false, nil + } + default: + addInvalidListenerCondition(fmt.Sprintf("Listener.TLS.Mode must be %q or %q.", gatewayapi_v1beta1.TLSModeTerminate, gatewayapi_v1beta1.TLSModePassthrough)) return false, nil } } @@ -588,7 +590,7 @@ func (p *GatewayAPIProcessor) getListenerRouteKinds(listener gatewayapi_v1beta1. case gatewayapi_v1beta1.HTTPSProtocolType: return []gatewayapi_v1beta1.Kind{KindHTTPRoute, KindGRPCRoute} case gatewayapi_v1beta1.TLSProtocolType: - return []gatewayapi_v1beta1.Kind{KindTLSRoute} + return []gatewayapi_v1beta1.Kind{KindTLSRoute, KindTCPRoute} case gatewayapi_v1beta1.TCPProtocolType: return []gatewayapi_v1beta1.Kind{KindTCPRoute} } @@ -627,7 +629,7 @@ func (p *GatewayAPIProcessor) getListenerRouteKinds(listener gatewayapi_v1beta1. ) continue } - if routeKind.Kind == KindTCPRoute && listener.Protocol != gatewayapi_v1beta1.TCPProtocolType { + if routeKind.Kind == KindTCPRoute && listener.Protocol != gatewayapi_v1beta1.TCPProtocolType && listener.Protocol != gatewayapi_v1beta1.TLSProtocolType { gwAccessor.AddListenerCondition( string(listener.Name), gatewayapi_v1beta1.ListenerConditionResolvedRefs, @@ -1654,7 +1656,13 @@ func (p *GatewayAPIProcessor) computeTCPRouteForListener(route *gatewayapi_v1alp return false } - p.dag.Listeners[listener.dagListenerName].TCPProxy = &proxy + if listener.tlsSecret != nil { + secure := p.dag.EnsureSecureVirtualHost(listener.dagListenerName, "*") + secure.Secret = listener.tlsSecret + secure.TCPProxy = &proxy + } else { + p.dag.Listeners[listener.dagListenerName].TCPProxy = &proxy + } return true } diff --git a/internal/dag/status_test.go b/internal/dag/status_test.go index 80318006d1f..37c12c09306 100644 --- a/internal/dag/status_test.go +++ b/internal/dag/status_test.go @@ -5061,25 +5061,39 @@ func TestDAGStatus(t *testing.T) { } -func validGatewayStatusUpdate(listenerName string, kind gatewayapi_v1beta1.Kind, attachedRoutes int) []*status.GatewayStatusUpdate { - // This applies to tests that the listener doesn't have allowed kind configured - // hence the wanted allowed kind is determined by the listener protocol only. +func validGatewayStatusUpdate(listenerName string, listenerProtocol gatewayapi_v1beta1.ProtocolType, attachedRoutes int) []*status.GatewayStatusUpdate { var supportedKinds []gatewayapi_v1beta1.RouteGroupKind - if kind == "HTTPRoute" || kind == "GRPCRoute" { - supportedKinds = append(supportedKinds, gatewayapi_v1beta1.RouteGroupKind{ - Group: ref.To(gatewayapi_v1beta1.Group(gatewayapi_v1beta1.GroupName)), - Kind: "HTTPRoute", - }) - supportedKinds = append(supportedKinds, gatewayapi_v1beta1.RouteGroupKind{ - Group: ref.To(gatewayapi_v1beta1.Group(gatewayapi_v1beta1.GroupName)), - Kind: "GRPCRoute", - }) - } else { - supportedKinds = append(supportedKinds, gatewayapi_v1beta1.RouteGroupKind{ - Group: ref.To(gatewayapi_v1beta1.Group(gatewayapi_v1beta1.GroupName)), - Kind: kind, - }) + switch listenerProtocol { + case gatewayapi_v1beta1.HTTPProtocolType, gatewayapi_v1beta1.HTTPSProtocolType: + supportedKinds = append(supportedKinds, + gatewayapi_v1beta1.RouteGroupKind{ + Group: ref.To(gatewayapi_v1beta1.Group(gatewayapi_v1beta1.GroupName)), + Kind: KindHTTPRoute, + }, + gatewayapi_v1beta1.RouteGroupKind{ + Group: ref.To(gatewayapi_v1beta1.Group(gatewayapi_v1beta1.GroupName)), + Kind: KindGRPCRoute, + }, + ) + case gatewayapi_v1beta1.TLSProtocolType: + supportedKinds = append(supportedKinds, + gatewayapi_v1beta1.RouteGroupKind{ + Group: ref.To(gatewayapi_v1beta1.Group(gatewayapi_v1beta1.GroupName)), + Kind: KindTLSRoute, + }, + gatewayapi_v1beta1.RouteGroupKind{ + Group: ref.To(gatewayapi_v1beta1.Group(gatewayapi_v1beta1.GroupName)), + Kind: KindTCPRoute, + }, + ) + case gatewayapi_v1beta1.TCPProtocolType: + supportedKinds = append(supportedKinds, + gatewayapi_v1beta1.RouteGroupKind{ + Group: ref.To(gatewayapi_v1beta1.Group(gatewayapi_v1beta1.GroupName)), + Kind: KindTCPRoute, + }, + ) } return []*status.GatewayStatusUpdate{ @@ -5311,7 +5325,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 1), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 1), }) run(t, "simple httproute with backendref namespace matching route's explicitly specified", testcase{ @@ -5357,7 +5371,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }}, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 1), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 1), }) run(t, "multiple httproutes", testcase{ @@ -5425,7 +5439,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }, }, - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 2), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 2), }) run(t, "prefix path match not starting with '/' for httproute", testcase{ @@ -5472,7 +5486,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 0), }) run(t, "exact path match not starting with '/' for httproute", testcase{ objs: []any{ @@ -5518,7 +5532,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 0), }) run(t, "regular expression path match with invalid value for httproute", testcase{ @@ -5560,7 +5574,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 0), }) run(t, "prefix path match with consecutive '/' characters for httproute", testcase{ @@ -5607,7 +5621,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 0), }) run(t, "exact path match with consecutive '/' characters for httproute", testcase{ @@ -5654,7 +5668,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 0), }) run(t, "invalid path match type for httproute", testcase{ @@ -5700,7 +5714,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 0), }) run(t, "invalid header match type not supported for httproute", testcase{ @@ -5753,7 +5767,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 0), }) run(t, "regular expression header match with invalid value for httproute", testcase{ @@ -5806,7 +5820,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 0), }) run(t, "regular expression query param match with valid value for httproute", testcase{ @@ -5859,7 +5873,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 1), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 1), }) run(t, "regular expression query param match with invalid value for httproute", testcase{ @@ -5912,7 +5926,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 0), }) run(t, "query param match with invalid type for httproute", testcase{ @@ -5965,7 +5979,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 0), }) run(t, "spec.rules.backendRef.name not specified", testcase{ @@ -6011,7 +6025,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }}, // This still results in an attached route because it returns a 404. - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 1), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 1), }) run(t, "spec.rules.backendRef.serviceName invalid on two matches", testcase{ @@ -6060,7 +6074,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }}, // This still results in an attached route because it returns a 404. - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 1), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 1), }) run(t, "spec.rules.backendRef.port not specified", testcase{ @@ -6106,7 +6120,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }}, // This still results in an attached route because it returns a 404. - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 1), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 1), }) run(t, "spec.rules.backendRefs not specified", testcase{ @@ -6141,7 +6155,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 0), }) run(t, "spec.rules.backendRef.namespace does not match route", testcase{ @@ -6191,7 +6205,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }}, // This still results in an attached route because it returns a 404. - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 1), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 1), }) // BEGIN TLS CertificateRef + ReferenceGrant tests @@ -6247,7 +6261,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }, }, - wantGatewayStatusUpdate: validGatewayStatusUpdate("https", "HTTPRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate("https", gatewayapi_v1beta1.HTTPSProtocolType, 0), }) run(t, "Gateway references TLS cert in different namespace, with no ReferenceGrant", testcase{ @@ -6388,7 +6402,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }, }, - wantGatewayStatusUpdate: validGatewayStatusUpdate("https", "HTTPRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate("https", gatewayapi_v1beta1.HTTPSProtocolType, 0), }) run(t, "Gateway references TLS cert in different namespace, with invalid ReferenceGrant (policy in wrong namespace)", testcase{ @@ -6938,7 +6952,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 0), }) run(t, "spec.rules.hostname: invalid hostname", testcase{ @@ -6980,7 +6994,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 0), }) run(t, "spec.rules.hostname: invalid hostname, ip address", testcase{ @@ -7020,7 +7034,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 0), }) run(t, "two HTTP listeners, route's hostname intersects with one of them", testcase{ @@ -7740,7 +7754,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }, }, - wantGatewayStatusUpdate: validGatewayStatusUpdate("listener-1", "HTTPRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate("listener-1", gatewayapi_v1beta1.HTTPProtocolType, 0), }) run(t, "More than one RequestMirror filters in HTTPRoute.Spec.Rules.Filters", testcase{ @@ -7790,7 +7804,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }}, // Invalid filters still result in an attached route. - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 1), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 1), }) run(t, "Invalid RequestMirror filter due to unspecified backendRef.name", testcase{ @@ -7838,7 +7852,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }}, // This still results in an attached route because it returns a 404. - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 1), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 1), }) run(t, "Invalid RequestMirror filter due to unspecified backendRef.port", testcase{ @@ -7886,7 +7900,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }}, // This still results in an attached route because it returns a 404. - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 1), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 1), }) run(t, "Invalid RequestMirror filter due to invalid backendRef.name on two matches", testcase{ @@ -7951,7 +7965,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }}, // This still results in an attached route because it returns a 404. - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 1), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 1), }) run(t, "Invalid RequestMirror filter due to unmatched backendRef.namespace", testcase{ @@ -8003,7 +8017,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }}, // This still results in an attached route because it returns a 404. - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 1), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 1), }) run(t, "HTTPRouteFilterRequestMirror not yet supported for httproute backendref", testcase{ @@ -8055,7 +8069,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }}, // Invalid filters still result in an attached route. - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 1), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 1), }) run(t, "HTTPRouteFilterURLRewrite with custom HTTPPathModifierType is not supported", testcase{ @@ -8105,7 +8119,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }}, // Invalid filters still result in an attached route. - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 1), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 1), }) run(t, "Invalid RequestHeaderModifier due to duplicated headers", testcase{ @@ -8151,7 +8165,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }}, // Invalid filters still result in an attached route. - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 1), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 1), }) run(t, "Invalid RequestHeaderModifier after forward due to invalid headers", testcase{ @@ -8204,7 +8218,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }}, // Invalid filters still result in an attached route. - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 1), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 1), }) run(t, "Invalid ResponseHeaderModifier due to duplicated headers", testcase{ @@ -8250,7 +8264,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }}, // Invalid filters still result in an attached route. - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 1), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 1), }) run(t, "Invalid ResponseHeaderModifier on backend due to invalid headers", testcase{ @@ -8303,7 +8317,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }}, // Invalid filters still result in an attached route. - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 1), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 1), }) run(t, "custom filter type is not supported", testcase{ @@ -8348,7 +8362,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, }}, // Invalid filters still result in an attached route. - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "HTTPRoute", 1), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 1), }) run(t, "gateway.spec.addresses results in invalid gateway", testcase{ @@ -8917,6 +8931,10 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { Group: ref.To(gatewayapi_v1beta1.Group(gatewayapi_v1beta1.GroupName)), Kind: "TLSRoute", }, + { + Group: ref.To(gatewayapi_v1beta1.Group(gatewayapi_v1beta1.GroupName)), + Kind: "TCPRoute", + }, }, Conditions: []metav1.Condition{ { @@ -8982,6 +9000,10 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { Group: ref.To(gatewayapi_v1beta1.Group(gatewayapi_v1beta1.GroupName)), Kind: "TLSRoute", }, + { + Group: ref.To(gatewayapi_v1beta1.Group(gatewayapi_v1beta1.GroupName)), + Kind: "TCPRoute", + }, }, Conditions: []metav1.Condition{ { @@ -9002,7 +9024,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }}, }) - run(t, "TLS listener with TLS.Mode=Terminate results in a listener condition", testcase{ + run(t, "TLS listener with TLS.Mode=Terminate without a certificate ref results in a listener condition", testcase{ objs: []any{}, gateway: &gatewayapi_v1beta1.Gateway{ ObjectMeta: metav1.ObjectMeta{ @@ -9021,9 +9043,6 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { }, TLS: &gatewayapi_v1beta1.GatewayTLSConfig{ Mode: ref.To(gatewayapi_v1beta1.TLSModeTerminate), - CertificateRefs: []gatewayapi_v1beta1.SecretObjectReference{ - gatewayapi.CertificateRef("tlscert", "projectcontour"), - }, }, }}, }, @@ -9047,13 +9066,17 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) { Group: ref.To(gatewayapi_v1beta1.Group(gatewayapi_v1beta1.GroupName)), Kind: "TLSRoute", }, + { + Group: ref.To(gatewayapi_v1beta1.Group(gatewayapi_v1beta1.GroupName)), + Kind: "TCPRoute", + }, }, Conditions: []metav1.Condition{ { Type: string(gatewayapi_v1beta1.ListenerConditionProgrammed), Status: metav1.ConditionFalse, - Reason: "Invalid", - Message: "Listener.TLS.Mode must be \"Passthrough\" when protocol is \"TLS\".", + Reason: string(gatewayapi_v1beta1.ListenerReasonInvalid), + Message: "Listener.TLS.CertificateRefs must contain exactly one entry", }, { Type: string(gatewayapi_v1beta1.ListenerConditionAccepted), @@ -9512,7 +9535,7 @@ func TestGatewayAPITLSRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate(string(gw.Spec.Listeners[0].Name), "TLSRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate(string(gw.Spec.Listeners[0].Name), gw.Spec.Listeners[0].Protocol, 0), }) run(t, "TLSRoute: spec.rules.backendRef.name invalid on two matches", testcase{ @@ -9555,7 +9578,7 @@ func TestGatewayAPITLSRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate(string(gw.Spec.Listeners[0].Name), "TLSRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate(string(gw.Spec.Listeners[0].Name), gw.Spec.Listeners[0].Protocol, 0), }) run(t, "TLSRoute: spec.rules.backendRef.port not specified", testcase{ @@ -9603,7 +9626,7 @@ func TestGatewayAPITLSRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate(string(gw.Spec.Listeners[0].Name), "TLSRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate(string(gw.Spec.Listeners[0].Name), gw.Spec.Listeners[0].Protocol, 0), }) run(t, "TLSRoute: spec.rules.backendRefs not specified", testcase{ @@ -9644,7 +9667,7 @@ func TestGatewayAPITLSRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate(string(gw.Spec.Listeners[0].Name), "TLSRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate(string(gw.Spec.Listeners[0].Name), gw.Spec.Listeners[0].Protocol, 0), }) run(t, "TLSRoute: spec.rules.hostname: invalid wildcard", testcase{ @@ -9687,7 +9710,7 @@ func TestGatewayAPITLSRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate(string(gw.Spec.Listeners[0].Name), "TLSRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate(string(gw.Spec.Listeners[0].Name), gw.Spec.Listeners[0].Protocol, 0), }) run(t, "TLSRoute: spec.rules.hostname: invalid hostname", testcase{ @@ -9730,7 +9753,7 @@ func TestGatewayAPITLSRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate(string(gw.Spec.Listeners[0].Name), "TLSRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate(string(gw.Spec.Listeners[0].Name), gw.Spec.Listeners[0].Protocol, 0), }) run(t, "TLSRoute: spec.rules.hostname: invalid hostname, ip address", testcase{ @@ -9771,7 +9794,7 @@ func TestGatewayAPITLSRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate(string(gw.Spec.Listeners[0].Name), "TLSRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate(string(gw.Spec.Listeners[0].Name), gw.Spec.Listeners[0].Protocol, 0), }) run(t, "TLSRoute: spec.rules.backendRefs has 0 weight", testcase{ @@ -9818,7 +9841,7 @@ func TestGatewayAPITLSRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate(string(gw.Spec.Listeners[0].Name), "TLSRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate(string(gw.Spec.Listeners[0].Name), gw.Spec.Listeners[0].Protocol, 0), }) run(t, "TLSRoute: backendrefs still validated when route not accepted", testcase{ @@ -9865,7 +9888,108 @@ func TestGatewayAPITLSRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate(string(gw.Spec.Listeners[0].Name), "TLSRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate(string(gw.Spec.Listeners[0].Name), gw.Spec.Listeners[0].Protocol, 0), + }) + + run(t, "TLS Listener with invalid TLS mode", testcase{ + gateway: &gatewayapi_v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "contour", + Namespace: "projectcontour", + }, + Spec: gatewayapi_v1beta1.GatewaySpec{ + Listeners: []gatewayapi_v1beta1.Listener{{ + Name: "tls", + Port: 443, + Protocol: gatewayapi_v1beta1.TLSProtocolType, + TLS: &gatewayapi_v1beta1.GatewayTLSConfig{ + Mode: ref.To(gatewayapi_v1beta1.TLSModeType("invalid-mode")), + }, + AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ + Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ + From: ref.To(gatewayapi_v1beta1.NamespacesFromAll), + }, + }, + }}, + }, + }, + objs: []any{ + kuardService, + &gatewayapi_v1alpha2.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic", + Namespace: "default", + }, + Spec: gatewayapi_v1alpha2.TLSRouteSpec{ + CommonRouteSpec: gatewayapi_v1alpha2.CommonRouteSpec{ + ParentRefs: []gatewayapi_v1alpha2.ParentReference{ + gatewayapi.GatewayListenerParentRef(gw.Namespace, gw.Name, "tls", 443), + }, + }, + Hostnames: []gatewayapi_v1alpha2.Hostname{"test.projectcontour.io"}, + Rules: []gatewayapi_v1alpha2.TLSRouteRule{{ + BackendRefs: gatewayapi.TLSRouteBackendRef("kuard", 8080, nil), + }}, + }, + }}, + wantRouteConditions: []*status.RouteStatusUpdate{{ + FullName: types.NamespacedName{Namespace: "default", Name: "basic"}, + RouteParentStatuses: []*gatewayapi_v1beta1.RouteParentStatus{ + { + ParentRef: gatewayapi.GatewayListenerParentRef(gw.Namespace, gw.Name, "tls", 443), + Conditions: []metav1.Condition{ + routeResolvedRefsCondition(), + { + Type: string(gatewayapi_v1beta1.RouteConditionAccepted), + Status: contour_api_v1.ConditionFalse, + Reason: string(gatewayapi_v1beta1.RouteReasonNoMatchingParent), + Message: "No listeners match this parent ref", + }, + }, + }, + }, + }}, + wantGatewayStatusUpdate: []*status.GatewayStatusUpdate{{ + FullName: types.NamespacedName{Namespace: "projectcontour", Name: "contour"}, + Conditions: map[gatewayapi_v1beta1.GatewayConditionType]metav1.Condition{ + gatewayapi_v1beta1.GatewayConditionAccepted: gatewayAcceptedCondition(), + gatewayapi_v1beta1.GatewayConditionProgrammed: { + Type: string(gatewayapi_v1beta1.GatewayConditionProgrammed), + Status: contour_api_v1.ConditionFalse, + Reason: string(gatewayapi_v1beta1.GatewayReasonListenersNotValid), + Message: "Listeners are not valid", + }, + }, + ListenerStatus: map[string]*gatewayapi_v1beta1.ListenerStatus{ + "tls": { + Name: "tls", + SupportedKinds: []gatewayapi_v1beta1.RouteGroupKind{ + { + Group: ref.To(gatewayapi_v1beta1.Group(gatewayapi_v1beta1.GroupName)), + Kind: "TLSRoute", + }, + { + Group: ref.To(gatewayapi_v1beta1.Group(gatewayapi_v1beta1.GroupName)), + Kind: "TCPRoute", + }, + }, + Conditions: []metav1.Condition{ + { + Type: string(gatewayapi_v1beta1.ListenerConditionAccepted), + Status: metav1.ConditionTrue, + Reason: string(gatewayapi_v1beta1.ListenerReasonAccepted), + Message: "Listener accepted", + }, + { + Type: string(gatewayapi_v1beta1.ListenerConditionProgrammed), + Status: metav1.ConditionFalse, + Reason: "Invalid", + Message: `Listener.TLS.Mode must be "Terminate" or "Passthrough".`, + }, + }, + }, + }, + }}, }) } @@ -10063,7 +10187,7 @@ func TestGatewayAPIGRPCRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "GRPCRoute", 1), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 1), }) run(t, "grpcroute: regular expression method match type is not yet supported", testcase{ @@ -10107,7 +10231,7 @@ func TestGatewayAPIGRPCRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "GRPCRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 0), }) run(t, "grpcroute: method match must have Service configured", testcase{ @@ -10150,7 +10274,7 @@ func TestGatewayAPIGRPCRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "GRPCRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 0), }) run(t, "grpcroute: method match must have Method configured", testcase{ @@ -10193,7 +10317,7 @@ func TestGatewayAPIGRPCRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "GRPCRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 0), }) run(t, "grpcroute: invalid header match type is not supported", testcase{ @@ -10248,7 +10372,7 @@ func TestGatewayAPIGRPCRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "GRPCRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 0), }) run(t, "grpcroute: regular expression header match has invalid value", testcase{ @@ -10303,7 +10427,7 @@ func TestGatewayAPIGRPCRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "GRPCRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 0), }) run(t, "grpcroute: invalid RequestHeaderModifier due to duplicated headers", testcase{ @@ -10352,7 +10476,7 @@ func TestGatewayAPIGRPCRouteDAGStatus(t *testing.T) { }, }}, // Invalid filters still result in an attached route. - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "GRPCRoute", 1), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 1), }) run(t, "grpcroute: invalid ResponseHeaderModifier due to invalid headers", testcase{ @@ -10402,7 +10526,7 @@ func TestGatewayAPIGRPCRouteDAGStatus(t *testing.T) { }, }}, // Invalid filters still result in an attached route. - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "GRPCRoute", 1), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 1), }) run(t, "grpcroute: more than one RequestMirror filters in GRPCRoute.Spec.Rules.Filters", testcase{ @@ -10455,7 +10579,7 @@ func TestGatewayAPIGRPCRouteDAGStatus(t *testing.T) { }, }}, // Invalid filters still result in an attached route. - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "GRPCRoute", 1), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 1), }) run(t, "grpcroute: invalid RequestMirror filter due to unspecified backendRef.name", testcase{ @@ -10506,7 +10630,7 @@ func TestGatewayAPIGRPCRouteDAGStatus(t *testing.T) { }, }}, // This still results in an attached route because it returns a 404. - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "GRPCRoute", 1), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 1), }) run(t, "grpcroute: custom filter type is not supported", testcase{ @@ -10553,7 +10677,7 @@ func TestGatewayAPIGRPCRouteDAGStatus(t *testing.T) { }, }}, // Invalid filters still result in an attached route. - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "GRPCRoute", 1), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 1), }) run(t, "grpcroute: at lease one backend need to be specified", testcase{ @@ -10592,7 +10716,7 @@ func TestGatewayAPIGRPCRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "GRPCRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 0), }) run(t, "grpcroute: still validate backendrefs when not accepted", testcase{ @@ -10644,7 +10768,7 @@ func TestGatewayAPIGRPCRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "GRPCRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 0), }) run(t, "grpcroute: invalid RequestHeaderModifier on backend due to duplicated headers", testcase{ @@ -10699,7 +10823,7 @@ func TestGatewayAPIGRPCRouteDAGStatus(t *testing.T) { }, }}, // Invalid filters still result in an attached route. - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "GRPCRoute", 1), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 1), }) run(t, "grpcroute: invalid ResponseHeaderModifier on backend due to invalid headers", testcase{ @@ -10755,7 +10879,7 @@ func TestGatewayAPIGRPCRouteDAGStatus(t *testing.T) { }, }}, // Invalid filters still result in an attached route. - wantGatewayStatusUpdate: validGatewayStatusUpdate("http", "GRPCRoute", 1), + wantGatewayStatusUpdate: validGatewayStatusUpdate("http", gatewayapi_v1beta1.HTTPProtocolType, 1), }) } @@ -11006,7 +11130,7 @@ func TestGatewayAPITCPRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate("tcp", "TCPRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate("tcp", gatewayapi_v1beta1.TCPProtocolType, 0), }) run(t, "TCPRoute with rule with no backends", testcase{ objs: []any{ @@ -11037,7 +11161,7 @@ func TestGatewayAPITCPRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate("tcp", "TCPRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate("tcp", gatewayapi_v1beta1.TCPProtocolType, 0), }) run(t, "TCPRoute with rule with ref to nonexistent backend", testcase{ objs: []any{ @@ -11070,7 +11194,7 @@ func TestGatewayAPITCPRouteDAGStatus(t *testing.T) { }, }, }}, - wantGatewayStatusUpdate: validGatewayStatusUpdate("tcp", "TCPRoute", 0), + wantGatewayStatusUpdate: validGatewayStatusUpdate("tcp", gatewayapi_v1beta1.TCPProtocolType, 0), }) } diff --git a/internal/featuretests/v3/tcproute_test.go b/internal/featuretests/v3/tcproute_test.go index fc2a6bbf427..1519a3d8eb8 100644 --- a/internal/featuretests/v3/tcproute_test.go +++ b/internal/featuretests/v3/tcproute_test.go @@ -26,6 +26,7 @@ import ( gatewayapi_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" envoy_v3 "github.com/projectcontour/contour/internal/envoy/v3" + "github.com/projectcontour/contour/internal/featuretests" "github.com/projectcontour/contour/internal/fixture" "github.com/projectcontour/contour/internal/gatewayapi" "github.com/projectcontour/contour/internal/ref" @@ -177,3 +178,105 @@ func TestTCPRoute(t *testing.T) { // check that there is no route config require.Empty(t, c.Request(routeType).Resources) } + +func TestTCPRoute_TLSTermination(t *testing.T) { + rh, c, done := setup(t) + defer done() + + svc1 := fixture.NewService("backend-1"). + WithPorts(v1.ServicePort{Port: 80, TargetPort: intstr.FromInt(8080)}) + + rh.OnAdd(svc1) + + sec1 := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tlscert", + Namespace: "projectcontour", + }, + Type: v1.SecretTypeTLS, + Data: featuretests.Secretdata(featuretests.CERTIFICATE, featuretests.RSA_PRIVATE_KEY), + } + + rh.OnAdd(sec1) + + rh.OnAdd(&gatewayapi_v1beta1.GatewayClass{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: fixture.ObjectMeta("test-gc"), + Spec: gatewayapi_v1beta1.GatewayClassSpec{ + ControllerName: "projectcontour.io/contour", + }, + Status: gatewayapi_v1beta1.GatewayClassStatus{ + Conditions: []metav1.Condition{ + { + Type: string(gatewayapi_v1beta1.GatewayClassConditionStatusAccepted), + Status: metav1.ConditionTrue, + }, + }, + }, + }) + + gateway := &gatewayapi_v1beta1.Gateway{ + ObjectMeta: fixture.ObjectMeta("projectcontour/contour"), + Spec: gatewayapi_v1beta1.GatewaySpec{ + Listeners: []gatewayapi_v1beta1.Listener{ + { + Name: "tls", + Port: 5000, + Protocol: gatewayapi_v1beta1.TLSProtocolType, + TLS: &gatewayapi_v1beta1.GatewayTLSConfig{ + Mode: ref.To(gatewayapi_v1beta1.TLSModeTerminate), + CertificateRefs: []gatewayapi_v1beta1.SecretObjectReference{ + gatewayapi.CertificateRef("tlscert", ""), + }, + }, + AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ + Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ + From: ref.To(gatewayapi_v1beta1.NamespacesFromAll), + }, + }, + }, + }, + }, + } + rh.OnAdd(gateway) + + route1 := &gatewayapi_v1alpha2.TCPRoute{ + ObjectMeta: fixture.ObjectMeta("tcproute-1"), + Spec: gatewayapi_v1alpha2.TCPRouteSpec{ + CommonRouteSpec: gatewayapi_v1beta1.CommonRouteSpec{ + ParentRefs: []gatewayapi_v1beta1.ParentReference{ + { + Namespace: ref.To(gatewayapi_v1beta1.Namespace("projectcontour")), + Name: gatewayapi_v1beta1.ObjectName("contour"), + SectionName: ref.To(gatewayapi_v1beta1.SectionName("tls")), + }, + }, + }, + Rules: []gatewayapi_v1alpha2.TCPRouteRule{{ + BackendRefs: gatewayapi.TLSRouteBackendRef("backend-1", 80, nil), + }}, + }, + } + rh.OnAdd(route1) + + c.Request(listenerType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + Resources: resources(t, + &envoy_listener_v3.Listener{ + Name: "https-5000", + Address: envoy_v3.SocketAddress("0.0.0.0", 13000), + ListenerFilters: envoy_v3.ListenerFilters( + envoy_v3.TLSInspector(), + ), + FilterChains: appendFilterChains( + filterchaintls("*", sec1, tcpproxy("https-5000", "default/backend-1/80/da39a3ee5e"), nil), + ), + SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(), + }, + statsListener(), + ), + TypeUrl: listenerType, + }) + + // check that there is no route config + require.Empty(t, c.Request(routeType).Resources) +} diff --git a/internal/featuretests/v3/tlsroute_test.go b/internal/featuretests/v3/tlsroute_test.go index 94087ef7b7d..0621ddcd65f 100644 --- a/internal/featuretests/v3/tlsroute_test.go +++ b/internal/featuretests/v3/tlsroute_test.go @@ -16,6 +16,7 @@ package v3 import ( "testing" + "github.com/projectcontour/contour/internal/featuretests" "github.com/projectcontour/contour/internal/gatewayapi" "github.com/projectcontour/contour/internal/ref" "github.com/stretchr/testify/require" @@ -31,7 +32,7 @@ import ( gatewayapi_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" ) -func TestTLSRoute(t *testing.T) { +func TestTLSRoute_TLSPassthrough(t *testing.T) { rh, c, done := setup(t) defer done() @@ -238,3 +239,135 @@ func TestTLSRoute(t *testing.T) { rh.OnDelete(route3) rh.OnDelete(route4) } + +func TestTLSRoute_TLSTermination(t *testing.T) { + rh, c, done := setup(t) + defer done() + + rh.OnAdd(fixture.NewService("svc1"). + WithPorts(v1.ServicePort{Port: 80, TargetPort: intstr.FromInt(8080)}), + ) + + rh.OnAdd(fixture.NewService("svc2"). + WithPorts(v1.ServicePort{Port: 80, TargetPort: intstr.FromInt(8080)}), + ) + + sec1 := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tlscert", + Namespace: "projectcontour", + }, + Type: v1.SecretTypeTLS, + Data: featuretests.Secretdata(featuretests.CERTIFICATE, featuretests.RSA_PRIVATE_KEY), + } + + rh.OnAdd(sec1) + + rh.OnAdd(gc) + + gateway := &gatewayapi_v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "contour", + Namespace: "projectcontour", + }, + Spec: gatewayapi_v1beta1.GatewaySpec{ + GatewayClassName: gatewayapi_v1beta1.ObjectName(gc.Name), + Listeners: []gatewayapi_v1beta1.Listener{ + { + Name: "tls", + Port: 5000, + Protocol: gatewayapi_v1beta1.TLSProtocolType, + TLS: &gatewayapi_v1beta1.GatewayTLSConfig{ + Mode: ref.To(gatewayapi_v1beta1.TLSModeTerminate), + CertificateRefs: []gatewayapi_v1beta1.SecretObjectReference{ + gatewayapi.CertificateRef("tlscert", ""), + }, + }, + Hostname: ref.To(gatewayapi_v1beta1.Hostname("*.projectcontour.io")), + AllowedRoutes: &gatewayapi_v1beta1.AllowedRoutes{ + Namespaces: &gatewayapi_v1beta1.RouteNamespaces{ + From: ref.To(gatewayapi_v1beta1.NamespacesFromAll), + }, + }, + }, + }, + }, + } + + rh.OnAdd(gateway) + + rh.OnAdd(&gatewayapi_v1alpha2.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic", + Namespace: "default", + }, + Spec: gatewayapi_v1alpha2.TLSRouteSpec{ + CommonRouteSpec: gatewayapi_v1beta1.CommonRouteSpec{ + ParentRefs: []gatewayapi_v1beta1.ParentReference{ + gatewayapi.GatewayParentRef("projectcontour", "contour"), + }, + }, + Hostnames: []gatewayapi_v1beta1.Hostname{ + "test1.projectcontour.io", + }, + Rules: []gatewayapi_v1alpha2.TLSRouteRule{{ + BackendRefs: gatewayapi.TLSRouteBackendRef("svc1", 80, ref.To(int32(1))), + }}, + }, + }) + + c.Request(listenerType, "https-5000").Equals(&envoy_discovery_v3.DiscoveryResponse{ + TypeUrl: listenerType, + Resources: resources(t, + &envoy_listener_v3.Listener{ + Name: "https-5000", + Address: envoy_v3.SocketAddress("0.0.0.0", 13000), + ListenerFilters: envoy_v3.ListenerFilters( + envoy_v3.TLSInspector(), + ), + FilterChains: appendFilterChains( + filterchaintls("test1.projectcontour.io", sec1, tcpproxy("https-5000", "default/svc1/80/da39a3ee5e"), nil), + ), + SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(), + }, + ), + }) + + rh.OnAdd(&gatewayapi_v1alpha2.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic-2", + Namespace: "default", + }, + Spec: gatewayapi_v1alpha2.TLSRouteSpec{ + CommonRouteSpec: gatewayapi_v1beta1.CommonRouteSpec{ + ParentRefs: []gatewayapi_v1beta1.ParentReference{ + gatewayapi.GatewayParentRef("projectcontour", "contour"), + }, + }, + Hostnames: []gatewayapi_v1beta1.Hostname{ + "test2.projectcontour.io", + }, + Rules: []gatewayapi_v1alpha2.TLSRouteRule{{ + BackendRefs: gatewayapi.TLSRouteBackendRef("svc2", 80, ref.To(int32(1))), + }}, + }, + }) + + c.Request(listenerType, "https-5000").Equals(&envoy_discovery_v3.DiscoveryResponse{ + TypeUrl: listenerType, + Resources: resources(t, + &envoy_listener_v3.Listener{ + Name: "https-5000", + Address: envoy_v3.SocketAddress("0.0.0.0", 13000), + ListenerFilters: envoy_v3.ListenerFilters( + envoy_v3.TLSInspector(), + ), + FilterChains: appendFilterChains( + filterchaintls("test1.projectcontour.io", sec1, tcpproxy("https-5000", "default/svc1/80/da39a3ee5e"), nil), + filterchaintls("test2.projectcontour.io", sec1, tcpproxy("https-5000", "default/svc2/80/da39a3ee5e"), nil), + ), + SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(), + }, + ), + }) +} diff --git a/internal/xdscache/v3/listener.go b/internal/xdscache/v3/listener.go index a5daf19f0f7..06824c82bc6 100644 --- a/internal/xdscache/v3/listener.go +++ b/internal/xdscache/v3/listener.go @@ -355,6 +355,8 @@ func (c *ListenerCache) OnChange(root *dag.DAG) { } for _, listener := range root.Listeners { + // A Listener-level TCPProxy proxies all traffic for + // the Listener port, i.e. no filter chain match. if listener.TCPProxy != nil { listeners[listener.Name] = envoy_v3.Listener( listener.Name, @@ -367,7 +369,6 @@ func (c *ListenerCache) OnChange(root *dag.DAG) { continue } - // If there are non-TLS vhosts bound to the listener, // add a listener with a single filter chain. // Note: Ensure the filter chain order matches with the filter chain