2018-10-03 18:18:55 +00:00
package xds
import (
"encoding/json"
"errors"
"fmt"
2019-09-26 02:55:52 +00:00
"net"
"net/url"
"regexp"
"strconv"
2019-04-29 16:27:57 +00:00
"strings"
2018-10-03 18:18:55 +00:00
envoy "github.com/envoyproxy/go-control-plane/envoy/api/v2"
envoyauth "github.com/envoyproxy/go-control-plane/envoy/api/v2/auth"
envoycore "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
envoylistener "github.com/envoyproxy/go-control-plane/envoy/api/v2/listener"
2019-04-29 16:27:57 +00:00
envoyroute "github.com/envoyproxy/go-control-plane/envoy/api/v2/route"
envoyhttp "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/http_connection_manager/v2"
2018-10-03 18:18:55 +00:00
envoytcp "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/tcp_proxy/v2"
2019-04-29 16:27:57 +00:00
envoytype "github.com/envoyproxy/go-control-plane/envoy/type"
2020-06-23 20:19:56 +00:00
"github.com/envoyproxy/go-control-plane/pkg/conversion"
"github.com/envoyproxy/go-control-plane/pkg/wellknown"
"github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto"
2020-08-27 17:20:58 +00:00
pbtypes "github.com/golang/protobuf/ptypes"
2020-06-23 20:19:56 +00:00
"github.com/golang/protobuf/ptypes/any"
pbstruct "github.com/golang/protobuf/ptypes/struct"
"github.com/golang/protobuf/ptypes/wrappers"
2019-08-19 18:03:03 +00:00
"github.com/hashicorp/consul/agent/connect"
2018-10-03 18:18:55 +00:00
"github.com/hashicorp/consul/agent/proxycfg"
"github.com/hashicorp/consul/agent/structs"
2020-06-23 20:19:56 +00:00
"github.com/hashicorp/consul/logging"
"github.com/hashicorp/go-hclog"
2018-10-03 18:18:55 +00:00
)
2019-06-24 19:05:36 +00:00
// listenersFromSnapshot returns the xDS API representation of the "listeners" in the snapshot.
2020-07-09 22:04:51 +00:00
func ( s * Server ) listenersFromSnapshot ( cInfo connectionInfo , cfgSnap * proxycfg . ConfigSnapshot ) ( [ ] proto . Message , error ) {
2018-10-03 18:18:55 +00:00
if cfgSnap == nil {
return nil , errors . New ( "nil config given" )
}
2019-06-24 19:05:36 +00:00
switch cfgSnap . Kind {
case structs . ServiceKindConnectProxy :
2020-07-09 22:04:51 +00:00
return s . listenersFromSnapshotConnectProxy ( cInfo , cfgSnap )
2020-04-13 16:33:01 +00:00
case structs . ServiceKindTerminatingGateway :
2020-07-09 22:04:51 +00:00
return s . listenersFromSnapshotGateway ( cInfo , cfgSnap )
2019-06-18 00:52:01 +00:00
case structs . ServiceKindMeshGateway :
2020-07-09 22:04:51 +00:00
return s . listenersFromSnapshotGateway ( cInfo , cfgSnap )
2020-04-16 21:00:48 +00:00
case structs . ServiceKindIngressGateway :
2020-07-09 22:04:51 +00:00
return s . listenersFromSnapshotGateway ( cInfo , cfgSnap )
2019-06-24 19:05:36 +00:00
default :
return nil , fmt . Errorf ( "Invalid service kind: %v" , cfgSnap . Kind )
}
}
2019-06-18 00:52:01 +00:00
// listenersFromSnapshotConnectProxy returns the "listeners" for a connect proxy service
2020-07-09 22:04:51 +00:00
func ( s * Server ) listenersFromSnapshotConnectProxy ( cInfo connectionInfo , cfgSnap * proxycfg . ConfigSnapshot ) ( [ ] proto . Message , error ) {
2018-10-03 18:18:55 +00:00
// One listener for each upstream plus the public one
resources := make ( [ ] proto . Message , len ( cfgSnap . Proxy . Upstreams ) + 1 )
// Configure public listener
var err error
2020-07-09 22:04:51 +00:00
resources [ 0 ] , err = s . makePublicListener ( cInfo , cfgSnap )
2018-10-03 18:18:55 +00:00
if err != nil {
return nil , err
}
for i , u := range cfgSnap . Proxy . Upstreams {
2019-07-02 03:10:51 +00:00
id := u . Identifier ( )
var chain * structs . CompiledDiscoveryChain
if u . DestinationType != structs . UpstreamDestTypePreparedQuery {
chain = cfgSnap . ConnectProxy . DiscoveryChain [ id ]
}
var upstreamListener proto . Message
2020-05-21 14:08:12 +00:00
upstreamListener , err = s . makeUpstreamListenerForDiscoveryChain (
& u ,
u . LocalBindAddress ,
chain ,
cfgSnap ,
nil ,
)
2018-10-03 18:18:55 +00:00
if err != nil {
return nil , err
}
2019-07-02 03:10:51 +00:00
resources [ i + 1 ] = upstreamListener
2018-10-03 18:18:55 +00:00
}
2019-09-26 02:55:52 +00:00
2020-01-28 23:50:41 +00:00
cfgSnap . Proxy . Expose . Finalize ( )
2019-09-26 02:55:52 +00:00
paths := cfgSnap . Proxy . Expose . Paths
// Add service health checks to the list of paths to create listeners for if needed
if cfgSnap . Proxy . Expose . Checks {
2020-01-24 15:04:58 +00:00
psid := structs . NewServiceID ( cfgSnap . Proxy . DestinationServiceID , & cfgSnap . ProxyID . EnterpriseMeta )
2019-12-10 02:26:41 +00:00
for _ , check := range s . CheckFetcher . ServiceHTTPBasedChecks ( psid ) {
2019-09-26 02:55:52 +00:00
p , err := parseCheckPath ( check )
if err != nil {
2020-01-28 23:50:41 +00:00
s . Logger . Warn ( "failed to create listener for" , "check" , check . CheckID , "error" , err )
2019-09-26 02:55:52 +00:00
continue
}
paths = append ( paths , p )
}
}
// Configure additional listener for exposed check paths
for _ , path := range paths {
clusterName := LocalAppClusterName
if path . LocalPathPort != cfgSnap . Proxy . LocalServicePort {
clusterName = makeExposeClusterName ( path . LocalPathPort )
}
l , err := s . makeExposedCheckListener ( cfgSnap , clusterName , path )
if err != nil {
return nil , err
}
resources = append ( resources , l )
}
2018-10-03 18:18:55 +00:00
return resources , nil
}
2019-09-26 02:55:52 +00:00
func parseCheckPath ( check structs . CheckType ) ( structs . ExposePath , error ) {
var path structs . ExposePath
if check . HTTP != "" {
path . Protocol = "http"
// Get path and local port from original HTTP target
u , err := url . Parse ( check . HTTP )
if err != nil {
return path , fmt . Errorf ( "failed to parse url '%s': %v" , check . HTTP , err )
}
path . Path = u . Path
_ , portStr , err := net . SplitHostPort ( u . Host )
if err != nil {
return path , fmt . Errorf ( "failed to parse port from '%s': %v" , check . HTTP , err )
}
path . LocalPathPort , err = strconv . Atoi ( portStr )
if err != nil {
return path , fmt . Errorf ( "failed to parse port from '%s': %v" , check . HTTP , err )
}
// Get listener port from proxied HTTP target
u , err = url . Parse ( check . ProxyHTTP )
if err != nil {
return path , fmt . Errorf ( "failed to parse url '%s': %v" , check . ProxyHTTP , err )
}
_ , portStr , err = net . SplitHostPort ( u . Host )
if err != nil {
return path , fmt . Errorf ( "failed to parse port from '%s': %v" , check . ProxyHTTP , err )
}
path . ListenerPort , err = strconv . Atoi ( portStr )
if err != nil {
return path , fmt . Errorf ( "failed to parse port from '%s': %v" , check . ProxyHTTP , err )
}
}
if check . GRPC != "" {
path . Path = "/grpc.health.v1.Health/Check"
path . Protocol = "http2"
// Get local port from original GRPC target of the form: host/service
proxyServerAndService := strings . SplitN ( check . GRPC , "/" , 2 )
_ , portStr , err := net . SplitHostPort ( proxyServerAndService [ 0 ] )
if err != nil {
return path , fmt . Errorf ( "failed to split host/port from '%s': %v" , check . GRPC , err )
}
path . LocalPathPort , err = strconv . Atoi ( portStr )
if err != nil {
return path , fmt . Errorf ( "failed to parse port from '%s': %v" , check . GRPC , err )
}
// Get listener port from proxied GRPC target of the form: host/service
proxyServerAndService = strings . SplitN ( check . ProxyGRPC , "/" , 2 )
_ , portStr , err = net . SplitHostPort ( proxyServerAndService [ 0 ] )
if err != nil {
return path , fmt . Errorf ( "failed to split host/port from '%s': %v" , check . ProxyGRPC , err )
}
path . ListenerPort , err = strconv . Atoi ( portStr )
if err != nil {
return path , fmt . Errorf ( "failed to parse port from '%s': %v" , check . ProxyGRPC , err )
}
}
path . ParsedFromCheck = true
return path , nil
}
2020-04-13 16:33:01 +00:00
// listenersFromSnapshotGateway returns the "listener" for a terminating-gateway or mesh-gateway service
2020-07-09 22:04:51 +00:00
func ( s * Server ) listenersFromSnapshotGateway ( cInfo connectionInfo , cfgSnap * proxycfg . ConfigSnapshot ) ( [ ] proto . Message , error ) {
2020-03-26 16:20:56 +00:00
cfg , err := ParseGatewayConfig ( cfgSnap . Proxy . Config )
2019-06-18 00:52:01 +00:00
if err != nil {
// Don't hard fail on a config typo, just warn. The parse func returns
// default config if there is an error so it's safe to continue.
2020-01-28 23:50:41 +00:00
s . Logger . Warn ( "failed to parse Connect.Proxy.Config" , "error" , err )
2019-06-18 00:52:01 +00:00
}
2020-04-13 16:33:01 +00:00
// Prevent invalid configurations of binding to the same port/addr twice
// including with the any addresses
type namedAddress struct {
name string
structs . ServiceAddress
}
seen := make ( map [ structs . ServiceAddress ] bool )
addrs := make ( [ ] namedAddress , 0 )
2019-06-18 00:52:01 +00:00
var resources [ ] proto . Message
if ! cfg . NoDefaultBind {
addr := cfgSnap . Address
if addr == "" {
addr = "0.0.0.0"
}
2020-04-13 16:33:01 +00:00
a := structs . ServiceAddress {
Address : addr ,
Port : cfgSnap . Port ,
}
if ! seen [ a ] {
addrs = append ( addrs , namedAddress { name : "default" , ServiceAddress : a } )
seen [ a ] = true
2019-06-18 00:52:01 +00:00
}
}
if cfg . BindTaggedAddresses {
for name , addrCfg := range cfgSnap . TaggedAddresses {
2020-04-13 16:33:01 +00:00
a := structs . ServiceAddress {
Address : addrCfg . Address ,
Port : addrCfg . Port ,
}
if ! seen [ a ] {
addrs = append ( addrs , namedAddress { name : name , ServiceAddress : a } )
seen [ a ] = true
2019-06-18 00:52:01 +00:00
}
}
}
for name , addrCfg := range cfg . BindAddresses {
2020-04-13 16:33:01 +00:00
a := structs . ServiceAddress {
Address : addrCfg . Address ,
Port : addrCfg . Port ,
}
if ! seen [ a ] {
addrs = append ( addrs , namedAddress { name : name , ServiceAddress : a } )
seen [ a ] = true
2019-06-18 00:52:01 +00:00
}
}
2020-04-13 16:33:01 +00:00
// Make listeners once deduplicated
for _ , a := range addrs {
var l * envoy . Listener
switch cfgSnap . Kind {
case structs . ServiceKindTerminatingGateway :
2020-07-09 22:04:51 +00:00
l , err = s . makeTerminatingGatewayListener ( cInfo , cfgSnap , a . name , a . Address , a . Port )
2020-04-13 16:33:01 +00:00
if err != nil {
return nil , err
}
2020-05-21 14:08:12 +00:00
case structs . ServiceKindIngressGateway :
listeners , err := s . makeIngressGatewayListeners ( a . Address , cfgSnap )
if err != nil {
return nil , err
}
resources = append ( resources , listeners ... )
2020-04-13 16:33:01 +00:00
case structs . ServiceKindMeshGateway :
l , err = s . makeMeshGatewayListener ( a . name , a . Address , a . Port , cfgSnap )
if err != nil {
return nil , err
}
}
2020-04-17 01:08:41 +00:00
if l != nil {
resources = append ( resources , l )
}
2020-04-13 16:33:01 +00:00
}
2019-06-18 00:52:01 +00:00
return resources , err
}
2020-05-21 14:08:12 +00:00
func ( s * Server ) makeIngressGatewayListeners ( address string , cfgSnap * proxycfg . ConfigSnapshot ) ( [ ] proto . Message , error ) {
2020-04-16 21:00:48 +00:00
var resources [ ] proto . Message
2020-05-21 14:08:12 +00:00
2020-04-16 23:24:11 +00:00
for listenerKey , upstreams := range cfgSnap . IngressGateway . Upstreams {
2020-04-27 23:36:20 +00:00
var tlsContext * envoyauth . DownstreamTlsContext
if cfgSnap . IngressGateway . TLSEnabled {
tlsContext = & envoyauth . DownstreamTlsContext {
CommonTlsContext : makeCommonTLSContextFromLeaf ( cfgSnap , cfgSnap . Leaf ( ) ) ,
2020-06-23 20:19:56 +00:00
RequireClientCertificate : & wrappers . BoolValue { Value : false } ,
2020-04-27 23:36:20 +00:00
}
}
2020-04-16 23:24:11 +00:00
if listenerKey . Protocol == "tcp" {
2020-04-30 14:32:09 +00:00
// We rely on the invariant of upstreams slice always having at least 1
// member, because this key/value pair is created only when a
// GatewayService is returned in the RPC
2020-04-16 23:24:11 +00:00
u := upstreams [ 0 ]
id := u . Identifier ( )
chain := cfgSnap . IngressGateway . DiscoveryChain [ id ]
var upstreamListener proto . Message
2020-05-21 14:08:12 +00:00
upstreamListener , err := s . makeUpstreamListenerForDiscoveryChain (
& u ,
address ,
chain ,
cfgSnap ,
tlsContext ,
)
2020-04-16 23:24:11 +00:00
if err != nil {
return nil , err
}
resources = append ( resources , upstreamListener )
} else {
// If multiple upstreams share this port, make a special listener for the protocol.
2020-05-21 14:08:12 +00:00
listener := makeListener ( listenerKey . Protocol , address , listenerKey . Port )
2020-08-27 17:20:58 +00:00
opts := listenerFilterOpts {
useRDS : true ,
protocol : listenerKey . Protocol ,
filterName : listenerKey . RouteName ( ) ,
2020-09-04 18:45:05 +00:00
routeName : listenerKey . RouteName ( ) ,
2020-08-27 17:20:58 +00:00
cluster : "" ,
statPrefix : "ingress_upstream_" ,
routePath : "" ,
ingress : false ,
httpAuthzFilter : nil ,
}
filter , err := makeListenerFilter ( opts )
2020-04-16 23:24:11 +00:00
if err != nil {
return nil , err
}
2020-04-16 21:00:48 +00:00
2020-06-23 20:19:56 +00:00
listener . FilterChains = [ ] * envoylistener . FilterChain {
2020-04-16 23:24:11 +00:00
{
2020-06-23 20:19:56 +00:00
Filters : [ ] * envoylistener . Filter {
2020-04-16 23:24:11 +00:00
filter ,
} ,
2020-04-27 23:36:20 +00:00
TlsContext : tlsContext ,
2020-04-16 23:24:11 +00:00
} ,
}
resources = append ( resources , listener )
2020-04-16 21:00:48 +00:00
}
}
return resources , nil
}
2018-10-03 18:18:55 +00:00
// makeListener returns a listener with name and bind details set. Filters must
// be added before it's useful.
//
// Note on names: Envoy listeners attempt graceful transitions of connections
// when their config changes but that means they can't have their bind address
// or port changed in a running instance. Since our users might choose to change
// a bind address or port for the public or upstream listeners, we need to
// encode those into the unique name for the listener such that if the user
// changes them, we actually create a whole new listener on the new address and
// port. Envoy should take care of closing the old one once it sees it's no
// longer in the config.
func makeListener ( name , addr string , port int ) * envoy . Listener {
return & envoy . Listener {
Name : fmt . Sprintf ( "%s:%s:%d" , name , addr , port ) ,
Address : makeAddress ( addr , port ) ,
}
}
// makeListenerFromUserConfig returns the listener config decoded from an
// arbitrary proto3 json format string or an error if it's invalid.
//
// For now we only support embedding in JSON strings because of the hcl parsing
2020-06-09 21:43:05 +00:00
// pain (see Background section in the comment for decode.HookWeakDecodeFromSlice).
// This may be fixed in decode.HookWeakDecodeFromSlice in the future.
2018-10-03 18:18:55 +00:00
//
// When we do that we can support just nesting the config directly into the
// JSON/hcl naturally but this is a stop-gap that gets us an escape hatch
// immediately. It's also probably not a bad thing to support long-term since
// any config generated by other systems will likely be in canonical protobuf
// from rather than our slight variant in JSON/hcl.
func makeListenerFromUserConfig ( configJSON string ) ( * envoy . Listener , error ) {
// Figure out if there is an @type field. We don't require is since we know
2020-06-23 20:19:56 +00:00
// this will be a listener but unmarshalling into any.Any fails if it's not
2018-10-03 18:18:55 +00:00
// there and unmarshalling into listener directly fails if it is...
var jsonFields map [ string ] * json . RawMessage
if err := json . Unmarshal ( [ ] byte ( configJSON ) , & jsonFields ) ; err != nil {
return nil , err
}
var l envoy . Listener
if _ , ok := jsonFields [ "@type" ] ; ok {
2020-06-23 20:19:56 +00:00
// Type field is present so decode it as a any.Any
var any any . Any
2018-10-03 18:18:55 +00:00
err := jsonpb . UnmarshalString ( configJSON , & any )
if err != nil {
return nil , err
}
// And then unmarshal the listener again...
err = proto . Unmarshal ( any . Value , & l )
if err != nil {
return nil , err
}
return & l , err
}
// No @type so try decoding as a straight listener.
err := jsonpb . UnmarshalString ( configJSON , & l )
return & l , err
}
2020-08-27 17:20:58 +00:00
// Ensure that the first filter in each filter chain of a public listener is
// the authz filter to prevent unauthorized access.
func ( s * Server ) injectConnectFilters ( _ connectionInfo , cfgSnap * proxycfg . ConfigSnapshot , listener * envoy . Listener ) error {
authzFilter , err := makeRBACNetworkFilter (
cfgSnap . ConnectProxy . Intentions ,
cfgSnap . IntentionDefaultAllow ,
)
2018-10-03 18:18:55 +00:00
if err != nil {
return err
}
2020-08-27 17:20:58 +00:00
2018-10-03 18:18:55 +00:00
for idx := range listener . FilterChains {
// Insert our authz filter before any others
listener . FilterChains [ idx ] . Filters =
2020-08-27 17:20:58 +00:00
append ( [ ] * envoylistener . Filter {
authzFilter ,
} , listener . FilterChains [ idx ] . Filters ... )
}
return nil
}
const httpConnectionManagerNewName = "envoy.filters.network.http_connection_manager"
// Locate the existing http connect manager L4 filter and inject our RBAC filter at the top.
func ( s * Server ) injectHTTPFilterOnFilterChains (
listener * envoy . Listener ,
authzFilter * envoyhttp . HttpFilter ,
) error {
for chainIdx , chain := range listener . FilterChains {
var (
hcmFilter * envoylistener . Filter
hcmFilterIdx int
)
for filterIdx , filter := range chain . Filters {
if filter . Name == wellknown . HTTPConnectionManager ||
filter . Name == httpConnectionManagerNewName {
hcmFilter = filter
hcmFilterIdx = filterIdx
break
}
}
if hcmFilter == nil {
return fmt . Errorf (
"filter chain %d lacks either a %q or %q filter" ,
chainIdx ,
wellknown . HTTPConnectionManager ,
httpConnectionManagerNewName ,
)
}
var (
hcm envoyhttp . HttpConnectionManager
isTyped bool
)
switch x := hcmFilter . ConfigType . ( type ) {
case * envoylistener . Filter_Config :
if err := conversion . StructToMessage ( x . Config , & hcm ) ; err != nil {
return err
}
isTyped = false
case * envoylistener . Filter_TypedConfig :
if err := pbtypes . UnmarshalAny ( x . TypedConfig , & hcm ) ; err != nil {
return err
}
isTyped = true
default :
return fmt . Errorf (
"filter chain %d has a %q filter with an unsupported config type: %T" ,
chainIdx ,
hcmFilter . Name ,
x ,
)
}
2018-10-03 18:18:55 +00:00
2020-08-27 17:20:58 +00:00
// Insert our authz filter before any others
hcm . HttpFilters = append ( [ ] * envoyhttp . HttpFilter {
authzFilter ,
} , hcm . HttpFilters ... )
// And persist the modified filter.
newFilter , err := makeFilter ( hcmFilter . Name , & hcm , isTyped )
if err != nil {
return err
}
chain . Filters [ hcmFilterIdx ] = newFilter
}
return nil
}
// Ensure every filter chain uses our TLS certs. We might allow users to work
// around this later if there is a good use case but this is actually a feature
// for now as it allows them to specify custom listener params in config but
// still get our certs delivered dynamically and intentions enforced without
// coming up with some complicated templating/merging solution.
func ( s * Server ) injectConnectTLSOnFilterChains ( _ connectionInfo , cfgSnap * proxycfg . ConfigSnapshot , listener * envoy . Listener ) error {
for idx := range listener . FilterChains {
2020-04-24 20:24:00 +00:00
listener . FilterChains [ idx ] . TlsContext = & envoyauth . DownstreamTlsContext {
2020-04-27 22:25:37 +00:00
CommonTlsContext : makeCommonTLSContextFromLeaf ( cfgSnap , cfgSnap . Leaf ( ) ) ,
2020-06-23 20:19:56 +00:00
RequireClientCertificate : & wrappers . BoolValue { Value : true } ,
2018-10-03 18:18:55 +00:00
}
}
return nil
}
2020-07-09 22:04:51 +00:00
func ( s * Server ) makePublicListener ( cInfo connectionInfo , cfgSnap * proxycfg . ConfigSnapshot ) ( proto . Message , error ) {
2018-10-03 18:18:55 +00:00
var l * envoy . Listener
var err error
2019-04-29 16:27:57 +00:00
cfg , err := ParseProxyConfig ( cfgSnap . Proxy . Config )
if err != nil {
// Don't hard fail on a config typo, just warn. The parse func returns
// default config if there is an error so it's safe to continue.
2020-01-28 23:50:41 +00:00
s . Logger . Warn ( "failed to parse Connect.Proxy.Config" , "error" , err )
2019-04-29 16:27:57 +00:00
}
if cfg . PublicListenerJSON != "" {
l , err = makeListenerFromUserConfig ( cfg . PublicListenerJSON )
if err != nil {
return l , err
2018-10-03 18:18:55 +00:00
}
2020-08-27 17:20:58 +00:00
// In the happy path don't return yet as we need to inject TLS and authz config still.
2018-10-03 18:18:55 +00:00
}
2020-08-27 17:20:58 +00:00
// This controls if we do L4 or L7 intention checks.
useHTTPFilter := structs . IsProtocolHTTPLike ( cfg . Protocol )
2018-10-03 18:18:55 +00:00
if l == nil {
// No user config, use default listener
addr := cfgSnap . Address
2019-07-05 15:06:47 +00:00
// Override with bind address if one is set, otherwise default
// to 0.0.0.0
if cfg . BindAddress != "" {
addr = cfg . BindAddress
} else if addr == "" {
2018-10-03 18:18:55 +00:00
addr = "0.0.0.0"
}
2019-07-05 15:06:47 +00:00
// Override with bind port if one is set, otherwise default to
// proxy service's address
port := cfgSnap . Port
if cfg . BindPort != 0 {
port = cfg . BindPort
}
l = makeListener ( PublicListenerName , addr , port )
2019-04-29 16:27:57 +00:00
2020-08-27 17:20:58 +00:00
opts := listenerFilterOpts {
useRDS : false ,
protocol : cfg . Protocol ,
filterName : "public_listener" ,
2020-09-04 18:45:05 +00:00
routeName : "public_listener" ,
2020-08-27 17:20:58 +00:00
cluster : LocalAppClusterName ,
statPrefix : "" ,
routePath : "" ,
ingress : true ,
}
if useHTTPFilter {
opts . httpAuthzFilter , err = makeRBACHTTPFilter (
cfgSnap . ConnectProxy . Intentions ,
cfgSnap . IntentionDefaultAllow ,
)
if err != nil {
return nil , err
}
}
filter , err := makeListenerFilter ( opts )
2018-10-03 18:18:55 +00:00
if err != nil {
2019-04-29 16:27:57 +00:00
return nil , err
2018-10-03 18:18:55 +00:00
}
2020-06-23 20:19:56 +00:00
l . FilterChains = [ ] * envoylistener . FilterChain {
2018-10-03 18:18:55 +00:00
{
2020-06-23 20:19:56 +00:00
Filters : [ ] * envoylistener . Filter {
2019-04-29 16:27:57 +00:00
filter ,
2018-10-03 18:18:55 +00:00
} ,
} ,
}
2020-08-27 17:20:58 +00:00
} else if useHTTPFilter {
httpAuthzFilter , err := makeRBACHTTPFilter (
cfgSnap . ConnectProxy . Intentions ,
cfgSnap . IntentionDefaultAllow ,
)
if err != nil {
return nil , err
}
// We're using the listener escape hatch, so try our best to inject the
// HTTP RBAC filter, but if we can't then just inject the RBAC Network
// filter instead.
if err := s . injectHTTPFilterOnFilterChains ( l , httpAuthzFilter ) ; err != nil {
s . Logger . Warn (
"could not inject the HTTP RBAC filter to enforce intentions on user-provided 'envoy_public_listener_json' config; falling back on the RBAC network filter instead" ,
"proxy" , cfgSnap . ProxyID ,
"error" , err ,
)
useHTTPFilter = false
}
}
if ! useHTTPFilter {
if err := s . injectConnectFilters ( cInfo , cfgSnap , l ) ; err != nil {
return nil , err
}
}
if err := s . injectConnectTLSOnFilterChains ( cInfo , cfgSnap , l ) ; err != nil {
return nil , err
2018-10-03 18:18:55 +00:00
}
return l , err
}
2019-09-26 02:55:52 +00:00
func ( s * Server ) makeExposedCheckListener ( cfgSnap * proxycfg . ConfigSnapshot , cluster string , path structs . ExposePath ) ( proto . Message , error ) {
cfg , err := ParseProxyConfig ( cfgSnap . Proxy . Config )
if err != nil {
// Don't hard fail on a config typo, just warn. The parse func returns
// default config if there is an error so it's safe to continue.
2020-01-28 23:50:41 +00:00
s . Logger . Warn ( "failed to parse Connect.Proxy.Config" , "error" , err )
2019-09-26 02:55:52 +00:00
}
// No user config, use default listener
addr := cfgSnap . Address
// Override with bind address if one is set, otherwise default to 0.0.0.0
if cfg . BindAddress != "" {
addr = cfg . BindAddress
} else if addr == "" {
addr = "0.0.0.0"
}
// Strip any special characters from path to make a valid and hopefully unique name
r := regexp . MustCompile ( ` [^a-zA-Z0-9]+ ` )
strippedPath := r . ReplaceAllString ( path . Path , "" )
listenerName := fmt . Sprintf ( "exposed_path_%s" , strippedPath )
l := makeListener ( listenerName , addr , path . ListenerPort )
filterName := fmt . Sprintf ( "exposed_path_filter_%s_%d" , strippedPath , path . ListenerPort )
2020-08-27 17:20:58 +00:00
opts := listenerFilterOpts {
useRDS : false ,
protocol : path . Protocol ,
filterName : filterName ,
2020-09-04 18:45:05 +00:00
routeName : filterName ,
2020-08-27 17:20:58 +00:00
cluster : cluster ,
statPrefix : "" ,
routePath : path . Path ,
ingress : true ,
httpAuthzFilter : nil ,
}
f , err := makeListenerFilter ( opts )
2019-09-26 02:55:52 +00:00
if err != nil {
return nil , err
}
2020-06-23 20:19:56 +00:00
chain := & envoylistener . FilterChain {
Filters : [ ] * envoylistener . Filter { f } ,
2019-09-26 02:55:52 +00:00
}
// For registered checks restrict traffic sources to localhost and Consul's advertise addr
if path . ParsedFromCheck {
// For the advertise addr we use a CidrRange that only matches one address
advertise := s . CfgFetcher . AdvertiseAddrLAN ( )
// Get prefix length based on whether address is ipv4 (32 bits) or ipv6 (128 bits)
advertiseLen := 32
ip := net . ParseIP ( advertise )
if ip != nil && strings . Contains ( advertise , ":" ) {
advertiseLen = 128
}
chain . FilterChainMatch = & envoylistener . FilterChainMatch {
SourcePrefixRanges : [ ] * envoycore . CidrRange {
2020-06-23 20:19:56 +00:00
{ AddressPrefix : "127.0.0.1" , PrefixLen : & wrappers . UInt32Value { Value : 8 } } ,
{ AddressPrefix : "::1" , PrefixLen : & wrappers . UInt32Value { Value : 128 } } ,
{ AddressPrefix : advertise , PrefixLen : & wrappers . UInt32Value { Value : uint32 ( advertiseLen ) } } ,
2019-09-26 02:55:52 +00:00
} ,
}
}
2020-06-23 20:19:56 +00:00
l . FilterChains = [ ] * envoylistener . FilterChain { chain }
2019-09-26 02:55:52 +00:00
return l , err
}
2020-07-09 22:04:51 +00:00
func ( s * Server ) makeTerminatingGatewayListener (
cInfo connectionInfo ,
cfgSnap * proxycfg . ConfigSnapshot ,
name , addr string ,
port int ,
) ( * envoy . Listener , error ) {
2020-04-13 16:33:01 +00:00
l := makeListener ( name , addr , port )
tlsInspector , err := makeTLSInspectorListenerFilter ( )
if err != nil {
return nil , err
}
2020-06-23 20:19:56 +00:00
l . ListenerFilters = [ ] * envoylistener . ListenerFilter { tlsInspector }
2020-04-13 16:33:01 +00:00
// Make a FilterChain for each linked service
// Match on the cluster name,
2020-08-27 17:20:58 +00:00
for _ , svc := range cfgSnap . TerminatingGateway . ValidServices ( ) {
2020-06-12 14:57:41 +00:00
clusterName := connect . ServiceSNI ( svc . Name , "" , svc . NamespaceOrDefault ( ) , cfgSnap . Datacenter , cfgSnap . Roots . TrustDomain )
2020-08-27 17:20:58 +00:00
// Resolvers are optional.
2020-04-14 14:59:23 +00:00
resolver , hasResolver := cfgSnap . TerminatingGateway . ServiceResolvers [ svc ]
2020-04-13 16:33:01 +00:00
2020-08-27 17:20:58 +00:00
intentions := cfgSnap . TerminatingGateway . Intentions [ svc ]
svcConfig := cfgSnap . TerminatingGateway . ServiceConfigs [ svc ]
cfg , err := ParseProxyConfig ( svcConfig . ProxyConfig )
if err != nil {
// Don't hard fail on a config typo, just warn. The parse func returns
// default config if there is an error so it's safe to continue.
s . Logger . Named ( logging . TerminatingGateway ) . Warn (
"failed to parse Connect.Proxy.Config for linked service" ,
"service" , svc . String ( ) ,
"error" , err ,
)
2020-04-17 01:04:14 +00:00
}
2020-08-27 17:20:58 +00:00
clusterChain , err := s . makeFilterChainTerminatingGateway (
cInfo ,
cfgSnap ,
name ,
clusterName ,
svc ,
intentions ,
cfg . Protocol ,
)
2020-04-13 16:33:01 +00:00
if err != nil {
2020-04-14 14:59:23 +00:00
return nil , fmt . Errorf ( "failed to make filter chain for cluster %q: %v" , clusterName , err )
2020-04-13 16:33:01 +00:00
}
2020-04-14 14:59:23 +00:00
l . FilterChains = append ( l . FilterChains , clusterChain )
2020-04-13 16:33:01 +00:00
2020-04-14 14:59:23 +00:00
// if there is a service-resolver for this service then also setup subset filter chains for it
if hasResolver {
// generate 1 filter chain for each service subset
for subsetName := range resolver . Subsets {
2020-08-27 17:20:58 +00:00
subsetClusterName := connect . ServiceSNI ( svc . Name , subsetName , svc . NamespaceOrDefault ( ) , cfgSnap . Datacenter , cfgSnap . Roots . TrustDomain )
subsetClusterChain , err := s . makeFilterChainTerminatingGateway (
cInfo ,
cfgSnap ,
name ,
subsetClusterName ,
svc ,
intentions ,
cfg . Protocol ,
)
2020-04-14 14:59:23 +00:00
if err != nil {
2020-08-27 17:20:58 +00:00
return nil , fmt . Errorf ( "failed to make filter chain for cluster %q: %v" , subsetClusterName , err )
2020-04-14 14:59:23 +00:00
}
2020-08-27 17:20:58 +00:00
l . FilterChains = append ( l . FilterChains , subsetClusterChain )
2020-04-14 14:59:23 +00:00
}
2020-04-13 16:33:01 +00:00
}
}
2020-04-14 21:13:25 +00:00
// This fallback catch-all filter ensures a listener will be present for health checks to pass
// Envoy will reset these connections since known endpoints are caught by filter chain matches above
tcpProxy , err := makeTCPProxyFilter ( name , "" , "terminating_gateway_" )
if err != nil {
return nil , err
}
2020-06-23 20:19:56 +00:00
fallback := & envoylistener . FilterChain {
Filters : [ ] * envoylistener . Filter {
2020-04-14 21:13:25 +00:00
{ Name : "envoy.filters.network.sni_cluster" } ,
tcpProxy ,
} ,
}
l . FilterChains = append ( l . FilterChains , fallback )
2020-04-13 16:33:01 +00:00
return l , nil
}
2020-08-27 17:20:58 +00:00
func ( s * Server ) makeFilterChainTerminatingGateway (
_ connectionInfo ,
2020-07-09 22:04:51 +00:00
cfgSnap * proxycfg . ConfigSnapshot ,
listener , cluster string ,
service structs . ServiceName ,
2020-08-27 17:20:58 +00:00
intentions structs . Intentions ,
protocol string ,
2020-07-09 22:04:51 +00:00
) ( * envoylistener . FilterChain , error ) {
2020-08-27 17:20:58 +00:00
filterChain := & envoylistener . FilterChain {
FilterChainMatch : makeSNIFilterChainMatch ( cluster ) ,
Filters : make ( [ ] * envoylistener . Filter , 0 , 3 ) ,
TlsContext : & envoyauth . DownstreamTlsContext {
CommonTlsContext : makeCommonTLSContextFromLeaf ( cfgSnap , cfgSnap . TerminatingGateway . ServiceLeaves [ service ] ) ,
RequireClientCertificate : & wrappers . BoolValue { Value : true } ,
} ,
2020-04-24 20:24:00 +00:00
}
2020-08-27 17:20:58 +00:00
// This controls if we do L4 or L7 intention checks.
useHTTPFilter := structs . IsProtocolHTTPLike ( protocol )
// If this is L4, the first filter we setup is to do intention checks.
if ! useHTTPFilter {
authFilter , err := makeRBACNetworkFilter (
intentions ,
cfgSnap . IntentionDefaultAllow ,
)
if err != nil {
return nil , err
}
filterChain . Filters = append ( filterChain . Filters , authFilter )
2020-04-14 14:59:23 +00:00
}
2020-08-27 17:20:58 +00:00
// Lastly we setup the actual proxying component. For L4 this is a straight
// tcp proxy. For L7 this is a very hands-off HTTP proxy just to inject an
// HTTP filter to do intention checks here instead.
2020-06-12 14:57:41 +00:00
statPrefix := fmt . Sprintf ( "terminating_gateway_%s_%s_" , service . NamespaceOrDefault ( ) , service . Name )
2020-08-27 17:20:58 +00:00
opts := listenerFilterOpts {
protocol : protocol ,
filterName : listener ,
2020-09-04 18:45:05 +00:00
routeName : cluster , // Set cluster name for route config since each will have its own
2020-08-27 17:20:58 +00:00
cluster : cluster ,
statPrefix : statPrefix ,
routePath : "" ,
ingress : false ,
}
if useHTTPFilter {
var err error
opts . httpAuthzFilter , err = makeRBACHTTPFilter (
intentions ,
cfgSnap . IntentionDefaultAllow ,
)
if err != nil {
return nil , err
}
2020-08-28 20:27:40 +00:00
opts . cluster = ""
opts . useRDS = true
2020-08-27 17:20:58 +00:00
}
filter , err := makeListenerFilter ( opts )
2020-04-14 14:59:23 +00:00
if err != nil {
2020-06-23 20:19:56 +00:00
return nil , err
2020-04-14 14:59:23 +00:00
}
2020-08-27 17:20:58 +00:00
filterChain . Filters = append ( filterChain . Filters , filter )
2020-04-14 14:59:23 +00:00
2020-08-27 17:20:58 +00:00
return filterChain , nil
2020-04-14 14:59:23 +00:00
}
2020-04-13 16:33:01 +00:00
func ( s * Server ) makeMeshGatewayListener ( name , addr string , port int , cfgSnap * proxycfg . ConfigSnapshot ) ( * envoy . Listener , error ) {
2019-06-18 00:52:01 +00:00
tlsInspector , err := makeTLSInspectorListenerFilter ( )
if err != nil {
return nil , err
}
sniCluster , err := makeSNIClusterFilter ( )
if err != nil {
return nil , err
}
// The cluster name here doesn't matter as the sni_cluster
// filter will fill it in for us.
tcpProxy , err := makeTCPProxyFilter ( name , "" , "mesh_gateway_local_" )
if err != nil {
return nil , err
}
2020-06-23 20:19:56 +00:00
sniClusterChain := & envoylistener . FilterChain {
Filters : [ ] * envoylistener . Filter {
2019-06-18 00:52:01 +00:00
sniCluster ,
tcpProxy ,
} ,
}
l := makeListener ( name , addr , port )
2020-06-23 20:19:56 +00:00
l . ListenerFilters = [ ] * envoylistener . ListenerFilter { tlsInspector }
2019-06-18 00:52:01 +00:00
// TODO (mesh-gateway) - Do we need to create clusters for all the old trust domains as well?
// We need 1 Filter Chain per datacenter
2020-03-09 20:59:02 +00:00
datacenters := cfgSnap . MeshGateway . Datacenters ( )
for _ , dc := range datacenters {
if dc == cfgSnap . Datacenter {
continue // skip local
}
2019-08-19 18:03:03 +00:00
clusterName := connect . DatacenterSNI ( dc , cfgSnap . Roots . TrustDomain )
2019-06-18 00:52:01 +00:00
filterName := fmt . Sprintf ( "%s_%s" , name , dc )
dcTCPProxy , err := makeTCPProxyFilter ( filterName , clusterName , "mesh_gateway_remote_" )
if err != nil {
return nil , err
}
2020-06-23 20:19:56 +00:00
l . FilterChains = append ( l . FilterChains , & envoylistener . FilterChain {
2019-06-18 00:52:01 +00:00
FilterChainMatch : & envoylistener . FilterChainMatch {
ServerNames : [ ] string { fmt . Sprintf ( "*.%s" , clusterName ) } ,
} ,
2020-06-23 20:19:56 +00:00
Filters : [ ] * envoylistener . Filter {
2019-06-18 00:52:01 +00:00
dcTCPProxy ,
} ,
} )
}
2020-03-09 20:59:02 +00:00
if cfgSnap . ServiceMeta [ structs . MetaWANFederationKey ] == "1" && cfgSnap . ServerSNIFn != nil {
for _ , dc := range datacenters {
if dc == cfgSnap . Datacenter {
continue // skip local
}
clusterName := cfgSnap . ServerSNIFn ( dc , "" )
filterName := fmt . Sprintf ( "%s_%s" , name , dc )
dcTCPProxy , err := makeTCPProxyFilter ( filterName , clusterName , "mesh_gateway_remote_" )
if err != nil {
return nil , err
}
2020-06-23 20:19:56 +00:00
l . FilterChains = append ( l . FilterChains , & envoylistener . FilterChain {
2020-03-09 20:59:02 +00:00
FilterChainMatch : & envoylistener . FilterChainMatch {
ServerNames : [ ] string { fmt . Sprintf ( "*.%s" , clusterName ) } ,
} ,
2020-06-23 20:19:56 +00:00
Filters : [ ] * envoylistener . Filter {
2020-03-09 20:59:02 +00:00
dcTCPProxy ,
} ,
} )
}
// Wildcard all flavors to each server.
for _ , srv := range cfgSnap . MeshGateway . ConsulServers {
clusterName := cfgSnap . ServerSNIFn ( cfgSnap . Datacenter , srv . Node . Node )
filterName := fmt . Sprintf ( "%s_%s" , name , cfgSnap . Datacenter )
dcTCPProxy , err := makeTCPProxyFilter ( filterName , clusterName , "mesh_gateway_local_server_" )
if err != nil {
return nil , err
}
2020-06-23 20:19:56 +00:00
l . FilterChains = append ( l . FilterChains , & envoylistener . FilterChain {
2020-03-09 20:59:02 +00:00
FilterChainMatch : & envoylistener . FilterChainMatch {
ServerNames : [ ] string { fmt . Sprintf ( "%s" , clusterName ) } ,
} ,
2020-06-23 20:19:56 +00:00
Filters : [ ] * envoylistener . Filter {
2020-03-09 20:59:02 +00:00
dcTCPProxy ,
} ,
} )
}
}
2019-06-18 00:52:01 +00:00
// This needs to get tacked on at the end as it has no
// matching and will act as a catch all
l . FilterChains = append ( l . FilterChains , sniClusterChain )
return l , nil
}
2019-07-02 03:10:51 +00:00
func ( s * Server ) makeUpstreamListenerForDiscoveryChain (
u * structs . Upstream ,
2020-05-21 14:08:12 +00:00
address string ,
2019-07-02 03:10:51 +00:00
chain * structs . CompiledDiscoveryChain ,
cfgSnap * proxycfg . ConfigSnapshot ,
2020-04-27 23:36:20 +00:00
tlsContext * envoyauth . DownstreamTlsContext ,
2019-07-02 03:10:51 +00:00
) ( proto . Message , error ) {
2020-05-21 14:08:12 +00:00
if address == "" {
address = "127.0.0.1"
2019-07-08 11:48:48 +00:00
}
2019-07-02 03:10:51 +00:00
upstreamID := u . Identifier ( )
2020-05-21 14:08:12 +00:00
l := makeListener ( upstreamID , address , u . LocalBindPort )
2019-07-02 03:10:51 +00:00
2020-05-21 14:08:12 +00:00
cfg := getAndModifyUpstreamConfigForListener ( s . Logger , u , chain )
if cfg . ListenerJSON != "" {
return makeListenerFromUserConfig ( cfg . ListenerJSON )
2019-07-08 11:48:48 +00:00
}
2019-10-17 21:44:59 +00:00
useRDS := true
2020-11-06 01:24:29 +00:00
var (
clusterName string
destination , datacenter , namespace string
)
2020-05-21 14:08:12 +00:00
if chain == nil || chain . IsDefault ( ) {
2020-11-06 01:24:29 +00:00
useRDS = false
2020-05-21 14:08:12 +00:00
dc := u . Datacenter
if dc == "" {
dc = cfgSnap . Datacenter
}
2020-11-06 01:24:29 +00:00
destination , datacenter , namespace = u . DestinationName , dc , u . DestinationNamespace
2020-05-21 14:08:12 +00:00
2020-11-06 01:24:29 +00:00
sni := connect . UpstreamSNI ( u , "" , dc , cfgSnap . Roots . TrustDomain )
2020-05-21 14:08:12 +00:00
clusterName = CustomizeClusterName ( sni , chain )
2020-11-06 01:24:29 +00:00
} else {
destination , datacenter , namespace = chain . ServiceName , chain . Datacenter , chain . Namespace
2020-05-21 14:08:12 +00:00
2020-11-06 01:24:29 +00:00
if cfg . Protocol == "tcp" {
useRDS = false
startNode := chain . Nodes [ chain . StartNode ]
if startNode == nil {
return nil , fmt . Errorf ( "missing first node in compiled discovery chain for: %s" , chain . ServiceName )
}
if startNode . Type != structs . DiscoveryGraphNodeTypeResolver {
return nil , fmt . Errorf ( "unexpected first node in discovery chain using protocol=%q: %s" , cfg . Protocol , startNode . Type )
}
targetID := startNode . Resolver . Target
target := chain . Targets [ targetID ]
clusterName = CustomizeClusterName ( target . Name , chain )
}
}
filterName := fmt . Sprintf ( "%s.%s" , destination , datacenter )
if namespace != "" {
filterName += fmt . Sprintf ( ".%s" , namespace )
}
if u . DestinationType == structs . UpstreamDestTypePreparedQuery {
// Avoid encoding dc and namespace for prepared queries.
// Those are defined in the query itself and are not available here.
filterName = upstreamID
2019-10-17 21:44:59 +00:00
}
2020-08-27 17:20:58 +00:00
opts := listenerFilterOpts {
useRDS : useRDS ,
protocol : cfg . Protocol ,
2020-11-06 01:24:29 +00:00
filterName : filterName ,
2020-09-04 18:45:05 +00:00
routeName : upstreamID ,
2020-08-27 17:20:58 +00:00
cluster : clusterName ,
2020-11-06 01:24:29 +00:00
statPrefix : "upstream." ,
2020-08-27 17:20:58 +00:00
routePath : "" ,
ingress : false ,
httpAuthzFilter : nil ,
}
filter , err := makeListenerFilter ( opts )
2019-07-02 03:10:51 +00:00
if err != nil {
return nil , err
}
2020-06-23 20:19:56 +00:00
l . FilterChains = [ ] * envoylistener . FilterChain {
2019-07-02 03:10:51 +00:00
{
2020-06-23 20:19:56 +00:00
Filters : [ ] * envoylistener . Filter {
2019-07-02 03:10:51 +00:00
filter ,
} ,
2020-04-27 23:36:20 +00:00
TlsContext : tlsContext ,
2019-07-02 03:10:51 +00:00
} ,
}
return l , nil
}
2020-05-21 14:08:12 +00:00
func getAndModifyUpstreamConfigForListener ( logger hclog . Logger , u * structs . Upstream , chain * structs . CompiledDiscoveryChain ) UpstreamConfig {
var (
cfg UpstreamConfig
err error
)
if chain == nil || chain . IsDefault ( ) {
cfg , err = ParseUpstreamConfig ( u . Config )
if err != nil {
// Don't hard fail on a config typo, just warn. The parse func returns
// default config if there is an error so it's safe to continue.
logger . Warn ( "failed to parse" , "upstream" , u . Identifier ( ) , "error" , err )
}
} else {
// Use NoDefaults here so that we can set the protocol to the chain
// protocol if necessary
cfg , err = ParseUpstreamConfigNoDefaults ( u . Config )
if err != nil {
// Don't hard fail on a config typo, just warn. The parse func returns
// default config if there is an error so it's safe to continue.
logger . Warn ( "failed to parse" , "upstream" , u . Identifier ( ) , "error" , err )
}
if cfg . ListenerJSON != "" {
logger . Warn ( "ignoring escape hatch setting because already configured for" ,
"discovery chain" , chain . ServiceName , "upstream" , u . Identifier ( ) , "config" , "envoy_listener_json" )
// Remove from config struct so we don't use it later on
cfg . ListenerJSON = ""
}
proto := cfg . Protocol
if proto == "" {
proto = chain . Protocol
}
if proto == "" {
proto = "tcp"
}
// set back on the config so that we can use it from return value
cfg . Protocol = proto
}
return cfg
}
2020-08-27 17:20:58 +00:00
type listenerFilterOpts struct {
useRDS bool
protocol string
filterName string
2020-09-04 18:45:05 +00:00
routeName string
2020-08-27 17:20:58 +00:00
cluster string
statPrefix string
routePath string
ingress bool
httpAuthzFilter * envoyhttp . HttpFilter
}
2019-09-26 02:55:52 +00:00
2020-08-27 17:20:58 +00:00
func makeListenerFilter ( opts listenerFilterOpts ) ( * envoylistener . Filter , error ) {
switch opts . protocol {
2020-09-04 18:45:05 +00:00
case "grpc" , "http2" , "http" :
return makeHTTPFilter ( opts )
2019-04-29 16:27:57 +00:00
case "tcp" :
fallthrough
default :
2020-08-27 17:20:58 +00:00
if opts . useRDS {
2020-06-23 20:19:56 +00:00
return nil , fmt . Errorf ( "RDS is not compatible with the tcp proxy filter" )
2020-08-27 17:20:58 +00:00
} else if opts . cluster == "" {
2020-06-23 20:19:56 +00:00
return nil , fmt . Errorf ( "cluster name is required for a tcp proxy filter" )
2019-10-17 21:44:59 +00:00
}
2020-08-27 17:20:58 +00:00
return makeTCPProxyFilter ( opts . filterName , opts . cluster , opts . statPrefix )
2019-04-29 16:27:57 +00:00
}
}
2020-06-23 20:19:56 +00:00
func makeTLSInspectorListenerFilter ( ) ( * envoylistener . ListenerFilter , error ) {
return & envoylistener . ListenerFilter { Name : wellknown . TlsInspector } , nil
2019-06-18 00:52:01 +00:00
}
2020-04-13 16:33:01 +00:00
func makeSNIFilterChainMatch ( sniMatch string ) * envoylistener . FilterChainMatch {
2019-06-18 00:52:01 +00:00
return & envoylistener . FilterChainMatch {
ServerNames : [ ] string { sniMatch } ,
2020-04-13 16:33:01 +00:00
}
2019-06-18 00:52:01 +00:00
}
2020-06-23 20:19:56 +00:00
func makeSNIClusterFilter ( ) ( * envoylistener . Filter , error ) {
2019-06-18 00:52:01 +00:00
// This filter has no config which is why we are not calling make
2020-06-23 20:19:56 +00:00
return & envoylistener . Filter { Name : "envoy.filters.network.sni_cluster" } , nil
2019-06-18 00:52:01 +00:00
}
2020-06-23 20:19:56 +00:00
func makeTCPProxyFilter ( filterName , cluster , statPrefix string ) ( * envoylistener . Filter , error ) {
2018-10-03 18:18:55 +00:00
cfg := & envoytcp . TcpProxy {
2019-06-07 12:10:43 +00:00
StatPrefix : makeStatPrefix ( "tcp" , statPrefix , filterName ) ,
ClusterSpecifier : & envoytcp . TcpProxy_Cluster { Cluster : cluster } ,
2018-10-03 18:18:55 +00:00
}
2020-08-27 17:20:58 +00:00
return makeFilter ( "envoy.tcp_proxy" , cfg , false )
2018-10-03 18:18:55 +00:00
}
2019-04-29 16:27:57 +00:00
func makeStatPrefix ( protocol , prefix , filterName string ) string {
// Replace colons here because Envoy does that in the metrics for the actual
// clusters but doesn't in the stat prefix here while dashboards assume they
// will match.
2020-11-06 01:24:29 +00:00
return fmt . Sprintf ( "%s%s.%s" , prefix , strings . Replace ( filterName , ":" , "_" , - 1 ) , protocol )
2019-04-29 16:27:57 +00:00
}
2020-09-04 18:45:05 +00:00
func makeHTTPFilter ( opts listenerFilterOpts ) ( * envoylistener . Filter , error ) {
2020-06-23 20:19:56 +00:00
op := envoyhttp . HttpConnectionManager_Tracing_INGRESS
2020-09-04 18:45:05 +00:00
if ! opts . ingress {
2020-06-23 20:19:56 +00:00
op = envoyhttp . HttpConnectionManager_Tracing_EGRESS
2019-04-29 16:27:57 +00:00
}
proto := "http"
2020-09-04 18:45:05 +00:00
if opts . protocol == "grpc" {
proto = opts . protocol
2019-04-29 16:27:57 +00:00
}
2019-09-26 02:55:52 +00:00
2019-04-29 16:27:57 +00:00
cfg := & envoyhttp . HttpConnectionManager {
2020-09-04 18:45:05 +00:00
StatPrefix : makeStatPrefix ( proto , opts . statPrefix , opts . filterName ) ,
2020-06-23 20:19:56 +00:00
CodecType : envoyhttp . HttpConnectionManager_AUTO ,
2019-07-02 03:10:51 +00:00
HttpFilters : [ ] * envoyhttp . HttpFilter {
2020-06-16 17:19:31 +00:00
{
2019-07-02 03:10:51 +00:00
Name : "envoy.router" ,
} ,
} ,
Tracing : & envoyhttp . HttpConnectionManager_Tracing {
OperationName : op ,
// Don't trace any requests by default unless the client application
// explicitly propagates trace headers that indicate this should be
// sampled.
RandomSampling : & envoytype . Percent { Value : 0.0 } ,
} ,
}
2020-09-04 18:45:05 +00:00
if opts . useRDS {
if opts . cluster != "" {
2020-06-23 20:19:56 +00:00
return nil , fmt . Errorf ( "cannot specify cluster name when using RDS" )
2019-07-02 03:10:51 +00:00
}
cfg . RouteSpecifier = & envoyhttp . HttpConnectionManager_Rds {
Rds : & envoyhttp . Rds {
2020-09-04 18:45:05 +00:00
RouteConfigName : opts . routeName ,
2020-06-23 20:19:56 +00:00
ConfigSource : & envoycore . ConfigSource {
2019-07-02 03:10:51 +00:00
ConfigSourceSpecifier : & envoycore . ConfigSource_Ads {
Ads : & envoycore . AggregatedConfigSource { } ,
} ,
} ,
} ,
}
} else {
2020-09-04 18:45:05 +00:00
if opts . cluster == "" {
2020-06-23 20:19:56 +00:00
return nil , fmt . Errorf ( "must specify cluster name when not using RDS" )
2019-07-02 03:10:51 +00:00
}
2020-06-23 20:19:56 +00:00
route := & envoyroute . Route {
Match : & envoyroute . RouteMatch {
2019-09-26 02:55:52 +00:00
PathSpecifier : & envoyroute . RouteMatch_Prefix {
Prefix : "/" ,
} ,
// TODO(banks) Envoy supports matching only valid GRPC
// requests which might be nice to add here for gRPC services
// but it's not supported in our current envoy SDK version
// although docs say it was supported by 1.8.0. Going to defer
// that until we've updated the deps.
} ,
Action : & envoyroute . Route_Route {
Route : & envoyroute . RouteAction {
ClusterSpecifier : & envoyroute . RouteAction_Cluster {
2020-09-04 18:45:05 +00:00
Cluster : opts . cluster ,
2019-09-26 02:55:52 +00:00
} ,
} ,
} ,
}
// If a path is provided, do not match on a catch-all prefix
2020-09-04 18:45:05 +00:00
if opts . routePath != "" {
route . Match . PathSpecifier = & envoyroute . RouteMatch_Path { Path : opts . routePath }
2019-09-26 02:55:52 +00:00
}
2019-07-02 03:10:51 +00:00
cfg . RouteSpecifier = & envoyhttp . HttpConnectionManager_RouteConfig {
2019-04-29 16:27:57 +00:00
RouteConfig : & envoy . RouteConfiguration {
2020-09-04 18:45:05 +00:00
Name : opts . routeName ,
2020-06-23 20:19:56 +00:00
VirtualHosts : [ ] * envoyroute . VirtualHost {
2019-09-26 02:55:52 +00:00
{
2020-09-04 18:45:05 +00:00
Name : opts . filterName ,
2019-04-29 16:27:57 +00:00
Domains : [ ] string { "*" } ,
2020-06-23 20:19:56 +00:00
Routes : [ ] * envoyroute . Route {
2019-09-26 02:55:52 +00:00
route ,
2019-04-29 16:27:57 +00:00
} ,
} ,
} ,
} ,
2019-07-02 03:10:51 +00:00
}
2019-04-29 16:27:57 +00:00
}
2020-09-04 18:45:05 +00:00
if opts . protocol == "http2" || opts . protocol == "grpc" {
2019-04-29 16:27:57 +00:00
cfg . Http2ProtocolOptions = & envoycore . Http2ProtocolOptions { }
}
2020-08-27 17:20:58 +00:00
// Like injectConnectFilters for L4, here we ensure that the first filter
// (other than the "envoy.grpc_http1_bridge" filter) in the http filter
// chain of a public listener is the authz filter to prevent unauthorized
// access and that every filter chain uses our TLS certs.
2020-09-04 18:45:05 +00:00
if opts . httpAuthzFilter != nil {
cfg . HttpFilters = append ( [ ] * envoyhttp . HttpFilter { opts . httpAuthzFilter } , cfg . HttpFilters ... )
2020-08-27 17:20:58 +00:00
}
2020-09-04 18:45:05 +00:00
if opts . protocol == "grpc" {
2020-08-27 17:20:58 +00:00
// Add grpc bridge before router and authz
2019-09-26 02:55:52 +00:00
cfg . HttpFilters = append ( [ ] * envoyhttp . HttpFilter { {
2019-06-07 12:10:43 +00:00
Name : "envoy.grpc_http1_bridge" ,
2020-06-23 20:19:56 +00:00
ConfigType : & envoyhttp . HttpFilter_Config { Config : & pbstruct . Struct { } } ,
2019-04-29 16:27:57 +00:00
} } , cfg . HttpFilters ... )
}
2020-08-27 17:20:58 +00:00
return makeFilter ( "envoy.http_connection_manager" , cfg , false )
2019-04-29 16:27:57 +00:00
}
2020-08-27 17:20:58 +00:00
func makeFilter ( name string , cfg proto . Message , typed bool ) ( * envoylistener . Filter , error ) {
filter := & envoylistener . Filter {
Name : name ,
2018-10-03 18:18:55 +00:00
}
2020-08-27 17:20:58 +00:00
if typed {
any , err := pbtypes . MarshalAny ( cfg )
if err != nil {
return nil , err
}
filter . ConfigType = & envoylistener . Filter_TypedConfig { TypedConfig : any }
} else {
// Ridiculous dance to make that struct into pbstruct.Struct by... encoding it
// as JSON and decoding again!!
cfgStruct , err := conversion . MessageToStruct ( cfg )
if err != nil {
return nil , err
}
filter . ConfigType = & envoylistener . Filter_Config { Config : cfgStruct }
}
return filter , nil
2018-10-03 18:18:55 +00:00
}
2020-08-27 17:20:58 +00:00
func makeEnvoyHTTPFilter ( name string , cfg proto . Message ) ( * envoyhttp . HttpFilter , error ) {
2020-06-23 20:19:56 +00:00
// Ridiculous dance to make that struct into pbstruct.Struct by... encoding it
2018-10-03 18:18:55 +00:00
// as JSON and decoding again!!
2020-06-23 20:19:56 +00:00
cfgStruct , err := conversion . MessageToStruct ( cfg )
2018-10-03 18:18:55 +00:00
if err != nil {
2020-06-23 20:19:56 +00:00
return nil , err
2018-10-03 18:18:55 +00:00
}
2020-08-27 17:20:58 +00:00
return & envoyhttp . HttpFilter {
2019-06-07 12:10:43 +00:00
Name : name ,
2020-08-27 17:20:58 +00:00
ConfigType : & envoyhttp . HttpFilter_Config { Config : cfgStruct } ,
2018-10-03 18:18:55 +00:00
} , nil
}
2020-04-27 22:25:37 +00:00
func makeCommonTLSContextFromLeaf ( cfgSnap * proxycfg . ConfigSnapshot , leaf * structs . IssuedCert ) * envoyauth . CommonTlsContext {
2018-10-03 18:18:55 +00:00
// Concatenate all the root PEMs into one.
2019-03-22 19:37:14 +00:00
if cfgSnap . Roots == nil {
return nil
}
2020-08-28 20:27:40 +00:00
// TODO(banks): verify this actually works with Envoy (docs are not clear).
rootPEMS := ""
2018-10-03 18:18:55 +00:00
for _ , root := range cfgSnap . Roots . Roots {
rootPEMS += root . RootCert
}
return & envoyauth . CommonTlsContext {
TlsParams : & envoyauth . TlsParameters { } ,
TlsCertificates : [ ] * envoyauth . TlsCertificate {
2020-06-16 17:19:31 +00:00
{
2018-10-03 18:18:55 +00:00
CertificateChain : & envoycore . DataSource {
Specifier : & envoycore . DataSource_InlineString {
2020-04-16 21:00:48 +00:00
InlineString : leaf . CertPEM ,
2018-10-03 18:18:55 +00:00
} ,
} ,
PrivateKey : & envoycore . DataSource {
Specifier : & envoycore . DataSource_InlineString {
2020-04-16 21:00:48 +00:00
InlineString : leaf . PrivateKeyPEM ,
2018-10-03 18:18:55 +00:00
} ,
} ,
} ,
} ,
ValidationContextType : & envoyauth . CommonTlsContext_ValidationContext {
ValidationContext : & envoyauth . CertificateValidationContext {
// TODO(banks): later for L7 support we may need to configure ALPN here.
TrustedCa : & envoycore . DataSource {
Specifier : & envoycore . DataSource_InlineString {
InlineString : rootPEMS ,
} ,
} ,
} ,
} ,
}
}
2020-04-27 22:25:37 +00:00
func makeCommonTLSContextFromFiles ( caFile , certFile , keyFile string ) * envoyauth . CommonTlsContext {
ctx := envoyauth . CommonTlsContext {
TlsParams : & envoyauth . TlsParameters { } ,
}
// Verify certificate of peer if caFile is specified
if caFile != "" {
ctx . ValidationContextType = & envoyauth . CommonTlsContext_ValidationContext {
ValidationContext : & envoyauth . CertificateValidationContext {
TrustedCa : & envoycore . DataSource {
Specifier : & envoycore . DataSource_Filename {
Filename : caFile ,
} ,
} ,
} ,
}
}
// Present certificate for mTLS if cert and key files are specified
if certFile != "" && keyFile != "" {
ctx . TlsCertificates = [ ] * envoyauth . TlsCertificate {
{
CertificateChain : & envoycore . DataSource {
Specifier : & envoycore . DataSource_Filename {
Filename : certFile ,
} ,
} ,
PrivateKey : & envoycore . DataSource {
Specifier : & envoycore . DataSource_Filename {
Filename : keyFile ,
} ,
} ,
} ,
}
}
return & ctx
}