Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include server name indication (SNI) in outgoing TLS connections. #409

Merged
merged 5 commits into from
May 16, 2019
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright (C) 2013-2018 Expedia Inc.
Copyright (C) 2013-2019 Expedia Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand All @@ -23,6 +23,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

import static com.google.common.base.Objects.firstNonNull;
Expand All @@ -47,6 +48,8 @@ public class TlsSettings {
private final char[] trustStorePassword;
private final List<String> protocols;
private final List<String> cipherSuites;
private final boolean sendSni;
private final Optional<String> sniHost;

private TlsSettings(Builder builder) {
this.trustAllCerts = requireNonNull(builder.trustAllCerts);
Expand All @@ -56,6 +59,8 @@ private TlsSettings(Builder builder) {
this.trustStorePassword = toCharArray(builder.trustStorePassword);
this.protocols = ImmutableList.copyOf(builder.protocols);
this.cipherSuites = ImmutableList.copyOf(builder.cipherSuites);
this.sendSni = builder.sendSni;
this.sniHost = Optional.ofNullable(builder.sniHost);
}

private char[] toCharArray(String password) {
Expand Down Expand Up @@ -94,6 +99,18 @@ public List<String> cipherSuites() {
return this.cipherSuites;
}

public boolean sendSni() {
return sendSni;
}

public Optional<String> sniHost() {
return sniHost;
}

public String getSniHost() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary, or used for anything? Consider removing if not.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is used for the serialization to JSON. I think it was implemented the same way in HealthCheckConfig for the URI(). We could also implement a different serialization method for Optionals or remove the optional for these JSON-serializable objects and use nullability instead, but I decided to follow conventions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. Let's put a comment.

return sniHost.orElse(null);
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
Expand All @@ -109,7 +126,9 @@ public boolean equals(Object obj) {
&& Objects.equals(this.trustStorePath, other.trustStorePath)
&& Arrays.equals(this.trustStorePassword, other.trustStorePassword)
&& Objects.equals(this.protocols, other.protocols)
&& Objects.equals(this.cipherSuites, other.cipherSuites);
&& Objects.equals(this.cipherSuites, other.cipherSuites)
&& Objects.equals(this.sniHost, other.sniHost)
&& Objects.equals(this.sendSni, other.sendSni);
}

@Override
Expand All @@ -122,13 +141,15 @@ public String toString() {
.add("trustStorePassword", this.trustStorePassword)
.add("protocols", this.protocols)
.add("cipherSuites", this.cipherSuites)
.add("sendSni", this.sendSni)
.add("sniHost", this.getSniHost())
.toString();
}

@Override
public int hashCode() {
return Objects.hash(trustAllCerts, sslProvider, additionalCerts,
trustStorePath, Arrays.hashCode(trustStorePassword), protocols, cipherSuites);
trustStorePath, Arrays.hashCode(trustStorePassword), protocols, cipherSuites, sendSni, this.getSniHost());
}


Expand All @@ -144,6 +165,8 @@ public static final class Builder {
private String trustStorePassword = System.getProperty("javax.net.ssl.trustStorePassword");
private List<String> protocols = Collections.emptyList();
private List<String> cipherSuites = Collections.emptyList();
private boolean sendSni = true;
private String sniHost;

/**
* Skips origin authentication.
Expand Down Expand Up @@ -218,6 +241,17 @@ public Builder cipherSuites(List<String> cipherSuites) {
return this;
}

public Builder sendSni(boolean sendSni) {
this.sendSni = sendSni;
return this;
}

public Builder sniHost(String sniHost) {
this.sniHost = sniHost;
return this;
}


public TlsSettings build() {
if (!trustAllCerts && trustStorePassword == null) {
throw new IllegalArgumentException("trustStorePassword must be supplied when remote peer authentication is enabled.");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright (C) 2013-2018 Expedia Inc.
Copyright (C) 2013-2019 Expedia Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -45,6 +45,7 @@

class FlowControllingHttpContentProducer {
private static final Logger LOGGER = LoggerFactory.getLogger(FlowControllingHttpContentProducer.class);
private static final int MAX_DEPTH = 1;

private final StateMachine<ProducerState> stateMachine;
private final String loggingPrefix;
Expand All @@ -65,7 +66,6 @@ class FlowControllingHttpContentProducer {
final AtomicLong queueDepthChunks = new AtomicLong(0);

private final Origin origin;
private final int MAX_DEPTH = 1;

private volatile Subscriber<? super ByteBuf> contentSubscriber;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright (C) 2013-2018 Expedia Inc.
Copyright (C) 2013-2019 Expedia Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -27,9 +27,11 @@
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpContentDecompressor;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslHandler;
import io.netty.util.AttributeKey;
import rx.Observable;

import java.util.Optional;
import java.util.concurrent.TimeUnit;

import static com.google.common.base.Objects.toStringHelper;
Expand All @@ -40,6 +42,7 @@
*/
public class NettyConnection implements Connection, TimeToFirstByteListener {
private static final AttributeKey<Object> CLOSED_BY_STYX = AttributeKey.newInstance("CLOSED_BY_STYX");
private static final int IGNORED_PORT_NUMBER = -1;

private final Origin origin;
private final Channel channel;
Expand All @@ -52,26 +55,33 @@ public class NettyConnection implements Connection, TimeToFirstByteListener {
/**
* Constructs an instance with an arbitrary UUID.
*
* @param origin the origin connected to
* @param channel the netty channel used
* @param origin the origin connected to
* @param channel the netty channel used
* @param requestOperationFactory used to create operation objects that send http requests via this connection
* @param httpConfig configuration settings for the <b>origin</b>
* @param sslContext TLS context in case of secure connections
* @param sendSni include the servername extension (server name indicator) in the TLS handshake
* @param sniHost hostname override for the server name indicator
*/
public NettyConnection(Origin origin, Channel channel, HttpRequestOperationFactory requestOperationFactory, HttpConfig httpConfig, SslContext sslContext) {
public NettyConnection(Origin origin, Channel channel, HttpRequestOperationFactory requestOperationFactory,
HttpConfig httpConfig, SslContext sslContext, boolean sendSni, Optional<String> sniHost) {
this.origin = requireNonNull(origin);
this.channel = requireNonNull(channel);
this.requestOperationFactory = requestOperationFactory;
this.channel.pipeline().addLast(new TimeToFirstByteHandler(this));
this.channel.closeFuture().addListener(future ->
listeners.announce().connectionClosed(NettyConnection.this));

addChannelHandlers(channel, httpConfig, sslContext);
addChannelHandlers(channel, httpConfig, sslContext, sendSni, sniHost.orElse(origin.host()));
}

private static void addChannelHandlers(Channel channel, HttpConfig httpConfig, SslContext sslContext) {
private static void addChannelHandlers(Channel channel, HttpConfig httpConfig, SslContext sslContext, boolean sendSni, String targetHost) {
ChannelPipeline pipeline = channel.pipeline();

if (sslContext != null) {
pipeline.addLast("ssl", sslContext.newHandler(channel.alloc()));
SslHandler sslHandler = sendSni
? sslContext.newHandler(channel.alloc(), targetHost, IGNORED_PORT_NUMBER)
: sslContext.newHandler(channel.alloc());
pipeline.addLast("ssl", sslHandler);
}

pipeline.addLast("http-codec", new HttpClientCodec(httpConfig.maxInitialLineLength(), httpConfig.maxHeadersSize(), httpConfig.maxChunkSize()));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright (C) 2013-2018 Expedia Inc.
Copyright (C) 2013-2019 Expedia Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -35,6 +35,7 @@
import io.netty.handler.ssl.SslContext;
import reactor.core.publisher.Mono;

import java.util.Optional;
import java.util.concurrent.CompletableFuture;

import static com.hotels.styx.client.HttpConfig.defaultHttpConfig;
Expand All @@ -51,6 +52,8 @@
public class NettyConnectionFactory implements Connection.Factory {
private final HttpConfig httpConfig;
private final SslContext sslContext;
private final boolean sendSni;
private final Optional<String> sniHost;
private final HttpRequestOperationFactory httpRequestOperationFactory;
private Bootstrap bootstrap;
private EventLoopGroup eventLoopGroup;
Expand All @@ -66,6 +69,8 @@ private NettyConnectionFactory(Builder builder) {
this.sslContext = builder.tlsSettings == null ? null : SslContextFactory.get(builder.tlsSettings);
this.clientSocketChannelClass = eventLoopGroupFactory.clientSocketChannelClass();
this.httpRequestOperationFactory = requireNonNull(builder.httpRequestOperationFactory);
this.sendSni = builder.tlsSettings != null && builder.tlsSettings.sendSni();
this.sniHost = builder.tlsSettings != null ? builder.tlsSettings.sniHost() : Optional.empty();
}

@Override
Expand All @@ -79,7 +84,8 @@ public Mono<Connection> createConnection(Origin origin, ConnectionSettings conne

channelFuture.addListener(future -> {
if (future.isSuccess()) {
sink.success(new NettyConnection(origin, channelFuture.channel(), httpRequestOperationFactory, httpConfig, sslContext));
sink.success(new NettyConnection(origin, channelFuture.channel(), httpRequestOperationFactory,
httpConfig, sslContext, sendSni, sniHost));
} else {
sink.error(new OriginUnreachableException(origin, future.cause()));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright (C) 2013-2018 Expedia Inc.
Copyright (C) 2013-2019 Expedia Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand All @@ -15,8 +15,8 @@
*/
package com.hotels.styx.client.netty.connectionpool;

import com.hotels.styx.client.Connection;
import com.hotels.styx.api.extension.Origin;
import com.hotels.styx.client.Connection;
import com.hotels.styx.client.HttpConfig;
import io.netty.channel.Channel;
import io.netty.channel.embedded.EmbeddedChannel;
Expand Down Expand Up @@ -66,7 +66,7 @@ public void notifiesListenersWhenConnectionIsClosed() throws Exception {
}

private Connection createConnection() {
return new NettyConnection(origin, channel, null, httpConfig, null);
return new NettyConnection(origin, channel, null, httpConfig, null, false, Optional.empty());
}

static class EventCapturingListener implements Connection.Listener {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright (C) 2013-2018 Expedia Inc.
Copyright (C) 2013-2019 Expedia Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -54,6 +54,12 @@ public interface TlsSettingsMixin {
@JsonProperty("cipherSuites")
List<String> cipherSuites();

@JsonProperty("sendSni")
boolean sendSni();

@JsonProperty("sniHost")
String getSniHost();

/**
* The builder for SSL settings.
*/
Expand Down Expand Up @@ -82,6 +88,12 @@ interface Builder {

@JsonProperty("cipherSuites")
Builder cipherSuites(List<String> cipherSuites);

@JsonProperty("sendSni")
Builder sendSni(boolean sendSni);

@JsonProperty("sniHost")
Builder sniHost(String sniHost);
}
}

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright (C) 2013-2018 Expedia Inc.
Copyright (C) 2013-2019 Expedia Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -27,6 +27,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

import static com.hotels.styx.api.extension.service.Certificate.certificate;
import static com.hotels.styx.infrastructure.configuration.json.ObjectMappers.addStyxMixins;
Expand Down Expand Up @@ -54,6 +55,8 @@ public void serialisesAllAttributes() throws Exception {
.trustStorePassword("bar")
.protocols(ImmutableList.of("TLSv1.2"))
.cipherSuites(ImmutableList.of("TLS_RSA_WITH_AES_128_CBC_SHA"))
.sendSni(false)
.sniHost("some.sni.host")
.build();

String result = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(tlsSettings);
Expand All @@ -67,6 +70,10 @@ public void serialisesAllAttributes() throws Exception {
assertThat(result, containsString("\"trustStorePassword\" : \"bar"));

assertThat(result, containsString("TLS_RSA_WITH_AES_128_CBC_SHA"));

assertThat(result, containsString("\"sendSni\" : " + tlsSettings.sendSni()));
assertThat(result, containsString("\"sniHost\" : \"" + tlsSettings.sniHost().orElse("") + "\""));

}

@Test
Expand All @@ -81,6 +88,8 @@ public void appliesDefaultTruststoreSettings() throws Exception {
assertThat(tlsSettings.trustStorePassword(), is("".toCharArray()));
assertThat(tlsSettings.protocols(), is(Collections.emptyList()));
assertThat(tlsSettings.cipherSuites(), is(Collections.emptyList()));
assertThat(tlsSettings.sendSni(), is(true));
assertThat(tlsSettings.sniHost(), is(Optional.empty()));
}

@Test(expectedExceptions = IllegalArgumentException.class)
Expand Down Expand Up @@ -215,9 +224,13 @@ public void toStringPrintsAttributeNames() throws Exception {

@Test
public void isImmutable() throws Exception {
List<String> protocols = new ArrayList<String>() {{ add("TLSv1"); }};
List<String> cipherSuites = new ArrayList<String>() {{ add("x"); }};
Certificate[] certificates = new Certificate[] { certificate("x", "x") };
List<String> protocols = new ArrayList<String>() {{
add("TLSv1");
}};
List<String> cipherSuites = new ArrayList<String>() {{
add("x");
}};
Certificate[] certificates = new Certificate[]{certificate("x", "x")};

TlsSettings tlsSettings = new TlsSettings.Builder()
.additionalCerts(certificates)
Expand Down
2 changes: 1 addition & 1 deletion distribution/conf/styx-env.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#
# Copyright (C) 2013-2018 Expedia Inc.
# Copyright (C) 2013-2019 Expedia Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down
6 changes: 5 additions & 1 deletion docs/user-guide/configure-tls.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,11 @@ be specified as separate backends.
When absent, enables all default protocols for the `sslProvider`.
Possible protocol names are: `TLS`, `TLSv1`, `TLSv1.1`, and `TLSv1.2`.

Attributes that accept lists can be defined with the following format: ['ITEM1', 'ITEM2']
- *sendSni* - Send the Origin server hostname in the TLS handshake as per the SNI extension (https://tools.ietf.org/html/rfc6066). This feature is enabled by default.

- *sniHost* - Override the hostname of the Origin server that will be sent in the SNI (server_name) extension. When this value is not set, the hostname will be the configured in `Origins\Host`.

Attributes that accept lists can be defined with the following format: ['ITEM1', 'ITEM2']

## Troubleshooting TLS Configuration

Expand Down