You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@zookeeper.apache.org by an...@apache.org on 2019/12/16 12:20:55 UTC

[zookeeper] branch branch-3.5 updated: ZOOKEEPER-3388: Allow client port to support plaintext and encrypted connections simultaneously (3.5)

This is an automated email from the ASF dual-hosted git repository.

andor pushed a commit to branch branch-3.5
in repository https://gitbox.apache.org/repos/asf/zookeeper.git


The following commit(s) were added to refs/heads/branch-3.5 by this push:
     new f21f951  ZOOKEEPER-3388: Allow client port to support plaintext and encrypted connections simultaneously (3.5)
f21f951 is described below

commit f21f951fe063bd30914db9a65721320bb8554fad
Author: Andor Molnar <an...@apache.org>
AuthorDate: Mon Dec 16 13:20:48 2019 +0100

    ZOOKEEPER-3388: Allow client port to support plaintext and encrypted connections simultaneously (3.5)
    
    Backport of #944
    
    Author: Andor Molnar <an...@apache.org>
    Author: Brian Nixon <ni...@fb.com>
    
    Reviewers: andor@apache.org
    
    Closes #1174 from anmolnar/ZOOKEEPER-3388_35 and squashes the following commits:
    
    b51f843a2 [Andor Molnar] ZOOKEEPER-3388. Added new tag to documentation
    744c938dd [Brian Nixon] ZOOKEEPER-3388: Allow client port to support plaintext and encrypted connections simultaneously
---
 .../src/main/resources/markdown/zookeeperAdmin.md  |   9 +-
 .../zookeeper/common/SSLContextAndOptions.java     |  23 ++-
 .../java/org/apache/zookeeper/common/X509Util.java |  18 +-
 .../zookeeper/server/NettyServerCnxnFactory.java   | 215 ++++++++++++++++-----
 .../zookeeper/server/quorum/QuorumPeerConfig.java  |   2 +-
 .../org/apache/zookeeper/test/ClientSSLTest.java   |  35 +++-
 6 files changed, 243 insertions(+), 59 deletions(-)

diff --git a/zookeeper-docs/src/main/resources/markdown/zookeeperAdmin.md b/zookeeper-docs/src/main/resources/markdown/zookeeperAdmin.md
index 77a1c62..3559527 100644
--- a/zookeeper-docs/src/main/resources/markdown/zookeeperAdmin.md
+++ b/zookeeper-docs/src/main/resources/markdown/zookeeperAdmin.md
@@ -1032,7 +1032,14 @@ encryption/authentication/authorization performed by the service.
     (Java system properties: **zookeeper.ssl.handshakeDetectionTimeoutMillis** and **zookeeper.ssl.quorum.handshakeDetectionTimeoutMillis**)
     **New in 3.5.5:**
     TBD
-        
+
+* *client.portUnification*:
+    (Java system properties: **zookeeper.client.portUnification**)
+    **New in 3.5.7**
+    Specifies that the client port should accept SSL connections
+    (using the same configuration as the secure client port).
+    Default: false
+
 
 <a name="Experimental+Options%2FFeatures"></a>
 
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/common/SSLContextAndOptions.java b/zookeeper-server/src/main/java/org/apache/zookeeper/common/SSLContextAndOptions.java
index 2d60ab8..232ab67 100644
--- a/zookeeper-server/src/main/java/org/apache/zookeeper/common/SSLContextAndOptions.java
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/common/SSLContextAndOptions.java
@@ -23,10 +23,16 @@ import java.net.Socket;
 import java.util.Arrays;
 
 import javax.net.ssl.SSLContext;
+import java.util.Collections;
+import java.util.List;
+
 import javax.net.ssl.SSLParameters;
 import javax.net.ssl.SSLServerSocket;
 import javax.net.ssl.SSLSocket;
 
