>From a3b372da2463e98b13e016c9b56344757b0e94bc Mon Sep 17 00:00:00 2001
From: Christopher Faulet <cfau...@qualys.com>
Date: Wed, 29 Jul 2015 16:01:57 +0200
Subject: [PATCH] MAJOR: ssl: add 'tcp-fallback' bind option for SSL listeners

This option can be use to fall back on TCP when a non-SSL connection is
established on a SSL listener. A delay must be defined to set the maximum time
to decide if an incoming connection is a SSL connection or a TCP connection. If
the timeout is reached or if the SSL detection failed, we fall back on TCP. By
setting it to 0, haproxy will wait infinitely more data to detect the connection
type.

If used, this option must be set on a ssl bind line:

  bind 0.0.0.0:1234 ssl ... tcp-fallback 250ms

It is a good idea to set a timeout if you expect to handle 'server-initiated'
protocols, like SMTP. Else the connection will be blocked on the SSL
detection. If you are sure to handle 'client-initiated' protocols only, it is
safe to set it to 0.

Internally, the detection is inverted. Each connection is started as a TCP
connection and we try to switch it in SSL by parsing the first few bytes sent by
the client to detect SSL ClientHello message.
---
 doc/configuration.txt      |  12 +++++
 include/proto/connection.h |   1 +
 include/proto/ssl_sock.h   |   1 +
 include/types/connection.h |   5 ++-
 include/types/listener.h   |  29 +++++++-----
 src/connection.c           |   4 ++
 src/listener.c             |   7 ++-
 src/session.c              |   7 ++-
 src/ssl_sock.c             | 107 ++++++++++++++++++++++++++++++++++++++++++++-
 9 files changed, 156 insertions(+), 17 deletions(-)

diff --git a/doc/configuration.txt b/doc/configuration.txt
index 7dd5744..89dcd0a 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -9960,6 +9960,18 @@ strict-sni
   a certificate. The default certificate is not used.
   See the "crt" option for more information.
 
+tcp-fallback <delay>
+  This setting is only available when support for OpenSSL was built in. It
+  enables a TCP fallback for SSL listeners when a non-SSL connection is
+  established or when the timeout is reached before the SSL ClientHello message
+  is received. By setting the delay to 0, haproxy will wait infinitely. It is a
+  good idea to set a delay if you expect to handle 'server-initiated'
+  protocols, like SMTP. Else the connection will be blocked, waiting a SSL
+  ClientHello message that could never be received. The drawback using this
+  option for such protocols is to add a latency for all non-SSL connections. If
+  you are sure to handle 'client-initiated' protocols only, it is safe to set
+  it to 0.
+
 tcp-ut <delay>
   Sets the TCP User Timeout for all incoming connections instanciated from this
   listening socket. This option is available on Linux since version 2.6.37. It
diff --git a/include/proto/connection.h b/include/proto/connection.h
index 952f9ea..b63aef1 100644
--- a/include/proto/connection.h
+++ b/include/proto/connection.h
@@ -482,6 +482,7 @@ static inline void conn_init(struct connection *conn)
 	conn->target = NULL;
 	conn->proxy_netns = NULL;
 	LIST_INIT(&conn->list);
+	conn->ssl_detection_exp = 0;
 }
 
 /* Tries to allocate a new connection and initialized its main fields. The
diff --git a/include/proto/ssl_sock.h b/include/proto/ssl_sock.h
index cb9a1e9..71dd727 100644
--- a/include/proto/ssl_sock.h
+++ b/include/proto/ssl_sock.h
@@ -43,6 +43,7 @@ int ssl_sock_is_ssl(struct connection *conn)
 }
 
 int ssl_sock_handshake(struct connection *conn, unsigned int flag);
+int ssl_sock_detection(struct connection *conn, int flag);
 int ssl_sock_prepare_ctx(struct bind_conf *bind_conf, SSL_CTX *ctx, struct proxy *proxy);
 int ssl_sock_prepare_all_ctx(struct bind_conf *bind_conf, struct proxy *px);
 int ssl_sock_prepare_srv_ctx(struct server *srv, struct proxy *px);
diff --git a/include/types/connection.h b/include/types/connection.h
index dfbff6a..185c630 100644
--- a/include/types/connection.h
+++ b/include/types/connection.h
@@ -107,10 +107,10 @@ enum {
 	CO_FL_SEND_PROXY    = 0x01000000,  /* send a valid PROXY protocol header */
 	CO_FL_SSL_WAIT_HS   = 0x02000000,  /* wait for an SSL handshake to complete */
 	CO_FL_ACCEPT_PROXY  = 0x04000000,  /* receive a valid PROXY protocol header */
