Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
Testing / vet-proto (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
852 lines
34 KiB
Go
852 lines
34 KiB
Go
/*
|
|
*
|
|
* Copyright 2026 gRPC authors.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*
|
|
*/
|
|
|
|
package xds_test
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
xdscreds "google.golang.org/grpc/credentials/xds"
|
|
"google.golang.org/grpc/internal/envconfig"
|
|
"google.golang.org/grpc/internal/stubserver"
|
|
"google.golang.org/grpc/internal/testutils"
|
|
"google.golang.org/grpc/internal/testutils/xds/e2e"
|
|
"google.golang.org/grpc/internal/testutils/xds/e2e/setup"
|
|
"google.golang.org/grpc/peer"
|
|
"google.golang.org/grpc/resolver"
|
|
"google.golang.org/grpc/resolver/manual"
|
|
"google.golang.org/grpc/status"
|
|
|
|
v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
|
|
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
|
v3endpointpb "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
|
|
v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
|
|
v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
|
v3tlspb "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
|
|
v3matcherpb "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
|
|
testgrpc "google.golang.org/grpc/interop/grpc_testing"
|
|
testpb "google.golang.org/grpc/interop/grpc_testing"
|
|
)
|
|
|
|
// validSNI matches the wildcard SAN (*.test.example.com) in the test server
|
|
// certificate.
|
|
const validSNI = "x.test.example.com"
|
|
|
|
// Tests the SNI and SAN validation logic by verifying that RPCs succeed when
|
|
// AutoSNISANValidation is enabled and the SNI matches a server certificate DNS
|
|
// SAN. Also verifies that RPCs fail with an 'Unavailable' status if the SNI is
|
|
// present but does not match any DNS SAN in the certificate.
|
|
func (s) TestClientSideXDS_SNISANValidation(t *testing.T) {
|
|
testutils.SetEnvConfig(t, &envconfig.XDSSNIEnabled, true)
|
|
|
|
// Spin up an xDS management server.
|
|
mgmtServer, nodeID, _, xdsResolver := setup.ManagementServerAndResolver(t)
|
|
|
|
// Create test backends for two clusters:
|
|
// - backend1 configured with TLS creds, represents cluster1 (valid SNI)
|
|
// - backend2 configured with TLS creds, represents cluster2 (invalid SNI)
|
|
serverCreds := testutils.CreateServerTLSCredentials(t, tls.RequireAndVerifyClientCert)
|
|
server1 := stubserver.StartTestService(t, nil, grpc.Creds(serverCreds))
|
|
defer server1.Stop()
|
|
server2 := stubserver.StartTestService(t, nil, grpc.Creds(serverCreds))
|
|
defer server2.Stop()
|
|
|
|
const serviceName = "my-service-client-side-xds"
|
|
const routeConfigName = "route-" + serviceName
|
|
const clusterName1 = "cluster1-" + serviceName
|
|
const clusterName2 = "cluster2-" + serviceName
|
|
const endpointName1 = "endpoint1-" + serviceName
|
|
const endpointName2 = "endpoint2-" + serviceName
|
|
|
|
listeners := []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeConfigName)}
|
|
|
|
// Route configuration:
|
|
// - "/grpc.testing.TestService/EmptyCall" --> cluster1 (valid SNI)
|
|
// - "/grpc.testing.TestService/UnaryCall" --> cluster2 (invalid SNI)
|
|
routes := []*v3routepb.RouteConfiguration{{
|
|
Name: routeConfigName,
|
|
VirtualHosts: []*v3routepb.VirtualHost{{
|
|
Domains: []string{serviceName},
|
|
Routes: []*v3routepb.Route{
|
|
{
|
|
Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/grpc.testing.TestService/EmptyCall"}},
|
|
Action: &v3routepb.Route_Route{Route: &v3routepb.RouteAction{ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName1}}},
|
|
},
|
|
{
|
|
Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/grpc.testing.TestService/UnaryCall"}},
|
|
Action: &v3routepb.Route_Route{Route: &v3routepb.RouteAction{ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName2}}},
|
|
},
|
|
},
|
|
}},
|
|
}}
|
|
|
|
// Configure cluster1 with valid SNI and AutoSniSanValidation set to true.
|
|
cluster1 := e2e.DefaultCluster(clusterName1, endpointName1, e2e.SecurityLevelMTLS)
|
|
cluster1.TransportSocket = &v3corepb.TransportSocket{
|
|
Name: "envoy.transport_sockets.tls",
|
|
ConfigType: &v3corepb.TransportSocket_TypedConfig{
|
|
TypedConfig: testutils.MarshalAny(t, &v3tlspb.UpstreamTlsContext{
|
|
Sni: validSNI,
|
|
AutoSniSanValidation: true,
|
|
CommonTlsContext: &v3tlspb.CommonTlsContext{
|
|
ValidationContextType: &v3tlspb.CommonTlsContext_ValidationContextCertificateProviderInstance{
|
|
ValidationContextCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{InstanceName: e2e.ClientSideCertProviderInstance},
|
|
},
|
|
TlsCertificateCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{InstanceName: e2e.ClientSideCertProviderInstance},
|
|
},
|
|
}),
|
|
},
|
|
}
|
|
|
|
// Configure cluster2 with invalid SNI and AutoSniSanValidation set to true.
|
|
cluster2 := e2e.DefaultCluster(clusterName2, endpointName2, e2e.SecurityLevelMTLS)
|
|
cluster2.TransportSocket = &v3corepb.TransportSocket{
|
|
Name: "envoy.transport_sockets.tls",
|
|
ConfigType: &v3corepb.TransportSocket_TypedConfig{
|
|
TypedConfig: testutils.MarshalAny(t, &v3tlspb.UpstreamTlsContext{
|
|
Sni: "wrong.sni.domain",
|
|
AutoSniSanValidation: true,
|
|
CommonTlsContext: &v3tlspb.CommonTlsContext{
|
|
ValidationContextType: &v3tlspb.CommonTlsContext_ValidationContextCertificateProviderInstance{
|
|
ValidationContextCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{
|
|
InstanceName: e2e.ClientSideCertProviderInstance,
|
|
CertificateName: "root",
|
|
},
|
|
},
|
|
TlsCertificateCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{
|
|
InstanceName: e2e.ClientSideCertProviderInstance,
|
|
CertificateName: "identity",
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
}
|
|
|
|
clusters := []*v3clusterpb.Cluster{cluster1, cluster2}
|
|
endpoints := []*v3endpointpb.ClusterLoadAssignment{
|
|
e2e.DefaultEndpoint(endpointName1, "localhost", []uint32{testutils.ParsePort(t, server1.Address)}),
|
|
e2e.DefaultEndpoint(endpointName2, "localhost", []uint32{testutils.ParsePort(t, server2.Address)}),
|
|
}
|
|
|
|
resources := e2e.UpdateOptions{
|
|
NodeID: nodeID,
|
|
Listeners: listeners,
|
|
Routes: routes,
|
|
Clusters: clusters,
|
|
Endpoints: endpoints,
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
|
defer cancel()
|
|
if err := mgmtServer.Update(ctx, resources); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
clientCreds, err := xdscreds.NewClientCredentials(xdscreds.ClientOptions{FallbackCreds: insecure.NewCredentials()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cc, err := grpc.NewClient(fmt.Sprintf("xds:///%s", serviceName), grpc.WithTransportCredentials(clientCreds), grpc.WithResolvers(xdsResolver))
|
|
if err != nil {
|
|
t.Fatalf("failed to dial local test server: %v", err)
|
|
}
|
|
defer cc.Close()
|
|
|
|
client := testgrpc.NewTestServiceClient(cc)
|
|
// RPC to cluster1 should succeed because auto_sni_san_validation is true
|
|
// and sni matches server cert SAN.
|
|
peerInfo := &peer.Peer{}
|
|
if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(peerInfo)); err != nil {
|
|
t.Fatalf("EmptyCall() failed: %v", err)
|
|
}
|
|
if got, want := peerInfo.Addr.String(), server1.Address; got != want {
|
|
t.Fatalf("EmptyCall() routed to %q, want to be routed to: %q", got, want)
|
|
}
|
|
|
|
// RPC to cluster2 should fail because even though auto_sni_san_validation
|
|
// is true, sni doesn't match server cert SAN.
|
|
const wantErr = "do not match the SNI"
|
|
if _, err := client.UnaryCall(ctx, &testpb.SimpleRequest{}); status.Code(err) != codes.Unavailable || !strings.Contains(err.Error(), wantErr) {
|
|
t.Fatalf("UnaryCall() failed: %v, wantCode: %s, wantErr: %s", err, codes.Unavailable, wantErr)
|
|
}
|
|
}
|
|
|
|
// Tests that when AutoHostSNI is enabled, the endpoint's hostname is used for
|
|
// the ServerName even though SNI is specified. It verifies that the TLS
|
|
// handshake and subsequent RPC succeed because the resolved SNI i.e. the
|
|
// hostname matches the server's certificate SAN.
|
|
func (s) TestClientSideXDS_AutoHostSNI(t *testing.T) {
|
|
testutils.SetEnvConfig(t, &envconfig.XDSSNIEnabled, true)
|
|
|
|
// Spin up an xDS management server.
|
|
mgmtServer, nodeID, _, xdsResolver := setup.ManagementServerAndResolver(t)
|
|
|
|
// Create test backend
|
|
serverCreds := testutils.CreateServerTLSCredentials(t, tls.RequireAndVerifyClientCert)
|
|
server := stubserver.StartTestService(t, nil, grpc.Creds(serverCreds))
|
|
defer server.Stop()
|
|
|
|
// Configure client side xDS resources on the management server.
|
|
const serviceName = "my-service-client-side-xds"
|
|
const routeConfigName = "route-" + serviceName
|
|
const clusterName = "cluster-" + serviceName
|
|
const endpointName = "endpoint-" + serviceName
|
|
|
|
listeners := []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeConfigName)}
|
|
routes := []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeConfigName, serviceName, clusterName)}
|
|
|
|
// Configure the cluster with AutoHostSni and AutoSniSanValidation set to
|
|
// true with a invalid SNI.
|
|
cluster := e2e.DefaultCluster(clusterName, endpointName, e2e.SecurityLevelMTLS)
|
|
cluster.TransportSocket = &v3corepb.TransportSocket{
|
|
Name: "envoy.transport_sockets.tls",
|
|
ConfigType: &v3corepb.TransportSocket_TypedConfig{
|
|
TypedConfig: testutils.MarshalAny(t, &v3tlspb.UpstreamTlsContext{
|
|
AutoHostSni: true,
|
|
AutoSniSanValidation: true,
|
|
Sni: "wrong.sni.domain",
|
|
CommonTlsContext: &v3tlspb.CommonTlsContext{
|
|
ValidationContextType: &v3tlspb.CommonTlsContext_ValidationContextCertificateProviderInstance{
|
|
ValidationContextCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{
|
|
InstanceName: e2e.ClientSideCertProviderInstance,
|
|
CertificateName: "root",
|
|
},
|
|
},
|
|
TlsCertificateCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{
|
|
InstanceName: e2e.ClientSideCertProviderInstance,
|
|
CertificateName: "identity",
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
}
|
|
|
|
// Endpoints configuring Hostname to the defaultTestCertSAN to verify
|
|
// AutoHostSni usage.
|
|
endpoints := []*v3endpointpb.ClusterLoadAssignment{
|
|
e2e.EndpointResourceWithOptions(e2e.EndpointOptions{
|
|
ClusterName: endpointName,
|
|
Host: "localhost",
|
|
Localities: []e2e.LocalityOptions{{
|
|
Weight: 1,
|
|
Backends: []e2e.BackendOptions{{
|
|
Ports: []uint32{testutils.ParsePort(t, server.Address)},
|
|
Hostname: validSNI,
|
|
}},
|
|
}},
|
|
}),
|
|
}
|
|
|
|
resources := e2e.UpdateOptions{
|
|
NodeID: nodeID,
|
|
Listeners: listeners,
|
|
Routes: routes,
|
|
Clusters: []*v3clusterpb.Cluster{cluster},
|
|
Endpoints: endpoints,
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
|
defer cancel()
|
|
if err := mgmtServer.Update(ctx, resources); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
clientCreds, err := xdscreds.NewClientCredentials(xdscreds.ClientOptions{FallbackCreds: insecure.NewCredentials()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cc, err := grpc.NewClient(fmt.Sprintf("xds:///%s", serviceName), grpc.WithTransportCredentials(clientCreds), grpc.WithResolvers(xdsResolver))
|
|
if err != nil {
|
|
t.Fatalf("failed to dial local test server: %v", err)
|
|
}
|
|
defer cc.Close()
|
|
|
|
client := testgrpc.NewTestServiceClient(cc)
|
|
// RPC should succeed because auto_host_sni sets SNI from the endpoint
|
|
// hostname and auto_sni_san_validation validates that the SNI matches
|
|
// server cert SAN.
|
|
peerInfo := &peer.Peer{}
|
|
if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(peerInfo)); err != nil {
|
|
t.Fatalf("EmptyCall() failed: %v", err)
|
|
}
|
|
if got, want := peerInfo.Addr.String(), server.Address; got != want {
|
|
t.Fatalf("EmptyCall() routed to %q, want to be routed to: %q", got, want)
|
|
}
|
|
}
|
|
|
|
// Tests the scenario where multiple endpoints are configured with distinct
|
|
// hostnames. It verifies that the client uses the specific hostname associated
|
|
// with each endpoint during the TLS handshake by simulating an EDS failover and
|
|
// confirming that the SNI is dynamically updated to match the active endpoint.
|
|
func (s) TestClientSideXDS_AutoHostSNI_MultipleEndpoints(t *testing.T) {
|
|
testutils.SetEnvConfig(t, &envconfig.XDSSNIEnabled, true)
|
|
|
|
// Spin up an xDS management server.
|
|
mgmtServer, nodeID, _, xdsResolver := setup.ManagementServerAndResolver(t)
|
|
|
|
// Create test backends.
|
|
serverCreds := testutils.CreateServerTLSCredentials(t, tls.RequireAndVerifyClientCert)
|
|
server1 := stubserver.StartTestService(t, nil, grpc.Creds(serverCreds))
|
|
defer server1.Stop()
|
|
server2 := stubserver.StartTestService(t, nil, grpc.Creds(serverCreds))
|
|
defer server2.Stop()
|
|
|
|
// Configure client side xDS resources on the management server.
|
|
const serviceName = "my-service-client-side-xds"
|
|
const routeConfigName = "route-" + serviceName
|
|
const clusterName = "cluster-" + serviceName
|
|
const endpointName = "endpoint-" + serviceName
|
|
|
|
listeners := []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeConfigName)}
|
|
routes := []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeConfigName, serviceName, clusterName)}
|
|
|
|
clusters := []*v3clusterpb.Cluster{e2e.DefaultCluster(clusterName, endpointName, e2e.SecurityLevelMTLS)}
|
|
clusters[0].TransportSocket = &v3corepb.TransportSocket{
|
|
Name: "envoy.transport_sockets.tls",
|
|
ConfigType: &v3corepb.TransportSocket_TypedConfig{
|
|
TypedConfig: testutils.MarshalAny(t, &v3tlspb.UpstreamTlsContext{
|
|
AutoHostSni: true,
|
|
AutoSniSanValidation: true,
|
|
Sni: "wrong.sni.domain",
|
|
CommonTlsContext: &v3tlspb.CommonTlsContext{
|
|
ValidationContextType: &v3tlspb.CommonTlsContext_ValidationContextCertificateProviderInstance{
|
|
ValidationContextCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{
|
|
InstanceName: e2e.ClientSideCertProviderInstance,
|
|
CertificateName: "root",
|
|
},
|
|
},
|
|
TlsCertificateCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{
|
|
InstanceName: e2e.ClientSideCertProviderInstance,
|
|
CertificateName: "identity",
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
}
|
|
|
|
// Configure only Priority 0 with an invalid hostname. RPC should fail due
|
|
// to SAN mismatch.
|
|
endpoints := []*v3endpointpb.ClusterLoadAssignment{
|
|
e2e.EndpointResourceWithOptions(e2e.EndpointOptions{
|
|
ClusterName: endpointName,
|
|
Host: "localhost",
|
|
Localities: []e2e.LocalityOptions{
|
|
{
|
|
Name: "locality-0",
|
|
Weight: 1,
|
|
Priority: 0,
|
|
Backends: []e2e.BackendOptions{{
|
|
Ports: []uint32{testutils.ParsePort(t, server1.Address)},
|
|
Hostname: "wrong.sni.domain",
|
|
}},
|
|
},
|
|
},
|
|
}),
|
|
}
|
|
|
|
resources := e2e.UpdateOptions{
|
|
NodeID: nodeID,
|
|
Listeners: listeners,
|
|
Routes: routes,
|
|
Clusters: clusters,
|
|
Endpoints: endpoints,
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
|
defer cancel()
|
|
if err := mgmtServer.Update(ctx, resources); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
clientCreds, err := xdscreds.NewClientCredentials(xdscreds.ClientOptions{FallbackCreds: insecure.NewCredentials()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cc, err := grpc.NewClient(fmt.Sprintf("xds:///%s", serviceName), grpc.WithTransportCredentials(clientCreds), grpc.WithResolvers(xdsResolver))
|
|
if err != nil {
|
|
t.Fatalf("failed to dial local test server: %v", err)
|
|
}
|
|
defer cc.Close()
|
|
|
|
client := testgrpc.NewTestServiceClient(cc)
|
|
|
|
// RPC should fail because hostname for endpoints in Priority 0 is invalid.
|
|
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err == nil {
|
|
t.Fatalf("EmptyCall() unexpectedly succeeded with invalid SNI and no failover")
|
|
} else if !strings.Contains(err.Error(), "authentication handshake failed") {
|
|
t.Fatalf("EmptyCall() failed with %v, want error containing 'authentication handshake failed'", err)
|
|
}
|
|
|
|
// Update the same EDS resource to add Priority 1 with an endpoint with
|
|
// valid hostname.
|
|
endpoints = []*v3endpointpb.ClusterLoadAssignment{
|
|
e2e.EndpointResourceWithOptions(e2e.EndpointOptions{
|
|
ClusterName: endpointName,
|
|
Host: "localhost",
|
|
Localities: []e2e.LocalityOptions{
|
|
{
|
|
Name: "locality-0",
|
|
Weight: 1,
|
|
Priority: 0,
|
|
Backends: []e2e.BackendOptions{{
|
|
Ports: []uint32{testutils.ParsePort(t, server1.Address)},
|
|
Hostname: "wrong.sni.domain",
|
|
}},
|
|
},
|
|
{
|
|
Name: "locality-1",
|
|
Weight: 1,
|
|
Priority: 1,
|
|
Backends: []e2e.BackendOptions{{
|
|
Ports: []uint32{testutils.ParsePort(t, server2.Address)},
|
|
Hostname: validSNI,
|
|
}},
|
|
},
|
|
},
|
|
}),
|
|
}
|
|
resources.Endpoints = endpoints
|
|
if err := mgmtServer.Update(ctx, resources); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// RPC should succeed because it automatically fails over to Priority 1. Id
|
|
// only one hostname would have been used per leaf cluster , this RPC should
|
|
// have failed due to invalid SNI in Priority 0.
|
|
peerInfo := &peer.Peer{}
|
|
if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(peerInfo), grpc.WaitForReady(true)); err != nil {
|
|
t.Fatalf("EmptyCall() failed after adding Priority 1: %v", err)
|
|
}
|
|
if got, want := peerInfo.Addr.String(), server2.Address; got != want {
|
|
t.Fatalf("EmptyCall() successes routed to %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
// TestClientSideXDS_FallbackSANMatchers tests that when AutoSniSanValidation is
|
|
// true, and no SNI is provided by the control plane and AutoHostSNI is not set,
|
|
// the validation falls back to using the explicit SAN matchers specified in the
|
|
// configuration. It verifies that RPCs succeed when the fallback matchers match
|
|
// the server certificate SAN and fail when they do not.
|
|
func (s) TestClientSideXDS_FallbackSANMatchers(t *testing.T) {
|
|
testutils.SetEnvConfig(t, &envconfig.XDSSNIEnabled, true)
|
|
|
|
// Spin up an xDS management server.
|
|
mgmtServer, nodeID, _, xdsResolver := setup.ManagementServerAndResolver(t)
|
|
|
|
// Create test backends.
|
|
serverCreds := testutils.CreateServerTLSCredentials(t, tls.RequireAndVerifyClientCert)
|
|
server1 := stubserver.StartTestService(t, nil, grpc.Creds(serverCreds))
|
|
defer server1.Stop()
|
|
server2 := stubserver.StartTestService(t, nil, grpc.Creds(serverCreds))
|
|
defer server2.Stop()
|
|
|
|
// Configure client side xDS resources on the management server.
|
|
const serviceName = "my-service-client-side-xds"
|
|
const routeConfigName = "route-" + serviceName
|
|
const clusterName1 = "cluster1-" + serviceName
|
|
const clusterName2 = "cluster2-" + serviceName
|
|
const endpointName1 = "endpoint1-" + serviceName
|
|
const endpointName2 = "endpoint2-" + serviceName
|
|
|
|
listeners := []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeConfigName)}
|
|
|
|
routes := []*v3routepb.RouteConfiguration{{
|
|
Name: routeConfigName,
|
|
VirtualHosts: []*v3routepb.VirtualHost{{
|
|
Domains: []string{serviceName},
|
|
Routes: []*v3routepb.Route{
|
|
{
|
|
Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/grpc.testing.TestService/EmptyCall"}},
|
|
Action: &v3routepb.Route_Route{Route: &v3routepb.RouteAction{ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName1}}},
|
|
},
|
|
{
|
|
Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/grpc.testing.TestService/UnaryCall"}},
|
|
Action: &v3routepb.Route_Route{Route: &v3routepb.RouteAction{ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName2}}},
|
|
},
|
|
},
|
|
}},
|
|
}}
|
|
|
|
// Configure cluster1 with AutoSniSanValidation set to true and no SNI
|
|
// provided for the handshake. The validation falls back to using the explicit
|
|
// SAN matchers specified in the configuration which matches the server1's
|
|
// certificate SAN.
|
|
cluster1 := e2e.DefaultCluster(clusterName1, endpointName1, e2e.SecurityLevelMTLS)
|
|
cluster1.TransportSocket = &v3corepb.TransportSocket{
|
|
Name: "envoy.transport_sockets.tls",
|
|
ConfigType: &v3corepb.TransportSocket_TypedConfig{
|
|
TypedConfig: testutils.MarshalAny(t, &v3tlspb.UpstreamTlsContext{
|
|
AutoSniSanValidation: true,
|
|
CommonTlsContext: &v3tlspb.CommonTlsContext{
|
|
ValidationContextType: &v3tlspb.CommonTlsContext_CombinedValidationContext{
|
|
CombinedValidationContext: &v3tlspb.CommonTlsContext_CombinedCertificateValidationContext{
|
|
DefaultValidationContext: &v3tlspb.CertificateValidationContext{
|
|
MatchSubjectAltNames: []*v3matcherpb.StringMatcher{
|
|
{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "*.test.example.com"}},
|
|
},
|
|
CaCertificateProviderInstance: &v3tlspb.CertificateProviderPluginInstance{
|
|
InstanceName: e2e.ClientSideCertProviderInstance,
|
|
CertificateName: "root",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
TlsCertificateProviderInstance: &v3tlspb.CertificateProviderPluginInstance{
|
|
InstanceName: e2e.ClientSideCertProviderInstance,
|
|
CertificateName: "identity",
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
}
|
|
|
|
// Configure cluster2 with AutoSniSanValidation set to true and no SNI
|
|
// provided for the handshake. The validation falls back to using the explicit
|
|
// SAN matchers specified in the configuration which does not match the
|
|
// server2's certificate SAN.
|
|
cluster2 := e2e.DefaultCluster(clusterName2, endpointName2, e2e.SecurityLevelMTLS)
|
|
cluster2.TransportSocket = &v3corepb.TransportSocket{
|
|
Name: "envoy.transport_sockets.tls",
|
|
ConfigType: &v3corepb.TransportSocket_TypedConfig{
|
|
TypedConfig: testutils.MarshalAny(t, &v3tlspb.UpstreamTlsContext{
|
|
AutoSniSanValidation: true,
|
|
CommonTlsContext: &v3tlspb.CommonTlsContext{
|
|
ValidationContextType: &v3tlspb.CommonTlsContext_CombinedValidationContext{
|
|
CombinedValidationContext: &v3tlspb.CommonTlsContext_CombinedCertificateValidationContext{
|
|
DefaultValidationContext: &v3tlspb.CertificateValidationContext{
|
|
MatchSubjectAltNames: []*v3matcherpb.StringMatcher{{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "wrong.san.domain"}}},
|
|
CaCertificateProviderInstance: &v3tlspb.CertificateProviderPluginInstance{
|
|
InstanceName: e2e.ClientSideCertProviderInstance,
|
|
CertificateName: "root",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
TlsCertificateProviderInstance: &v3tlspb.CertificateProviderPluginInstance{
|
|
InstanceName: e2e.ClientSideCertProviderInstance,
|
|
CertificateName: "identity",
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
}
|
|
|
|
clusters := []*v3clusterpb.Cluster{cluster1, cluster2}
|
|
endpoints := []*v3endpointpb.ClusterLoadAssignment{
|
|
e2e.DefaultEndpoint(endpointName1, "localhost", []uint32{testutils.ParsePort(t, server1.Address)}),
|
|
e2e.DefaultEndpoint(endpointName2, "localhost", []uint32{testutils.ParsePort(t, server2.Address)}),
|
|
}
|
|
|
|
resources := e2e.UpdateOptions{
|
|
NodeID: nodeID,
|
|
Listeners: listeners,
|
|
Routes: routes,
|
|
Clusters: clusters,
|
|
Endpoints: endpoints,
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 50*defaultTestTimeout)
|
|
defer cancel()
|
|
if err := mgmtServer.Update(ctx, resources); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
clientCreds, err := xdscreds.NewClientCredentials(xdscreds.ClientOptions{FallbackCreds: insecure.NewCredentials()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cc, err := grpc.NewClient(fmt.Sprintf("xds:///%s", serviceName), grpc.WithTransportCredentials(clientCreds), grpc.WithResolvers(xdsResolver))
|
|
if err != nil {
|
|
t.Fatalf("failed to dial local test server: %v", err)
|
|
}
|
|
defer cc.Close()
|
|
|
|
client := testgrpc.NewTestServiceClient(cc)
|
|
|
|
// RPC to cluster1 should succeed because fallback SAN mathchers are used
|
|
// and they match server cert SAN.
|
|
peerInfo := &peer.Peer{}
|
|
if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(peerInfo)); err != nil {
|
|
t.Fatalf("EmptyCall() failed: %v", err)
|
|
}
|
|
if got, want := peerInfo.Addr.String(), server1.Address; got != want {
|
|
t.Fatalf("EmptyCall() routed to %q, want to be routed to: %q", got, want)
|
|
}
|
|
|
|
// RPC to cluster2 should fail because fallback SAN matchers are used
|
|
// but they don't match server cert SAN.
|
|
const wantErr = "do not match any of the accepted SANs"
|
|
if _, err := client.UnaryCall(ctx, &testpb.SimpleRequest{}); status.Code(err) != codes.Unavailable || !strings.Contains(err.Error(), wantErr) {
|
|
t.Fatalf("UnaryCall() failed: %v, wantCode: %s, wantErr: %s", err, codes.Unavailable, wantErr)
|
|
}
|
|
}
|
|
|
|
// Tests that when the XDSSNIEnabled environment variable is set to false, SNI
|
|
// is not used for validation even if AutoSniSanValidation is true and SNI is
|
|
// set. It verifies that the system falls back to using explicit SAN matchers if
|
|
// provided, and the TLS handshake succeeds when they match the server
|
|
// certificate SAN.
|
|
func (s) TestClientSideXDS_SNIEnvVarDisabled(t *testing.T) {
|
|
testutils.SetEnvConfig(t, &envconfig.XDSSNIEnabled, false)
|
|
|
|
// Spin up an xDS management server.
|
|
mgmtServer, nodeID, _, xdsResolver := setup.ManagementServerAndResolver(t)
|
|
|
|
// Create test backends.
|
|
serverCreds := testutils.CreateServerTLSCredentials(t, tls.RequireAndVerifyClientCert)
|
|
server1 := stubserver.StartTestService(t, nil, grpc.Creds(serverCreds))
|
|
defer server1.Stop()
|
|
server2 := stubserver.StartTestService(t, nil, grpc.Creds(serverCreds))
|
|
defer server2.Stop()
|
|
|
|
// Configure client side xDS resources on the management server.
|
|
const serviceName = "my-service-client-side-xds"
|
|
const routeConfigName = "route-" + serviceName
|
|
const clusterName1 = "cluster1-" + serviceName
|
|
const clusterName2 = "cluster2-" + serviceName
|
|
const endpointName1 = "endpoint1-" + serviceName
|
|
const endpointName2 = "endpoint2-" + serviceName
|
|
|
|
listeners := []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeConfigName)}
|
|
|
|
routes := []*v3routepb.RouteConfiguration{{
|
|
Name: routeConfigName,
|
|
VirtualHosts: []*v3routepb.VirtualHost{{
|
|
Domains: []string{serviceName},
|
|
Routes: []*v3routepb.Route{
|
|
{
|
|
Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/grpc.testing.TestService/EmptyCall"}},
|
|
Action: &v3routepb.Route_Route{Route: &v3routepb.RouteAction{ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName1}}},
|
|
},
|
|
{
|
|
Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/grpc.testing.TestService/UnaryCall"}},
|
|
Action: &v3routepb.Route_Route{Route: &v3routepb.RouteAction{ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName2}}},
|
|
},
|
|
},
|
|
}},
|
|
}}
|
|
|
|
// cluster1 configuration with AutoSniSanValidation set to true and wrong SNI
|
|
// provided for the handshake. The validation falls back to using the explicit
|
|
// SAN matchers specified in the configuration which matches the server1's
|
|
// certificate SAN.
|
|
cluster1 := e2e.DefaultCluster(clusterName1, endpointName1, e2e.SecurityLevelMTLS)
|
|
cluster1.TransportSocket = &v3corepb.TransportSocket{
|
|
Name: "envoy.transport_sockets.tls",
|
|
ConfigType: &v3corepb.TransportSocket_TypedConfig{
|
|
TypedConfig: testutils.MarshalAny(t, &v3tlspb.UpstreamTlsContext{
|
|
Sni: "incorrect.sni",
|
|
AutoSniSanValidation: true,
|
|
CommonTlsContext: &v3tlspb.CommonTlsContext{
|
|
ValidationContextType: &v3tlspb.CommonTlsContext_CombinedValidationContext{
|
|
CombinedValidationContext: &v3tlspb.CommonTlsContext_CombinedCertificateValidationContext{
|
|
DefaultValidationContext: &v3tlspb.CertificateValidationContext{
|
|
MatchSubjectAltNames: []*v3matcherpb.StringMatcher{
|
|
{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "*.test.example.com"}},
|
|
},
|
|
CaCertificateProviderInstance: &v3tlspb.CertificateProviderPluginInstance{
|
|
InstanceName: e2e.ClientSideCertProviderInstance,
|
|
CertificateName: "root",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
TlsCertificateProviderInstance: &v3tlspb.CertificateProviderPluginInstance{
|
|
InstanceName: e2e.ClientSideCertProviderInstance,
|
|
CertificateName: "identity",
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
}
|
|
|
|
// Configure cluster2 with AutoSniSanValidation set to true and correct SNI
|
|
// provided for the handshake. The validation falls back to using the
|
|
// explicit SAN matchers specified in the configuration which does not match
|
|
// the server2's certificate SAN.
|
|
cluster2 := e2e.DefaultCluster(clusterName2, endpointName2, e2e.SecurityLevelMTLS)
|
|
cluster2.TransportSocket = &v3corepb.TransportSocket{
|
|
Name: "envoy.transport_sockets.tls",
|
|
ConfigType: &v3corepb.TransportSocket_TypedConfig{
|
|
TypedConfig: testutils.MarshalAny(t, &v3tlspb.UpstreamTlsContext{
|
|
Sni: validSNI,
|
|
AutoSniSanValidation: true,
|
|
CommonTlsContext: &v3tlspb.CommonTlsContext{
|
|
ValidationContextType: &v3tlspb.CommonTlsContext_CombinedValidationContext{
|
|
CombinedValidationContext: &v3tlspb.CommonTlsContext_CombinedCertificateValidationContext{
|
|
DefaultValidationContext: &v3tlspb.CertificateValidationContext{
|
|
MatchSubjectAltNames: []*v3matcherpb.StringMatcher{{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "wrong.san.domain"}}},
|
|
CaCertificateProviderInstance: &v3tlspb.CertificateProviderPluginInstance{
|
|
InstanceName: e2e.ClientSideCertProviderInstance,
|
|
CertificateName: "root",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
TlsCertificateProviderInstance: &v3tlspb.CertificateProviderPluginInstance{
|
|
InstanceName: e2e.ClientSideCertProviderInstance,
|
|
CertificateName: "identity",
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
}
|
|
|
|
clusters := []*v3clusterpb.Cluster{cluster1, cluster2}
|
|
endpoints := []*v3endpointpb.ClusterLoadAssignment{
|
|
e2e.DefaultEndpoint(endpointName1, "localhost", []uint32{testutils.ParsePort(t, server1.Address)}),
|
|
e2e.DefaultEndpoint(endpointName2, "localhost", []uint32{testutils.ParsePort(t, server2.Address)}),
|
|
}
|
|
|
|
resources := e2e.UpdateOptions{
|
|
NodeID: nodeID,
|
|
Listeners: listeners,
|
|
Routes: routes,
|
|
Clusters: clusters,
|
|
Endpoints: endpoints,
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 50*defaultTestTimeout)
|
|
defer cancel()
|
|
if err := mgmtServer.Update(ctx, resources); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
clientCreds, err := xdscreds.NewClientCredentials(xdscreds.ClientOptions{FallbackCreds: insecure.NewCredentials()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cc, err := grpc.NewClient(fmt.Sprintf("xds:///%s", serviceName), grpc.WithTransportCredentials(clientCreds), grpc.WithResolvers(xdsResolver))
|
|
if err != nil {
|
|
t.Fatalf("failed to dial local test server: %v", err)
|
|
}
|
|
defer cc.Close()
|
|
|
|
client := testgrpc.NewTestServiceClient(cc)
|
|
|
|
// RPC to cluster1 should succeed because fallback SAN mathchers are used
|
|
// and they match server cert SAN.
|
|
peerInfo := &peer.Peer{}
|
|
if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(peerInfo)); err != nil {
|
|
t.Fatalf("EmptyCall() failed: %v", err)
|
|
}
|
|
if got, want := peerInfo.Addr.String(), server1.Address; got != want {
|
|
t.Fatalf("EmptyCall() routed to %q, want to be routed to: %q", got, want)
|
|
}
|
|
|
|
// RPC to cluster2 should fail because fallback SAN matchers are used but they
|
|
// don't match server cert SAN.
|
|
const wantErr = "do not match any of the accepted SANs"
|
|
if _, err := client.UnaryCall(ctx, &testpb.SimpleRequest{}); status.Code(err) != codes.Unavailable || !strings.Contains(err.Error(), wantErr) {
|
|
t.Fatalf("UnaryCall() failed: %v, wantCode: %s, wantErr: %s", err, codes.Unavailable, wantErr)
|
|
}
|
|
}
|
|
|
|
// Tests that when AutoHostSNI is enabled for a Logical DNS cluster, the SNI is
|
|
// resolved from the DNSHostName in the cluster configuration. It verifies that
|
|
// the TLS handshake succeeds when the DNSHostName matches the server's
|
|
// certificate SAN.
|
|
func (s) TestClientSideXDS_AutoHostSNI_LogicalDNS(t *testing.T) {
|
|
testutils.SetEnvConfig(t, &envconfig.XDSSNIEnabled, true)
|
|
|
|
// Spin up an xDS management server.
|
|
mgmtServer, nodeID, _, xdsResolver := setup.ManagementServerAndResolver(t)
|
|
|
|
// Create test backend
|
|
serverCreds := testutils.CreateServerTLSCredentials(t, tls.RequireAndVerifyClientCert)
|
|
server := stubserver.StartTestService(t, nil, grpc.Creds(serverCreds))
|
|
defer server.Stop()
|
|
|
|
// Replace DNS resolver with a manual resolver.
|
|
dnsR := manual.NewBuilderWithScheme("dns")
|
|
originalDNS := resolver.Get("dns")
|
|
resolver.Register(dnsR)
|
|
t.Cleanup(func() { resolver.Register(originalDNS) })
|
|
|
|
dnsR.UpdateState(resolver.State{Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: server.Address}}}}})
|
|
|
|
// Configure client side xDS resources on the management server.
|
|
const serviceName = "my-service-client-side-xds"
|
|
const routeConfigName = "route-" + serviceName
|
|
const clusterName = "cluster-" + serviceName
|
|
|
|
listeners := []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeConfigName)}
|
|
routes := []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeConfigName, serviceName, clusterName)}
|
|
|
|
// Cluster of Type LogicalDNS. with DNSHostName set to match the server's cert
|
|
// SAN.
|
|
cluster := e2e.ClusterResourceWithOptions(e2e.ClusterOptions{
|
|
Type: e2e.ClusterTypeLogicalDNS,
|
|
ClusterName: clusterName,
|
|
DNSHostName: validSNI,
|
|
DNSPort: uint32(testutils.ParsePort(t, server.Address)),
|
|
SecurityLevel: e2e.SecurityLevelMTLS,
|
|
})
|
|
cluster.TransportSocket.ConfigType.(*v3corepb.TransportSocket_TypedConfig).TypedConfig = testutils.MarshalAny(t, &v3tlspb.UpstreamTlsContext{
|
|
AutoHostSni: true,
|
|
AutoSniSanValidation: true,
|
|
CommonTlsContext: &v3tlspb.CommonTlsContext{
|
|
ValidationContextType: &v3tlspb.CommonTlsContext_ValidationContextCertificateProviderInstance{
|
|
ValidationContextCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{
|
|
InstanceName: e2e.ClientSideCertProviderInstance,
|
|
},
|
|
},
|
|
TlsCertificateCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{
|
|
InstanceName: e2e.ClientSideCertProviderInstance,
|
|
},
|
|
},
|
|
})
|
|
|
|
resources := e2e.UpdateOptions{
|
|
NodeID: nodeID,
|
|
Listeners: listeners,
|
|
Routes: routes,
|
|
Clusters: []*v3clusterpb.Cluster{cluster},
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
|
defer cancel()
|
|
if err := mgmtServer.Update(ctx, resources); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
clientCreds, err := xdscreds.NewClientCredentials(xdscreds.ClientOptions{FallbackCreds: insecure.NewCredentials()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cc, err := grpc.NewClient(fmt.Sprintf("xds:///%s", serviceName), grpc.WithTransportCredentials(clientCreds), grpc.WithResolvers(xdsResolver))
|
|
if err != nil {
|
|
t.Fatalf("failed to dial local test server: %v", err)
|
|
}
|
|
defer cc.Close()
|
|
|
|
client := testgrpc.NewTestServiceClient(cc)
|
|
// RPC should succeed because DNSHostName matches the server's certificate
|
|
// SAN.
|
|
if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.WaitForReady(true)); err != nil {
|
|
t.Fatalf("EmptyCall() failed: %v", err)
|
|
}
|
|
}
|