+import io.netty.handler.ssl.IdentityCipherSuiteFilter;
+import io.netty.handler.ssl.JdkSslContext;
+import io.netty.handler.ssl.SslContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -45,6 +51,7 @@ public class SSLContextAndOptions {
     private final X509Util x509Util;
     private final String[] enabledProtocols;
     private final String[] cipherSuites;
+    private final List<String> cipherSuitesAsList;
     private final X509Util.ClientAuth clientAuth;
     private final SSLContext sslContext;
     private final int handshakeDetectionTimeoutMillis;
@@ -60,7 +67,9 @@ public class SSLContextAndOptions {
         this.x509Util = requireNonNull(x509Util);
         this.sslContext = requireNonNull(sslContext);
         this.enabledProtocols = getEnabledProtocols(requireNonNull(config), sslContext);
-        this.cipherSuites = getCipherSuites(config);
+        String[] ciphers = getCipherSuites(config);
+        this.cipherSuites = ciphers;
+        this.cipherSuitesAsList = Collections.unmodifiableList(Arrays.asList(ciphers));
         this.clientAuth = getClientAuth(config);
         this.handshakeDetectionTimeoutMillis = getHandshakeDetectionTimeoutMillis(config);
     }
@@ -97,6 +106,18 @@ public class SSLContextAndOptions {
         return configureSSLServerSocket(sslServerSocket);
     }
 
+    public SslContext createNettyJdkSslContext(SSLContext sslContext, boolean isClientSocket) {
+        return new JdkSslContext(
+                sslContext,
+                isClientSocket,
+                cipherSuitesAsList,
+                IdentityCipherSuiteFilter.INSTANCE,
+                null,
+                isClientSocket ? X509Util.ClientAuth.NONE.toNettyClientAuth() : clientAuth.toNettyClientAuth(),
+                enabledProtocols,
+                false);
+    }
+
     public int getHandshakeDetectionTimeoutMillis() {
         return handshakeDetectionTimeoutMillis;
     }
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/common/X509Util.java b/zookeeper-server/src/main/java/org/apache/zookeeper/common/X509Util.java
index 2e45ef2..6e06d84 100644
--- a/zookeeper-server/src/main/java/org/apache/zookeeper/common/X509Util.java
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/common/X509Util.java
@@ -82,7 +82,7 @@ public abstract class X509Util implements Closeable, AutoCloseable {
         }
     }
 
