MINOR: acl: add support for TLS server name matching using SNI

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.
This commit is contained in:
Willy Tarreau 2011-12-12 17:23:41 +01:00
parent 82a04566ec
commit b6672b547a
2 changed files with 199 additions and 12 deletions

View 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" through TCP request content inspection. Please see the "tcp-request content"
keyword for more detailed information on the subject. 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> req_len <integer>
Returns true when the length of the data in the request buffer matches the 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 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 of detecting the RDP protocol, as clients generally send the MSTS or MSTSHASH
cookies. 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> req_ssl_ver <decimal>
Returns true when data in the request buffer look like SSL, with a protocol 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 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 that TLSv1 is announced as SSL version 3.1. This test was designed to be used
with TCP request content inspection. 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 wait_end
Waits for the end of the analysis period to return true. This may be used in 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. conjunction with content analysis to avoid returning a wrong verdict early.

170
src/acl.c
View File

@ -278,6 +278,175 @@ acl_fetch_req_ssl_ver(struct proxy *px, struct session *l4, void *l7, int dir,
return 0; 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. /* Fetch the RDP cookie identified in the expression.
* Note: this decoder only works with non-wrapping data. * 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_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", 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_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 #if 0
{ "time", acl_parse_time, acl_fetch_time, acl_match_time }, { "time", acl_parse_time, acl_fetch_time, acl_match_time },
#endif #endif