diff --git a/captive-portal/src/com/untangle/app/captive_portal/CaptivePortalSSLEngine.java b/captive-portal/src/com/untangle/app/captive_portal/CaptivePortalSSLEngine.java index 68f96dd6a6..db98a5214e 100644 --- a/captive-portal/src/com/untangle/app/captive_portal/CaptivePortalSSLEngine.java +++ b/captive-portal/src/com/untangle/app/captive_portal/CaptivePortalSSLEngine.java @@ -164,7 +164,7 @@ private boolean clientDataWorker(AppTCPSession session, ByteBuffer data) throws if (sniHostname == null){ try{ - sniHostname = HttpUtility.extractSniHostname(data.duplicate()); + sniHostname = HttpUtility.extractSniHostname(data.duplicate(),false); }catch (Exception exn) { // The client is almost certainly sending us a bad TLS packet. session.release(); diff --git a/http-casing/src/com/untangle/app/http/HttpUtility.java b/http-casing/src/com/untangle/app/http/HttpUtility.java index 83185280d9..ed17f20706 100644 --- a/http-casing/src/com/untangle/app/http/HttpUtility.java +++ b/http-casing/src/com/untangle/app/http/HttpUtility.java @@ -12,6 +12,7 @@ import org.apache.hc.core5.net.URIBuilder; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; +import org.apache.commons.lang3.StringUtils; /** * Utility class for Extract host name @@ -23,6 +24,8 @@ public class HttpUtility { private static int CLIENT_HELLO = 0x01; private static int SERVER_NAME = 0x0000; private static int HOST_NAME = 0x00; + private static int ENCRYPTED_CLIENT_HELLO = 0xfe0d; + public static final String ECH_BLOCKED= "encrypted_client_hello"; private static final HttpUtility INSTANCE = new HttpUtility(); @@ -45,10 +48,11 @@ public static HttpUtility getInstance() /** * Function for extracting the SNI hostname from the client request. * @param data + * @param isEchBlocked * @return The SNI hostname extracted from the client request, or null * @throws Exception */ - public static String extractSniHostname(ByteBuffer data) throws Exception{ + public static String extractSniHostname(ByteBuffer data, boolean isEchBlocked) throws Exception{ int counter = 0; int pos=0; @@ -99,7 +103,7 @@ public static String extractSniHostname(ByteBuffer data) throws Exception{ logger.debug("No extensions found in TLS handshake message"); return (null); } - return extractSniHostNameFromExtensions(data, counter); + return extractSniHostNameFromExtensions(data, counter, isEchBlocked); } /** @@ -144,13 +148,18 @@ public static void setDataPositions(ByteBuffer data, int pos) throws Exception{ * Process all extensions to find the SNI signature. * @param data * @param counter + * @param isEchBlocked * @return The extracted SNI hostname, or null if not found. */ - public static String extractSniHostNameFromExtensions(ByteBuffer data, int counter){ + public static String extractSniHostNameFromExtensions(ByteBuffer data, int counter, boolean isEchBlocked) { // get the total size of extension data block int extensionLength = Math.abs(data.getShort()); - + boolean encryptedClientHelloFound = false; + // if ECH check enbled check for ech extention + if(isEchBlocked){ + encryptedClientHelloFound = checkEchExtension(extensionLength, data.duplicate(),counter); + } // walk through all of the extensions looking for SNI signature while (counter < extensionLength) { if (data.remaining() < 2) throw new BufferUnderflowException(); @@ -185,12 +194,42 @@ public static String extractSniHostNameFromExtensions(ByteBuffer data, int count // found a valid host name so adjust the position to skip over // the list length and name type info we directly accessed above if (data.remaining() < 5) throw new BufferUnderflowException(); - - return extractedSNIHostname(data, nameLength); + String hostname = extractedSNIHostname(data, nameLength); + //check for ech extention and encrypted hostname, if found return encrypted_client_hello + if(encryptedClientHelloFound && StringUtils.isEmpty(hostname)){ + return ECH_BLOCKED; + } + return hostname; } return null; } + /** + * Check for ECH extention + * @param extensionLength + * @param data + * @param counter + * @return encryptedClientHelloFound + */ + public static boolean checkEchExtension(int extensionLength, ByteBuffer data, int counter){ + while (counter < extensionLength) { + if (data.remaining() < 2) throw new BufferUnderflowException(); + + int extType = data.getShort() & 0xFFFF; + int extSize = Math.abs(data.getShort()); + // Check for "encrypted client hello" extension first + if (extType == ENCRYPTED_CLIENT_HELLO) { + return true; + } + + // If not "encrypted client hello", process other extensions + if (data.remaining() < extSize) throw new BufferUnderflowException(); + data.position(data.position() + extSize); + counter += (extSize + 4); + } + return false; + } + /** * Extracth hostName based on nameLength * @param data diff --git a/ssl-inspector/src/com/untangle/app/ssl_inspector/SslInspectorManager.java b/ssl-inspector/src/com/untangle/app/ssl_inspector/SslInspectorManager.java index e4d99f3660..1818c79e1b 100644 --- a/ssl-inspector/src/com/untangle/app/ssl_inspector/SslInspectorManager.java +++ b/ssl-inspector/src/com/untangle/app/ssl_inspector/SslInspectorManager.java @@ -539,7 +539,7 @@ public void setDataMode(boolean argMode) */ public String extractSNIhostname(ByteBuffer data) throws Exception { - return HttpUtility.extractSniHostname(data); + return HttpUtility.extractSniHostname(data, false); } /** diff --git a/threat-prevention/src/com/untangle/app/threat-prevention/ThreatPreventionHttpsSniHandler.java b/threat-prevention/src/com/untangle/app/threat-prevention/ThreatPreventionHttpsSniHandler.java index f806dd338f..e62796bdf1 100644 --- a/threat-prevention/src/com/untangle/app/threat-prevention/ThreatPreventionHttpsSniHandler.java +++ b/threat-prevention/src/com/untangle/app/threat-prevention/ThreatPreventionHttpsSniHandler.java @@ -153,7 +153,7 @@ private void checkClientRequest(AppTCPSession sess, ByteBuffer data) // scan the buffer for the SNI hostname try { - domain = HttpUtility.extractSniHostname(buff.duplicate()); + domain = HttpUtility.extractSniHostname(buff.duplicate(), false); } // on underflow exception we stuff the partial packet into a buffer diff --git a/uvm/js/common/util/Renderer.js b/uvm/js/common/util/Renderer.js index 481539310f..021f12dc1d 100644 --- a/uvm/js/common/util/Renderer.js +++ b/uvm/js/common/util/Renderer.js @@ -574,6 +574,7 @@ Ext.define('Ung.util.Renderer', { B: 'in Temporary Unblocked list'.t(), F: 'in Rules list'.t(), K: 'Kid-friendly redirect'.t(), + X: 'ECH blocked'.t(), default: 'no rule applied'.t() }, httpReason: function( value, metaData ) { diff --git a/uvm/js/common/util/_Map.js b/uvm/js/common/util/_Map.js index da382afa56..341145e5b7 100644 --- a/uvm/js/common/util/_Map.js +++ b/uvm/js/common/util/_Map.js @@ -1600,6 +1600,7 @@ Ext.define('Ung.util.Map', { B: 'in Temporary Unblocked list'.t(), F: 'in Rules list'.t(), K: 'Kid-friendly redirect'.t(), + X: 'ECH blocked'.t(), default: 'no rule applied'.t() }, diff --git a/web-filter-base/src/com/untangle/app/web_filter/DecisionEngine.java b/web-filter-base/src/com/untangle/app/web_filter/DecisionEngine.java index 3ca52f4012..bfe9f166fd 100644 --- a/web-filter-base/src/com/untangle/app/web_filter/DecisionEngine.java +++ b/web-filter-base/src/com/untangle/app/web_filter/DecisionEngine.java @@ -38,6 +38,7 @@ public abstract class DecisionEngine { private Map i18nMap; Long i18nMapLastUpdated = 0L; + private static final String ECH_HOSTNAME = "encrypted_client_hello"; private final Logger logger = LogManager.getLogger(getClass()); @@ -162,6 +163,21 @@ public HttpRedirect checkRequest(AppTCPSession sess, InetAddress clientIp, int p header.addField("X-GoogApps-Allowed-Domains", allowedDomains); } } + // If ECH request then directly block the request and raise event + if (host.equals(ECH_HOSTNAME)) { + WebFilterEvent hbe = new WebFilterEvent(requestLine.getRequestLine(), sess.sessionEvent(), Boolean.TRUE, Boolean.TRUE, Reason.BLOCK_ECH, bestCategory.getId(), 0, "ECH BLOCKED", app.getName()); + if (logger.isDebugEnabled()) logger.debug("LOG: ECH block : " + requestLine.getRequestLine()); + app.logEvent(hbe); + app.incrementFlagCount(); + + return ( + new HttpRedirect( + app.generateBlockResponse( + new WebFilterRedirectDetails( app.getSettings(), "", "", "ECH blocked", clientIp, app.getAppTitle(), Reason.BLOCK_ECH, "ECH REQUEST"), + sess, uri.toString(), header), + HttpRedirect.RedirectType.BLOCK)); + + } // check client IP pass list // If a client is on the pass list is is passed regardless of any other settings diff --git a/web-filter-base/src/com/untangle/app/web_filter/Reason.java b/web-filter-base/src/com/untangle/app/web_filter/Reason.java index f93e8a8e29..873bae0631 100644 --- a/web-filter-base/src/com/untangle/app/web_filter/Reason.java +++ b/web-filter-base/src/com/untangle/app/web_filter/Reason.java @@ -21,7 +21,8 @@ public enum Reason PASS_UNBLOCK('B', "in Bypass list"), FILTER_RULE('F', "matched Rule list"), REDIRECT_KIDS('K', "redirected to kid-friendly search engine"), - DEFAULT('N', "no rule applied"); + DEFAULT('N', "no rule applied"), + BLOCK_ECH('X',"ECH blocked"); // THIS IS FOR ECLIPSE - @formatter:on diff --git a/web-filter-base/src/com/untangle/app/web_filter/WebFilterHttpsSniHandler.java b/web-filter-base/src/com/untangle/app/web_filter/WebFilterHttpsSniHandler.java index bc2ccc8287..6f2755aa83 100644 --- a/web-filter-base/src/com/untangle/app/web_filter/WebFilterHttpsSniHandler.java +++ b/web-filter-base/src/com/untangle/app/web_filter/WebFilterHttpsSniHandler.java @@ -154,10 +154,10 @@ private void checkClientRequest(AppTCPSession sess, ByteBuffer data) logger.debug("HANDLE_CHUNK = " + buff.toString()); app.incrementScanCount(); - + boolean isEchBlock = app.getSettings().getBlockECH(); // scan the buffer for the SNI hostname try { - domain = HttpUtility.extractSniHostname(buff.duplicate()); + domain = HttpUtility.extractSniHostname(buff.duplicate(), isEchBlock); } // on underflow exception we stuff the partial packet into a buffer