From db6bea0f4d4319a071c022c95a884a77fb868c79 Mon Sep 17 00:00:00 2001 From: James Peach Date: Wed, 29 Apr 2020 16:56:36 +1000 Subject: [PATCH] internal: filter misdirected TLS requests TLS routes are specialized to a unique virtual hostname. However, if wildcard certificates are being used, browsers will aggressively coalesce and reuse server connections even when the full origin hostname doesn't match. This results on 404 responses because each TLS virtual host only has routes for one host. We can avoid this behaviour bleeding out to users by generating a 421 Misdirected Request response if the request authority doesn't match the FQDN of the virtual host. In this case, the browser is supposed to understand that the request wasn't processed and re-send it on a new connection. Signed-off-by: James Peach --- internal/contour/listener.go | 3 ++ internal/envoy/listener.go | 59 +++++++++++++++++++++++++++++++----- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/internal/contour/listener.go b/internal/contour/listener.go index 5ca7dfb0f4a..51dafc99fc4 100644 --- a/internal/contour/listener.go +++ b/internal/contour/listener.go @@ -297,6 +297,7 @@ func visitListeners(root dag.Vertex, lvc *ListenerVisitorConfig) map[string]*v2. // Add a listener if there are vhosts bound to http. if lv.http { cm := envoy.HTTPConnectionManagerBuilder(). + DefaultFilters(). RouteConfigName(ENVOY_HTTP_LISTENER). MetricsPrefix(ENVOY_HTTP_LISTENER). AccessLoggers(lvc.newInsecureAccessLog()). @@ -366,6 +367,8 @@ func (v *listenerVisitor) visit(vertex dag.Vertex) { // coded into monitoring dashboards. filters = envoy.Filters( envoy.HTTPConnectionManagerBuilder(). + AddFilter(envoy.FilterMisdirectedRequests(vh.VirtualHost.Name)). + DefaultFilters(). RouteConfigName(path.Join("https", vh.VirtualHost.Name)). MetricsPrefix(ENVOY_HTTPS_LISTENER). AccessLoggers(v.ListenerVisitorConfig.newSecureAccessLog()). diff --git a/internal/envoy/listener.go b/internal/envoy/listener.go index 55c77419716..e9a1db02dd2 100644 --- a/internal/envoy/listener.go +++ b/internal/envoy/listener.go @@ -14,6 +14,7 @@ package envoy import ( + "fmt" "sort" "time" @@ -22,6 +23,7 @@ import ( envoy_api_v2_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core" envoy_api_v2_listener "github.com/envoyproxy/go-control-plane/envoy/api/v2/listener" accesslog "github.com/envoyproxy/go-control-plane/envoy/config/filter/accesslog/v2" + lua "github.com/envoyproxy/go-control-plane/envoy/config/filter/http/lua/v2" http "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/http_connection_manager/v2" tcp "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/tcp_proxy/v2" "github.com/envoyproxy/go-control-plane/pkg/wellknown" @@ -70,6 +72,7 @@ type httpConnectionManagerBuilder struct { metricsPrefix string accessLoggers []*accesslog.AccessLog requestTimeout time.Duration + filters []*http.HttpFilter } // RouteConfigName sets the name of the RDS element that contains @@ -102,6 +105,27 @@ func (b *httpConnectionManagerBuilder) RequestTimeout(timeout time.Duration) *ht return b } +func (b *httpConnectionManagerBuilder) DefaultFilters() *httpConnectionManagerBuilder { + b.filters = append(b.filters, + &http.HttpFilter{ + Name: wellknown.Gzip, + }, + &http.HttpFilter{ + Name: wellknown.GRPCWeb, + }, + &http.HttpFilter{ + Name: wellknown.Router, + }, + ) + + return b +} + +func (b *httpConnectionManagerBuilder) AddFilter(f *http.HttpFilter) *httpConnectionManagerBuilder { + b.filters = append(b.filters, f) + return b +} + // Get returns a new http.HttpConnectionManager filter, constructed // from the builder settings. // @@ -114,13 +138,7 @@ func (b *httpConnectionManagerBuilder) Get() *envoy_api_v2_listener.Filter { ConfigSource: ConfigSource("contour"), }, }, - HttpFilters: []*http.HttpFilter{{ - Name: wellknown.Gzip, - }, { - Name: wellknown.GRPCWeb, - }, { - Name: wellknown.Router, - }}, + HttpFilters: b.filters, CommonHttpProtocolOptions: &envoy_api_v2_core.HttpProtocolOptions{ // Sets the idle timeout for HTTP connections to 60 seconds. // This is chosen as a rough default to stop idle connections wasting resources, @@ -169,6 +187,7 @@ func HTTPConnectionManager(routename string, accesslogger []*accesslog.AccessLog MetricsPrefix(routename). AccessLoggers(accesslogger). RequestTimeout(requestTimeout). + DefaultFilters(). Get() } @@ -284,6 +303,32 @@ func FilterChains(filters ...*envoy_api_v2_listener.Filter) []*envoy_api_v2_list } } +func FilterMisdirectedRequests(fqdn string) *http.HttpFilter { + code := ` +function envoy_on_request(request_handle) + local headers = request_handle:headers() + local host = headers:get(":authority") + + if host ~= "%s" then + request_handle:respond({ + [":status"] = "421", + }, + "" + ) + end +end +` + + return &http.HttpFilter{ + Name: "envoy.filters.http.lua", + ConfigType: &http.HttpFilter_TypedConfig{ + TypedConfig: toAny(&lua.Lua{ + InlineCode: fmt.Sprintf(code, fqdn), + }), + }, + } +} + // FilterChainTLS returns a TLS enabled envoy_api_v2_listener.FilterChain, func FilterChainTLS(domain string, downstream *envoy_api_v2_auth.DownstreamTlsContext, filters []*envoy_api_v2_listener.Filter) *envoy_api_v2_listener.FilterChain { fc := &envoy_api_v2_listener.FilterChain{