diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java index a25abac981b9..daf1122656fe 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java @@ -26,7 +26,6 @@ import java.net.InetSocketAddress; import java.net.URISyntaxException; import java.net.URL; -import java.util.HashSet; import java.util.Hashtable; import java.util.Map; import java.util.Properties; @@ -83,12 +82,62 @@ public class ConsoleProxy { static String encryptorPassword = "Dummy"; static final String[] skipProperties = new String[]{"certificate", "cacertificate", "keystore_password", "privatekey"}; - static Set allowedSessions = new HashSet<>(); + static Set allowedSessions = ConcurrentHashMap.newKeySet(); + private static final Object allowedSessionsLock = new Object(); + private static final Map sessionReconnectGrants = new ConcurrentHashMap<>(); + static int sessionReconnectGraceSeconds = 1; + + // Invoked through reflection public static void addAllowedSession(String sessionUuid) { allowedSessions.add(sessionUuid); } + /** + * Grant a short, single-use window to reconnect with the same session UUID in case of a disconnection. + * The grant is bound to the client IP that was using the session so it cannot be redeemed by another client. + * @param sessionUuid session UUID to grant a reconnect window for + * @param clientIp source IP of the client the session was granted to + */ + public static void grantReconnectWindow(String sessionUuid, String clientIp) { + sessionReconnectGrants.put(sessionUuid, new ReconnectGrant(System.currentTimeMillis() + sessionReconnectGraceSeconds * 1000L, clientIp)); + } + + private static boolean consumeReconnectGrant(String sessionUuid, String clientIp) { + ReconnectGrant grant = sessionReconnectGrants.remove(sessionUuid); + if (grant == null || grant.isExpired(System.currentTimeMillis())) { + return false; + } + if (grant.clientIp != null && !grant.clientIp.equals(clientIp)) { + LOGGER.warn("Rejecting reconnect for session " + sessionUuid + " as it was requested from IP " + + clientIp + " but the reconnect window was granted to IP " + grant.clientIp); + return false; + } + return true; + } + + /** + * Drops expired, unclaimed reconnect grants so sessionReconnectGrants doesn't grow unbounded + * when a client never reconnects after a disconnection. Invoked periodically by {@link ConsoleProxyGCThread}. + */ + static void cleanupExpiredReconnectGrants() { + sessionReconnectGrants.entrySet().removeIf(entry -> entry.getValue().isExpired(System.currentTimeMillis())); + } + + private static final class ReconnectGrant { + final long expiryMillis; + final String clientIp; + + ReconnectGrant(long expiryMillis, String clientIp) { + this.expiryMillis = expiryMillis; + this.clientIp = clientIp; + } + + boolean isExpired(long now) { + return now >= expiryMillis; + } + } + private static void configLog4j() { final ClassLoader loader = Thread.currentThread().getContextClassLoader(); URL configUrl = loader.getResource("/conf/log4j-cloud.xml"); @@ -166,6 +215,12 @@ private static void configProxy(Properties conf) { defaultBufferSize = Integer.parseInt(s); LOGGER.info("Setting defaultBufferSize=" + defaultBufferSize); } + + s = conf.getProperty("consoleproxy.sessionReconnectGraceSeconds"); + if (s != null) { + sessionReconnectGraceSeconds = Integer.parseInt(s); + LOGGER.info("Setting sessionReconnectGraceSeconds=" + sessionReconnectGraceSeconds); + } } public static ConsoleProxyServerFactory getHttpServerFactory() { @@ -209,13 +264,16 @@ public static ConsoleProxyAuthenticationResult authenticateConsoleAccess(Console } String sessionUuid = param.getSessionUuid(); - if (allowedSessions.contains(sessionUuid)) { - LOGGER.debug("Acquiring the session " + sessionUuid + " not available for future use"); - allowedSessions.remove(sessionUuid); - } else { - LOGGER.info("Session " + sessionUuid + " has already been used, cannot connect"); - authResult.setSuccess(false); - return authResult; + synchronized (allowedSessionsLock) { + if (allowedSessions.remove(sessionUuid)) { + LOGGER.debug("Acquiring the session " + sessionUuid + " for use"); + } else if (consumeReconnectGrant(sessionUuid, param.getClientIp())) { + LOGGER.info("Reconnecting the session " + sessionUuid + " after a dropped connection"); + } else { + LOGGER.info("Session " + sessionUuid + " has already been used, cannot connect"); + authResult.setSuccess(false); + return authResult; + } } String websocketUrl = param.getWebsocketUrl(); @@ -625,7 +683,7 @@ public static ConsoleProxyNoVncClient getNoVncViewer(ConsoleProxyClientParam par } catch (IOException e) { LOGGER.error("Exception while disconnect session of novnc viewer object: " + viewer, e); } - removeViewer(viewer); + viewer.closeClient(); viewer = new ConsoleProxyNoVncClient(session); viewer.initClient(param); connectionMap.put(clientKey, viewer); diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyGCThread.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyGCThread.java index 0e8f576cf6db..74d83fb591ff 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyGCThread.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyGCThread.java @@ -75,6 +75,7 @@ public void run() { while (true) { cleanupLogging(); + ConsoleProxy.cleanupExpiredReconnectGrants(); bReportLoad = false; if (logger.isDebugEnabled()) { diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCHandler.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCHandler.java index a148b988e40d..dba38d5453ce 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCHandler.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCHandler.java @@ -175,12 +175,20 @@ private boolean checkSessionSourceIp(final Session session, final String sourceI @OnWebSocketClose public void onClose(Session session, int statusCode, String reason) throws IOException, InterruptedException { - String sessionSourceIp = session.getRemoteAddress().getAddress().getHostAddress(); - logger.debug("Closing WebSocket session [source IP: {}, status code: {}].", sessionSourceIp, statusCode); if (viewer != null) { - ConsoleProxy.removeViewer(viewer); + viewer.closeClient(); + } + String sessionSourceIp = getRemoteAddressSafely(session); + logger.debug("WebSocket session [source IP: {}, status code: {}, reason: {}] closed successfully.", sessionSourceIp, statusCode, reason); + } + + private String getRemoteAddressSafely(Session session) { + try { + return session.getRemoteAddress().getAddress().getHostAddress(); + } catch (Exception e) { + logger.debug("Failed to get remote address from WebSocket session", e); + return "unknown"; } - logger.debug("WebSocket session [source IP: {}, status code: {}] closed successfully.", sessionSourceIp, statusCode); } @OnWebSocketFrame diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java index 36dce8b8554c..22cd546a78a9 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java @@ -177,6 +177,7 @@ public void run() { } } logger.info("Connection with client [{}] [IP: {}] is dead.", clientId, clientSourceIp); + ConsoleProxy.grantReconnectWindow(sessionUuid, clientSourceIp); } catch (IOException e) { logger.error("Error on VNC client", e); } @@ -374,6 +375,9 @@ public void closeClient() { this.connectionAlive = false; // Clear buffer reference to allow GC when client disconnects this.readBuffer = null; + if (client != null) { + client.close(); + } ConsoleProxy.removeViewer(this); } diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java index ca7577d2bfcb..938b810356be 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java @@ -129,6 +129,28 @@ public void proxyMsgOverWebSocketConnection(ByteBuffer msg) { } } + public void close() { + if (nioSocketConnection != null) { + nioSocketConnection.close(); + } + if (webSocketReverseProxy != null) { + webSocketReverseProxy.close(); + } + if (socket != null) { + try { + if (is != null) { + is.close(); + } + if (os != null) { + os.close(); + } + socket.close(); + } catch (IOException e) { + logger.debug("Error closing socket: " + e.getMessage(), e); + } + } + } + private void setTunnelSocketStreams() throws IOException { this.is = new DataInputStream(this.socket.getInputStream()); this.os = new DataOutputStream(this.socket.getOutputStream()); diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocket.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocket.java index 4ab88ea9fc72..60ec0f312353 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocket.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocket.java @@ -120,4 +120,28 @@ protected int writeToSocketChannel(ByteBuffer buf, int len) { return 0; } } + + public void close() { + try { + if (socketChannel != null) { + socketChannel.close(); + } + } catch (IOException e) { + logger.debug("Error closing socket channel: " + e.getMessage(), e); + } + try { + if (readSelector != null) { + readSelector.close(); + } + } catch (IOException e) { + logger.debug("Error closing read selector: " + e.getMessage(), e); + } + try { + if (writeSelector != null) { + writeSelector.close(); + } + } catch (IOException e) { + logger.debug("Error closing write selector: " + e.getMessage(), e); + } + } } diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketHandler.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketHandler.java index 757f9c126ec0..02ee2ad7e508 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketHandler.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketHandler.java @@ -41,4 +41,5 @@ public interface NioSocketHandler { void flushWriteBuffer(); void startTLSConnection(NioSocketSSLEngineManager sslEngineManager); boolean isTLSConnection(); + void close(); } diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketHandlerImpl.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketHandlerImpl.java index fc19c36b3edd..0472a338a855 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketHandlerImpl.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketHandlerImpl.java @@ -28,10 +28,12 @@ public class NioSocketHandlerImpl implements NioSocketHandler { private NioSocketInputStream inputStream; private NioSocketOutputStream outputStream; private boolean isTLS = false; + private final NioSocket socket; protected Logger logger = LogManager.getLogger(getClass()); public NioSocketHandlerImpl(NioSocket socket) { + this.socket = socket; this.inputStream = new NioSocketInputStream(ConsoleProxy.defaultBufferSize, socket); this.outputStream = new NioSocketOutputStream(ConsoleProxy.defaultBufferSize, socket); } @@ -109,4 +111,9 @@ public NioSocketInputStream getInputStream() { public NioSocketOutputStream getOutputStream() { return outputStream; } + + @Override + public void close() { + socket.close(); + } } diff --git a/systemvm/agent/conf/consoleproxy.properties b/systemvm/agent/conf/consoleproxy.properties index 361b0a33a054..6255a6906524 100644 --- a/systemvm/agent/conf/consoleproxy.properties +++ b/systemvm/agent/conf/consoleproxy.properties @@ -22,3 +22,4 @@ consoleproxy.jarDir=./applet/ consoleproxy.viewerLinger=180 consoleproxy.reconnectMaxRetry=5 consoleproxy.defaultBufferSize=65536 +consoleproxy.sessionReconnectGraceSeconds=1