-	/* unused : 0x08000000 */
+	CO_FL_SSL_DETECTION = 0x08000000,  /* try to detect ssl connection */
 
 	/* below we have all handshake flags grouped into one */
-	CO_FL_HANDSHAKE     = CO_FL_SEND_PROXY | CO_FL_SSL_WAIT_HS | CO_FL_ACCEPT_PROXY,
+	CO_FL_HANDSHAKE     = CO_FL_SEND_PROXY | CO_FL_SSL_WAIT_HS | CO_FL_ACCEPT_PROXY | CO_FL_SSL_DETECTION,
 
 	/* when any of these flags is set, polling is defined by socket-layer
 	 * operations, as opposed to data-layer. Transport is explicitly not
@@ -257,6 +257,7 @@ struct connection {
 	void *xprt_ctx;               /* general purpose pointer, initialized to NULL */
 	void *owner;                  /* pointer to upper layer's entity (eg: stream interface) */
 	int xprt_st;                  /* transport layer state, initialized to zero */
+	int ssl_detection_exp;        /* expiration date for the SSL detection */
 
 	union {                       /* definitions which depend on connection type */
 		struct {              /*** information used by socket-based connections ***/
diff --git a/include/types/listener.h b/include/types/listener.h
index 4da6cac..b1fcf5d 100644
--- a/include/types/listener.h
+++ b/include/types/listener.h
@@ -80,18 +80,21 @@ enum li_state {
  */
 
 /* listener socket options */
-#define LI_O_NONE	0x0000
-#define LI_O_NOLINGER	0x0001	/* disable linger on this socket */
-#define LI_O_FOREIGN	0x0002	/* permit listening on foreing addresses */
-#define LI_O_NOQUICKACK	0x0004	/* disable quick ack of immediate data (linux) */
-#define LI_O_DEF_ACCEPT	0x0008	/* wait up to 1 second for data before accepting */
-#define LI_O_TCP_RULES  0x0010  /* run TCP rules checks on the incoming connection */
-#define LI_O_CHK_MONNET 0x0020  /* check the source against a monitor-net rule */
-#define LI_O_ACC_PROXY  0x0040  /* find the proxied address in the first request line */
-#define LI_O_UNLIMITED  0x0080  /* listener not subject to global limits (peers & stats socket) */
-#define LI_O_TCP_FO     0x0100  /* enable TCP Fast Open (linux >= 3.7) */
-#define LI_O_V6ONLY     0x0200  /* bind to IPv6 only on Linux >= 2.4.21 */
-#define LI_O_V4V6       0x0400  /* bind to IPv4/IPv6 on Linux >= 2.4.21 */
+#define LI_O_NONE          0x0000
+#define LI_O_NOLINGER      0x0001  /* disable linger on this socket */
+#define LI_O_FOREIGN       0x0002  /* permit listening on foreing addresses */
+#define LI_O_NOQUICKACK    0x0004  /* disable quick ack of immediate data (linux) */
+#define LI_O_DEF_ACCEPT    0x0008  /* wait up to 1 second for data before accepting */
+#define LI_O_TCP_RULES     0x0010  /* run TCP rules checks on the incoming connection */
+#define LI_O_CHK_MONNET    0x0020  /* check the source against a monitor-net rule */
+#define LI_O_ACC_PROXY     0x0040  /* find the proxied address in the first request line */
+#define LI_O_UNLIMITED     0x0080  /* listener not subject to global limits (peers & stats socket) */
+#define LI_O_TCP_FO        0x0100  /* enable TCP Fast Open (linux >= 3.7) */
+#define LI_O_V6ONLY        0x0200  /* bind to IPv6 only on Linux >= 2.4.21 */
+#define LI_O_V4V6          0x0400  /* bind to IPv4/IPv6 on Linux >= 2.4.21 */
+#define LI_O_SSL_DETECTION 0x0800  /* enable SSL detection before starting SSL
+				    * handshake and fall back on TCP on failure
+				    * or timeout */
 
 /* Note: if a listener uses LI_O_UNLIMITED, it is highly recommended that it adds its own
  * maxconn setting to the global.maxsock value so that its resources are reserved.
@@ -139,6 +142,8 @@ struct bind_conf {
 
 	X509     *ca_sign_cert;    /* CA certificate referenced by ca_file */
 	EVP_PKEY *ca_sign_pkey;    /* CA private key referenced by ca_key */
+
+	unsigned int ssl_detection_delay;
 #endif
 	int is_ssl;                /* SSL is required for these listeners */
 	int generate_certs;        /* 1 if generate-certificates option is set, else 0 */
diff --git a/src/connection.c b/src/connection.c
index 6eab5e1..529ebdb 100644
--- a/src/connection.c
+++ b/src/connection.c
@@ -70,6 +70,10 @@ int conn_fd_handler(int fd)
 			if (!conn_si_send_proxy(conn, CO_FL_SEND_PROXY))
 				goto leave;
 #ifdef USE_OPENSSL
+		if (conn->flags & CO_FL_SSL_DETECTION)
+			if (!ssl_sock_detection(conn, CO_FL_SSL_DETECTION))
+				goto leave;
+
 		if (conn->flags & CO_FL_SSL_WAIT_HS)
 			if (!ssl_sock_handshake(conn, CO_FL_SSL_WAIT_HS))
 				goto leave;
diff --git a/src/listener.c b/src/listener.c
index 3759c78..fe3c469 100644
--- a/src/listener.c
+++ b/src/listener.c
@@ -495,7 +495,12 @@ void listener_accept(int fd)
 				global.sps_max = global.sess_per_sec.curr_ctr;
 		}
 #ifdef USE_OPENSSL
-		if (!(l->options & LI_O_UNLIMITED) && l->bind_conf && l->bind_conf->is_ssl) {
+		/* if tcp-fallback is enabled, we must be sure to have a ssl
+		 * connection before updating ssl/sec frequency, so we defer
+		 * it */
+		if (!(l->options & LI_O_SSL_DETECTION) &&
+		    !(l->options & LI_O_UNLIMITED)     &&
+		    l->bind_conf && l->bind_conf->is_ssl) {
 
 			update_freq_ctr(&global.ssl_per_sec, 1);
 			if (global.ssl_per_sec.curr_ctr > global.ssl_max)
diff --git a/src/session.c b/src/session.c
index 44fccbf..1763001 100644
--- a/src/session.c
+++ b/src/session.c
@@ -136,12 +136,17 @@ int session_accept_fd(struct listener *l, int cfd, struct sockaddr_storage *addr
 
 	conn_ctrl_init(cli_conn);
 
+	/* try to detect ssl connection */
+	if (l->options & LI_O_SSL_DETECTION) {
+		cli_conn->flags |= CO_FL_SSL_DETECTION;
+		conn_sock_want_recv(cli_conn);
+	}
+
 	/* wait for a PROXY protocol header */
 	if (l->options & LI_O_ACC_PROXY) {
 		cli_conn->flags |= CO_FL_ACCEPT_PROXY;
 		conn_sock_want_recv(cli_conn);
 	}
-
 	conn_data_want_recv(cli_conn);
 	if (conn_xprt_init(cli_conn) < 0)
 		goto out_free_conn;
diff --git a/src/ssl_sock.c b/src/ssl_sock.c
index 5cec6a4..8621b98 100644
--- a/src/ssl_sock.c
+++ b/src/ssl_sock.c
@@ -83,6 +83,7 @@
 #include <proto/server.h>
 #include <proto/log.h>
 #include <proto/proxy.h>
+#include <proto/raw_sock.h>
 #include <proto/shctx.h>
 #include <proto/ssl_sock.h>
 #include <proto/stream.h>
@@ -997,7 +998,6 @@ int ssl_sock_bind_verifycbk(int ok, X509_STORE_CTX *x_store)
 
 	ssl = X509_STORE_CTX_get_ex_data(x_store, SSL_get_ex_data_X509_STORE_CTX_idx());
 	conn = (struct connection *)SSL_get_app_data(ssl);
-
 	conn->xprt_st |= SSL_SOCK_ST_FL_VERIFY_DONE;
 
 	if (ok) /* no errors */
@@ -3547,6 +3547,87 @@ reneg_ok:
 	return 0;
 }
 
+
+int ssl_sock_detection(struct connection *conn, int flag)
+{
+	struct bind_conf *bind;
+
+	/* we might have been called just after an asynchronous shutr */
+	if (conn->flags & CO_FL_SOCK_RD_SH)
+		goto fail;
+	if (!conn_ctrl_ready(conn))
+		goto fail;
+	if (ssl_sock_is_ssl(conn))
+		goto done;
+
+	conn->flags |= CO_FL_WAIT_L6_CONN;
+	if (!fd_recv_ready(conn->t.sock.fd))
+		goto missing_data;
+	trash.len = recv(conn->t.sock.fd, trash.str, trash.size, MSG_PEEK);
+	if (trash.len < 0) {
+		if (errno == EAGAIN)
+			goto missing_data;
+		goto sock_error;
+	}
+	conn->flags &= ~CO_FL_WAIT_L6_CONN;
+
+	if (trash.len) {
+		unsigned char *b = (unsigned char *)trash.str;
+		if ((b[0] & 0x80) == 0x80) {
+			/* try to match SSLv2 */
+			if (trash.len >= 3 && (((b[0] & 0x7F) << 8 | b[1]) > 9) && b[2] == 0x01)
+				goto ssl_detected;
+		}
+		else if (b[0] == 0x16) {
+			/* try to match SSLv3, TLSv1.0, TLSv1.1 or TLSv1.2 */
+			if (trash.len >= 6 && b[1] == 0x03 && b[5] == 0x01)
+				goto ssl_detected;
+		}
+	}
+	goto done; /* TCP fallback */
+
+ ssl_detected:
+	conn->xprt = &ssl_sock;
+	conn->flags &= ~CO_FL_XPRT_READY;
+	conn_data_want_recv(conn);
+	if (conn_xprt_init(conn) < 0)
+		goto sock_error;
+
+	if (!(objt_listener(conn->target)->options & LI_O_UNLIMITED)) {
+		update_freq_ctr(&global.ssl_per_sec, 1);
+		if (global.ssl_per_sec.curr_ctr > global.ssl_max)
+			global.ssl_max = global.ssl_per_sec.curr_ctr;
+	}
+
+ done:
+	conn->flags &= ~flag;
+	return 1;
+
+ missing_data:
+	bind = objt_listener(conn->target)->bind_conf;
+	if (bind->ssl_detection_delay) {
+		if (!conn->ssl_detection_exp)
+			conn->ssl_detection_exp =
+				tick_add(now_ms, bind->ssl_detection_delay);
+		else if (tick_is_expired(conn->ssl_detection_exp, now_ms)) {
+			conn->flags &= ~CO_FL_WAIT_L6_CONN;
+			goto done; /* TCP fallback */
+		}
+	}
+	fd_may_recv(conn->t.sock.fd);
+	return 0;
+
+ sock_error:
+	conn->err_code = CO_ER_SOCK_ERR;
+	conn->flags |= CO_FL_SOCK_RD_SH | CO_FL_SOCK_WR_SH;
+
+ fail:
+	__conn_sock_stop_both(conn);
+	conn->flags |= CO_FL_ERROR;
+	conn->flags &= ~flag;
+	return 0;
+}
+
 /* Receive up to <count> bytes from connection <conn>'s socket and store them
  * into buffer <buf>. Only one call to recv() is performed, unless the
  * buffer wraps, in which case a second call may be performed. The connection's
@@ -5448,6 +5529,28 @@ static int bind_parse_verify(char **args, int cur_arg, struct proxy *px, struct
 	return 0;
 }
 
+static int bind_parse_tcp_fallback(char **args, int cur_arg, struct proxy *px, struct bind_conf *conf, char **err)
+{
+	struct listener *l;
+
+	if (!*args[cur_arg + 1]) {
+		if (err)
+			memprintf(err, "'%s' : missing tcp fallback timeout", args[cur_arg]);
+		return ERR_ALERT | ERR_FATAL;
+	}
+	if (parse_time_err(args[cur_arg+1], &conf->ssl_detection_delay, TIME_UNIT_MS)) {
+		memprintf(err, "'%s' : expects a positive timeout in milliseconds\n", args[cur_arg]);
+		return ERR_ALERT | ERR_FATAL;
+	}
+
+	/* Use raw socket operations here to do the SSL detection */
+	list_for_each_entry(l, &conf->listeners, by_bind) {
+		l->xprt     = &raw_sock;
+		l->options |= LI_O_SSL_DETECTION;
+	}
+	return 0;
+}
+
 /************** "server" keywords ****************/
 
 /* parse the "ca-file" server keyword */
@@ -5909,6 +6012,7 @@ static struct bind_kw_list bind_kws = { "SSL", { }, {
 	{ "tls-ticket-keys",       bind_parse_tls_ticket_keys, 1 }, /* set file to load TLS ticket keys from */
 	{ "verify",                bind_parse_verify,          1 }, /* set SSL verify method */
 	{ "npn",                   bind_parse_npn,             1 }, /* set NPN supported protocols */
+	{ "tcp-fallback",          bind_parse_tcp_fallback,    1 },  /* enable tcp fallback*/
 	{ NULL, NULL, 0 },
 }};
 
@@ -5962,6 +6066,7 @@ struct xprt_ops ssl_sock = {
 	.init     = ssl_sock_init,
 };
 
+
 #if (OPENSSL_VERSION_NUMBER >= 0x1000200fL && !defined OPENSSL_NO_TLSEXT && !defined OPENSSL_IS_BORINGSSL && !defined LIBRESSL_VERSION_NUMBER)
 
 static void ssl_sock_sctl_free_func(void *parent, void *ptr, CRYPTO_EX_DATA *ad, int idx, long argl, void *argp)
-- 
2.5.0

Reply via email to