Skip to content

Commit

Permalink
Support controlling peerHost and SNI hostname inference for GRPC clie…
Browse files Browse the repository at this point in the history
…nts (#1678)

Motivation:

This change follows from [a change to HTTP client builders](#1561)
and exposes the same capabilities to GRPC client builders.

Modifications:

- `SingleAddressGrpcClientBuilder` has new methods: `inferPeerHost`,
 `inferPeerPort`, and `inferSniHostname` which allow configuring the
 behaviour of the TLS handshake and inference of the corresponding
 configurations.

Result:

Users are able to disable peerHost and peerPort inference and can now
use an empty peerHost when they so desire. The SNI hostname inference
from the provided address can also be disabled in cases when the
address used for connection should not be passed in the SNI extension.
  • Loading branch information
Dariusz Jedrzejczyk authored Jul 21, 2021
1 parent 8f8feb1 commit 21cca84
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ public abstract GrpcClientBuilder<U, R> appendConnectionFilter(Predicate<Streami
@Override
public abstract GrpcClientBuilder<U, R> sslConfig(ClientSslConfig sslConfig);

@Override
public abstract GrpcClientBuilder<U, R> inferPeerHost(boolean shouldInfer);

@Override
public abstract GrpcClientBuilder<U, R> inferPeerPort(boolean shouldInfer);

@Override
public abstract GrpcClientBuilder<U, R> inferSniHostname(boolean shouldInfer);

@Override
public abstract GrpcClientBuilder<U, R> autoRetryStrategy(
AutoRetryStrategyProvider autoRetryStrategyProvider);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,31 @@ SingleAddressGrpcClientBuilder<U, R, SDE> appendConnectionFilter(Predicate<Strea
*/
SingleAddressGrpcClientBuilder<U, R, SDE> sslConfig(ClientSslConfig sslConfig);

/**
* Toggle inference of value to use instead of {@link ClientSslConfig#peerHost()}
* from client's address when peer host is not specified. By default, inference is enabled.
* @param shouldInfer value indicating whether inference is on ({@code true}) or off ({@code false}).
* @return {@code this}
*/
SingleAddressGrpcClientBuilder<U, R, SDE> inferPeerHost(boolean shouldInfer);

/**
* Toggle inference of value to use instead of {@link ClientSslConfig#peerPort()}
* from client's address when peer port is not specified (equals {@code -1}). By default, inference is enabled.
* @param shouldInfer value indicating whether inference is on ({@code true}) or off ({@code false}).
* @return {@code this}
*/
SingleAddressGrpcClientBuilder<U, R, SDE> inferPeerPort(boolean shouldInfer);

/**
* Toggle <a href="https://datatracker.ietf.org/doc/html/rfc6066#section-3">SNI</a>
* hostname inference from client's address if not explicitly specified
* via {@link #sslConfig(ClientSslConfig)}. By default, inference is enabled.
* @param shouldInfer value indicating whether inference is on ({@code true}) or off ({@code false}).
* @return {@code this}
*/
SingleAddressGrpcClientBuilder<U, R, SDE> inferSniHostname(boolean shouldInfer);

/**
* Updates the automatic retry strategy for the clients generated by this builder. Automatic retries are done by
* the clients automatically when allowed by the passed {@link AutoRetryStrategyProvider}. These retries are not a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,24 @@ public GrpcClientBuilder<U, R> sslConfig(final ClientSslConfig sslConfig) {
return this;
}

@Override
public GrpcClientBuilder<U, R> inferPeerHost(final boolean shouldInfer) {
httpClientBuilder.inferPeerHost(shouldInfer);
return this;
}

@Override
public GrpcClientBuilder<U, R> inferPeerPort(final boolean shouldInfer) {
httpClientBuilder.inferPeerPort(shouldInfer);
return this;
}

@Override
public GrpcClientBuilder<U, R> inferSniHostname(final boolean shouldInfer) {
httpClientBuilder.inferSniHostname(shouldInfer);
return this;
}

@Override
public GrpcClientBuilder<U, R> autoRetryStrategy(
final AutoRetryStrategyProvider autoRetryStrategyProvider) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/*
* Copyright © 2021 Apple Inc. and the ServiceTalk project 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 io.servicetalk.grpc.netty;

import io.servicetalk.grpc.api.GrpcClientBuilder;
import io.servicetalk.grpc.api.GrpcStatusException;
import io.servicetalk.grpc.netty.TesterProto.Tester.BlockingTesterClient;
import io.servicetalk.test.resources.DefaultTestCerts;
import io.servicetalk.transport.api.ClientSslConfigBuilder;
import io.servicetalk.transport.api.HostAndPort;
import io.servicetalk.transport.api.ServerContext;
import io.servicetalk.transport.api.ServerSslConfig;
import io.servicetalk.transport.api.ServerSslConfigBuilder;
import io.servicetalk.transport.netty.internal.StacklessClosedChannelException;

import org.junit.jupiter.api.Test;

import java.net.InetSocketAddress;
import javax.net.ssl.SSLHandshakeException;

import static io.servicetalk.grpc.netty.ExecutionStrategyTestServices.DEFAULT_STRATEGY_ASYNC_SERVICE;
import static io.servicetalk.test.resources.DefaultTestCerts.serverPemHostname;
import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress;
import static io.servicetalk.transport.netty.internal.AddressUtils.serverHostAndPort;
import static java.net.InetAddress.getLoopbackAddress;
import static java.util.Collections.singletonMap;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.jupiter.api.Assertions.assertThrows;

class GrpcSslAndNonSslConnectionsTest {

private static final TesterProto.TestRequest REQUEST = TesterProto.TestRequest.newBuilder().setName("test").build();

private ServerContext nonSecureGrpcServer() throws Exception {
return GrpcServers.forAddress(localAddress(0))
.listenAndAwait(serviceFactory());
}

private ServerContext secureGrpcServer()
throws Exception {
return GrpcServers.forAddress(localAddress(0))
.sslConfig(
trustedServerConfig()
)
.listenAndAwait(serviceFactory());
}

private GrpcClientBuilder<HostAndPort, InetSocketAddress> secureGrpcClient(
final ServerContext serverContext, final ClientSslConfigBuilder sslConfigBuilder) {
return GrpcClients.forAddress(serverHostAndPort(serverContext)).sslConfig(sslConfigBuilder.build());
}

private BlockingTesterClient nonSecureGrpcClient(ServerContext serverContext) {
return GrpcClients.forAddress(serverHostAndPort(serverContext))
.buildBlocking(clientFactory());
}

private TesterProto.Tester.ClientFactory clientFactory() {
return new TesterProto.Tester.ClientFactory();
}

private TesterProto.Tester.ServiceFactory serviceFactory() {
return new TesterProto.Tester.ServiceFactory.Builder()
.test(DEFAULT_STRATEGY_ASYNC_SERVICE)
.build();
}

private static ServerSslConfig untrustedServerConfig() {
// Need a key that won't be trusted by the client, just use the client's key.
return new ServerSslConfigBuilder(DefaultTestCerts::loadClientPem, DefaultTestCerts::loadClientKey).build();
}

private ServerSslConfig trustedServerConfig() {
return new ServerSslConfigBuilder(DefaultTestCerts::loadServerPem, DefaultTestCerts::loadServerKey).build();
}

@Test
void connectingToSecureServerWithSecureClient() throws Exception {
try (ServerContext serverContext = secureGrpcServer();
BlockingTesterClient client = secureGrpcClient(serverContext,
new ClientSslConfigBuilder(DefaultTestCerts::loadServerCAPem)
.peerHost(serverPemHostname()))
.buildBlocking(clientFactory())) {
final TesterProto.TestResponse response = client.test(REQUEST);
assertThat(response, is(notNullValue()));
assertThat(response.getMessage(), is(notNullValue()));
}
}

@Test
void secureClientToNonSecureServerClosesConnection() throws Exception {
try (ServerContext serverContext = nonSecureGrpcServer();
BlockingTesterClient client = secureGrpcClient(serverContext,
new ClientSslConfigBuilder(DefaultTestCerts::loadServerCAPem)
.peerHost(serverPemHostname()))
.buildBlocking(clientFactory())) {
GrpcStatusException e = assertThrows(GrpcStatusException.class, () -> client.test(REQUEST));
assertThat(e.getCause(), instanceOf(SSLHandshakeException.class));
}
}

@Test
void nonSecureClientToSecureServerClosesConnection() throws Exception {
try (ServerContext serverContext = secureGrpcServer();
BlockingTesterClient client = nonSecureGrpcClient(serverContext)) {
GrpcStatusException e = assertThrows(GrpcStatusException.class, () -> client.test(REQUEST));
assertThat(e.getCause(), instanceOf(StacklessClosedChannelException.class));
}
}

@Test
void secureClientToSecureServerWithoutPeerHostSucceeds() throws Exception {
try (ServerContext serverContext = secureGrpcServer();
BlockingTesterClient client = secureGrpcClient(serverContext,
new ClientSslConfigBuilder(DefaultTestCerts::loadServerCAPem)
.peerHost(null)
// if verification is not disabled, identity check fails against the undefined address
.hostnameVerificationAlgorithm(""))
.inferPeerHost(false)
.buildBlocking(clientFactory())) {
final TesterProto.TestResponse response = client.test(REQUEST);
assertThat(response, is(notNullValue()));
assertThat(response.getMessage(), is(notNullValue()));
}
}

@Test
void noSniClientDefaultServerFallbackSuccess() throws Exception {
try (ServerContext serverContext = GrpcServers.forAddress(localAddress(0))
.sslConfig(
trustedServerConfig(),
singletonMap(getLoopbackAddress().getHostName(), untrustedServerConfig())
)
.listenAndAwait(serviceFactory());
BlockingTesterClient client = GrpcClients.forAddress(
getLoopbackAddress().getHostName(), serverHostAndPort(serverContext).port())
.sslConfig(new ClientSslConfigBuilder(DefaultTestCerts::loadServerCAPem)
.peerHost(serverPemHostname()).build())
.inferSniHostname(false)
.buildBlocking(clientFactory());
) {
final TesterProto.TestResponse response = client.test(REQUEST);
assertThat(response, is(notNullValue()));
assertThat(response.getMessage(), is(notNullValue()));
}
}

@Test
void noSniClientDefaultServerFallbackFailExpected() throws Exception {
try (ServerContext serverContext = GrpcServers.forAddress(localAddress(0))
.sslConfig(
untrustedServerConfig(),
singletonMap(getLoopbackAddress().getHostName(), trustedServerConfig())
)
.listenAndAwait(serviceFactory());
BlockingTesterClient client = GrpcClients.forAddress(
getLoopbackAddress().getHostName(), serverHostAndPort(serverContext).port())
.sslConfig(new ClientSslConfigBuilder(DefaultTestCerts::loadServerCAPem)
.peerHost(serverPemHostname()).build())
.inferSniHostname(false)
.buildBlocking(clientFactory());
) {
GrpcStatusException e = assertThrows(GrpcStatusException.class, () -> client.test(REQUEST));
assertThat(e.getCause(), instanceOf(SSLHandshakeException.class));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ void noSniClientDefaultServerFallbackSuccess(List<HttpProtocol> protocols, boole
try (ServerContext serverContext = HttpServers.forAddress(localAddress(0))
.protocols(protocolConfigs(protocols))
.sslConfig(trustedServerConfig(alpnIds(protocols, useALPN)),
singletonMap("localhost", untrustedServerConfig()))
singletonMap(getLoopbackAddress().getHostName(), untrustedServerConfig()))
.listenBlockingAndAwait(newSslVerifyService());
BlockingHttpClient client = HttpClients.forSingleAddress(
getLoopbackAddress().getHostName(),
Expand Down

0 comments on commit 21cca84

Please sign in to comment.