]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MINOR: acl: add support for TLS server name matching using SNI
authorWilly Tarreau <w@1wt.eu>
Mon, 12 Dec 2011 16:23:41 +0000 (17:23 +0100)
committerWilly Tarreau <w@1wt.eu>
Mon, 12 Dec 2011 16:26:23 +0000 (17:26 +0100)
Server Name Indication (SNI) is a TLS extension which makes a client
present the name of the server it is connecting to in the client hello.
It allows a transparent proxy to take a decision based on the beginning
of an SSL/TLS stream without deciphering it.

The new ACL "req_ssl_sni" matches the name extracted from the TLS
handshake against a list of names which may be loaded from a file if
needed.

doc/configuration.txt
src/acl.c

index 8aeeb272d0aeca7477bbb634b52181121122b865..0066ee9f831bda8e5d015a8d0c5e2e2c41009558 100644 (file)
@@ -7598,6 +7598,12 @@ during analysis. This requires that some data has been buffered, for instance
 through TCP request content inspection. Please see the "tcp-request content"
 keyword for more detailed information on the subject.
 
+rep_ssl_hello_type <integer>
+  Returns true when data in the response buffer looks like a complete SSL (v3
+  or superior) hello message and handshake type is equal to <integer>.
+  This test was designed to be used with TCP response content inspection: a
+  SSL session ID may be fetched.
+
 req_len <integer>
   Returns true when the length of the data in the request buffer matches the
   specified range. It is important to understand that this test does not
@@ -7633,6 +7639,29 @@ req_rdp_cookie_cnt(<name>) <integer>
   of detecting the RDP protocol, as clients generally send the MSTS or MSTSHASH
   cookies.
 
+req_ssl_hello_type <integer>
+  Returns true when data in the request buffer looks like a complete SSL (v3
+  or superior) hello message and handshake type is equal to <integer>.
+  This test was designed to be used with TCP request content inspection: an
+  SSL session ID may be fetched.
+
+req_ssl_sni <string>
+  Returns true when data in the request buffer looks like a complete SSL (v3
+  or superior) client hello message with a Server Name Indication TLS extension
+  (SNI) matching <string>. SNI normally contains the name of the host the
+  client tries to connect to (for recent browsers). SNI is useful for allowing
+  or denying access to certain hosts when SSL/TLS is used by the client. This
+  test was designed to be used with TCP request content inspection. If content
+  switching is needed, it is recommended to first wait for a complete client
+  hello (type 1), like in the example below.
+
+  Examples :
+     # Wait for a client hello for at most 5 seconds
+     tcp-request inspect-delay 5s
+     tcp-request content accept if { req_ssl_hello_type 1 }
+     use_backend bk_allow if { req_ssl_sni -f allowed_sites }
+     default_backend bk_sorry_page
+
 req_ssl_ver <decimal>
   Returns true when data in the request buffer look like SSL, with a protocol
   version matching the specified range. Both SSLv2 hello messages and SSLv3
@@ -7642,18 +7671,6 @@ req_ssl_ver <decimal>
   that TLSv1 is announced as SSL version 3.1. This test was designed to be used
   with TCP request content inspection.
 
-req_ssl_hello_type <integer>
-  Returns true when data in the request buffer looks like a complete SSL (v3
-  or superior) hello message and handshake type is equal to <integer>.
-  This test was designed to be used with TCP request content inspection: an
-  SSL session ID may be fetched.
-
-rep_ssl_hello_type <integer>
-  Returns true when data in the response buffer looks like a complete SSL (v3
-  or superior) hello message and handshake type is equal to <integer>.
-  This test was designed to be used with TCP response content inspection: a
-  SSL session ID may be fetched.
-
 wait_end
   Waits for the end of the analysis period to return true. This may be used in
   conjunction with content analysis to avoid returning a wrong verdict early.
