You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by GitBox <gi...@apache.org> on 2022/12/22 08:39:40 UTC

[GitHub] [cloudstack] DaanHoogland commented on a diff in pull request #7015: Secure KVM VNC Console Access Using the CA Framework

DaanHoogland commented on code in PR #7015:
URL: https://github.com/apache/cloudstack/pull/7015#discussion_r1055191875


##########
services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java:
##########
@@ -137,18 +145,80 @@ public void run() {
 
     /**
      * Authenticate to VNC server when not using websockets
+     *
+     * Since we are supporting the 3.8 version of the RFB protocol, there are changes on the stages:
+     * 1. Handshake:
+     *    1.a. Protocol version
+     *    1.b. Security types
+     * 2. Security types
+     * 3. Initialisation
+     *
+     * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#7protocol-messages
      * @throws IOException
      */
     private void authenticateToVNCServer() throws IOException {
-        if (!client.isVncOverWebSocketConnection()) {
+        if (client.isVncOverWebSocketConnection()) {
+            return;
+        }
+
+        if (client.isVncOverTunnel()) {
             String ver = client.handshake();
             session.getRemote().sendBytes(ByteBuffer.wrap(ver.getBytes(), 0, ver.length()));
 
-            byte[] b = client.authenticate(getClientHostPassword());
+            byte[] b = client.authenticateTunnel(getClientHostPassword());
             session.getRemote().sendBytes(ByteBuffer.wrap(b, 0, 4));
+        } else {
+            ByteBuffer verStr = client.handshakeProtocolVersion();
+            sendMessageToVNCClient(verStr.array(), 12);
+
+            int secType = client.handshakeSecurityType();
+            byte[] numberTypesToClient = new byte[] { 1, (byte) secType };
+            sendMessageToVNCClient(numberTypesToClient, 2);
+
+            client.processHandshakeSecurityType(secType, getClientHostPassword(),
+                    getClientHostAddress(), getClientHostPort());
+
+            byte[] securityResultToClient = new byte[] { 0, 0, 0, 0 };
+            sendMessageToVNCClient(securityResultToClient, 4);
+            client.setWaitForNoVnc(true);
+
+            while (client.isWaitForNoVnc()) {
+                s_logger.debug("Waiting");
+            }

Review Comment:
   do we want this repetitive single word message in the log file? It seems that it would not be helpful during debugging by an operator. I would sugest one start message and than keeping track of time for a second message:
   ```suggestion
               s_logger.debug("Waiting for NoVnc");
               int cycles = 0;
               while (client.isWaitForNoVnc()) {
                   cycles++
               }
               if (s_logger.isDebugEnabled()) {
                   s_logger.debug(String.format("Waited %d cycles for NoVnc", cycles));
               }
   ```



##########
services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java:
##########
@@ -137,18 +145,80 @@ public void run() {
 
     /**
      * Authenticate to VNC server when not using websockets
+     *
+     * Since we are supporting the 3.8 version of the RFB protocol, there are changes on the stages:
+     * 1. Handshake:
+     *    1.a. Protocol version
+     *    1.b. Security types
+     * 2. Security types
+     * 3. Initialisation
+     *
+     * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#7protocol-messages
      * @throws IOException
      */
     private void authenticateToVNCServer() throws IOException {
-        if (!client.isVncOverWebSocketConnection()) {
+        if (client.isVncOverWebSocketConnection()) {
+            return;
+        }
+
+        if (client.isVncOverTunnel()) {
             String ver = client.handshake();
             session.getRemote().sendBytes(ByteBuffer.wrap(ver.getBytes(), 0, ver.length()));
 
-            byte[] b = client.authenticate(getClientHostPassword());
+            byte[] b = client.authenticateTunnel(getClientHostPassword());
             session.getRemote().sendBytes(ByteBuffer.wrap(b, 0, 4));
+        } else {
+            ByteBuffer verStr = client.handshakeProtocolVersion();
+            sendMessageToVNCClient(verStr.array(), 12);
+
+            int secType = client.handshakeSecurityType();
+            byte[] numberTypesToClient = new byte[] { 1, (byte) secType };
+            sendMessageToVNCClient(numberTypesToClient, 2);
+
+            client.processHandshakeSecurityType(secType, getClientHostPassword(),
+                    getClientHostAddress(), getClientHostPort());
+
+            byte[] securityResultToClient = new byte[] { 0, 0, 0, 0 };
+            sendMessageToVNCClient(securityResultToClient, 4);
+            client.setWaitForNoVnc(true);
+
+            while (client.isWaitForNoVnc()) {
+                s_logger.debug("Waiting");
+            }
+
+            String serverName = String.format("%s %s", clientParam.getClientDisplayName(),
+                    client.isTLSConnectionEstablished() ? "(TLS backend)" : "");
+            byte[] bytesServerInit = rewriteServerNameInServerInit(client.readServerInit(), serverName);
+            s_logger.info(String.format("Server init message is %s (%s)", Arrays.toString(bytesServerInit), new String(bytesServerInit)));
+            session.getRemote().sendBytes(ByteBuffer.wrap(bytesServerInit));
+            client.setWaitForNoVnc(true);
+            while (client.isWaitForNoVnc()) {
+                s_logger.debug("Waiting");
+            }

Review Comment:
   see above, but put it in another method



##########
services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java:
##########
@@ -239,16 +273,349 @@ public byte[] encodePassword(byte[] challenge, String password) throws Exception
         return response;
     }
 
+    /**
+     * Decide the RFB protocol version with the VNC server
+     * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#711protocolversion
+     */
+    protected String handshakeProtocolVersion(RemoteEndpoint clientRemote) throws IOException {
+        // Read protocol version
+        byte[] buf = new byte[12];
+        tunnelInputStream.readFully(buf);
+        String rfbProtocol = new String(buf);
+
+        // Server should use RFB protocol 3.x
+        if (!rfbProtocol.contains(RfbConstants.RFB_PROTOCOL_VERSION_MAJOR)) {
+            s_logger.error("Cannot handshake with VNC server. Unsupported protocol version: \"" + rfbProtocol + "\".");
+            throw new RuntimeException(
+                    "Cannot handshake with VNC server. Unsupported protocol version: \"" + rfbProtocol + "\".");
+        }
+        tunnelOutputStream.write(buf);
+        return RfbConstants.RFB_PROTOCOL_VERSION + "\n";
+    }
+
+    /**
+     * Agree on the security type with the VNC server
+     * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#712security
+     * @return list of the security types to be processed
+     */
+    protected List<VncSecurity> handshakeSecurityTypes(RemoteEndpoint clientRemote, String vmPassword,
+                                                       String host, int port) throws IOException {
+        int securityType = selectFromTheServerOfferedSecurityTypes();
+
+        // Inform the server about our decision
+        this.tunnelOutputStream.writeByte(securityType);
+
+        byte[] numberTypesToClient = new byte[] { 1, (byte) securityType };
+        clientRemote.sendBytes(ByteBuffer.wrap(numberTypesToClient, 0, 2));
+
+        if (securityType == RfbConstants.V_ENCRYPT) {
+            securityType = getVEncryptSecuritySubtype();
+        }
+        return VncSecurity.getSecurityStack(securityType, vmPassword, host, port);
+    }
+
+    /**
+     * Obtain the VEncrypt subtype from the VNC server
+     *
+     * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#724vencrypt
+     */
+    protected int getVEncryptSecuritySubtype() throws IOException {
+        int majorVEncryptVersion = socketConnection.readUnsignedInteger(8);
+        int minorVEncryptVersion = socketConnection.readUnsignedInteger(8);
+        int vEncryptVersion = (majorVEncryptVersion << 8) | minorVEncryptVersion;
+        s_logger.debug("VEncrypt version: " + vEncryptVersion);
+        socketConnection.writeUnsignedInteger(8, majorVEncryptVersion);
+        if (vEncryptVersion >= 0x0002) {
+            socketConnection.writeUnsignedInteger(8, 2);
+            socketConnection.flushWriteBuffer();
+        } else {
+            socketConnection.writeUnsignedInteger(8, 0);
+            socketConnection.flushWriteBuffer();
+            throw new CloudRuntimeException("Server reported an unsupported VeNCrypt version");
+        }
+        int ack = socketConnection.readUnsignedInteger(8);
+        if (ack != 0) {
+            throw new IOException("The VNC server did not agree on the VEncrypt version");
+        }
+
+        int numberOfSubtypes = socketConnection.readUnsignedInteger(8);
+        if (numberOfSubtypes <= 0) {
+            throw new CloudRuntimeException("The server reported no VeNCrypt sub-types");
+        }
+        int selectedSubtype = 0;
+        for (int i = 0; i < numberOfSubtypes; i++) {
+            while (!socketConnection.checkIfBytesAreAvailableForReading(4)) {
+                s_logger.trace("Waiting for vEncrypt subtype");
+            }
+            int subtype = socketConnection.readUnsignedInteger(32);
+            if (subtype == RfbConstants.V_ENCRYPT_X509_VNC) {
+                selectedSubtype = subtype;
+                break;
+            }
+        }
+
+        s_logger.info("Selected VEncrypt subtype " + selectedSubtype);
+        socketConnection.writeUnsignedInteger(32, selectedSubtype);
+        socketConnection.flushWriteBuffer();
+
+        return selectedSubtype;
+    }
+
+    private int selectFromTheServerOfferedSecurityTypes() throws IOException {
+        int numberOfSecurityTypes = tunnelInputStream.readByte();
+        if (numberOfSecurityTypes == 0) {
+            int reasonLength = tunnelInputStream.readInt();
+            byte[] reasonBuffer = new byte[reasonLength];
+            tunnelInputStream.readFully(reasonBuffer);
+            String reason = new String(reasonBuffer);
+            String errMsg = "No security type provided by the VNC server, reason: " + reason;
+            s_logger.error(errMsg);
+            throw new IOException(errMsg);
+        }
+
+        for (int i = 0; i < numberOfSecurityTypes; i++) {
+            int securityType = tunnelInputStream.readByte();
+            if (securityType != 0 && VncSecurity.supportedSecurityTypes.contains(securityType)) {
+                s_logger.info("Selected the security type: " + securityType);
+                return securityType;
+            }
+        }
+        throw new IOException("Could not select a supported or valid security type from the offered by the server");
+    }
+
+    /**
+     * VNC authentication.
+     */
+    public void processSecurityResult(String password)
+            throws IOException {
+        // Read security result
+        int authResult = this.tunnelInputStream.readInt();
+
+        switch (authResult) {
+            case RfbConstants.VNC_AUTH_OK: {
+                // Nothing to do
+                break;
+            }
+
+            case RfbConstants.VNC_AUTH_TOO_MANY:
+                s_logger.error("Connection to VNC server failed: too many wrong attempts.");
+                throw new RuntimeException("Connection to VNC server failed: too many wrong attempts.");
+
+            case RfbConstants.VNC_AUTH_FAILED:
+                s_logger.error("Connection to VNC server failed: wrong password.");
+                throw new RuntimeException("Connection to VNC server failed: wrong password.");
+
+            default:
+                s_logger.error("Connection to VNC server failed, reason code: " + authResult);
+                throw new RuntimeException("Connection to VNC server failed, reason code: " + authResult);
+        }
+    }
+
     public int read(byte[] b) throws IOException {
-        return is.read(b);
+        return tunnelInputStream.read(b);
     }
 
     public void write(byte[] b) throws IOException {
         if (isVncOverWebSocketConnection()) {
             proxyMsgOverWebSocketConnection(ByteBuffer.wrap(b));
+        } else if (!isVncOverTunnel()) {
+            this.socketConnection.writeBytes(b, 0, b.length);
+        } else {
+            tunnelOutputStream.write(b);
+        }
+    }
+
+    public void writeFrame(Frame frame) {
+        byte[] data = new byte[frame.getPayloadLength()];
+        frame.getPayload().get(data);
+
+        if (securityPhaseCompleted) {
+            socketConnection.writeBytes(ByteBuffer.wrap(data), data.length);
+            socketConnection.flushWriteBuffer();
+            if (writerLeft == null) {
+                writerLeft = 3;
+                setWaitForNoVnc(false);
+            } else if (writerLeft > 0) {
+                writerLeft--;
+            }
+        } else {
+            socketConnection.writeBytes(data, 0, data.length);
+            if (flushAfterReceivingNoVNCData) {
+                socketConnection.flushWriteBuffer();
+                flushAfterReceivingNoVNCData = false;
+            }

Review Comment:
   `writeInsecureFrame()`



##########
services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java:
##########
@@ -239,16 +273,349 @@ public byte[] encodePassword(byte[] challenge, String password) throws Exception
         return response;
     }
 
+    /**
+     * Decide the RFB protocol version with the VNC server
+     * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#711protocolversion
+     */
+    protected String handshakeProtocolVersion(RemoteEndpoint clientRemote) throws IOException {
+        // Read protocol version
+        byte[] buf = new byte[12];
+        tunnelInputStream.readFully(buf);
+        String rfbProtocol = new String(buf);
+
+        // Server should use RFB protocol 3.x
+        if (!rfbProtocol.contains(RfbConstants.RFB_PROTOCOL_VERSION_MAJOR)) {
+            s_logger.error("Cannot handshake with VNC server. Unsupported protocol version: \"" + rfbProtocol + "\".");
+            throw new RuntimeException(
+                    "Cannot handshake with VNC server. Unsupported protocol version: \"" + rfbProtocol + "\".");
+        }
+        tunnelOutputStream.write(buf);
+        return RfbConstants.RFB_PROTOCOL_VERSION + "\n";
+    }
+
+    /**
+     * Agree on the security type with the VNC server
+     * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#712security
+     * @return list of the security types to be processed
+     */
+    protected List<VncSecurity> handshakeSecurityTypes(RemoteEndpoint clientRemote, String vmPassword,
+                                                       String host, int port) throws IOException {
+        int securityType = selectFromTheServerOfferedSecurityTypes();
+
+        // Inform the server about our decision
+        this.tunnelOutputStream.writeByte(securityType);
+
+        byte[] numberTypesToClient = new byte[] { 1, (byte) securityType };
+        clientRemote.sendBytes(ByteBuffer.wrap(numberTypesToClient, 0, 2));
+
+        if (securityType == RfbConstants.V_ENCRYPT) {
+            securityType = getVEncryptSecuritySubtype();
+        }
+        return VncSecurity.getSecurityStack(securityType, vmPassword, host, port);
+    }
+
+    /**
+     * Obtain the VEncrypt subtype from the VNC server
+     *
+     * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#724vencrypt
+     */
+    protected int getVEncryptSecuritySubtype() throws IOException {
+        int majorVEncryptVersion = socketConnection.readUnsignedInteger(8);
+        int minorVEncryptVersion = socketConnection.readUnsignedInteger(8);
+        int vEncryptVersion = (majorVEncryptVersion << 8) | minorVEncryptVersion;
+        s_logger.debug("VEncrypt version: " + vEncryptVersion);
+        socketConnection.writeUnsignedInteger(8, majorVEncryptVersion);
+        if (vEncryptVersion >= 0x0002) {
+            socketConnection.writeUnsignedInteger(8, 2);
+            socketConnection.flushWriteBuffer();
+        } else {
+            socketConnection.writeUnsignedInteger(8, 0);
+            socketConnection.flushWriteBuffer();
+            throw new CloudRuntimeException("Server reported an unsupported VeNCrypt version");
+        }
+        int ack = socketConnection.readUnsignedInteger(8);
+        if (ack != 0) {
+            throw new IOException("The VNC server did not agree on the VEncrypt version");
+        }
+
+        int numberOfSubtypes = socketConnection.readUnsignedInteger(8);
+        if (numberOfSubtypes <= 0) {
+            throw new CloudRuntimeException("The server reported no VeNCrypt sub-types");
+        }
+        int selectedSubtype = 0;
+        for (int i = 0; i < numberOfSubtypes; i++) {
+            while (!socketConnection.checkIfBytesAreAvailableForReading(4)) {
+                s_logger.trace("Waiting for vEncrypt subtype");

Review Comment:
   is this repetetive trace useful?



##########
services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java:
##########
@@ -137,18 +145,80 @@ public void run() {
 
     /**
      * Authenticate to VNC server when not using websockets
+     *
+     * Since we are supporting the 3.8 version of the RFB protocol, there are changes on the stages:
+     * 1. Handshake:
+     *    1.a. Protocol version
+     *    1.b. Security types
+     * 2. Security types
+     * 3. Initialisation
+     *
+     * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#7protocol-messages
      * @throws IOException
      */
     private void authenticateToVNCServer() throws IOException {
-        if (!client.isVncOverWebSocketConnection()) {
+        if (client.isVncOverWebSocketConnection()) {
+            return;
+        }
+
+        if (client.isVncOverTunnel()) {
             String ver = client.handshake();
             session.getRemote().sendBytes(ByteBuffer.wrap(ver.getBytes(), 0, ver.length()));
 
-            byte[] b = client.authenticate(getClientHostPassword());
+            byte[] b = client.authenticateTunnel(getClientHostPassword());
             session.getRemote().sendBytes(ByteBuffer.wrap(b, 0, 4));
+        } else {
+            ByteBuffer verStr = client.handshakeProtocolVersion();
+            sendMessageToVNCClient(verStr.array(), 12);
+
+            int secType = client.handshakeSecurityType();
+            byte[] numberTypesToClient = new byte[] { 1, (byte) secType };
+            sendMessageToVNCClient(numberTypesToClient, 2);
+
+            client.processHandshakeSecurityType(secType, getClientHostPassword(),
+                    getClientHostAddress(), getClientHostPort());
+
+            byte[] securityResultToClient = new byte[] { 0, 0, 0, 0 };
+            sendMessageToVNCClient(securityResultToClient, 4);
+            client.setWaitForNoVnc(true);
+
+            while (client.isWaitForNoVnc()) {
+                s_logger.debug("Waiting");
+            }
+
+            String serverName = String.format("%s %s", clientParam.getClientDisplayName(),
+                    client.isTLSConnectionEstablished() ? "(TLS backend)" : "");
+            byte[] bytesServerInit = rewriteServerNameInServerInit(client.readServerInit(), serverName);
+            s_logger.info(String.format("Server init message is %s (%s)", Arrays.toString(bytesServerInit), new String(bytesServerInit)));
+            session.getRemote().sendBytes(ByteBuffer.wrap(bytesServerInit));
+            client.setWaitForNoVnc(true);
+            while (client.isWaitForNoVnc()) {
+                s_logger.debug("Waiting");
+            }
+            s_logger.info("Authenticated successfully");

Review Comment:
   can this go in a new method?



##########
services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java:
##########
@@ -239,16 +273,349 @@ public byte[] encodePassword(byte[] challenge, String password) throws Exception
         return response;
     }
 
+    /**
+     * Decide the RFB protocol version with the VNC server
+     * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#711protocolversion
+     */
+    protected String handshakeProtocolVersion(RemoteEndpoint clientRemote) throws IOException {
+        // Read protocol version
+        byte[] buf = new byte[12];
+        tunnelInputStream.readFully(buf);
+        String rfbProtocol = new String(buf);
+
+        // Server should use RFB protocol 3.x
+        if (!rfbProtocol.contains(RfbConstants.RFB_PROTOCOL_VERSION_MAJOR)) {
+            s_logger.error("Cannot handshake with VNC server. Unsupported protocol version: \"" + rfbProtocol + "\".");
+            throw new RuntimeException(
+                    "Cannot handshake with VNC server. Unsupported protocol version: \"" + rfbProtocol + "\".");
+        }
+        tunnelOutputStream.write(buf);
+        return RfbConstants.RFB_PROTOCOL_VERSION + "\n";
+    }
+
+    /**
+     * Agree on the security type with the VNC server
+     * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#712security
+     * @return list of the security types to be processed
+     */
+    protected List<VncSecurity> handshakeSecurityTypes(RemoteEndpoint clientRemote, String vmPassword,
+                                                       String host, int port) throws IOException {
+        int securityType = selectFromTheServerOfferedSecurityTypes();
+
+        // Inform the server about our decision
+        this.tunnelOutputStream.writeByte(securityType);
+
+        byte[] numberTypesToClient = new byte[] { 1, (byte) securityType };
+        clientRemote.sendBytes(ByteBuffer.wrap(numberTypesToClient, 0, 2));
+
+        if (securityType == RfbConstants.V_ENCRYPT) {
+            securityType = getVEncryptSecuritySubtype();
+        }
+        return VncSecurity.getSecurityStack(securityType, vmPassword, host, port);
+    }
+
+    /**
+     * Obtain the VEncrypt subtype from the VNC server
+     *
+     * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#724vencrypt
+     */
+    protected int getVEncryptSecuritySubtype() throws IOException {

Review Comment:
   this method is too big and too complicated, please disect.



##########
services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyHttpHandlerHelper.java:
##########
@@ -71,6 +71,12 @@ public static Map<String, String> getQueryMap(String query) {
                 } else {
                     s_logger.error("decode token. tag info is not found!");
                 }
+                if (param.getClientDisplayName() != null) {
+                    s_logger.debug("decode token. displayname: " + param.getClientDisplayName());

Review Comment:
   ```suggestion
                       if (s_logger.isDebugEnabled()) {
                           s_logger.debug(String.format("decode token. displayname: %s", param.getClientDisplayName()));
                       }
   ```



##########
services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncTLSSecurity.java:
##########
@@ -0,0 +1,116 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you 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 com.cloud.consoleproxy.vnc.security;
+
+import com.cloud.consoleproxy.util.Logger;
+import com.cloud.consoleproxy.vnc.RfbConstants;
+import com.cloud.consoleproxy.vnc.network.NioSocketHandler;
+import com.cloud.consoleproxy.vnc.network.SSLEngineManager;
+import com.cloud.utils.exception.CloudRuntimeException;
+import com.cloud.utils.nio.Link;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.util.ArrayList;
+
+public class VncTLSSecurity implements VncSecurity {
+
+    private static final Logger s_logger = Logger.getLogger(VncTLSSecurity.class);
+
+    private SSLContext ctx;
+    private SSLEngine engine;
+    private SSLEngineManager manager;
+
+    private boolean anon;
+    private final String host;
+    private final int port;
+
+    public VncTLSSecurity(String host, int port) {
+        this.host = host;
+        this.port = port;
+        this.anon = false;
+    }
+
+    private void initGlobal() {
+        try {
+            ctx = Link.initClientSSLContext();
+        } catch (GeneralSecurityException | IOException e) {
+            throw new CloudRuntimeException("Unable to initialize SSL context", e);
+        }
+    }
+
+    private void setParam() {
+        engine = ctx.createSSLEngine(this.host, this.port);
+        engine.setUseClientMode(true);
+
+        String[] supported = engine.getSupportedProtocols();
+        ArrayList<String> enabled = new ArrayList<String>();
+        for (int i = 0; i < supported.length; i++)
+            if (supported[i].matches("TLS.*"))
+                enabled.add(supported[i]);
+        engine.setEnabledProtocols(enabled.toArray(new String[0]));
+
+        if (anon) {
+            supported = engine.getSupportedCipherSuites();
+            enabled = new ArrayList<String>();
+            // prefer ECDH over DHE
+            for (int i = 0; i < supported.length; i++)
+                if (supported[i].matches("TLS_ECDH_anon.*"))
+                    enabled.add(supported[i]);
+            for (int i = 0; i < supported.length; i++)
+                if (supported[i].matches("TLS_DH_anon.*"))
+                    enabled.add(supported[i]);
+            engine.setEnabledCipherSuites(enabled.toArray(new String[0]));
+        } else {
+            engine.setEnabledCipherSuites(engine.getSupportedCipherSuites());
+        }
+    }
+
+    @Override
+    public void process(NioSocketHandler socketHandler) {
+        s_logger.info("Processing VNC TLS security");
+
+        initGlobal();
+
+        if (manager == null) {
+            if (socketHandler.readUnsignedInteger(8) == 0) {
+                int result = socketHandler.readUnsignedInteger(32);
+                String reason;
+                if (result == RfbConstants.VNC_AUTH_FAILED || result == RfbConstants.VNC_AUTH_TOO_MANY) {
+                    reason = socketHandler.readString();
+                } else {
+                    reason = "Authentication failure (protocol error)";
+                }
+                throw new CloudRuntimeException(reason);
+            }
+            setParam();
+        }

Review Comment:
   move to a handleErrorState() type of method?



##########
services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java:
##########
@@ -239,16 +273,349 @@ public byte[] encodePassword(byte[] challenge, String password) throws Exception
         return response;
     }
 
+    /**
+     * Decide the RFB protocol version with the VNC server
+     * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#711protocolversion
+     */
+    protected String handshakeProtocolVersion(RemoteEndpoint clientRemote) throws IOException {
+        // Read protocol version
+        byte[] buf = new byte[12];
+        tunnelInputStream.readFully(buf);
+        String rfbProtocol = new String(buf);
+
+        // Server should use RFB protocol 3.x
+        if (!rfbProtocol.contains(RfbConstants.RFB_PROTOCOL_VERSION_MAJOR)) {
+            s_logger.error("Cannot handshake with VNC server. Unsupported protocol version: \"" + rfbProtocol + "\".");
+            throw new RuntimeException(
+                    "Cannot handshake with VNC server. Unsupported protocol version: \"" + rfbProtocol + "\".");
+        }
+        tunnelOutputStream.write(buf);
+        return RfbConstants.RFB_PROTOCOL_VERSION + "\n";
+    }
+
+    /**
+     * Agree on the security type with the VNC server
+     * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#712security
+     * @return list of the security types to be processed
+     */
+    protected List<VncSecurity> handshakeSecurityTypes(RemoteEndpoint clientRemote, String vmPassword,
+                                                       String host, int port) throws IOException {
+        int securityType = selectFromTheServerOfferedSecurityTypes();
+
+        // Inform the server about our decision
+        this.tunnelOutputStream.writeByte(securityType);
+
+        byte[] numberTypesToClient = new byte[] { 1, (byte) securityType };
+        clientRemote.sendBytes(ByteBuffer.wrap(numberTypesToClient, 0, 2));
+
+        if (securityType == RfbConstants.V_ENCRYPT) {
+            securityType = getVEncryptSecuritySubtype();
+        }
+        return VncSecurity.getSecurityStack(securityType, vmPassword, host, port);
+    }
+
+    /**
+     * Obtain the VEncrypt subtype from the VNC server
+     *
+     * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#724vencrypt
+     */
+    protected int getVEncryptSecuritySubtype() throws IOException {
+        int majorVEncryptVersion = socketConnection.readUnsignedInteger(8);
+        int minorVEncryptVersion = socketConnection.readUnsignedInteger(8);
+        int vEncryptVersion = (majorVEncryptVersion << 8) | minorVEncryptVersion;
+        s_logger.debug("VEncrypt version: " + vEncryptVersion);
+        socketConnection.writeUnsignedInteger(8, majorVEncryptVersion);
+        if (vEncryptVersion >= 0x0002) {
+            socketConnection.writeUnsignedInteger(8, 2);
+            socketConnection.flushWriteBuffer();
+        } else {
+            socketConnection.writeUnsignedInteger(8, 0);
+            socketConnection.flushWriteBuffer();
+            throw new CloudRuntimeException("Server reported an unsupported VeNCrypt version");
+        }
+        int ack = socketConnection.readUnsignedInteger(8);
+        if (ack != 0) {
+            throw new IOException("The VNC server did not agree on the VEncrypt version");
+        }
+
+        int numberOfSubtypes = socketConnection.readUnsignedInteger(8);
+        if (numberOfSubtypes <= 0) {
+            throw new CloudRuntimeException("The server reported no VeNCrypt sub-types");
+        }
+        int selectedSubtype = 0;
+        for (int i = 0; i < numberOfSubtypes; i++) {
+            while (!socketConnection.checkIfBytesAreAvailableForReading(4)) {
+                s_logger.trace("Waiting for vEncrypt subtype");
+            }
+            int subtype = socketConnection.readUnsignedInteger(32);
+            if (subtype == RfbConstants.V_ENCRYPT_X509_VNC) {
+                selectedSubtype = subtype;
+                break;
+            }
+        }
+
+        s_logger.info("Selected VEncrypt subtype " + selectedSubtype);
+        socketConnection.writeUnsignedInteger(32, selectedSubtype);
+        socketConnection.flushWriteBuffer();
+
+        return selectedSubtype;
+    }
+
+    private int selectFromTheServerOfferedSecurityTypes() throws IOException {
+        int numberOfSecurityTypes = tunnelInputStream.readByte();
+        if (numberOfSecurityTypes == 0) {
+            int reasonLength = tunnelInputStream.readInt();
+            byte[] reasonBuffer = new byte[reasonLength];
+            tunnelInputStream.readFully(reasonBuffer);
+            String reason = new String(reasonBuffer);
+            String errMsg = "No security type provided by the VNC server, reason: " + reason;
+            s_logger.error(errMsg);
+            throw new IOException(errMsg);
+        }

Review Comment:
   separate method



##########
services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/SSLEngineManager.java:
##########
@@ -0,0 +1,180 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you 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 com.cloud.consoleproxy.vnc.network;
+
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLEngineResult;
+import javax.net.ssl.SSLSession;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public class SSLEngineManager {
+
+    private SSLEngine engine = null;
+
+    private ByteBuffer myNetData;
+    private ByteBuffer peerNetData;
+
+    private Executor executor;
+    private NioSocketInputStream inputStream;
+    private NioSocketOutputStream outputStream;
+
+    public SSLEngineManager(SSLEngine sslEngine, NioSocketHandler socket) throws IOException {
+        this.inputStream = socket.getInputStream();
+        this.outputStream = socket.getOutputStream();
+        engine = sslEngine;
+
+        executor = Executors.newSingleThreadExecutor();
+
+        int pktBufSize = engine.getSession().getPacketBufferSize();
+        myNetData = ByteBuffer.allocate(pktBufSize);
+        peerNetData = ByteBuffer.allocate(pktBufSize);
+    }
+
+    public void doHandshake() throws Exception {
+
+        // Begin handshake
+        engine.beginHandshake();
+        SSLEngineResult.HandshakeStatus hs = engine.getHandshakeStatus();
+
+        // Process handshaking message
+        SSLEngineResult res = null;
+        int appBufSize = engine.getSession().getApplicationBufferSize();
+        ByteBuffer peerAppData = ByteBuffer.allocate(appBufSize);
+        ByteBuffer myAppData = ByteBuffer.allocate(appBufSize);
+        while (hs != SSLEngineResult.HandshakeStatus.FINISHED &&

Review Comment:
   a while with double nested switch statements. Can this be restructured somehow?



##########
services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java:
##########
@@ -239,16 +273,349 @@ public byte[] encodePassword(byte[] challenge, String password) throws Exception
         return response;
     }
 
+    /**
+     * Decide the RFB protocol version with the VNC server
+     * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#711protocolversion
+     */
+    protected String handshakeProtocolVersion(RemoteEndpoint clientRemote) throws IOException {
+        // Read protocol version
+        byte[] buf = new byte[12];
+        tunnelInputStream.readFully(buf);
+        String rfbProtocol = new String(buf);
+
+        // Server should use RFB protocol 3.x
+        if (!rfbProtocol.contains(RfbConstants.RFB_PROTOCOL_VERSION_MAJOR)) {
+            s_logger.error("Cannot handshake with VNC server. Unsupported protocol version: \"" + rfbProtocol + "\".");
+            throw new RuntimeException(
+                    "Cannot handshake with VNC server. Unsupported protocol version: \"" + rfbProtocol + "\".");
+        }
+        tunnelOutputStream.write(buf);
+        return RfbConstants.RFB_PROTOCOL_VERSION + "\n";
+    }
+
+    /**
+     * Agree on the security type with the VNC server
+     * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#712security
+     * @return list of the security types to be processed
+     */
+    protected List<VncSecurity> handshakeSecurityTypes(RemoteEndpoint clientRemote, String vmPassword,
+                                                       String host, int port) throws IOException {
+        int securityType = selectFromTheServerOfferedSecurityTypes();
+
+        // Inform the server about our decision
+        this.tunnelOutputStream.writeByte(securityType);
+
+        byte[] numberTypesToClient = new byte[] { 1, (byte) securityType };
+        clientRemote.sendBytes(ByteBuffer.wrap(numberTypesToClient, 0, 2));
+
+        if (securityType == RfbConstants.V_ENCRYPT) {
+            securityType = getVEncryptSecuritySubtype();
+        }
+        return VncSecurity.getSecurityStack(securityType, vmPassword, host, port);
+    }
+
+    /**
+     * Obtain the VEncrypt subtype from the VNC server
+     *
+     * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#724vencrypt
+     */
+    protected int getVEncryptSecuritySubtype() throws IOException {
+        int majorVEncryptVersion = socketConnection.readUnsignedInteger(8);
+        int minorVEncryptVersion = socketConnection.readUnsignedInteger(8);
+        int vEncryptVersion = (majorVEncryptVersion << 8) | minorVEncryptVersion;
+        s_logger.debug("VEncrypt version: " + vEncryptVersion);
+        socketConnection.writeUnsignedInteger(8, majorVEncryptVersion);
+        if (vEncryptVersion >= 0x0002) {
+            socketConnection.writeUnsignedInteger(8, 2);
+            socketConnection.flushWriteBuffer();
+        } else {
+            socketConnection.writeUnsignedInteger(8, 0);
+            socketConnection.flushWriteBuffer();
+            throw new CloudRuntimeException("Server reported an unsupported VeNCrypt version");
+        }
+        int ack = socketConnection.readUnsignedInteger(8);
+        if (ack != 0) {
+            throw new IOException("The VNC server did not agree on the VEncrypt version");
+        }
+
+        int numberOfSubtypes = socketConnection.readUnsignedInteger(8);
+        if (numberOfSubtypes <= 0) {
+            throw new CloudRuntimeException("The server reported no VeNCrypt sub-types");
+        }
+        int selectedSubtype = 0;
+        for (int i = 0; i < numberOfSubtypes; i++) {
+            while (!socketConnection.checkIfBytesAreAvailableForReading(4)) {
+                s_logger.trace("Waiting for vEncrypt subtype");
+            }
+            int subtype = socketConnection.readUnsignedInteger(32);
+            if (subtype == RfbConstants.V_ENCRYPT_X509_VNC) {
+                selectedSubtype = subtype;
+                break;
+            }
+        }
+
+        s_logger.info("Selected VEncrypt subtype " + selectedSubtype);
+        socketConnection.writeUnsignedInteger(32, selectedSubtype);
+        socketConnection.flushWriteBuffer();
+
+        return selectedSubtype;
+    }
+
+    private int selectFromTheServerOfferedSecurityTypes() throws IOException {
+        int numberOfSecurityTypes = tunnelInputStream.readByte();
+        if (numberOfSecurityTypes == 0) {
+            int reasonLength = tunnelInputStream.readInt();
+            byte[] reasonBuffer = new byte[reasonLength];
+            tunnelInputStream.readFully(reasonBuffer);
+            String reason = new String(reasonBuffer);
+            String errMsg = "No security type provided by the VNC server, reason: " + reason;
+            s_logger.error(errMsg);
+            throw new IOException(errMsg);
+        }
+
+        for (int i = 0; i < numberOfSecurityTypes; i++) {
+            int securityType = tunnelInputStream.readByte();
+            if (securityType != 0 && VncSecurity.supportedSecurityTypes.contains(securityType)) {
+                s_logger.info("Selected the security type: " + securityType);
+                return securityType;
+            }
+        }
+        throw new IOException("Could not select a supported or valid security type from the offered by the server");
+    }
+
+    /**
+     * VNC authentication.
+     */
+    public void processSecurityResult(String password)
+            throws IOException {
+        // Read security result
+        int authResult = this.tunnelInputStream.readInt();
+
+        switch (authResult) {
+            case RfbConstants.VNC_AUTH_OK: {
+                // Nothing to do
+                break;
+            }
+
+            case RfbConstants.VNC_AUTH_TOO_MANY:
+                s_logger.error("Connection to VNC server failed: too many wrong attempts.");
+                throw new RuntimeException("Connection to VNC server failed: too many wrong attempts.");
+
+            case RfbConstants.VNC_AUTH_FAILED:
+                s_logger.error("Connection to VNC server failed: wrong password.");
+                throw new RuntimeException("Connection to VNC server failed: wrong password.");
+
+            default:
+                s_logger.error("Connection to VNC server failed, reason code: " + authResult);
+                throw new RuntimeException("Connection to VNC server failed, reason code: " + authResult);
+        }
+    }
+
     public int read(byte[] b) throws IOException {
-        return is.read(b);
+        return tunnelInputStream.read(b);
     }
 
     public void write(byte[] b) throws IOException {
         if (isVncOverWebSocketConnection()) {
             proxyMsgOverWebSocketConnection(ByteBuffer.wrap(b));
+        } else if (!isVncOverTunnel()) {
+            this.socketConnection.writeBytes(b, 0, b.length);
+        } else {
+            tunnelOutputStream.write(b);
+        }
+    }
+
+    public void writeFrame(Frame frame) {
+        byte[] data = new byte[frame.getPayloadLength()];
+        frame.getPayload().get(data);
+
+        if (securityPhaseCompleted) {
+            socketConnection.writeBytes(ByteBuffer.wrap(data), data.length);
+            socketConnection.flushWriteBuffer();
+            if (writerLeft == null) {
+                writerLeft = 3;
+                setWaitForNoVnc(false);
+            } else if (writerLeft > 0) {
+                writerLeft--;
+            }

Review Comment:
   `writeSecureFrame()`?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@cloudstack.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org