diff --git a/agent/src/main/java/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java b/agent/src/main/java/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java index d150ea728c73..257a1fc984a1 100644 --- a/agent/src/main/java/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java +++ b/agent/src/main/java/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java @@ -113,7 +113,7 @@ public Answer executeRequest(final Command cmd) { private Answer execute(StartConsoleProxyAgentHttpHandlerCommand cmd) { s_logger.info("Invoke launchConsoleProxy() in responding to StartConsoleProxyAgentHttpHandlerCommand"); - launchConsoleProxy(cmd.getKeystoreBits(), cmd.getKeystorePassword(), cmd.getEncryptorPassword()); + launchConsoleProxy(cmd.getKeystoreBits(), cmd.getKeystorePassword(), cmd.getEncryptorPassword(), cmd.isSourceIpCheckEnabled()); return new Answer(cmd); } @@ -313,7 +313,7 @@ public String getName() { return _name; } - private void launchConsoleProxy(final byte[] ksBits, final String ksPassword, final String encryptorPassword) { + private void launchConsoleProxy(final byte[] ksBits, final String ksPassword, final String encryptorPassword, final Boolean isSourceIpCheckEnabled) { final Object resource = this; s_logger.info("Building class loader for com.cloud.consoleproxy.ConsoleProxy"); if (_consoleProxyMain == null) { @@ -325,8 +325,8 @@ protected void runInContext() { Class consoleProxyClazz = Class.forName("com.cloud.consoleproxy.ConsoleProxy"); try { s_logger.info("Invoke startWithContext()"); - Method method = consoleProxyClazz.getMethod("startWithContext", Properties.class, Object.class, byte[].class, String.class, String.class); - method.invoke(null, _properties, resource, ksBits, ksPassword, encryptorPassword); + Method method = consoleProxyClazz.getMethod("startWithContext", Properties.class, Object.class, byte[].class, String.class, String.class, Boolean.class); + method.invoke(null, _properties, resource, ksBits, ksPassword, encryptorPassword, isSourceIpCheckEnabled); } catch (SecurityException e) { s_logger.error("Unable to launch console proxy due to SecurityException", e); System.exit(ExitStatus.Error.value()); @@ -358,6 +358,8 @@ protected void runInContext() { Class consoleProxyClazz = Class.forName("com.cloud.consoleproxy.ConsoleProxy"); Method methodSetup = consoleProxyClazz.getMethod("setEncryptorPassword", String.class); methodSetup.invoke(null, encryptorPassword); + methodSetup = consoleProxyClazz.getMethod("setIsSourceIpCheckEnabled", Boolean.class); + methodSetup.invoke(null, isSourceIpCheckEnabled); } catch (SecurityException e) { s_logger.error("Unable to launch console proxy due to SecurityException", e); System.exit(ExitStatus.Error.value()); diff --git a/core/src/main/java/com/cloud/agent/api/proxy/StartConsoleProxyAgentHttpHandlerCommand.java b/core/src/main/java/com/cloud/agent/api/proxy/StartConsoleProxyAgentHttpHandlerCommand.java index 223cf8adae28..2294244da498 100644 --- a/core/src/main/java/com/cloud/agent/api/proxy/StartConsoleProxyAgentHttpHandlerCommand.java +++ b/core/src/main/java/com/cloud/agent/api/proxy/StartConsoleProxyAgentHttpHandlerCommand.java @@ -31,6 +31,8 @@ public class StartConsoleProxyAgentHttpHandlerCommand extends Command { @LogLevel(Log4jLevel.Off) private String encryptorPassword; + private Boolean isSourceIpCheckEnabled; + public StartConsoleProxyAgentHttpHandlerCommand() { super(); } @@ -68,4 +70,12 @@ public String getEncryptorPassword() { public void setEncryptorPassword(String encryptorPassword) { this.encryptorPassword = encryptorPassword; } + + public Boolean isSourceIpCheckEnabled() { + return isSourceIpCheckEnabled; + } + + public void setIsSourceIpCheckEnabled(Boolean isSourceIpCheckEnabled) { + this.isSourceIpCheckEnabled = isSourceIpCheckEnabled; + } } diff --git a/server/src/main/java/com/cloud/api/ApiServlet.java b/server/src/main/java/com/cloud/api/ApiServlet.java index ebdc4f8602ef..e9786c9d88f7 100644 --- a/server/src/main/java/com/cloud/api/ApiServlet.java +++ b/server/src/main/java/com/cloud/api/ApiServlet.java @@ -389,7 +389,7 @@ private boolean isSpecificAPI(String commandName) { } //This method will try to get login IP of user even if servlet is behind reverseProxy or loadBalancer - static InetAddress getClientAddress(final HttpServletRequest request) throws UnknownHostException { + public static InetAddress getClientAddress(final HttpServletRequest request) throws UnknownHostException { for(final String header : s_clientAddressHeaders) { final String ip = getCorrectIPAddress(request.getHeader(header)); if (ip != null) { diff --git a/server/src/main/java/com/cloud/consoleproxy/AgentHookBase.java b/server/src/main/java/com/cloud/consoleproxy/AgentHookBase.java index c3911826feb6..6a06774f3b18 100644 --- a/server/src/main/java/com/cloud/consoleproxy/AgentHookBase.java +++ b/server/src/main/java/com/cloud/consoleproxy/AgentHookBase.java @@ -208,6 +208,7 @@ public void startAgentHttpHandlerInVM(StartupProxyCommand startupCmd) { cmd = new StartConsoleProxyAgentHttpHandlerCommand(ksBits, storePassword); cmd.setEncryptorPassword(getEncryptorPassword()); + cmd.setIsSourceIpCheckEnabled(Boolean.parseBoolean(_configDao.getValue(ConsoleProxyManager.NoVncConsoleSourceIpCheckEnabled.key()))); HostVO consoleProxyHost = findConsoleProxyHost(startupCmd); diff --git a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java index 875bbc524bb6..f7f88b0da66e 100644 --- a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java +++ b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java @@ -41,6 +41,9 @@ public interface ConsoleProxyManager extends Manager, ConsoleProxyService { public static final ConfigKey NoVncConsoleDefault = new ConfigKey("Advanced", Boolean.class, "novnc.console.default", "true", "If true, noVNC console will be default console for virtual machines", true); + public static final ConfigKey NoVncConsoleSourceIpCheckEnabled = new ConfigKey("Advanced", Boolean.class, "novnc.console.sourceip.check.enabled", "false", + "If true, The source IP to access novnc console must be same as the IP in request to management server for console URL. Needs to reconnect CPVM to management server when this changes (via restart CPVM, or management server, or cloud service in CPVM)", false); + public void setManagementState(ConsoleProxyManagementState state); public ConsoleProxyManagementState getManagementState(); diff --git a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java index b0eac2bcf448..8dfa1f67db0a 100644 --- a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java +++ b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java @@ -1755,7 +1755,7 @@ public String getConfigComponentName() { @Override public ConfigKey[] getConfigKeys() { - return new ConfigKey[] { NoVncConsoleDefault }; + return new ConfigKey[] { NoVncConsoleDefault, NoVncConsoleSourceIpCheckEnabled }; } } diff --git a/server/src/main/java/com/cloud/servlet/ConsoleProxyClientParam.java b/server/src/main/java/com/cloud/servlet/ConsoleProxyClientParam.java index ce06a9624a95..3d587c247e8c 100644 --- a/server/src/main/java/com/cloud/servlet/ConsoleProxyClientParam.java +++ b/server/src/main/java/com/cloud/servlet/ConsoleProxyClientParam.java @@ -33,6 +33,8 @@ public class ConsoleProxyClientParam { private String username; private String password; + private String sourceIP; + public ConsoleProxyClientParam() { clientHostPort = 0; } @@ -140,4 +142,12 @@ public void setPassword(String password) { public String getPassword() { return password; } + + public String getSourceIP() { + return sourceIP; + } + + public void setSourceIP(String sourceIP) { + this.sourceIP = sourceIP; + } } diff --git a/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java b/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java index b735be811e2a..3cbb5f4b99e4 100644 --- a/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java +++ b/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java @@ -17,7 +17,9 @@ package com.cloud.servlet; import java.io.IOException; +import java.net.InetAddress; import java.net.URLEncoder; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collections; import java.util.Date; @@ -46,6 +48,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.cloud.api.ApiServlet; import com.cloud.consoleproxy.ConsoleProxyManager; import com.cloud.exception.PermissionDeniedException; import com.cloud.host.HostVO; @@ -289,8 +292,15 @@ private void handleAccessRequest(HttpServletRequest req, HttpServletResponse res } } + InetAddress remoteAddress = null; + try { + remoteAddress = ApiServlet.getClientAddress(req); + } catch (UnknownHostException e) { + s_logger.warn("UnknownHostException when trying to lookup remote IP-Address. This should never happen. Blocking request.", e); + } + StringBuffer sb = new StringBuffer(); - sb.append("").append(escapeHTML(vmName)).append("").append(escapeHTML(vmName)).append(""); s_logger.debug("the console url is :: " + sb.toString()); sendResponse(resp, sb.toString()); @@ -417,7 +427,7 @@ private String composeThumbnailUrl(String rootUrl, VirtualMachine vm, HostVO hos return sb.toString(); } - private String composeConsoleAccessUrl(String rootUrl, VirtualMachine vm, HostVO hostVo) { + private String composeConsoleAccessUrl(String rootUrl, VirtualMachine vm, HostVO hostVo, InetAddress addr) { StringBuffer sb = new StringBuffer(rootUrl); String host = hostVo.getPrivateIpAddress(); @@ -465,6 +475,7 @@ private String composeConsoleAccessUrl(String rootUrl, VirtualMachine vm, HostVO param.setClientHostPassword(sid); param.setClientTag(tag); param.setTicket(ticket); + param.setSourceIP(addr != null ? addr.getHostAddress(): null); if (details != null) { param.setLocale(details.getValue()); 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 7a70a38b7868..2c2bf0f45182 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 @@ -57,6 +57,7 @@ public class ConsoleProxy { // dynamically changing to customer supplied certificate) public static byte[] ksBits; public static String ksPassword; + public static Boolean isSourceIpCheckEnabled; public static Method authMethod; public static Method reportMethod; @@ -232,7 +233,7 @@ public static void ensureRoute(String address) { } } - public static void startWithContext(Properties conf, Object context, byte[] ksBits, String ksPassword, String password) { + public static void startWithContext(Properties conf, Object context, byte[] ksBits, String ksPassword, String password, Boolean isSourceIpCheckEnabled) { setEncryptorPassword(password); configLog4j(); Logger.setFactory(new ConsoleProxyLoggerFactory()); @@ -248,6 +249,7 @@ public static void startWithContext(Properties conf, Object context, byte[] ksBi ConsoleProxy.context = context; ConsoleProxy.ksBits = ksBits; ConsoleProxy.ksPassword = ksPassword; + ConsoleProxy.isSourceIpCheckEnabled = isSourceIpCheckEnabled; try { final ClassLoader loader = Thread.currentThread().getContextClassLoader(); Class contextClazz = loader.loadClass("com.cloud.agent.resource.consoleproxy.ConsoleProxyResource"); @@ -526,6 +528,10 @@ public static void setEncryptorPassword(String password) { encryptorPassword = password; } + public static void setIsSourceIpCheckEnabled(Boolean isEnabled) { + isSourceIpCheckEnabled = isEnabled; + } + static class ThreadExecutor implements Executor { @Override public void execute(Runnable r) { diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyClientParam.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyClientParam.java index e62ac4531b94..ad2fc25026b9 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyClientParam.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyClientParam.java @@ -37,6 +37,8 @@ public class ConsoleProxyClientParam { private String username; private String password; + private String sourceIP; + public ConsoleProxyClientParam() { clientHostPort = 0; } @@ -143,4 +145,12 @@ public void setPassword(String password) { public String getPassword() { return password; } + + public String getSourceIP() { + return sourceIP; + } + + public void setSourceIP(String sourceIP) { + this.sourceIP = sourceIP; + } } diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyHttpHandlerHelper.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyHttpHandlerHelper.java index 51a703ae4b4a..4bed1506a280 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyHttpHandlerHelper.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyHttpHandlerHelper.java @@ -91,6 +91,8 @@ public static Map getQueryMap(String query) { map.put("username", param.getUsername()); if (param.getPassword() != null) map.put("password", param.getPassword()); + if (param.getSourceIP() != null) + map.put("sourceIP", param.getSourceIP()); } else { s_logger.error("Unable to decode token"); } 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 349d98408a13..2b705564b2d4 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 @@ -87,6 +87,7 @@ public void onConnect(final Session session) throws IOException, InterruptedExce String hypervHost = queryMap.get("hypervHost"); String username = queryMap.get("username"); String password = queryMap.get("password"); + String sourceIP = queryMap.get("sourceIP"); if (tag == null) tag = ""; @@ -113,6 +114,10 @@ public void onConnect(final Session session) throws IOException, InterruptedExce } } + if (! checkSessionSourceIp(session, sourceIP)) { + return; + } + try { ConsoleProxyClientParam param = new ConsoleProxyClientParam(); param.setClientHostAddress(host); @@ -133,6 +138,18 @@ public void onConnect(final Session session) throws IOException, InterruptedExce } } + private boolean checkSessionSourceIp(final Session session, final String sourceIP) throws IOException { + // Verify source IP + String sessionSourceIP = session.getRemoteAddress().getAddress().getHostAddress(); + s_logger.info("Get websocket connection request from remote IP : " + sessionSourceIP); + if (ConsoleProxy.isSourceIpCheckEnabled && (sessionSourceIP == null || ! sessionSourceIP.equals(sourceIP))) { + s_logger.warn("Failed to access console as the source IP to request the console is " + sourceIP); + session.disconnect(); + return false; + } + return true; + } + @OnWebSocketClose public void onClose(Session session, int statusCode, String reason) throws IOException, InterruptedException { ConsoleProxy.removeViewer(viewer);