index 3546ef746456117265ce4a60dbcd43e426c5c788..f0966a51cb50a22407ad004801d5887815b48b9b 100644 (file)
--- a/src/acl.c
+++ b/src/acl.c
@@ -278,6 +278,175 @@ acl_fetch_req_ssl_ver(struct proxy *px, struct session *l4, void *l7, int dir,
        return 0;
 }
 
+/* Try to extract the Server Name Indication that may be presented in a TLS
+ * client hello handshake message. The format of the message is the following
+ * (cf RFC5246 + RFC6066) :
+ * TLS frame :
+ *   - uint8  type                            = 0x16   (Handshake)
+ *   - uint16 version                        >= 0x0301 (TLSv1)
+ *   - uint16 length                                   (frame length)
+ *   - TLS handshake :
+ *     - uint8  msg_type                      = 0x01   (ClientHello)
+ *     - uint24 length                                 (handshake message length)
+ *     - ClientHello :
+ *       - uint16 client_version             >= 0x0301 (TLSv1)
+ *       - uint8 Random[32]
+ *       - SessionID :
+ *         - uint8 session_id_len (0..32)              (SessionID len in bytes)
+ *         - uint8 session_id[session_id_len]
+ *       - CipherSuite :
+ *         - uint16 cipher_len               >= 2      (Cipher length in bytes)
+ *         - uint16 ciphers[cipher_len/2]
+ *       - CompressionMethod :
+ *         - uint8 compression_len           >= 1      (# of supported methods)
+ *         - uint8 compression_methods[compression_len]
+ *       - optional client_extension_len               (in bytes)
+ *       - optional sequence of ClientHelloExtensions  (as many bytes as above):
+ *         - uint16 extension_type            = 0 for server_name
+ *         - uint16 extension_len
+ *         - opaque extension_data[extension_len]
+ *           - uint16 server_name_list_len             (# of bytes here)
+ *           - opaque server_names[server_name_list_len bytes]
+ *             - uint8 name_type              = 0 for host_name
+ *             - uint16 name_len
+ *             - opaque hostname[name_len bytes]
+ */
+static int
+acl_fetch_ssl_hello_sni(struct proxy *px, struct session *l4, void *l7, int dir,
+                       struct acl_expr *expr, struct acl_test *test)
+{
+       int hs_len, ext_len, bleft;
+       struct buffer *b;
+       unsigned char *data;
+
+       if (!l4)
+               goto not_ssl_hello;
+
+       b = ((dir & ACL_DIR_MASK) == ACL_DIR_RTR) ? l4->rep : l4->req;
+
+       bleft = b->l;
+       data = (unsigned char *)b->w;
+
+       /* Check for SSL/TLS Handshake */
+       if (!bleft)
+               goto too_short;
+       if (*data != 0x16)
+               goto not_ssl_hello;
+
+       /* Check for TLSv1 or later (SSL version >= 3.1) */
+       if (bleft < 3)
+               goto too_short;
+       if (data[1] < 0x03 || data[2] < 0x01)
+               goto not_ssl_hello;
+
+       if (bleft < 5)
+               goto too_short;
+       hs_len = (data[3] << 8) + data[4];
+       if (hs_len < 1 + 3 + 2 + 32 + 1 + 2 + 2 + 1 + 1 + 2 + 2)
+               goto not_ssl_hello; /* too short to have an extension */
+
+       data += 5; /* enter TLS handshake */
+       bleft -= 5;
+
+       /* Check for a complete client hello starting at <data> */
+       if (bleft < 1)
+               goto too_short;
+       if (data[0] != 0x01) /* msg_type = Client Hello */
+               goto not_ssl_hello;
+
+       /* Check the Hello's length */
+       if (bleft < 4)
+               goto too_short;
+       hs_len = (data[1] << 16) + (data[2] << 8) + data[3];
+       if (hs_len < 2 + 32 + 1 + 2 + 2 + 1 + 1 + 2 + 2)
+               goto not_ssl_hello; /* too short to have an extension */
+
+       /* We want the full handshake here */
+       if (bleft < hs_len)
+               goto too_short;
+
+       data += 4;
+       /* Start of the ClientHello message */
+       if (data[0] < 0x03 || data[1] < 0x01) /* TLSv1 minimum */
+               goto not_ssl_hello;
+
+       ext_len = data[35];
+       if (ext_len > 32 || ext_len > (hs_len - 35)) /* check for correct session_id len */
+               goto not_ssl_hello;
+
+       /* Jump to cipher suite */
+       hs_len -= 35 + ext_len;
+       data   += 35 + ext_len;
+
+       if (hs_len < 4 ||                               /* minimum one cipher */
+           (ext_len = (data[0] << 8) + data[1]) < 2 || /* minimum 2 bytes for a cipher */
+           ext_len > hs_len)
+               goto not_ssl_hello;
+
+       /* Jump to the compression methods */
+       hs_len -= 2 + ext_len;
+       data   += 2 + ext_len;
+
+       if (hs_len < 2 ||                       /* minimum one compression method */
+           data[0] < 1 || data[0] > hs_len)    /* minimum 1 bytes for a method */
+               goto not_ssl_hello;
+
+       /* Jump to the extensions */
+       hs_len -= 1 + data[0];
+       data   += 1 + data[0];
+
+       if (hs_len < 2 ||                       /* minimum one extension list length */
+           (ext_len = (data[0] << 8) + data[1]) > hs_len - 2) /* list too long */
+               goto not_ssl_hello;
+
+       hs_len = ext_len; /* limit ourselves to the extension length */
+       data += 2;
+
+       while (hs_len >= 4) {
+               int ext_type, name_type, srv_len, name_len;
+
+               ext_type = (data[0] << 8) + data[1];
+               ext_len  = (data[2] << 8) + data[3];
+
+               if (ext_len > hs_len - 4) /* Extension too long */
+                       goto not_ssl_hello;
+
+               if (ext_type == 0) { /* Server name */
+                       if (ext_len < 2) /* need one list length */
+                               goto not_ssl_hello;
+
+                       srv_len = (data[4] << 8) + data[5];
+                       if (srv_len < 4 || srv_len > hs_len - 6)
+                               goto not_ssl_hello; /* at least 4 bytes per server name */
+
+                       name_type = data[6];
+                       name_len = (data[7] << 8) + data[8];
+
+                       if (name_type == 0) { /* hostname */
+                               test->ptr = data + 9;
+                               test->len = name_len;
+                               test->flags = ACL_TEST_F_VOLATILE;
+                               //fprintf(stderr, "found SNI : <");
+                               //write(2, test->ptr, test->len);
+                               //fprintf(stderr, ">\n");
+                               return 1;
+                       }
+               }
+
+               hs_len -= 4 + ext_len;
+               data   += 4 + ext_len;
+       }
+       /* server name not found */
+       goto not_ssl_hello;
+
+ too_short:
+       test->flags = ACL_TEST_F_MAY_CHANGE;
+
+ not_ssl_hello:
+
+       return 0;
+}
+
 /* Fetch the RDP cookie identified in the expression.
  * Note: this decoder only works with non-wrapping data.
  */
@@ -1884,6 +2053,7 @@ static struct acl_kw_list acl_kws = {{ },{
        { "req_ssl_ver",         acl_parse_dotted_ver, acl_fetch_req_ssl_ver,    acl_match_int,     ACL_USE_L6REQ_VOLATILE },
        { "req_rdp_cookie",      acl_parse_str,        acl_fetch_rdp_cookie,     acl_match_str,     ACL_USE_L6REQ_VOLATILE|ACL_MAY_LOOKUP },
        { "req_rdp_cookie_cnt",  acl_parse_int,        acl_fetch_rdp_cookie_cnt, acl_match_int,     ACL_USE_L6REQ_VOLATILE },
+       { "req_ssl_sni",         acl_parse_str,        acl_fetch_ssl_hello_sni,  acl_match_str,     ACL_USE_L6REQ_VOLATILE|ACL_MAY_LOOKUP },
 #if 0
        { "time",       acl_parse_time,  acl_fetch_time,   acl_match_time  },
 #endif