1// Copyright 2018 Istio Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package client_test
16
17import (
18	"context"
19	"fmt"
20	"net"
21	"testing"
22
23	v2 "github.com/envoyproxy/go-control-plane/envoy/api/v2"
24	core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
25	listener "github.com/envoyproxy/go-control-plane/envoy/api/v2/listener"
26	route "github.com/envoyproxy/go-control-plane/envoy/api/v2/route"
27	hcm "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/http_connection_manager/v2"
28	discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v2"
29	"github.com/envoyproxy/go-control-plane/pkg/cache/types"
30	"github.com/envoyproxy/go-control-plane/pkg/cache/v2"
31	xds "github.com/envoyproxy/go-control-plane/pkg/server/v2"
32	"github.com/envoyproxy/go-control-plane/pkg/wellknown"
33
34	"google.golang.org/grpc"
35
36	meshconfig "istio.io/api/mesh/v1alpha1"
37
38	"istio.io/istio/mixer/test/client/env"
39	"istio.io/istio/pilot/pkg/model"
40	"istio.io/istio/pilot/pkg/networking"
41	"istio.io/istio/pilot/pkg/networking/plugin"
42	"istio.io/istio/pilot/pkg/networking/plugin/mixer"
43	pilotutil "istio.io/istio/pilot/pkg/networking/util"
44	"istio.io/istio/pkg/config/host"
45	"istio.io/istio/pkg/config/labels"
46)
47
48const (
49	envoyConf = `
50admin:
51  access_log_path: {{.AccessLogPath}}
52  address:
53    socket_address:
54      address: 127.0.0.1
55      port_value: {{.Ports.AdminPort}}
56node:
57  id: id
58  cluster: unknown
59  metadata:
60    # these two must come together and they need to be set
61    NODE_UID: pod.ns
62    NODE_NAMESPACE: ns
63dynamic_resources:
64  lds_config: { ads: {} }
65  ads_config:
66    api_type: GRPC
67    grpc_services:
68      envoy_grpc:
69        cluster_name: xds
70static_resources:
71  clusters:
72  - name: xds
73    http2_protocol_options: {}
74    connect_timeout: 5s
75    type: STATIC
76    hosts:
77    - socket_address:
78        address: 127.0.0.1
79        port_value: {{.Ports.DiscoveryPort}}
80  - name: "outbound|||svc.ns3"
81    connect_timeout: 5s
82    type: STATIC
83    hosts:
84    - socket_address:
85        address: 127.0.0.1
86        port_value: {{.Ports.ServerProxyPort}}
87  - name: "inbound|||backend"
88    connect_timeout: 5s
89    type: STATIC
90    hosts:
91    - socket_address:
92        address: 127.0.0.1
93        port_value: {{.Ports.BackendPort}}
94  - name: "outbound|9091||mixer_server"
95    http2_protocol_options: {}
96    connect_timeout: 5s
97    type: STATIC
98    hosts:
99    - socket_address:
100        address: 127.0.0.1
101        port_value: {{.Ports.MixerPort}}
102`
103
104	checkAttributesOkOutbound = `
105{
106  "connection.mtls": false,
107  "origin.ip": "[127 0 0 1]",
108  "context.protocol": "http",
109  "context.reporter.kind": "outbound",
110  "context.reporter.uid": "kubernetes://pod2.ns2",
111  "context.proxy_version": "1.1.1",
112  "destination.service.host": "svc.ns3",
113  "destination.service.name": "svc",
114  "destination.service.namespace": "ns3",
115  "destination.service.uid": "istio://ns3/services/svc",
116  "source.uid": "kubernetes://pod2.ns2",
117  "source.namespace": "ns2",
118  "request.headers": {
119     ":method": "GET",
120     ":path": "/echo",
121     ":authority": "*",
122     "x-forwarded-proto": "http",
123     "x-request-id": "*"
124  },
125  "request.host": "*",
126  "request.path": "/echo",
127  "request.time": "*",
128  "request.useragent": "Go-http-client/1.1",
129  "request.method": "GET",
130  "request.scheme": "http",
131  "request.url_path": "/echo"
132}
133`
134	checkAttributesOkInbound = `
135{
136  "connection.mtls": false,
137  "origin.ip": "[127 0 0 1]",
138  "context.protocol": "http",
139  "context.reporter.kind": "inbound",
140  "context.reporter.uid": "kubernetes://pod1.ns2",
141  "context.proxy_version": "1.1.1",
142  "destination.ip": "[0 0 0 0 0 0 0 0 0 0 255 255 127 0 0 1]",
143  "destination.port": "*",
144  "destination.namespace": "ns2",
145  "destination.uid": "kubernetes://pod1.ns2",
146  "destination.mesh.id": "helloworld",
147  "destination.service.host": "svc.ns3",
148  "destination.service.name": "svc",
149  "destination.service.namespace": "ns3",
150  "destination.service.uid": "istio://ns3/services/svc",
151  "source.uid": "kubernetes://pod2.ns2",
152  "request.headers": {
153     ":method": "GET",
154     ":path": "/echo",
155     ":authority": "*",
156     "x-forwarded-proto": "http",
157     "x-request-id": "*"
158  },
159  "request.host": "*",
160  "request.path": "/echo",
161  "request.time": "*",
162  "request.useragent": "Go-http-client/1.1",
163  "request.method": "GET",
164  "request.scheme": "http",
165  "request.url_path": "/echo"
166}
167`
168	reportAttributesOkOutbound = `
169{
170  "connection.mtls": false,
171  "origin.ip": "[127 0 0 1]",
172  "context.protocol": "http",
173  "context.proxy_error_code": "-",
174  "context.reporter.kind": "outbound",
175  "context.reporter.uid": "kubernetes://pod2.ns2",
176  "context.proxy_version": "1.1.1",
177  "destination.ip": "[127 0 0 1]",
178  "destination.port": "*",
179  "destination.service.host": "svc.ns3",
180  "destination.service.name": "svc",
181  "destination.service.namespace": "ns3",
182  "destination.service.uid": "istio://ns3/services/svc",
183  "source.uid": "kubernetes://pod2.ns2",
184  "source.namespace": "ns2",
185  "check.cache_hit": false,
186  "quota.cache_hit": false,
187  "request.headers": {
188     ":method": "GET",
189     ":path": "/echo",
190     ":authority": "*",
191     "x-forwarded-proto": "http",
192     "x-istio-attributes": "-",
193     "x-request-id": "*"
194  },
195  "request.host": "*",
196  "request.path": "/echo",
197  "request.time": "*",
198  "request.useragent": "Go-http-client/1.1",
199  "request.method": "GET",
200  "request.scheme": "http",
201  "request.size": 0,
202  "request.total_size": "*",
203  "request.url_path": "/echo",
204  "response.time": "*",
205  "response.size": 0,
206  "response.duration": "*",
207  "response.code": 200,
208  "response.headers": {
209     "date": "*",
210     "content-length": "0",
211     ":status": "200",
212     "server": "envoy"
213  },
214  "response.total_size": "*"
215}`
216
217	reportAttributesOkInbound = `
218{
219  "connection.mtls": false,
220  "origin.ip": "[127 0 0 1]",
221  "context.protocol": "http",
222  "context.proxy_error_code": "-",
223  "context.reporter.kind": "inbound",
224  "context.reporter.uid": "kubernetes://pod1.ns2",
225  "context.proxy_version": "1.1.1",
226  "destination.ip": "[0 0 0 0 0 0 0 0 0 0 255 255 127 0 0 1]",
227  "destination.port": "*",
228  "destination.namespace": "ns2",
229  "destination.uid": "kubernetes://pod1.ns2",
230  "destination.mesh.id": "helloworld",
231  "destination.service.host": "svc.ns3",
232  "destination.service.name": "svc",
233  "destination.service.namespace": "ns3",
234  "destination.service.uid": "istio://ns3/services/svc",
235  "source.uid": "kubernetes://pod2.ns2",
236  "check.cache_hit": false,
237  "quota.cache_hit": false,
238  "request.headers": {
239     ":method": "GET",
240     ":path": "/echo",
241     ":authority": "*",
242     "x-forwarded-proto": "http",
243     "x-istio-attributes": "-",
244     "x-request-id": "*"
245  },
246  "request.host": "*",
247  "request.path": "/echo",
248  "request.time": "*",
249  "request.useragent": "Go-http-client/1.1",
250  "request.method": "GET",
251  "request.scheme": "http",
252  "request.size": 0,
253  "request.total_size": "*",
254  "request.url_path": "/echo",
255  "response.time": "*",
256  "response.size": 0,
257  "response.duration": "*",
258  "response.code": 200,
259  "response.headers": {
260     "date": "*",
261     "content-length": "0",
262     ":status": "200",
263     "server": "envoy"
264  },
265  "response.total_size": "*"
266}`
267)
268
269func TestPilotPlugin(t *testing.T) {
270	s := env.NewTestSetup(env.PilotPluginTest, t)
271	s.EnvoyTemplate = envoyConf
272	grpcServer := grpc.NewServer()
273	lis, err := net.Listen("tcp", fmt.Sprintf(":%d", s.Ports().DiscoveryPort))
274	if err != nil {
275		t.Fatal(err)
276	}
277
278	snapshots := cache.NewSnapshotCache(true, mock{}, nil)
279	_ = snapshots.SetSnapshot(id, makeSnapshot(s, t))
280	server := xds.NewServer(context.Background(), snapshots, nil)
281	discovery.RegisterAggregatedDiscoveryServiceServer(grpcServer, server)
282	go func() {
283		_ = grpcServer.Serve(lis)
284	}()
285	defer grpcServer.GracefulStop()
286
287	s.SetMixerSourceUID("pod.ns")
288
289	if err := s.SetUp(); err != nil {
290		t.Fatalf("Failed to setup test: %v", err)
291	}
292	defer s.TearDown()
293
294	s.WaitEnvoyReady()
295
296	// Issues a GET echo request with 0 size body
297	if _, _, err := env.HTTPGet(fmt.Sprintf("http://localhost:%d/echo", s.Ports().ClientProxyPort)); err != nil {
298		t.Errorf("Failed in request: %v", err)
299	}
300	s.VerifyCheck("http-outbound", checkAttributesOkOutbound)
301	s.VerifyCheck("http-inbound", checkAttributesOkInbound)
302	s.VerifyTwoReports("http", reportAttributesOkOutbound, reportAttributesOkInbound)
303}
304
305type mock struct{}
306
307func (mock) ID(*core.Node) string {
308	return id
309}
310func (mock) GetProxyServiceInstances(_ *model.Proxy) ([]*model.ServiceInstance, error) {
311	return nil, nil
312}
313func (mock) GetProxyWorkloadLabels(proxy *model.Proxy) (labels.Collection, error) {
314	return nil, nil
315}
316func (mock) GetService(_ host.Name) (*model.Service, error) { return nil, nil }
317func (mock) InstancesByPort(_ *model.Service, _ int, _ labels.Collection) ([]*model.ServiceInstance, error) {
318	return nil, nil
319}
320func (mock) ManagementPorts(_ string) model.PortList                        { return nil }
321func (mock) Services() ([]*model.Service, error)                            { return nil, nil }
322func (mock) WorkloadHealthCheckInfo(_ string) model.ProbeList               { return nil }
323func (mock) GetIstioServiceAccounts(_ *model.Service, ports []int) []string { return nil }
324
325const (
326	id = "id"
327)
328
329var (
330	svc = model.Service{
331		Hostname: "svc.ns3",
332		Attributes: model.ServiceAttributes{
333			Name:      "svc",
334			Namespace: "ns3",
335			UID:       "istio://ns3/services/svc",
336		},
337	}
338	pushContext = model.PushContext{
339		ServiceByHostnameAndNamespace: map[host.Name]map[string]*model.Service{
340			host.Name("svc.ns3"): {
341				"ns3": &svc,
342			},
343		},
344		Mesh: &meshconfig.MeshConfig{
345			MixerCheckServer:            "mixer_server:9091",
346			MixerReportServer:           "mixer_server:9091",
347			EnableClientSidePolicyCheck: true,
348		},
349		ServiceDiscovery: mock{},
350	}
351	serverParams = plugin.InputParams{
352		ListenerProtocol: networking.ListenerProtocolHTTP,
353		Node: &model.Proxy{
354			ID:           "pod1.ns2",
355			Type:         model.SidecarProxy,
356			IstioVersion: &model.IstioVersion{Major: 1, Minor: 1, Patch: 1},
357			Metadata:     &model.NodeMetadata{MeshID: "helloworld"},
358		},
359		ServiceInstance: &model.ServiceInstance{Service: &svc},
360		Push:            &pushContext,
361	}
362	clientParams = plugin.InputParams{
363		ListenerProtocol: networking.ListenerProtocolHTTP,
364		Node: &model.Proxy{
365			ID:           "pod2.ns2",
366			Type:         model.SidecarProxy,
367			IstioVersion: &model.IstioVersion{Major: 1, Minor: 1, Patch: 1},
368			Metadata:     &model.NodeMetadata{MeshID: "helloworld"},
369		},
370		Service: &svc,
371		Push:    &pushContext,
372	}
373)
374
375func makeRoute(cluster string) *v2.RouteConfiguration {
376	return &v2.RouteConfiguration{
377		Name: cluster,
378		VirtualHosts: []*route.VirtualHost{{
379			Name:    cluster,
380			Domains: []string{"*"},
381			Routes: []*route.Route{{
382				Match: &route.RouteMatch{PathSpecifier: &route.RouteMatch_Prefix{Prefix: "/"}},
383				Action: &route.Route_Route{Route: &route.RouteAction{
384					ClusterSpecifier: &route.RouteAction_Cluster{Cluster: cluster},
385				}},
386			}},
387		}},
388	}
389}
390
391func makeListener(port uint16, route string) (*v2.Listener, *hcm.HttpConnectionManager) {
392	return &v2.Listener{
393			Name: route,
394			Address: &core.Address{Address: &core.Address_SocketAddress{SocketAddress: &core.SocketAddress{
395				Address:       "127.0.0.1",
396				PortSpecifier: &core.SocketAddress_PortValue{PortValue: uint32(port)}}}},
397		}, &hcm.HttpConnectionManager{
398			CodecType:  hcm.HttpConnectionManager_AUTO,
399			StatPrefix: route,
400			RouteSpecifier: &hcm.HttpConnectionManager_Rds{
401				Rds: &hcm.Rds{RouteConfigName: route, ConfigSource: &core.ConfigSource{
402					ConfigSourceSpecifier: &core.ConfigSource_Ads{Ads: &core.AggregatedConfigSource{}},
403				}},
404			},
405			HttpFilters: []*hcm.HttpFilter{{Name: wellknown.Router}},
406		}
407}
408
409func makeSnapshot(s *env.TestSetup, t *testing.T) cache.Snapshot {
410	clientListener, clientManager := makeListener(s.Ports().ClientProxyPort, "outbound|||svc.ns3")
411	serverListener, serverManager := makeListener(s.Ports().ServerProxyPort, "inbound|||backend")
412	clientRoute := makeRoute("outbound|||svc.ns3")
413	serverRoute := makeRoute("inbound|||backend")
414
415	p := mixer.NewPlugin()
416
417	serverMutable := networking.MutableObjects{Listener: serverListener, FilterChains: []networking.FilterChain{{}}}
418	if err := p.OnInboundListener(&serverParams, &serverMutable); err != nil {
419		t.Error(err)
420	}
421	serverManager.HttpFilters = append(serverMutable.FilterChains[0].HTTP, serverManager.HttpFilters...)
422	serverListener.FilterChains = []*listener.FilterChain{{Filters: []*listener.Filter{{
423		Name:       "http",
424		ConfigType: &listener.Filter_TypedConfig{TypedConfig: pilotutil.MessageToAny(serverManager)},
425	}}}}
426
427	clientMutable := networking.MutableObjects{Listener: clientListener, FilterChains: []networking.FilterChain{{}}}
428	if err := p.OnOutboundListener(&clientParams, &clientMutable); err != nil {
429		t.Error(err)
430	}
431	clientManager.HttpFilters = append(clientMutable.FilterChains[0].HTTP, clientManager.HttpFilters...)
432	clientListener.FilterChains = []*listener.FilterChain{{Filters: []*listener.Filter{{
433		Name:       "http",
434		ConfigType: &listener.Filter_TypedConfig{TypedConfig: pilotutil.MessageToAny(clientManager)},
435	}}}}
436
437	p.OnInboundRouteConfiguration(&serverParams, serverRoute)
438	p.OnOutboundRouteConfiguration(&clientParams, clientRoute)
439
440	snapshot := cache.Snapshot{}
441	snapshot.Resources[types.Route] = cache.NewResources("http", []types.Resource{clientRoute, serverRoute})
442	snapshot.Resources[types.Listener] = cache.NewResources("http", []types.Resource{clientListener, serverListener})
443	return snapshot
444}
445