-    static final String DEFAULT_PROTOCOL = "TLSv1.2";
+    public static final String DEFAULT_PROTOCOL = "TLSv1.2";
     private static String[] getGCMCiphers() {
         return new String[] {
             "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
@@ -131,9 +131,15 @@ public abstract class X509Util implements Closeable, AutoCloseable {
      * If the config property is not set, the default value is NEED.
      */
     public enum ClientAuth {
-        NONE,
-        WANT,
-        NEED;
+        NONE(io.netty.handler.ssl.ClientAuth.NONE),
+        WANT(io.netty.handler.ssl.ClientAuth.OPTIONAL),
+        NEED(io.netty.handler.ssl.ClientAuth.REQUIRE);
+
+        private final io.netty.handler.ssl.ClientAuth nettyAuth;
+
+        ClientAuth(io.netty.handler.ssl.ClientAuth nettyAuth) {
+            this.nettyAuth = nettyAuth;
+        }
 
         /**
          * Converts a property value to a ClientAuth enum. If the input string is empty or null, returns
@@ -148,6 +154,10 @@ public abstract class X509Util implements Closeable, AutoCloseable {
             }
             return ClientAuth.valueOf(prop.toUpperCase());
         }
+
+        public io.netty.handler.ssl.ClientAuth toNettyClientAuth() {
+            return nettyAuth;
+        }
     }
 
     private String sslProtocolProperty = getConfigPrefix() + "protocol";
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/server/NettyServerCnxnFactory.java b/zookeeper-server/src/main/java/org/apache/zookeeper/server/NettyServerCnxnFactory.java
index 48f2157..3b334ee 100644
--- a/zookeeper-server/src/main/java/org/apache/zookeeper/server/NettyServerCnxnFactory.java
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/server/NettyServerCnxnFactory.java
@@ -25,6 +25,7 @@ import java.security.KeyManagementException;
 import java.security.NoSuchAlgorithmException;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -32,6 +33,7 @@ import java.util.concurrent.atomic.AtomicReference;
 
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLException;
 import javax.net.ssl.SSLPeerUnverifiedException;
 import javax.net.ssl.SSLSession;
 import javax.net.ssl.X509KeyManager;
@@ -43,6 +45,7 @@ import io.netty.buffer.ByteBufAllocator;
 import io.netty.channel.Channel;
 import io.netty.channel.ChannelDuplexHandler;
 import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandler;
 import io.netty.channel.ChannelHandler.Sharable;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelInitializer;
@@ -54,6 +57,9 @@ import io.netty.channel.group.ChannelGroup;
 import io.netty.channel.group.ChannelGroupFuture;
 import io.netty.channel.group.DefaultChannelGroup;
 import io.netty.channel.socket.SocketChannel;
+import io.netty.handler.ssl.OpenSsl;
+import io.netty.handler.ssl.OptionalSslHandler;
+import io.netty.handler.ssl.SslContext;
 import io.netty.handler.ssl.SslHandler;
 import io.netty.util.AttributeKey;
 import io.netty.util.ReferenceCountUtil;
@@ -63,16 +69,32 @@ import io.netty.util.concurrent.GenericFutureListener;
 import org.apache.zookeeper.KeeperException;
 import org.apache.zookeeper.common.ClientX509Util;
 import org.apache.zookeeper.common.NettyUtils;
+import org.apache.zookeeper.common.SSLContextAndOptions;
 import org.apache.zookeeper.common.X509Exception;
 import org.apache.zookeeper.common.X509Exception.SSLContextException;
 import org.apache.zookeeper.server.auth.ProviderRegistry;
 import org.apache.zookeeper.server.auth.X509AuthenticationProvider;
+import org.apache.zookeeper.server.quorum.QuorumPeerConfig;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 public class NettyServerCnxnFactory extends ServerCnxnFactory {
     private static final Logger LOG = LoggerFactory.getLogger(NettyServerCnxnFactory.class);
 
+    /**
+     * Allow client-server sockets to accept both SSL and plaintext connections
+     */
+    public static final String PORT_UNIFICATION_KEY = "zookeeper.client.portUnification";
+    private final boolean shouldUsePortUnification;
+
+    /**
+     * The first byte in TLS protocol is the content type of the subsequent record.
+     * Handshakes use value 22 (0x16) so the first byte offered on any TCP connection
+     * attempting to establish a TLS connection will be this value.
+     * https://tools.ietf.org/html/rfc8446#page-79
+     */
+    private static final byte TLS_HANDSHAKE_RECORD_TYPE = 0x16;
+
     private final ServerBootstrap bootstrap;
     private Channel parentChannel;
     private final ChannelGroup allChannels =
@@ -91,6 +113,66 @@ public class NettyServerCnxnFactory extends ServerCnxnFactory {
             new AtomicReference<>(null);
 
     /**
+     * A handler that detects whether the client would like to use
+     * TLS or not and responds in kind. The first bytes are examined
+     * for the static TLS headers to make the determination and
+     * placed back in the stream with the correct ChannelHandler
+     * instantiated.
+     */
+    class DualModeSslHandler extends OptionalSslHandler {
+        DualModeSslHandler(SslContext sslContext) {
+            super(sslContext);
+        }
+
+        @Override
+        protected void decode(ChannelHandlerContext context, ByteBuf in, List<Object> out) throws Exception {
+            if (in.readableBytes() >= 5) {
+                super.decode(context, in, out);
+            } else if (in.readableBytes() > 0) {
+                // It requires 5 bytes to detect a proper ssl connection. In the
+                // case that the server receives fewer, check if we can fail to plaintext.
+                // This will occur when for any four letter work commands.
+                if (TLS_HANDSHAKE_RECORD_TYPE != in.getByte(0)) {
+                    LOG.debug("first byte {} does not match TLS handshake, failing to plaintext", in.getByte(0));
+                    handleNonSsl(context);
+                }
+            }
+        }
+
+        /**
+         * pulled directly from OptionalSslHandler to allow for access
+         * @param context
+         */
+        private void handleNonSsl(ChannelHandlerContext context) {
+            ChannelHandler handler = this.newNonSslHandler(context);
+            if (handler != null) {
+                context.pipeline().replace(this, this.newNonSslHandlerName(), handler);
+            } else {
+                context.pipeline().remove(this);
+            }
+        }
+
+        @Override
+        protected SslHandler newSslHandler(ChannelHandlerContext context, SslContext sslContext) {
+            NettyServerCnxn cnxn = Objects.requireNonNull(context.channel().attr(CONNECTION_ATTRIBUTE).get());
+            LOG.debug("creating ssl handler for session {}", cnxn.getSessionId());
+            SslHandler handler = super.newSslHandler(context, sslContext);
+            Future<Channel> handshakeFuture = handler.handshakeFuture();
+            handshakeFuture.addListener(new CertificateVerifier(handler, cnxn));
+            return handler;
+        }
+
+        @Override
+        protected ChannelHandler newNonSslHandler(ChannelHandlerContext context) {
+            NettyServerCnxn cnxn = Objects.requireNonNull(context.channel().attr(CONNECTION_ATTRIBUTE).get());
+            LOG.debug("creating plaintext handler for session {}", cnxn.getSessionId());
+            allChannels.add(context.channel());
+            addCnxn(cnxn);
+            return super.newNonSslHandler(context);
+        }
+    }
+
+    /**
      * This is an inner class since we need to extend ChannelDuplexHandler, but
      * NettyServerCnxnFactory already extends ServerCnxnFactory. By making it inner
      * this class gets access to the member variables and methods.
@@ -112,7 +194,7 @@ public class NettyServerCnxnFactory extends ServerCnxnFactory {
                 SslHandler sslHandler = ctx.pipeline().get(SslHandler.class);
                 Future<Channel> handshakeFuture = sslHandler.handshakeFuture();
                 handshakeFuture.addListener(new CertificateVerifier(sslHandler, cnxn));
-            } else {
+            } else if (!shouldUsePortUnification) {
                 allChannels.add(ctx.channel());
                 addCnxn(cnxn);
             }
@@ -206,28 +288,51 @@ public class NettyServerCnxnFactory extends ServerCnxnFactory {
             }
             super.write(ctx, msg, promise);
         }
+    }
 
-        private final class CertificateVerifier implements GenericFutureListener<Future<Channel>> {
-            private final SslHandler sslHandler;
-            private final NettyServerCnxn cnxn;
+    final class CertificateVerifier implements GenericFutureListener<Future<Channel>> {
+        private final SslHandler sslHandler;
+        private final NettyServerCnxn cnxn;
 
-            CertificateVerifier(SslHandler sslHandler, NettyServerCnxn cnxn) {
-                this.sslHandler = sslHandler;
-                this.cnxn = cnxn;
-            }
+        CertificateVerifier(SslHandler sslHandler, NettyServerCnxn cnxn) {
+            this.sslHandler = sslHandler;
+            this.cnxn = cnxn;
+        }
 
-            /**
-             * Only allow the connection to stay open if certificate passes auth
-             */
-            public void operationComplete(Future<Channel> future) throws SSLPeerUnverifiedException {
-                if (future.isSuccess()) {
-                    if (LOG.isDebugEnabled()) {
-                        LOG.debug("Successful handshake with session 0x{}",
-                                Long.toHexString(cnxn.getSessionId()));
-                    }
-                    SSLEngine eng = sslHandler.engine();
+        /**
+         * Only allow the connection to stay open if certificate passes auth
+         */
+        public void operationComplete(Future<Channel> future) {
+            if (future.isSuccess()) {
+                if (LOG.isDebugEnabled()) {
+                    LOG.debug("Successful handshake with session 0x{}",
+                            Long.toHexString(cnxn.getSessionId()));
+                }
+                SSLEngine eng = sslHandler.engine();
+                // Don't try to verify certificate if we didn't ask client to present one
+                if (eng.getNeedClientAuth() || eng.getWantClientAuth()) {
                     SSLSession session = eng.getSession();
-                    cnxn.setClientCertificateChain(session.getPeerCertificates());
+                    try {
+                        cnxn.setClientCertificateChain(session.getPeerCertificates());
+                    } catch (SSLPeerUnverifiedException e) {
+                        if (eng.getNeedClientAuth()) {
+                            // Certificate was requested but not present
+                            LOG.error("Error getting peer certificates", e);
+                            cnxn.close();
+                            return;
+                        } else {
+                            // Certificate was requested but was optional
+                            // TODO: what auth info should we set on the connection?
+                            final Channel futureChannel = future.getNow();
+                            allChannels.add(Objects.requireNonNull(futureChannel));
+                            addCnxn(cnxn);
+                            return;
+                        }
+                    } catch (Exception e) {
+                        LOG.error("Error getting peer certificates", e);
+                        cnxn.close();
+                        return;
+                    }
 
                     String authProviderProp
                             = System.getProperty(x509Util.getSslAuthProviderProperty(), "x509");
@@ -237,7 +342,7 @@ public class NettyServerCnxnFactory extends ServerCnxnFactory {
                                     ProviderRegistry.getProvider(authProviderProp);
 
                     if (authProvider == null) {
-                        LOG.error("Auth provider not found: {}", authProviderProp);
+                        LOG.error("X509 Auth provider not found: {}", authProviderProp);
                         cnxn.close();
                         return;
                     }
@@ -249,15 +354,15 @@ public class NettyServerCnxnFactory extends ServerCnxnFactory {
                         cnxn.close();
                         return;
                     }
-
-                    final Channel futureChannel = future.getNow();
-                    allChannels.add(Objects.requireNonNull(futureChannel));
-                    addCnxn(cnxn);
-                } else {
-                    LOG.error("Unsuccessful handshake with session 0x{}",
-                            Long.toHexString(cnxn.getSessionId()));
-                    cnxn.close();
                 }
+
+                final Channel futureChannel = future.getNow();
+                allChannels.add(Objects.requireNonNull(futureChannel));
+                addCnxn(cnxn);
+            } else {
+                LOG.error("Unsuccessful handshake with session 0x{}",
+                        Long.toHexString(cnxn.getSessionId()));
+                cnxn.close();
             }
         }
     }
@@ -278,6 +383,18 @@ public class NettyServerCnxnFactory extends ServerCnxnFactory {
     NettyServerCnxnFactory() {
         x509Util = new ClientX509Util();
 
+        boolean usePortUnification = Boolean.getBoolean(PORT_UNIFICATION_KEY);
+        LOG.info("{}={}", PORT_UNIFICATION_KEY, usePortUnification);
+        if (usePortUnification) {
+            try {
+                QuorumPeerConfig.configureSSLAuth();
+            } catch (QuorumPeerConfig.ConfigException e) {
+                LOG.error("unable to set up SslAuthProvider, turning off client port unification", e);
+                usePortUnification = false;
+            }
+        }
+        this.shouldUsePortUnification = usePortUnification;
+
         EventLoopGroup bossGroup = NettyUtils.newNioOrEpollEventLoopGroup(
                 NettyUtils.getClientReachableLocalInetAddressCount());
         EventLoopGroup workerGroup = NettyUtils.newNioOrEpollEventLoopGroup();
@@ -294,7 +411,9 @@ public class NettyServerCnxnFactory extends ServerCnxnFactory {
                     protected void initChannel(SocketChannel ch) throws Exception {
                         ChannelPipeline pipeline = ch.pipeline();
                         if (secure) {
-                            initSSL(pipeline);
+                            initSSL(pipeline, false);
+                        } else if (shouldUsePortUnification) {
+                            initSSL(pipeline, true);
                         }
                         pipeline.addLast("servercnxnfactory", channelHandler);
                     }
@@ -303,37 +422,41 @@ public class NettyServerCnxnFactory extends ServerCnxnFactory {
         this.bootstrap.validate();
     }
 
-    private synchronized void initSSL(ChannelPipeline p)
+    private synchronized void initSSL(ChannelPipeline p, boolean supportPlaintext)
             throws X509Exception, KeyManagementException, NoSuchAlgorithmException {
         String authProviderProp = System.getProperty(x509Util.getSslAuthProviderProperty());
-        SSLContext sslContext;
+        SslContext nettySslContext;
         if (authProviderProp == null) {
-            sslContext = x509Util.getDefaultSSLContext();
+            SSLContextAndOptions sslContextAndOptions = x509Util.getDefaultSSLContextAndOptions();
+            nettySslContext = sslContextAndOptions.createNettyJdkSslContext(
+                        sslContextAndOptions.getSSLContext(), false);
         } else {
-            sslContext = SSLContext.getInstance("TLSv1");
+            SSLContext sslContext = SSLContext.getInstance(ClientX509Util.DEFAULT_PROTOCOL);
             X509AuthenticationProvider authProvider =
-                    (X509AuthenticationProvider)ProviderRegistry.getProvider(
+                    (X509AuthenticationProvider) ProviderRegistry.getProvider(
                             System.getProperty(x509Util.getSslAuthProviderProperty(), "x509"));
 
-            if (authProvider == null)
-            {
+            if (authProvider == null) {
                 LOG.error("Auth provider not found: {}", authProviderProp);
                 throw new SSLContextException(
                         "Could not create SSLContext with specified auth provider: " +
-                        authProviderProp);
+                                authProviderProp);
             }
 
-            sslContext.init(new X509KeyManager[] { authProvider.getKeyManager() },
-                            new X509TrustManager[] { authProvider.getTrustManager() },
-                            null);
+            sslContext.init(new X509KeyManager[]{authProvider.getKeyManager()},
+                    new X509TrustManager[]{authProvider.getTrustManager()},
+                    null);
+            nettySslContext = x509Util.getDefaultSSLContextAndOptions()
+                    .createNettyJdkSslContext(sslContext,false);
         }
 
-        SSLEngine sslEngine = sslContext.createSSLEngine();
-        sslEngine.setUseClientMode(false);
-        sslEngine.setNeedClientAuth(true);
-
-        p.addLast("ssl", new SslHandler(sslEngine));
-        LOG.info("SSL handler added for channel: {}", p.channel());
+        if (supportPlaintext) {
+            p.addLast("ssl", new DualModeSslHandler(nettySslContext));
+            LOG.debug("dual mode SSL handler added for channel: {}", p.channel());
+        } else {
+            p.addLast("ssl", nettySslContext.newHandler(p.channel().alloc()));
+            LOG.debug("SSL handler added for channel: {}", p.channel());
+        }
     }
 
     @Override
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/QuorumPeerConfig.java b/zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/QuorumPeerConfig.java
index e0245f7..dc5aec2 100644
--- a/zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/QuorumPeerConfig.java
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/QuorumPeerConfig.java
@@ -433,7 +433,7 @@ public class QuorumPeerConfig {
      *             If authentication scheme is configured but authentication
      *             provider is not configured.
      */
-    private void configureSSLAuth() throws ConfigException {
+    public static void configureSSLAuth() throws ConfigException {
         try (ClientX509Util clientX509Util = new ClientX509Util()) {
             String sslAuthProp = "zookeeper.authProvider." + System.getProperty(clientX509Util.getSslAuthProviderProperty(), "x509");
             if (System.getProperty(sslAuthProp) == null) {
diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/test/ClientSSLTest.java b/zookeeper-server/src/test/java/org/apache/zookeeper/test/ClientSSLTest.java
index 1504834..019ec5f 100644
--- a/zookeeper-server/src/test/java/org/apache/zookeeper/test/ClientSSLTest.java
+++ b/zookeeper-server/src/test/java/org/apache/zookeeper/test/ClientSSLTest.java
@@ -28,6 +28,7 @@ import org.apache.zookeeper.ZooDefs;
 import org.apache.zookeeper.ZooKeeper;
 import org.apache.zookeeper.client.ZKClientConfig;
 import org.apache.zookeeper.common.ClientX509Util;
+import org.apache.zookeeper.server.NettyServerCnxnFactory;
 import org.apache.zookeeper.server.ServerCnxnFactory;
 import org.apache.zookeeper.server.quorum.QuorumPeerTestBase;
 import org.junit.After;
@@ -41,6 +42,7 @@ public class ClientSSLTest extends QuorumPeerTestBase {
 
     @Before
     public void setup() {
+        System.setProperty(NettyServerCnxnFactory.PORT_UNIFICATION_KEY, Boolean.TRUE.toString());
         clientX509Util = new ClientX509Util();
         String testDataPath = System.getProperty("test.data.dir", "src/test/resources/data");
         System.setProperty(ServerCnxnFactory.ZOOKEEPER_SERVER_CNXN_FACTORY, "org.apache.zookeeper.server.NettyServerCnxnFactory");
@@ -53,7 +55,8 @@ public class ClientSSLTest extends QuorumPeerTestBase {
     }
 
     @After
-    public void teardown() throws Exception {
+    public void teardown() {
+        System.clearProperty(NettyServerCnxnFactory.PORT_UNIFICATION_KEY);
         System.clearProperty(ServerCnxnFactory.ZOOKEEPER_SERVER_CNXN_FACTORY);
         System.clearProperty(ZKClientConfig.ZOOKEEPER_CLIENT_CNXN_SOCKET);
         System.clearProperty(ZKClientConfig.SECURE_CLIENT);
@@ -65,6 +68,18 @@ public class ClientSSLTest extends QuorumPeerTestBase {
     }
 
     /**
+     * This test checks that client SSL connections work in the absence of a
+     * secure port when port unification is set up for the plaintext port.
+     *
+     * This single client port will be tested for handling both plaintext
+     * and SSL traffic.
+     */
+    @Test
+    public void testClientServerUnifiedPort() throws Exception {
+        testClientServerSSL(false);
+    }
+
+    /**
      * This test checks that client <-> server SSL works in cluster setup of ZK servers, which includes:
      * 1. setting "secureClientPort" in "zoo.cfg" file.
      * 2. setting jvm flags for serverCnxn, keystore, truststore.
@@ -75,6 +90,10 @@ public class ClientSSLTest extends QuorumPeerTestBase {
      */
     @Test
     public void testClientServerSSL() throws Exception {
+        testClientServerSSL(true);
+    }
+
+    public void testClientServerSSL(boolean useSecurePort) throws Exception {
         final int SERVER_COUNT = 3;
         final int clientPorts[] = new int[SERVER_COUNT];
         final Integer secureClientPorts[] = new Integer[SERVER_COUNT];
@@ -82,16 +101,20 @@ public class ClientSSLTest extends QuorumPeerTestBase {
         for (int i = 0; i < SERVER_COUNT; i++) {
             clientPorts[i] = PortAssignment.unique();
             secureClientPorts[i] = PortAssignment.unique();
-            String server = String.format("server.%d=localhost:%d:%d:participant;localhost:%d",
+            String server = String.format("server.%d=127.0.0.1:%d:%d:participant;127.0.0.1:%d%n",
                     i, PortAssignment.unique(), PortAssignment.unique(), clientPorts[i]);
-            sb.append(server + "\n");
+            sb.append(server);
         }
         String quorumCfg = sb.toString();
 
 
         MainThread[] mt = new MainThread[SERVER_COUNT];
         for (int i = 0; i < SERVER_COUNT; i++) {
-            mt[i] = new MainThread(i, quorumCfg, secureClientPorts[i], true);
+            if (useSecurePort) {
+                mt[i] = new MainThread(i, quorumCfg, secureClientPorts[i], true);
+            } else {
+                mt[i] = new MainThread(i, quorumCfg, true);
+            }
             mt[i].start();
         }
 
@@ -103,8 +126,8 @@ public class ClientSSLTest extends QuorumPeerTestBase {
         for (int i = 0; i < SERVER_COUNT; i++) {
             Assert.assertTrue("waiting for server " + i + " being up",
                     ClientBase.waitForServerUp("127.0.0.1:" + clientPorts[i], TIMEOUT));
-
-            ZooKeeper zk = ClientBase.createZKClient("127.0.0.1:" + secureClientPorts[i], TIMEOUT);
+            final int port = useSecurePort ? secureClientPorts[i] : clientPorts[i];
+            ZooKeeper zk = ClientBase.createZKClient("127.0.0.1:" + port, TIMEOUT);
             // Do a simple operation to make sure the connection is fine.
             zk.create("/test", "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
             zk.delete("/test", -1);