github.com/sl1pm4t/consul@v1.4.5-0.20190325224627-74c31c540f9c/agent/xds/server_test.go (about)

     1  package xds
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"log"
     9  	"os"
    10  	"strings"
    11  	"sync"
    12  	"sync/atomic"
    13  	"testing"
    14  	"text/template"
    15  	"time"
    16  
    17  	envoy "github.com/envoyproxy/go-control-plane/envoy/api/v2"
    18  	"github.com/gogo/protobuf/jsonpb"
    19  	"github.com/stretchr/testify/require"
    20  	"google.golang.org/grpc/codes"
    21  	"google.golang.org/grpc/metadata"
    22  	"google.golang.org/grpc/status"
    23  
    24  	"github.com/hashicorp/consul/acl"
    25  	"github.com/hashicorp/consul/agent/cache"
    26  	"github.com/hashicorp/consul/agent/proxycfg"
    27  	"github.com/hashicorp/consul/agent/structs"
    28  )
    29  
    30  // testManager is a mock of proxycfg.Manager that's simpler to control for
    31  // testing. It also implements ConnectAuthz to allow control over authorization.
    32  type testManager struct {
    33  	sync.Mutex
    34  	chans   map[string]chan *proxycfg.ConfigSnapshot
    35  	cancels chan string
    36  	authz   map[string]connectAuthzResult
    37  }
    38  
    39  type connectAuthzResult struct {
    40  	authz  bool
    41  	reason string
    42  	m      *cache.ResultMeta
    43  	err    error
    44  }
    45  
    46  func newTestManager(t *testing.T) *testManager {
    47  	return &testManager{
    48  		chans:   map[string]chan *proxycfg.ConfigSnapshot{},
    49  		cancels: make(chan string, 10),
    50  		authz:   make(map[string]connectAuthzResult),
    51  	}
    52  }
    53  
    54  // RegisterProxy simulates a proxy registration
    55  func (m *testManager) RegisterProxy(t *testing.T, proxyID string) {
    56  	m.Lock()
    57  	defer m.Unlock()
    58  	m.chans[proxyID] = make(chan *proxycfg.ConfigSnapshot, 1)
    59  }
    60  
    61  // Deliver simulates a proxy registration
    62  func (m *testManager) DeliverConfig(t *testing.T, proxyID string, cfg *proxycfg.ConfigSnapshot) {
    63  	t.Helper()
    64  	m.Lock()
    65  	defer m.Unlock()
    66  	select {
    67  	case m.chans[proxyID] <- cfg:
    68  	case <-time.After(10 * time.Millisecond):
    69  		t.Fatalf("took too long to deliver config")
    70  	}
    71  }
    72  
    73  // Watch implements ConfigManager
    74  func (m *testManager) Watch(proxyID string) (<-chan *proxycfg.ConfigSnapshot, proxycfg.CancelFunc) {
    75  	m.Lock()
    76  	defer m.Unlock()
    77  	// ch might be nil but then it will just block forever
    78  	return m.chans[proxyID], func() {
    79  		m.cancels <- proxyID
    80  	}
    81  }
    82  
    83  // AssertWatchCancelled checks that the most recent call to a Watch cancel func
    84  // was from the specified proxyID and that one is made in a short time. This
    85  // probably won't work if you are running multiple Watches in parallel on
    86  // multiple proxyIDS due to timing/ordering issues but I don't think we need to
    87  // do that.
    88  func (m *testManager) AssertWatchCancelled(t *testing.T, proxyID string) {
    89  	t.Helper()
    90  	select {
    91  	case got := <-m.cancels:
    92  		require.Equal(t, proxyID, got)
    93  	case <-time.After(50 * time.Millisecond):
    94  		t.Fatalf("timed out waiting for Watch cancel for %s", proxyID)
    95  	}
    96  }
    97  
    98  // ConnectAuthorize implements ConnectAuthz
    99  func (m *testManager) ConnectAuthorize(token string, req *structs.ConnectAuthorizeRequest) (authz bool, reason string, meta *cache.ResultMeta, err error) {
   100  	m.Lock()
   101  	defer m.Unlock()
   102  	if res, ok := m.authz[token]; ok {
   103  		return res.authz, res.reason, res.m, res.err
   104  	}
   105  	// Default allow but with reason that won't match by accident in a test case
   106  	return true, "OK: allowed by default test implementation", nil, nil
   107  }
   108  
   109  func TestServer_StreamAggregatedResources_BasicProtocol(t *testing.T) {
   110  	logger := log.New(os.Stderr, "", log.LstdFlags)
   111  	mgr := newTestManager(t)
   112  	aclResolve := func(id string) (acl.Authorizer, error) {
   113  		// Allow all
   114  		return acl.RootAuthorizer("manage"), nil
   115  	}
   116  	envoy := NewTestEnvoy(t, "web-sidecar-proxy", "")
   117  	defer envoy.Close()
   118  
   119  	s := Server{
   120  		Logger:       logger,
   121  		CfgMgr:       mgr,
   122  		Authz:        mgr,
   123  		ResolveToken: aclResolve,
   124  	}
   125  	s.Initialize()
   126  
   127  	go func() {
   128  		err := s.StreamAggregatedResources(envoy.stream)
   129  		require.NoError(t, err)
   130  	}()
   131  
   132  	// Register the proxy to create state needed to Watch() on
   133  	mgr.RegisterProxy(t, "web-sidecar-proxy")
   134  
   135  	// Send initial cluster discover
   136  	envoy.SendReq(t, ClusterType, 0, 0)
   137  
   138  	// Check no response sent yet
   139  	assertChanBlocked(t, envoy.stream.sendCh)
   140  
   141  	// Deliver a new snapshot
   142  	snap := proxycfg.TestConfigSnapshot(t)
   143  	mgr.DeliverConfig(t, "web-sidecar-proxy", snap)
   144  
   145  	assertResponseSent(t, envoy.stream.sendCh, expectClustersJSON(t, snap, "", 1, 1))
   146  
   147  	// Envoy then tries to discover endpoints for those clusters. Technically it
   148  	// includes the cluster names in the ResourceNames field but we ignore that
   149  	// completely for now so not bothering to simulate that.
   150  	envoy.SendReq(t, EndpointType, 0, 0)
   151  
   152  	// It also (in parallel) issues the next cluster request (which acts as an ACK
   153  	// of the version we sent)
   154  	envoy.SendReq(t, ClusterType, 1, 1)
   155  
   156  	// We should get a response immediately since the config is already present in
   157  	// the server for endpoints. Note that this should not be racy if the server
   158  	// is behaving well since the Cluster send above should be blocked until we
   159  	// deliver a new config version.
   160  	assertResponseSent(t, envoy.stream.sendCh, expectEndpointsJSON(t, snap, "", 1, 2))
   161  
   162  	// And no other response yet
   163  	assertChanBlocked(t, envoy.stream.sendCh)
   164  
   165  	// Envoy now sends listener request along with next endpoint one
   166  	envoy.SendReq(t, ListenerType, 0, 0)
   167  	envoy.SendReq(t, EndpointType, 1, 2)
   168  
   169  	// And should get a response immediately.
   170  	assertResponseSent(t, envoy.stream.sendCh, expectListenerJSON(t, snap, "", 1, 3))
   171  
   172  	// Now send Route request along with next listener one
   173  	envoy.SendReq(t, RouteType, 0, 0)
   174  	envoy.SendReq(t, ListenerType, 1, 3)
   175  
   176  	// We don't serve routes yet so this should block with no response
   177  	assertChanBlocked(t, envoy.stream.sendCh)
   178  
   179  	// WOOP! Envoy now has full connect config. Lets verify that if we update it,
   180  	// all the responses get resent with the new version. We don't actually want
   181  	// to change everything because that's tedious - our implementation will
   182  	// actually resend all blocked types on the new "version" anyway since it
   183  	// doesn't know _what_ changed. We could do something trivial but let's
   184  	// simulate a leaf cert expiring and being rotated.
   185  	snap.Leaf = proxycfg.TestLeafForCA(t, snap.Roots.Roots[0])
   186  	mgr.DeliverConfig(t, "web-sidecar-proxy", snap)
   187  
   188  	// All 3 response that have something to return should return with new version
   189  	// note that the ordering is not deterministic in general. Trying to make this
   190  	// test order-agnostic though is a massive pain since we are comparing
   191  	// non-identical JSON strings (so can simply sort by anything) and because we
   192  	// don't know the order the nonces will be assigned. For now we rely and
   193  	// require our implementation to always deliver updates in a specific order
   194  	// which is reasonable anyway to ensure consistency of the config Envoy sees.
   195  	assertResponseSent(t, envoy.stream.sendCh, expectClustersJSON(t, snap, "", 2, 4))
   196  	assertResponseSent(t, envoy.stream.sendCh, expectEndpointsJSON(t, snap, "", 2, 5))
   197  	assertResponseSent(t, envoy.stream.sendCh, expectListenerJSON(t, snap, "", 2, 6))
   198  
   199  	// Let's pretend that Envoy doesn't like that new listener config. It will ACK
   200  	// all the others (same version) but NACK the listener. This is the most
   201  	// subtle part of xDS and the server implementation so I'll elaborate. A full
   202  	// description of the protocol can be found at
   203  	// https://github.com/envoyproxy/data-plane-api/blob/master/XDS_PROTOCOL.md.
   204  	// Envoy delays making a followup request for a type until after it has
   205  	// processed and applied the last response. The next request then will include
   206  	// the nonce in the last response which acknowledges _receiving_ and handling
   207  	// that response. It also includes the currently applied version. If all is
   208  	// good and it successfully applies the config, then the version in the next
   209  	// response will be the same version just sent. This is considered to be an
   210  	// ACK of that version for that type. If envoy fails to apply the config for
   211  	// some reason, it will still acknowledge that it received it (still return
   212  	// the responses nonce), but will show the previous version it's still using.
   213  	// This is considered a NACK. It's important that the server pay attention to
   214  	// the _nonce_ and not the version when deciding what to send otherwise a bad
   215  	// version that can't be applied in Envoy will cause a busy loop.
   216  	//
   217  	// In this case we are simulating that Envoy failed to apply the Listener
   218  	// response but did apply the other types so all get the new nonces, but
   219  	// listener stays on v1.
   220  	envoy.SendReq(t, ClusterType, 2, 4)
   221  	envoy.SendReq(t, EndpointType, 2, 5)
   222  	envoy.SendReq(t, ListenerType, 1, 6) // v1 is a NACK
   223  
   224  	// Even though we nacked, we should still NOT get then v2 listeners
   225  	// redelivered since nothing has changed.
   226  	assertChanBlocked(t, envoy.stream.sendCh)
   227  
   228  	// Change config again and make sure it's delivered to everyone!
   229  	snap.Leaf = proxycfg.TestLeafForCA(t, snap.Roots.Roots[0])
   230  	mgr.DeliverConfig(t, "web-sidecar-proxy", snap)
   231  
   232  	assertResponseSent(t, envoy.stream.sendCh, expectClustersJSON(t, snap, "", 3, 7))
   233  	assertResponseSent(t, envoy.stream.sendCh, expectEndpointsJSON(t, snap, "", 3, 8))
   234  	assertResponseSent(t, envoy.stream.sendCh, expectListenerJSON(t, snap, "", 3, 9))
   235  }
   236  
   237  func expectListenerJSONResources(t *testing.T, snap *proxycfg.ConfigSnapshot, token string, v, n uint64) map[string]string {
   238  	tokenVal := ""
   239  	if token != "" {
   240  		tokenVal = fmt.Sprintf(",\n"+`"value": "%s"`, token)
   241  	}
   242  	return map[string]string{
   243  		"public_listener": `{
   244  													"@type": "type.googleapis.com/envoy.api.v2.Listener",
   245  													"name": "public_listener:0.0.0.0:9999",
   246  													"address": {
   247  														"socketAddress": {
   248  															"address": "0.0.0.0",
   249  															"portValue": 9999
   250  														}
   251  													},
   252  													"filterChains": [
   253  														{
   254  															"tlsContext": ` + expectedPublicTLSContextJSON(t, snap) + `,
   255  															"filters": [
   256  																{
   257  																	"name": "envoy.ext_authz",
   258  																	"config": {
   259  																			"grpc_service": {
   260  																					"envoy_grpc": {
   261  																						"cluster_name": "local_agent"
   262  																					},
   263  																					"initial_metadata": [
   264  																						{
   265  																							"key": "x-consul-token"
   266  																							` + tokenVal + `
   267  																						}
   268  																					]
   269  																				},
   270  																			"stat_prefix": "connect_authz"
   271  																		}
   272  																},
   273  																{
   274  																	"name": "envoy.tcp_proxy",
   275  																	"config": {
   276  																			"cluster": "local_app",
   277  																			"stat_prefix": "public_listener"
   278  																		}
   279  																}
   280  															]
   281  														}
   282  													]
   283  												}`,
   284  		"service:db": `{
   285  										"@type": "type.googleapis.com/envoy.api.v2.Listener",
   286  										"name": "service:db:127.0.0.1:9191",
   287  										"address": {
   288  											"socketAddress": {
   289  												"address": "127.0.0.1",
   290  												"portValue": 9191
   291  											}
   292  										},
   293  										"filterChains": [
   294  											{
   295  												"filters": [
   296  													{
   297  														"name": "envoy.tcp_proxy",
   298  														"config": {
   299  																"cluster": "service:db",
   300  																"stat_prefix": "service:db"
   301  															}
   302  													}
   303  												]
   304  											}
   305  										]
   306  									}`,
   307  		"prepared_query:geo-cache": `{
   308  																	"@type": "type.googleapis.com/envoy.api.v2.Listener",
   309  																	"name": "prepared_query:geo-cache:127.10.10.10:8181",
   310  																	"address": {
   311  																		"socketAddress": {
   312  																			"address": "127.10.10.10",
   313  																			"portValue": 8181
   314  																		}
   315  																	},
   316  																	"filterChains": [
   317  																		{
   318  																			"filters": [
   319  																				{
   320  																					"name": "envoy.tcp_proxy",
   321  																					"config": {
   322  																							"cluster": "prepared_query:geo-cache",
   323  																							"stat_prefix": "prepared_query:geo-cache"
   324  																						}
   325  																				}
   326  																			]
   327  																		}
   328  																	]
   329  																}`,
   330  	}
   331  }
   332  
   333  func expectListenerJSONFromResources(t *testing.T, snap *proxycfg.ConfigSnapshot, token string, v, n uint64, resourcesJSON map[string]string) string {
   334  	resJSON := ""
   335  	// Sort resources into specific order because that matters in JSONEq
   336  	// comparison later.
   337  	keyOrder := []string{"public_listener"}
   338  	for _, u := range snap.Proxy.Upstreams {
   339  		keyOrder = append(keyOrder, u.Identifier())
   340  	}
   341  	for _, k := range keyOrder {
   342  		j, ok := resourcesJSON[k]
   343  		if !ok {
   344  			continue
   345  		}
   346  		if resJSON != "" {
   347  			resJSON += ",\n"
   348  		}
   349  		resJSON += j
   350  	}
   351  	return `{
   352  		"versionInfo": "` + hexString(v) + `",
   353  		"resources": [` + resJSON + `],
   354  		"typeUrl": "type.googleapis.com/envoy.api.v2.Listener",
   355  		"nonce": "` + hexString(n) + `"
   356  		}`
   357  }
   358  
   359  func expectListenerJSON(t *testing.T, snap *proxycfg.ConfigSnapshot, token string, v, n uint64) string {
   360  	return expectListenerJSONFromResources(t, snap, token, v, n,
   361  		expectListenerJSONResources(t, snap, token, v, n))
   362  }
   363  
   364  func expectClustersJSONResources(t *testing.T, snap *proxycfg.ConfigSnapshot, token string, v, n uint64) map[string]string {
   365  	return map[string]string{
   366  		"local_app": `
   367  			{
   368  				"@type": "type.googleapis.com/envoy.api.v2.Cluster",
   369  				"name": "local_app",
   370  				"connectTimeout": "5s",
   371  				"hosts": [
   372  					{
   373  						"socketAddress": {
   374  							"address": "127.0.0.1",
   375  							"portValue": 8080
   376  						}
   377  					}
   378  				]
   379  			}`,
   380  		"service:db": `
   381  			{
   382  				"@type": "type.googleapis.com/envoy.api.v2.Cluster",
   383  				"name": "service:db",
   384  				"type": "EDS",
   385  				"edsClusterConfig": {
   386  					"edsConfig": {
   387  						"ads": {
   388  
   389  						}
   390  					}
   391  				},
   392  				"outlierDetection": {
   393  
   394  				},
   395  				"connectTimeout": "1s",
   396  				"tlsContext": ` + expectedUpstreamTLSContextJSON(t, snap) + `
   397  			}`,
   398  		"prepared_query:geo-cache": `
   399  			{
   400  				"@type": "type.googleapis.com/envoy.api.v2.Cluster",
   401  				"name": "prepared_query:geo-cache",
   402  				"type": "EDS",
   403  				"edsClusterConfig": {
   404  					"edsConfig": {
   405  						"ads": {
   406  
   407  						}
   408  					}
   409  				},
   410  				"outlierDetection": {
   411  
   412  				},
   413  				"connectTimeout": "5s",
   414  				"tlsContext": ` + expectedUpstreamTLSContextJSON(t, snap) + `
   415  			}`,
   416  	}
   417  }
   418  
   419  func expectClustersJSONFromResources(t *testing.T, snap *proxycfg.ConfigSnapshot, token string, v, n uint64, resourcesJSON map[string]string) string {
   420  	resJSON := ""
   421  
   422  	// Sort resources into specific order because that matters in JSONEq
   423  	// comparison later.
   424  	keyOrder := []string{"local_app"}
   425  	for _, u := range snap.Proxy.Upstreams {
   426  		keyOrder = append(keyOrder, u.Identifier())
   427  	}
   428  	for _, k := range keyOrder {
   429  		j, ok := resourcesJSON[k]
   430  		if !ok {
   431  			continue
   432  		}
   433  		if resJSON != "" {
   434  			resJSON += ",\n"
   435  		}
   436  		resJSON += j
   437  	}
   438  
   439  	return `{
   440  		"versionInfo": "` + hexString(v) + `",
   441  		"resources": [` + resJSON + `],
   442  		"typeUrl": "type.googleapis.com/envoy.api.v2.Cluster",
   443  		"nonce": "` + hexString(n) + `"
   444  		}`
   445  }
   446  
   447  func expectClustersJSON(t *testing.T, snap *proxycfg.ConfigSnapshot, token string, v, n uint64) string {
   448  	return expectClustersJSONFromResources(t, snap, token, v, n,
   449  		expectClustersJSONResources(t, snap, token, v, n))
   450  }
   451  
   452  func expectEndpointsJSON(t *testing.T, snap *proxycfg.ConfigSnapshot, token string, v, n uint64) string {
   453  	return `{
   454  		"versionInfo": "` + hexString(v) + `",
   455  		"resources": [
   456  			{
   457  				"@type": "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment",
   458  				"clusterName": "service:db",
   459  				"endpoints": [
   460  					{
   461  						"lbEndpoints": [
   462  							{
   463  								"endpoint": {
   464  									"address": {
   465  										"socketAddress": {
   466  											"address": "10.10.1.1",
   467  											"portValue": 0
   468  										}
   469  									}
   470  								},
   471  								"healthStatus": "HEALTHY",
   472  								"loadBalancingWeight": 1
   473  							},
   474  							{
   475  								"endpoint": {
   476  									"address": {
   477  										"socketAddress": {
   478  											"address": "10.10.1.2",
   479  											"portValue": 0
   480  										}
   481  									}
   482  								},
   483  								"healthStatus": "HEALTHY",
   484  								"loadBalancingWeight": 1
   485  							}
   486  						]
   487  					}
   488  				]
   489  			}
   490  		],
   491  		"typeUrl": "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment",
   492  		"nonce": "` + hexString(n) + `"
   493  	}`
   494  }
   495  
   496  func expectedUpstreamTLSContextJSON(t *testing.T, snap *proxycfg.ConfigSnapshot) string {
   497  	return expectedTLSContextJSON(t, snap, false)
   498  }
   499  
   500  func expectedPublicTLSContextJSON(t *testing.T, snap *proxycfg.ConfigSnapshot) string {
   501  	return expectedTLSContextJSON(t, snap, true)
   502  }
   503  
   504  func expectedTLSContextJSON(t *testing.T, snap *proxycfg.ConfigSnapshot, requireClientCert bool) string {
   505  	// Assume just one root for now, can get fancier later if needed.
   506  	caPEM := snap.Roots.Roots[0].RootCert
   507  	reqClient := ""
   508  	if requireClientCert {
   509  		reqClient = `,
   510  		"requireClientCertificate": true`
   511  	}
   512  	return `{
   513  		"commonTlsContext": {
   514  			"tlsParams": {},
   515  			"tlsCertificates": [
   516  				{
   517  					"certificateChain": {
   518  						"inlineString": "` + strings.Replace(snap.Leaf.CertPEM, "\n", "\\n", -1) + `"
   519  					},
   520  					"privateKey": {
   521  						"inlineString": "` + strings.Replace(snap.Leaf.PrivateKeyPEM, "\n", "\\n", -1) + `"
   522  					}
   523  				}
   524  			],
   525  			"validationContext": {
   526  				"trustedCa": {
   527  					"inlineString": "` + strings.Replace(caPEM, "\n", "\\n", -1) + `"
   528  				}
   529  			}
   530  		}
   531  		` + reqClient + `
   532  	}`
   533  }
   534  
   535  func assertChanBlocked(t *testing.T, ch chan *envoy.DiscoveryResponse) {
   536  	t.Helper()
   537  	select {
   538  	case r := <-ch:
   539  		t.Fatalf("chan should block but received: %v", r)
   540  	case <-time.After(10 * time.Millisecond):
   541  		return
   542  	}
   543  }
   544  
   545  func assertResponseSent(t *testing.T, ch chan *envoy.DiscoveryResponse, wantJSON string) {
   546  	t.Helper()
   547  	select {
   548  	case r := <-ch:
   549  		assertResponse(t, r, wantJSON)
   550  	case <-time.After(50 * time.Millisecond):
   551  		t.Fatalf("no response received after 50ms")
   552  	}
   553  }
   554  
   555  // assertResponse is a helper to test a envoy.DiscoveryResponse matches the
   556  // JSON representation we expect. We use JSON because the responses use protobuf
   557  // Any type which includes binary protobuf encoding and would make creating
   558  // expected structs require the same code that is under test!
   559  func assertResponse(t *testing.T, r *envoy.DiscoveryResponse, wantJSON string) {
   560  	t.Helper()
   561  	m := jsonpb.Marshaler{
   562  		Indent: "  ",
   563  	}
   564  	gotJSON, err := m.MarshalToString(r)
   565  	require.NoError(t, err)
   566  	require.JSONEqf(t, wantJSON, gotJSON, "got:\n%s", gotJSON)
   567  }
   568  
   569  func TestServer_StreamAggregatedResources_ACLEnforcement(t *testing.T) {
   570  
   571  	tests := []struct {
   572  		name        string
   573  		defaultDeny bool
   574  		acl         string
   575  		token       string
   576  		wantDenied  bool
   577  	}{
   578  		// Note that although we've stubbed actual ACL checks in the testManager
   579  		// ConnectAuthorize mock, by asserting against specific reason strings here
   580  		// even in the happy case which can't match the default one returned by the
   581  		// mock we are implicitly validating that the implementation used the
   582  		// correct token from the context.
   583  		{
   584  			name:        "no ACLs configured",
   585  			defaultDeny: false,
   586  			wantDenied:  false,
   587  		},
   588  		{
   589  			name:        "default deny, no token",
   590  			defaultDeny: true,
   591  			wantDenied:  true,
   592  		},
   593  		{
   594  			name:        "default deny, service:write token",
   595  			defaultDeny: true,
   596  			acl:         `service "web" { policy = "write" }`,
   597  			token:       "service-write-on-web",
   598  			wantDenied:  false,
   599  		},
   600  		{
   601  			name:        "default deny, service:read token",
   602  			defaultDeny: true,
   603  			acl:         `service "web" { policy = "read" }`,
   604  			token:       "service-write-on-web",
   605  			wantDenied:  true,
   606  		},
   607  		{
   608  			name:        "default deny, service:write token on different service",
   609  			defaultDeny: true,
   610  			acl:         `service "not-web" { policy = "write" }`,
   611  			token:       "service-write-on-not-web",
   612  			wantDenied:  true,
   613  		},
   614  	}
   615  
   616  	for _, tt := range tests {
   617  		t.Run(tt.name, func(t *testing.T) {
   618  			logger := log.New(os.Stderr, "", log.LstdFlags)
   619  			mgr := newTestManager(t)
   620  			aclResolve := func(id string) (acl.Authorizer, error) {
   621  				if !tt.defaultDeny {
   622  					// Allow all
   623  					return acl.RootAuthorizer("allow"), nil
   624  				}
   625  				if tt.acl == "" {
   626  					// No token and defaultDeny is denied
   627  					return acl.RootAuthorizer("deny"), nil
   628  				}
   629  				// Ensure the correct token was passed
   630  				require.Equal(t, tt.token, id)
   631  				// Parse the ACL and enforce it
   632  				policy, err := acl.NewPolicyFromSource("", 0, tt.acl, acl.SyntaxLegacy, nil)
   633  				require.NoError(t, err)
   634  				return acl.NewPolicyAuthorizer(acl.RootAuthorizer("deny"), []*acl.Policy{policy}, nil)
   635  			}
   636  			envoy := NewTestEnvoy(t, "web-sidecar-proxy", tt.token)
   637  			defer envoy.Close()
   638  
   639  			s := Server{
   640  				Logger:       logger,
   641  				CfgMgr:       mgr,
   642  				Authz:        mgr,
   643  				ResolveToken: aclResolve,
   644  			}
   645  			s.Initialize()
   646  
   647  			errCh := make(chan error, 1)
   648  			go func() {
   649  				errCh <- s.StreamAggregatedResources(envoy.stream)
   650  			}()
   651  
   652  			// Register the proxy to create state needed to Watch() on
   653  			mgr.RegisterProxy(t, "web-sidecar-proxy")
   654  
   655  			// Deliver a new snapshot
   656  			snap := proxycfg.TestConfigSnapshot(t)
   657  			mgr.DeliverConfig(t, "web-sidecar-proxy", snap)
   658  
   659  			// Send initial listener discover, in real life Envoy always sends cluster
   660  			// first but it doesn't really matter and listener has a response that
   661  			// includes the token in the ext authz filter so lets us test more stuff.
   662  			envoy.SendReq(t, ListenerType, 0, 0)
   663  
   664  			if !tt.wantDenied {
   665  				assertResponseSent(t, envoy.stream.sendCh, expectListenerJSON(t, snap, tt.token, 1, 1))
   666  				// Close the client stream since all is well. We _don't_ do this in the
   667  				// expected error case because we want to verify the error closes the
   668  				// stream from server side.
   669  				envoy.Close()
   670  			}
   671  
   672  			select {
   673  			case err := <-errCh:
   674  				if tt.wantDenied {
   675  					require.Error(t, err)
   676  					require.Contains(t, err.Error(), "permission denied")
   677  					mgr.AssertWatchCancelled(t, "web-sidecar-proxy")
   678  				} else {
   679  					require.NoError(t, err)
   680  				}
   681  			case <-time.After(50 * time.Millisecond):
   682  				t.Fatalf("timed out waiting for handler to finish")
   683  			}
   684  		})
   685  	}
   686  }
   687  
   688  func TestServer_StreamAggregatedResources_ACLTokenDeleted_StreamTerminatedDuringDiscoveryRequest(t *testing.T) {
   689  	aclRules := `service "web" { policy = "write" }`
   690  	token := "service-write-on-web"
   691  
   692  	policy, err := acl.NewPolicyFromSource("", 0, aclRules, acl.SyntaxLegacy, nil)
   693  	require.NoError(t, err)
   694  
   695  	var validToken atomic.Value
   696  	validToken.Store(token)
   697  
   698  	logger := log.New(os.Stderr, "", log.LstdFlags)
   699  	mgr := newTestManager(t)
   700  	aclResolve := func(id string) (acl.Authorizer, error) {
   701  		if token := validToken.Load(); token == nil || id != token.(string) {
   702  			return nil, acl.ErrNotFound
   703  		}
   704  
   705  		return acl.NewPolicyAuthorizer(acl.RootAuthorizer("deny"), []*acl.Policy{policy}, nil)
   706  	}
   707  	envoy := NewTestEnvoy(t, "web-sidecar-proxy", token)
   708  	defer envoy.Close()
   709  
   710  	s := Server{
   711  		Logger:             logger,
   712  		CfgMgr:             mgr,
   713  		Authz:              mgr,
   714  		ResolveToken:       aclResolve,
   715  		AuthCheckFrequency: 1 * time.Hour, // make sure this doesn't kick in
   716  	}
   717  	s.Initialize()
   718  
   719  	errCh := make(chan error, 1)
   720  	go func() {
   721  		errCh <- s.StreamAggregatedResources(envoy.stream)
   722  	}()
   723  
   724  	getError := func() (gotErr error, ok bool) {
   725  		select {
   726  		case err := <-errCh:
   727  			return err, true
   728  		default:
   729  			return nil, false
   730  		}
   731  	}
   732  
   733  	// Register the proxy to create state needed to Watch() on
   734  	mgr.RegisterProxy(t, "web-sidecar-proxy")
   735  
   736  	// Send initial cluster discover (OK)
   737  	envoy.SendReq(t, ClusterType, 0, 0)
   738  	{
   739  		err, ok := getError()
   740  		require.NoError(t, err)
   741  		require.False(t, ok)
   742  	}
   743  
   744  	// Check no response sent yet
   745  	assertChanBlocked(t, envoy.stream.sendCh)
   746  	{
   747  		err, ok := getError()
   748  		require.NoError(t, err)
   749  		require.False(t, ok)
   750  	}
   751  
   752  	// Deliver a new snapshot
   753  	snap := proxycfg.TestConfigSnapshot(t)
   754  	mgr.DeliverConfig(t, "web-sidecar-proxy", snap)
   755  
   756  	assertResponseSent(t, envoy.stream.sendCh, expectClustersJSON(t, snap, token, 1, 1))
   757  
   758  	// Now nuke the ACL token.
   759  	validToken.Store("")
   760  
   761  	// It also (in parallel) issues the next cluster request (which acts as an ACK
   762  	// of the version we sent)
   763  	envoy.SendReq(t, ClusterType, 1, 1)
   764  
   765  	select {
   766  	case err := <-errCh:
   767  		require.Error(t, err)
   768  		gerr, ok := status.FromError(err)
   769  		require.Truef(t, ok, "not a grpc status error: type='%T' value=%v", err, err)
   770  		require.Equal(t, codes.Unauthenticated, gerr.Code())
   771  		require.Equal(t, "unauthenticated: ACL not found", gerr.Message())
   772  
   773  		mgr.AssertWatchCancelled(t, "web-sidecar-proxy")
   774  	case <-time.After(50 * time.Millisecond):
   775  		t.Fatalf("timed out waiting for handler to finish")
   776  	}
   777  }
   778  
   779  func TestServer_StreamAggregatedResources_ACLTokenDeleted_StreamTerminatedInBackground(t *testing.T) {
   780  	aclRules := `service "web" { policy = "write" }`
   781  	token := "service-write-on-web"
   782  
   783  	policy, err := acl.NewPolicyFromSource("", 0, aclRules, acl.SyntaxLegacy, nil)
   784  	require.NoError(t, err)
   785  
   786  	var validToken atomic.Value
   787  	validToken.Store(token)
   788  
   789  	logger := log.New(os.Stderr, "", log.LstdFlags)
   790  	mgr := newTestManager(t)
   791  	aclResolve := func(id string) (acl.Authorizer, error) {
   792  		if token := validToken.Load(); token == nil || id != token.(string) {
   793  			return nil, acl.ErrNotFound
   794  		}
   795  
   796  		return acl.NewPolicyAuthorizer(acl.RootAuthorizer("deny"), []*acl.Policy{policy}, nil)
   797  	}
   798  	envoy := NewTestEnvoy(t, "web-sidecar-proxy", token)
   799  	defer envoy.Close()
   800  
   801  	s := Server{
   802  		Logger:             logger,
   803  		CfgMgr:             mgr,
   804  		Authz:              mgr,
   805  		ResolveToken:       aclResolve,
   806  		AuthCheckFrequency: 100 * time.Millisecond, // Make this short.
   807  	}
   808  	s.Initialize()
   809  
   810  	errCh := make(chan error, 1)
   811  	go func() {
   812  		errCh <- s.StreamAggregatedResources(envoy.stream)
   813  	}()
   814  
   815  	getError := func() (gotErr error, ok bool) {
   816  		select {
   817  		case err := <-errCh:
   818  			return err, true
   819  		default:
   820  			return nil, false
   821  		}
   822  	}
   823  
   824  	// Register the proxy to create state needed to Watch() on
   825  	mgr.RegisterProxy(t, "web-sidecar-proxy")
   826  
   827  	// Send initial cluster discover (OK)
   828  	envoy.SendReq(t, ClusterType, 0, 0)
   829  	{
   830  		err, ok := getError()
   831  		require.NoError(t, err)
   832  		require.False(t, ok)
   833  	}
   834  
   835  	// Check no response sent yet
   836  	assertChanBlocked(t, envoy.stream.sendCh)
   837  	{
   838  		err, ok := getError()
   839  		require.NoError(t, err)
   840  		require.False(t, ok)
   841  	}
   842  
   843  	// Deliver a new snapshot
   844  	snap := proxycfg.TestConfigSnapshot(t)
   845  	mgr.DeliverConfig(t, "web-sidecar-proxy", snap)
   846  
   847  	assertResponseSent(t, envoy.stream.sendCh, expectClustersJSON(t, snap, token, 1, 1))
   848  
   849  	// It also (in parallel) issues the next cluster request (which acts as an ACK
   850  	// of the version we sent)
   851  	envoy.SendReq(t, ClusterType, 1, 1)
   852  
   853  	// Check no response sent yet
   854  	assertChanBlocked(t, envoy.stream.sendCh)
   855  	{
   856  		err, ok := getError()
   857  		require.NoError(t, err)
   858  		require.False(t, ok)
   859  	}
   860  
   861  	// Now nuke the ACL token while there's no activity.
   862  	validToken.Store("")
   863  
   864  	select {
   865  	case err := <-errCh:
   866  		require.Error(t, err)
   867  		gerr, ok := status.FromError(err)
   868  		require.Truef(t, ok, "not a grpc status error: type='%T' value=%v", err, err)
   869  		require.Equal(t, codes.Unauthenticated, gerr.Code())
   870  		require.Equal(t, "unauthenticated: ACL not found", gerr.Message())
   871  
   872  		mgr.AssertWatchCancelled(t, "web-sidecar-proxy")
   873  	case <-time.After(200 * time.Millisecond):
   874  		t.Fatalf("timed out waiting for handler to finish")
   875  	}
   876  }
   877  
   878  // This tests the ext_authz service method that implements connect authz.
   879  func TestServer_Check(t *testing.T) {
   880  
   881  	tests := []struct {
   882  		name            string
   883  		source          string
   884  		dest            string
   885  		sourcePrincipal string
   886  		destPrincipal   string
   887  		authzResult     connectAuthzResult
   888  		wantErr         bool
   889  		wantErrCode     codes.Code
   890  		wantDenied      bool
   891  		wantReason      string
   892  	}{
   893  		{
   894  			name:        "auth allowed",
   895  			source:      "web",
   896  			dest:        "db",
   897  			authzResult: connectAuthzResult{true, "default allow", nil, nil},
   898  			wantDenied:  false,
   899  			wantReason:  "default allow",
   900  		},
   901  		{
   902  			name:        "auth denied",
   903  			source:      "web",
   904  			dest:        "db",
   905  			authzResult: connectAuthzResult{false, "default deny", nil, nil},
   906  			wantDenied:  true,
   907  			wantReason:  "default deny",
   908  		},
   909  		{
   910  			name:            "no source",
   911  			sourcePrincipal: "",
   912  			dest:            "db",
   913  			// Should never make it to authz call.
   914  			wantErr:     true,
   915  			wantErrCode: codes.InvalidArgument,
   916  		},
   917  		{
   918  			name:   "no dest",
   919  			source: "web",
   920  			dest:   "",
   921  			// Should never make it to authz call.
   922  			wantErr:     true,
   923  			wantErrCode: codes.InvalidArgument,
   924  		},
   925  		{
   926  			name:          "dest invalid format",
   927  			source:        "web",
   928  			destPrincipal: "not-a-spiffe-id",
   929  			// Should never make it to authz call.
   930  			wantDenied: true,
   931  			wantReason: "Destination Principal is not a valid Connect identity",
   932  		},
   933  		{
   934  			name:          "dest not a service URI",
   935  			source:        "web",
   936  			destPrincipal: "spiffe://trust-domain.consul",
   937  			// Should never make it to authz call.
   938  			wantDenied: true,
   939  			wantReason: "Destination Principal is not a valid Service identity",
   940  		},
   941  		{
   942  			name:        "ACL not got permission for authz call",
   943  			source:      "web",
   944  			dest:        "db",
   945  			authzResult: connectAuthzResult{false, "", nil, acl.ErrPermissionDenied},
   946  			wantErr:     true,
   947  			wantErrCode: codes.PermissionDenied,
   948  		},
   949  		{
   950  			name:        "Random error running authz",
   951  			source:      "web",
   952  			dest:        "db",
   953  			authzResult: connectAuthzResult{false, "", nil, errors.New("gremlin attack")},
   954  			wantErr:     true,
   955  			wantErrCode: codes.Internal,
   956  		},
   957  	}
   958  
   959  	for _, tt := range tests {
   960  		t.Run(tt.name, func(t *testing.T) {
   961  			token := "my-real-acl-token"
   962  			logger := log.New(os.Stderr, "", log.LstdFlags)
   963  			mgr := newTestManager(t)
   964  
   965  			// Setup expected auth result against that token no lock as no other
   966  			// goroutine is touching this yet.
   967  			mgr.authz[token] = tt.authzResult
   968  
   969  			aclResolve := func(id string) (acl.Authorizer, error) {
   970  				return nil, nil
   971  			}
   972  			envoy := NewTestEnvoy(t, "web-sidecar-proxy", token)
   973  			defer envoy.Close()
   974  
   975  			s := Server{
   976  				Logger:       logger,
   977  				CfgMgr:       mgr,
   978  				Authz:        mgr,
   979  				ResolveToken: aclResolve,
   980  			}
   981  			s.Initialize()
   982  
   983  			// Create a context with the correct token
   984  			ctx := metadata.NewIncomingContext(context.Background(),
   985  				metadata.Pairs("x-consul-token", token))
   986  
   987  			r := TestCheckRequest(t, tt.source, tt.dest)
   988  			// If sourcePrincipal is set override, or if source is also not set
   989  			// explicitly override to empty.
   990  			if tt.sourcePrincipal != "" || tt.source == "" {
   991  				r.Attributes.Source.Principal = tt.sourcePrincipal
   992  			}
   993  			if tt.destPrincipal != "" || tt.dest == "" {
   994  				r.Attributes.Destination.Principal = tt.destPrincipal
   995  			}
   996  			resp, err := s.Check(ctx, r)
   997  			// Denied is not an error
   998  			if tt.wantErr {
   999  				require.Error(t, err)
  1000  				grpcStatus := status.Convert(err)
  1001  				require.Equal(t, tt.wantErrCode, grpcStatus.Code())
  1002  				require.Nil(t, resp)
  1003  				return
  1004  			}
  1005  			require.NoError(t, err)
  1006  			if tt.wantDenied {
  1007  				require.Equal(t, int32(codes.PermissionDenied), resp.Status.Code)
  1008  			} else {
  1009  				require.Equal(t, int32(codes.OK), resp.Status.Code)
  1010  			}
  1011  			require.Contains(t, resp.Status.Message, tt.wantReason)
  1012  		})
  1013  	}
  1014  }
  1015  
  1016  func TestServer_ConfigOverridesListeners(t *testing.T) {
  1017  
  1018  	tests := []struct {
  1019  		name  string
  1020  		setup func(snap *proxycfg.ConfigSnapshot) string
  1021  	}{
  1022  		{
  1023  			name: "sanity check no custom",
  1024  			setup: func(snap *proxycfg.ConfigSnapshot) string {
  1025  				// Default snap and expectation
  1026  				return expectListenerJSON(t, snap, "my-token", 1, 1)
  1027  			},
  1028  		},
  1029  		{
  1030  			name: "custom public_listener no type",
  1031  			setup: func(snap *proxycfg.ConfigSnapshot) string {
  1032  				snap.Proxy.Config["envoy_public_listener_json"] =
  1033  					customListenerJSON(t, customListenerJSONOptions{
  1034  						Name:        "custom-public-listen",
  1035  						IncludeType: false,
  1036  					})
  1037  				resources := expectListenerJSONResources(t, snap, "my-token", 1, 1)
  1038  
  1039  				// Replace the public listener with the custom one WITH type since
  1040  				// that's how it comes out the other end, and with TLS and authz
  1041  				// overridden.
  1042  				resources["public_listener"] = customListenerJSON(t, customListenerJSONOptions{
  1043  					Name: "custom-public-listen",
  1044  					// We should add type, TLS and authz
  1045  					IncludeType:   true,
  1046  					OverrideAuthz: true,
  1047  					TLSContext:    expectedPublicTLSContextJSON(t, snap),
  1048  				})
  1049  				return expectListenerJSONFromResources(t, snap, "my-token", 1, 1, resources)
  1050  			},
  1051  		},
  1052  		{
  1053  			name: "custom public_listener with type",
  1054  			setup: func(snap *proxycfg.ConfigSnapshot) string {
  1055  				snap.Proxy.Config["envoy_public_listener_json"] =
  1056  					customListenerJSON(t, customListenerJSONOptions{
  1057  						Name:        "custom-public-listen",
  1058  						IncludeType: true,
  1059  					})
  1060  				resources := expectListenerJSONResources(t, snap, "my-token", 1, 1)
  1061  
  1062  				// Replace the public listener with the custom one WITH type since
  1063  				// that's how it comes out the other end, and with TLS and authz
  1064  				// overridden.
  1065  				resources["public_listener"] = customListenerJSON(t, customListenerJSONOptions{
  1066  					Name: "custom-public-listen",
  1067  					// We should add type, TLS and authz
  1068  					IncludeType:   true,
  1069  					OverrideAuthz: true,
  1070  					TLSContext:    expectedPublicTLSContextJSON(t, snap),
  1071  				})
  1072  				return expectListenerJSONFromResources(t, snap, "my-token", 1, 1, resources)
  1073  			},
  1074  		},
  1075  		{
  1076  			name: "custom public_listener with TLS should be overridden",
  1077  			setup: func(snap *proxycfg.ConfigSnapshot) string {
  1078  				snap.Proxy.Config["envoy_public_listener_json"] =
  1079  					customListenerJSON(t, customListenerJSONOptions{
  1080  						Name:        "custom-public-listen",
  1081  						IncludeType: true,
  1082  						TLSContext:  `{"requireClientCertificate": false}`,
  1083  					})
  1084  				resources := expectListenerJSONResources(t, snap, "my-token", 1, 1)
  1085  
  1086  				// Replace the public listener with the custom one WITH type since
  1087  				// that's how it comes out the other end, and with TLS and authz
  1088  				// overridden.
  1089  				resources["public_listener"] = customListenerJSON(t, customListenerJSONOptions{
  1090  					Name: "custom-public-listen",
  1091  					// We should add type, TLS and authz
  1092  					IncludeType:   true,
  1093  					OverrideAuthz: true,
  1094  					TLSContext:    expectedPublicTLSContextJSON(t, snap),
  1095  				})
  1096  				return expectListenerJSONFromResources(t, snap, "my-token", 1, 1, resources)
  1097  			},
  1098  		},
  1099  		{
  1100  			name: "custom upstream no type",
  1101  			setup: func(snap *proxycfg.ConfigSnapshot) string {
  1102  				snap.Proxy.Upstreams[0].Config["envoy_listener_json"] =
  1103  					customListenerJSON(t, customListenerJSONOptions{
  1104  						Name:        "custom-upstream",
  1105  						IncludeType: false,
  1106  					})
  1107  				resources := expectListenerJSONResources(t, snap, "my-token", 1, 1)
  1108  
  1109  				// Replace an upstream listener with the custom one WITH type since
  1110  				// that's how it comes out the other end. Note we do override TLS
  1111  				resources["service:db"] =
  1112  					customListenerJSON(t, customListenerJSONOptions{
  1113  						Name: "custom-upstream",
  1114  						// We should add type
  1115  						IncludeType: true,
  1116  					})
  1117  				return expectListenerJSONFromResources(t, snap, "my-token", 1, 1, resources)
  1118  			},
  1119  		},
  1120  		{
  1121  			name: "custom upstream with type",
  1122  			setup: func(snap *proxycfg.ConfigSnapshot) string {
  1123  				snap.Proxy.Upstreams[0].Config["envoy_listener_json"] =
  1124  					customListenerJSON(t, customListenerJSONOptions{
  1125  						Name:        "custom-upstream",
  1126  						IncludeType: true,
  1127  					})
  1128  				resources := expectListenerJSONResources(t, snap, "my-token", 1, 1)
  1129  
  1130  				// Replace an upstream listener with the custom one WITH type since
  1131  				// that's how it comes out the other end.
  1132  				resources["service:db"] =
  1133  					customListenerJSON(t, customListenerJSONOptions{
  1134  						Name: "custom-upstream",
  1135  						// We should add type
  1136  						IncludeType: true,
  1137  					})
  1138  				return expectListenerJSONFromResources(t, snap, "my-token", 1, 1, resources)
  1139  			},
  1140  		},
  1141  	}
  1142  
  1143  	for _, tt := range tests {
  1144  		t.Run(tt.name, func(t *testing.T) {
  1145  			require := require.New(t)
  1146  
  1147  			// Sanity check default with no overrides first
  1148  			snap := proxycfg.TestConfigSnapshot(t)
  1149  			expect := tt.setup(snap)
  1150  
  1151  			listeners, err := listenersFromSnapshot(snap, "my-token")
  1152  			require.NoError(err)
  1153  			r, err := createResponse(ListenerType, "00000001", "00000001", listeners)
  1154  			require.NoError(err)
  1155  
  1156  			assertResponse(t, r, expect)
  1157  		})
  1158  	}
  1159  }
  1160  
  1161  func TestServer_ConfigOverridesClusters(t *testing.T) {
  1162  
  1163  	tests := []struct {
  1164  		name  string
  1165  		setup func(snap *proxycfg.ConfigSnapshot) string
  1166  	}{
  1167  		{
  1168  			name: "sanity check no custom",
  1169  			setup: func(snap *proxycfg.ConfigSnapshot) string {
  1170  				// Default snap and expectation
  1171  				return expectClustersJSON(t, snap, "my-token", 1, 1)
  1172  			},
  1173  		},
  1174  		{
  1175  			name: "custom public with no type",
  1176  			setup: func(snap *proxycfg.ConfigSnapshot) string {
  1177  				snap.Proxy.Config["envoy_local_cluster_json"] =
  1178  					customAppClusterJSON(t, customClusterJSONOptions{
  1179  						Name:        "mylocal",
  1180  						IncludeType: false,
  1181  					})
  1182  				resources := expectClustersJSONResources(t, snap, "my-token", 1, 1)
  1183  
  1184  				// Replace an upstream listener with the custom one WITH type since
  1185  				// that's how it comes out the other end.
  1186  				resources["local_app"] =
  1187  					customAppClusterJSON(t, customClusterJSONOptions{
  1188  						Name:        "mylocal",
  1189  						IncludeType: true,
  1190  					})
  1191  				return expectClustersJSONFromResources(t, snap, "my-token", 1, 1, resources)
  1192  			},
  1193  		},
  1194  		{
  1195  			name: "custom public with type",
  1196  			setup: func(snap *proxycfg.ConfigSnapshot) string {
  1197  				snap.Proxy.Config["envoy_local_cluster_json"] =
  1198  					customAppClusterJSON(t, customClusterJSONOptions{
  1199  						Name:        "mylocal",
  1200  						IncludeType: true,
  1201  					})
  1202  				resources := expectClustersJSONResources(t, snap, "my-token", 1, 1)
  1203  
  1204  				// Replace an upstream listener with the custom one WITH type since
  1205  				// that's how it comes out the other end.
  1206  				resources["local_app"] =
  1207  					customAppClusterJSON(t, customClusterJSONOptions{
  1208  						Name:        "mylocal",
  1209  						IncludeType: true,
  1210  					})
  1211  				return expectClustersJSONFromResources(t, snap, "my-token", 1, 1, resources)
  1212  			},
  1213  		},
  1214  		{
  1215  			name: "custom upstream with no type",
  1216  			setup: func(snap *proxycfg.ConfigSnapshot) string {
  1217  				snap.Proxy.Upstreams[0].Config["envoy_cluster_json"] =
  1218  					customEDSClusterJSON(t, customClusterJSONOptions{
  1219  						Name:        "myservice",
  1220  						IncludeType: false,
  1221  					})
  1222  				resources := expectClustersJSONResources(t, snap, "my-token", 1, 1)
  1223  
  1224  				// Replace an upstream listener with the custom one WITH type since
  1225  				// that's how it comes out the other end.
  1226  				resources["service:db"] =
  1227  					customEDSClusterJSON(t, customClusterJSONOptions{
  1228  						Name:        "myservice",
  1229  						IncludeType: true,
  1230  						TLSContext:  expectedUpstreamTLSContextJSON(t, snap),
  1231  					})
  1232  				return expectClustersJSONFromResources(t, snap, "my-token", 1, 1, resources)
  1233  			},
  1234  		},
  1235  		{
  1236  			name: "custom upstream with type",
  1237  			setup: func(snap *proxycfg.ConfigSnapshot) string {
  1238  				snap.Proxy.Upstreams[0].Config["envoy_cluster_json"] =
  1239  					customEDSClusterJSON(t, customClusterJSONOptions{
  1240  						Name:        "myservice",
  1241  						IncludeType: true,
  1242  					})
  1243  				resources := expectClustersJSONResources(t, snap, "my-token", 1, 1)
  1244  
  1245  				// Replace an upstream listener with the custom one WITH type since
  1246  				// that's how it comes out the other end.
  1247  				resources["service:db"] =
  1248  					customEDSClusterJSON(t, customClusterJSONOptions{
  1249  						Name:        "myservice",
  1250  						IncludeType: true,
  1251  						TLSContext:  expectedUpstreamTLSContextJSON(t, snap),
  1252  					})
  1253  				return expectClustersJSONFromResources(t, snap, "my-token", 1, 1, resources)
  1254  			},
  1255  		},
  1256  	}
  1257  
  1258  	for _, tt := range tests {
  1259  		t.Run(tt.name, func(t *testing.T) {
  1260  			require := require.New(t)
  1261  
  1262  			// Sanity check default with no overrides first
  1263  			snap := proxycfg.TestConfigSnapshot(t)
  1264  			expect := tt.setup(snap)
  1265  
  1266  			clusters, err := clustersFromSnapshot(snap, "my-token")
  1267  			require.NoError(err)
  1268  			r, err := createResponse(ClusterType, "00000001", "00000001", clusters)
  1269  			require.NoError(err)
  1270  
  1271  			fmt.Println(r)
  1272  
  1273  			assertResponse(t, r, expect)
  1274  		})
  1275  	}
  1276  }
  1277  
  1278  type customListenerJSONOptions struct {
  1279  	Name          string
  1280  	IncludeType   bool
  1281  	OverrideAuthz bool
  1282  	TLSContext    string
  1283  }
  1284  
  1285  const customListenerJSONTpl = `{
  1286  	{{ if .IncludeType -}}
  1287  	"@type": "type.googleapis.com/envoy.api.v2.Listener",
  1288  	{{- end }}
  1289  	"name": "{{ .Name }}",
  1290  	"address": {
  1291  		"socketAddress": {
  1292  			"address": "11.11.11.11",
  1293  			"portValue": 11111
  1294  		}
  1295  	},
  1296  	"filterChains": [
  1297  		{
  1298  			{{ if .TLSContext -}}
  1299  			"tlsContext": {{ .TLSContext }},
  1300  			{{- end }}
  1301  			"filters": [
  1302  				{{ if .OverrideAuthz -}}
  1303  				{
  1304  					"name": "envoy.ext_authz",
  1305  					"config": {
  1306  							"grpc_service": {
  1307  										"envoy_grpc": {
  1308  													"cluster_name": "local_agent"
  1309  												},
  1310  										"initial_metadata": [
  1311  													{
  1312  																"key": "x-consul-token",
  1313  																"value": "my-token"
  1314  															}
  1315  												]
  1316  									},
  1317  							"stat_prefix": "connect_authz"
  1318  						}
  1319  				},
  1320  				{{- end }}
  1321  				{
  1322  					"name": "envoy.tcp_proxy",
  1323  					"config": {
  1324  							"cluster": "random-cluster",
  1325  							"stat_prefix": "foo-stats"
  1326  						}
  1327  				}
  1328  			]
  1329  		}
  1330  	]
  1331  }`
  1332  
  1333  var customListenerJSONTemplate = template.Must(template.New("").Parse(customListenerJSONTpl))
  1334  
  1335  func customListenerJSON(t *testing.T, opts customListenerJSONOptions) string {
  1336  	t.Helper()
  1337  	var buf bytes.Buffer
  1338  	err := customListenerJSONTemplate.Execute(&buf, opts)
  1339  	require.NoError(t, err)
  1340  	return buf.String()
  1341  }
  1342  
  1343  type customClusterJSONOptions struct {
  1344  	Name        string
  1345  	IncludeType bool
  1346  	TLSContext  string
  1347  }
  1348  
  1349  var customEDSClusterJSONTpl = `{
  1350  	{{ if .IncludeType -}}
  1351  	"@type": "type.googleapis.com/envoy.api.v2.Cluster",
  1352  	{{- end }}
  1353  	{{ if .TLSContext -}}
  1354  	"tlsContext": {{ .TLSContext }},
  1355  	{{- end }}
  1356  	"name": "{{ .Name }}",
  1357  	"type": "EDS",
  1358  	"edsClusterConfig": {
  1359  		"edsConfig": {
  1360  			"ads": {
  1361  
  1362  			}
  1363  		}
  1364  	},
  1365  	"connectTimeout": "5s"
  1366  }`
  1367  
  1368  var customEDSClusterJSONTemplate = template.Must(template.New("").Parse(customEDSClusterJSONTpl))
  1369  
  1370  func customEDSClusterJSON(t *testing.T, opts customClusterJSONOptions) string {
  1371  	t.Helper()
  1372  	var buf bytes.Buffer
  1373  	err := customEDSClusterJSONTemplate.Execute(&buf, opts)
  1374  	require.NoError(t, err)
  1375  	return buf.String()
  1376  }
  1377  
  1378  var customAppClusterJSONTpl = `{
  1379  	{{ if .IncludeType -}}
  1380  	"@type": "type.googleapis.com/envoy.api.v2.Cluster",
  1381  	{{- end }}
  1382  	{{ if .TLSContext -}}
  1383  	"tlsContext": {{ .TLSContext }},
  1384  	{{- end }}
  1385  	"name": "{{ .Name }}",
  1386  	"connectTimeout": "5s",
  1387  	"hosts": [
  1388  		{
  1389  			"socketAddress": {
  1390  				"address": "127.0.0.1",
  1391  				"portValue": 8080
  1392  			}
  1393  		}
  1394  	]
  1395  }`
  1396  
  1397  var customAppClusterJSONTemplate = template.Must(template.New("").Parse(customAppClusterJSONTpl))
  1398  
  1399  func customAppClusterJSON(t *testing.T, opts customClusterJSONOptions) string {
  1400  	t.Helper()
  1401  	var buf bytes.Buffer
  1402  	err := customAppClusterJSONTemplate.Execute(&buf, opts)
  1403  	require.NoError(t, err)
  1404  	return buf.String()
  1405  }