MEDIUM: checks: add send/expect tcp based check

This is a generic health check which can be used to match a
banner or send a request and analyse a server response.
It works in a send/expect ways and many exchange can be done between
HAProxy and a server to decide the server status, making HAProxy able to
speak the server's protocol.

It can send arbitrary regular or binary strings and match content as a
regular or binary string or a regex.

Signed-off-by: Baptiste Assmann <bedis9@gmail.com>
diff --git a/doc/configuration.txt b/doc/configuration.txt
index 81b4295..1e71342 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -1144,6 +1144,9 @@
 http-check send-state                     X          -         X         X
 http-request                              -          X         X         X
 http-response                             -          X         X         X
+tcp-check expect                          -          -         X         X
+tcp-check send                            -          -         X         X
+tcp-check send-binary                     -          -         X         X
 http-send-name-header                     -          -         X         X
 id                                        -          X         X         X
 ignore-persist                            -          X         X         X
@@ -1165,6 +1168,7 @@
 option forceclose                    (*)  X          X         X         X
 -- keyword -------------------------- defaults - frontend - listen -- backend -
 option forwardfor                         X          X         X         X
+option tcp-check                          X          -         X         X
 option http-no-delay                 (*)  X          X         X         X
 option http-pretend-keepalive        (*)  X          X         X         X
 option http-server-close             (*)  X          X         X         X
@@ -2936,6 +2940,132 @@
   See also : "http-request", section 3.4 about userlists and section 7 about
              ACL usage.
 
+
+tcp-check expect [!] <match> <pattern>
+  Specify data to be collected and analysed during a generic health check
+  May be used in sections:   defaults | frontend | listen | backend
+                               no     |    no    |   yes  |   yes
+
+  Arguments :
+    <match>   is a keyword indicating how to look for a specific pattern in the
+              response. The keyword may be one of "string", "rstring" or
+              binary.
+              The keyword may be preceded by an exclamation mark ("!") to negate
+              the match. Spaces are allowed between the exclamation mark and the
+              keyword. See below for more details on the supported keywords.
+
+    <pattern> is the pattern to look for. It may be a string or a regular
+              expression. If the pattern contains spaces, they must be escaped
+              with the usual backslash ('\').
+              If the match is set to binary, then the pattern must be passed as
+              a serie of hexadecimal digits in an even number. Each sequence of
+              two digits will represent a byte. The hexadecimal digits may be
+              used upper or lower case.
+
+
+  The available matches are intentionally similar to their http-check cousins :
+
+    string <string> : test the exact string matches in the response buffer.
+                      A health check response will be considered valid if the
+                      response's buffer contains this exact string. If the
+                      "string" keyword is prefixed with "!", then the response
+                      will be considered invalid if the body contains this
+                      string. This can be used to look for a mandatory pattern
+                      in a protocol response, or to detect a failure when a
+                      specific error appears in a protocol banner.
+
+    rstring <regex> : test a regular expression on the response buffer.
+                      A health check response will be considered valid if the
+                      response's buffer matches this expression. If the
+                      "rstring" keyword is prefixed with "!", then the response
+                      will be considered invalid if the body matches the
+                      expression.
+
+    binary <hexstring> : test the exact string in its hexadecimal form matches
+                         in the response buffer. A health check response will
+                         be considered valid if the response's buffer contains
+                         this exact hexadecimal string.
+                         Purpose is to match data on binary protocols.
+
+  It is important to note that the responses will be limited to a certain size
+  defined by the global "tune.chksize" option, which defaults to 16384 bytes.
+  Thus, too large responses may not contain the mandatory pattern when using
+  "string", "rstring" or binary. If a large response is absolutely required, it
+  is possible to change the default max size by setting the global variable.
+  However, it is worth keeping in mind that parsing very large responses can
+  waste some CPU cycles, especially when regular expressions are used, and that
+  it is always better to focus the checks on smaller resources. Also, in its
+  current state, the check will not find any string nor regex past a null
+  character in the response. Similarly it is not possible to request matching
+  the null character.
+
+  Examples :
+         # perform a POP check
+         option tcp-check
+         tcp-check expect string +OK\ POP3\ ready
+
+         # perform an IMAP check
+         option tcp-check
+         tcp-check expect string *\ OK\ IMAP4\ ready
+
+         # look for the redis master server
+         option tcp-check
+         tcp-check send PING\r\n
+         tcp-check expect +PONG
+         tcp-check send info\ replication\r\n
+         tcp-check expect string role:master
+         tcp-check send QUIT\r\n
+         tcp-check expect string +OK
+
+
+  See also : "option tcp-check", "tcp-check send", "http-check expect",
+             tune.chksize
+
+
+tcp-check send <data>
+  Specify a string to be sent as a question during a generic health check
+  May be used in sections:   defaults | frontend | listen | backend
+                               no     |    no    |   yes  |   yes
+
+    <data> : the data to be sent as a question during a generic health check
+             session. For now, <data> must be a string.
+
+  Examples :
+         # look for the redis master server
+         option tcp-check
+         tcp-check send info\ replication\r\n
+         tcp-check expect string role:master
+
+  See also : "option tcp-check", "tcp-check expect", "tcp-check send-binary",
+             tune.chksize
+
+
+tcp-check send-binary <hexastring>
+  Specify an hexa digits string to be sent as a binary question during a raw
+  tcp health check
+  May be used in sections:   defaults | frontend | listen | backend
+                               no     |    no    |   yes  |   yes
+
+    <data> : the data to be sent as a question during a generic health check
+             session. For now, <data> must be a string.
+    <hexastring> : test the exact string in its hexadecimal form matches in the
+                   response buffer. A health check response will be considered
+                   valid if the response's buffer contains this exact
+                   hexadecimal string.
+                   Purpose is to send binary data to ask on binary protocols.
+
+  Examples :
+         # redis check in binary
+         option tcp-check
+         tcp-check send-binary 50494e470d0a # PING\r\n
+         tcp-check expect binary 2b504F4e47 # +PONG
+
+
+  See also : "option tcp-check", "tcp-check expect", "tcp-check send",
+             tune.chksize
+
+
+
 http-send-name-header [<header>]
   Add the server name to a request. Use the header string given by <header>
 
@@ -3631,6 +3761,75 @@
              "option forceclose"
 
 
+option tcp-check
+  Perform health checks using tcp-check send/expect sequences
+  May be used in sections:   defaults | frontend | listen | backend
+                               yes    |    no    |   yes  |   yes
+
+  This health check method is intended to be combined with "tcp-check" command
+  lists in order to support send/expect types of health check sequences.
+
+  TCP checks currently support 4 modes of operations :
+    - no "tcp-check" directive : the health check only consists in a connection
+      attempt, which remains the default mode.
+
+    - "tcp-check send" or "tcp-check send-binary" only is mentionned : this is
+      used to send a string along with a connection opening. With some
+      protocols, it helps sending a "QUIT" message for example that prevents
+      the server from logging a connection error for each health check. The
+      check result will still be based on the ability to open the connection
+      only.
+
+    - "tcp-check expect" only is mentionned : this is used to test a banner.
+      The connection is opened and haproxy waits for the server to present some
+      contents which must validate some rules. The check result will be based
+      on the matching between the contents and the rules. This is suited for
+      POP, IMAP, SMTP, FTP, SSH, TELNET.
+
+    - both "tcp-check send" and "tcp-check expect" are mentionned : this is
+      used to test a hello-type protocol. Haproxy sends a message, the server
+      responds and its response is analysed. the check result will be based on
+      the maching between the response contents and the rules. This is often
+      suited for protocols which require a binding or a request/response model.
+      LDAP, MySQL, Redis and SSL are example of such protocols, though they
+      already all have their dedicated checks with a deeper understanding of
+      the respective protocols.
+      In this mode, many questions may be sent and many answers may be
+      analysed.
+
+  Examples :
+         # perform a POP check (analyse only server's banner)
+         option tcp-check
+         tcp-check expect string +OK\ POP3\ ready
+
+         # perform an IMAP check (analyse only server's banner)
+         option tcp-check
+         tcp-check expect string *\ OK\ IMAP4\ ready
+
+         # look for the redis master server after ensuring it speaks well
+         # redis protocol, then it exits properly.
+         # (send a command then analyse the response 3 tims)
+         option tcp-check
+         tcp-check send PING\r\n
+         tcp-check expect +PONG
+         tcp-check send info\ replication\r\n
+         tcp-check expect string role:master
+         tcp-check send QUIT\r\n
+         tcp-check expect string +OK
+
+         forge a HTTP request, then analyse the response
+         (send many headers before analyzing)
+         option tcp-check
+         tcp-check send HEAD\ /\ HTTP/1.1\r\n
+         tcp-check send Host:\ www.mydomain.com\r\n
+         tcp-check send User-Agent:\ HAProxy\ tcpcheck\r\n
+         tcp-check send \r\n
+         tcp-check expect rstring HTTP/1\..\ (2..|3..)
+
+
+  See also : "tcp-check expect", "tcp-check send"
+
+
 option http-no-delay
 no option http-no-delay
   Instruct the system to favor low interactive delays over performance in HTTP
diff --git a/include/types/checks.h b/include/types/checks.h
index 09a4eee..d7d9725 100644
--- a/include/types/checks.h
+++ b/include/types/checks.h
@@ -106,4 +106,21 @@
 	unsigned char lr[HANA_OBS_SIZE];	/* result for l4/l7: 0 = ignore, 1 - error, 2 - OK */
 };
 
+/* bits for tcpcheck_rule->action */
+enum {
+	TCPCHK_ACT_SEND        = 1,             /* send action, regular string format */
+	TCPCHK_ACT_EXPECT,                      /* expect action, either regular or binary string */
+};
+
+struct tcpcheck_rule {
+	struct list list;                       /* list linked to from the proxy */
+	int action;                             /* action: send or expect */
+	/* match type uses NON-NULL pointer from either string or expect_regex below */
+	/* sent string is string */
+	char *string;                           /* sent or expected string */
+	int string_len;                         /* string lenght */
+	regex_t *expect_regex;                  /* expected */
+	int inverse;                            /* 0 = regular match, 1 = inverse match */
+};
+
 #endif /* _TYPES_CHECKS_H */
diff --git a/include/types/proxy.h b/include/types/proxy.h
index 8b9f2fb..4883827 100644
--- a/include/types/proxy.h
+++ b/include/types/proxy.h
@@ -152,7 +152,8 @@
 #define PR_O2_LDAP_CHK  0x60000000      /* use LDAP check for server health */
 #define PR_O2_SSL3_CHK  0x70000000      /* use SSLv3 CLIENT_HELLO packets for server health */
 #define PR_O2_LB_AGENT_CHK 0x80000000   /* use a TCP connection to obtain a metric of server health */
-/* unused: 0x90000000 to 0xF000000, reserved for health checks */
+#define PR_O2_TCPCHK_CHK 0x90000000     /* use TCPCHK check for server health */
+/* unused: 0xA0000000 to 0xF000000, reserved for health checks */
 #define PR_O2_CHK_ANY   0xF0000000      /* Mask to cover any check */
 /* end of proxy->options2 */
 
@@ -324,6 +325,7 @@
 
 	struct task *task;			/* the associated task, mandatory to manage rate limiting, stopping and resource shortage, NULL if disabled */
 	int grace;				/* grace time after stop request */
+	struct list tcpcheck_rules;		/* tcp-check send / expect rules */
 	char *check_req;			/* HTTP or SSL request to use for PR_O_HTTP_CHK|PR_O_SSL3_CHK */
 	int check_len;				/* Length of the HTTP or SSL3 request */
 	char *expect_str;			/* http-check expected content : string or text version of the regex */
diff --git a/include/types/server.h b/include/types/server.h
index 6b73a39..82eb55f 100644
--- a/include/types/server.h
+++ b/include/types/server.h
@@ -122,6 +122,7 @@
 	char desc[HCHK_DESC_LEN];		/* health check descritpion */
 	int use_ssl;				/* use SSL for health checks */
 	int send_proxy;				/* send a PROXY protocol header with checks */
+	struct tcpcheck_rule *current_step;     /* current step when using tcpcheck */
 	int inter, fastinter, downinter;        /* checks: time in milliseconds */
 	int result;				/* health-check result : SRV_CHK_* */
 	int state;				/* health-check result : CHK_* */
diff --git a/src/cfgparse.c b/src/cfgparse.c
index e267de8..73a08a4 100644
--- a/src/cfgparse.c
+++ b/src/cfgparse.c
@@ -3732,6 +3732,16 @@
 			memcpy(curproxy->check_req, DEF_LDAP_CHECK_REQ, sizeof(DEF_LDAP_CHECK_REQ) - 1);
 			curproxy->check_len = sizeof(DEF_LDAP_CHECK_REQ) - 1;
 		}
+		else if (!strcmp(args[1], "tcp-check")) {
+			/* use raw TCPCHK send/expect to check servers' health */
+			if (warnifnotcap(curproxy, PR_CAP_BE, file, linenum, args[1], NULL))
+				err_code |= ERR_WARN;
+
+			free(curproxy->check_req);
+			curproxy->check_req = NULL;
+			curproxy->options2 &= ~PR_O2_CHK_ANY;
+			curproxy->options2 |= PR_O2_TCPCHK_CHK;
+		}
 		else if (!strcmp(args[1], "forwardfor")) {
 			int cur_arg;
 
@@ -3969,6 +3979,161 @@
 			goto out;
 		}
 	}
+	else if (!strcmp(args[0], "tcp-check")) {
+		if (warnifnotcap(curproxy, PR_CAP_BE, file, linenum, args[0], NULL))
+			err_code |= ERR_WARN;
+
+		if (strcmp(args[1], "send") == 0) {
+			if (! *(args[2]) ) {
+				/* SEND string expected */
+				Alert("parsing [%s:%d] : '%s %s %s' expects <STRING> as argument.\n",
+				      file, linenum, args[0], args[1], args[2]);
+				err_code |= ERR_ALERT | ERR_FATAL;
+				goto out;
+			} else {
+				struct tcpcheck_rule *tcpcheck;
+
+				tcpcheck = (struct tcpcheck_rule *)calloc(1, sizeof(*tcpcheck));
+
+				tcpcheck->action = TCPCHK_ACT_SEND;
+				tcpcheck->string_len = strlen(args[2]);
+				tcpcheck->string = strdup(args[2]);
+				tcpcheck->expect_regex = NULL;
+
+				LIST_ADDQ(&curproxy->tcpcheck_rules, &tcpcheck->list);
+			}
+		}
+		else if (strcmp(args[1], "send-binary") == 0) {
+			if (! *(args[2]) ) {
+				/* SEND binary string expected */
+				Alert("parsing [%s:%d] : '%s %s %s' expects <BINARY STRING> as argument.\n",
+				      file, linenum, args[0], args[1], args[2]);
+				err_code |= ERR_ALERT | ERR_FATAL;
+				goto out;
+			} else {
+				struct tcpcheck_rule *tcpcheck;
+				char *err = NULL;
+
+				tcpcheck = (struct tcpcheck_rule *)calloc(1, sizeof(*tcpcheck));
+
+				tcpcheck->action = TCPCHK_ACT_SEND;
+				if (parse_binary(args[2], &tcpcheck->string, &tcpcheck->string_len, &err) == 0) {
+					Alert("parsing [%s:%d] : '%s %s %s' expects <BINARY STRING> as argument, but %s\n",
+					      file, linenum, args[0], args[1], args[2], err);
+					err_code |= ERR_ALERT | ERR_FATAL;
+					goto out;
+				}
+				tcpcheck->expect_regex = NULL;
+
+				LIST_ADDQ(&curproxy->tcpcheck_rules, &tcpcheck->list);
+			}
+		}
+		else if (strcmp(args[1], "expect") == 0) {
+			const char *ptr_arg;
+			int cur_arg;
+			int inverse = 0;
+
+			if (curproxy->options2 & PR_O2_EXP_TYPE) {
+				Alert("parsing [%s:%d] : '%s %s' already specified.\n", file, linenum, args[0], args[1]);
+				err_code |= ERR_ALERT | ERR_FATAL;
+				goto out;
+			}
+
+			cur_arg = 2;
+			/* consider exclamation marks, sole or at the beginning of a word */
+			while (*(ptr_arg = args[cur_arg])) {
+				while (*ptr_arg == '!') {
+					inverse = !inverse;
+					ptr_arg++;
+				}
+				if (*ptr_arg)
+					break;
+				cur_arg++;
+			}
+			/* now ptr_arg points to the beginning of a word past any possible
+			 * exclamation mark, and cur_arg is the argument which holds this word.
+			 */
+			if (strcmp(ptr_arg, "binary") == 0) {
+				if (!*(args[cur_arg + 1])) {
+					Alert("parsing [%s:%d] : '%s %s %s' expects <binary string> as an argument.\n",
+					      file, linenum, args[0], args[1], ptr_arg);
+					err_code |= ERR_ALERT | ERR_FATAL;
+					goto out;
+				}
+				struct tcpcheck_rule *tcpcheck;
+				char *err = NULL;
+
+				tcpcheck = (struct tcpcheck_rule *)calloc(1, sizeof(*tcpcheck));
+
+				tcpcheck->action = TCPCHK_ACT_EXPECT;
+				if (parse_binary(args[cur_arg + 1], &tcpcheck->string, &tcpcheck->string_len, &err) == 0) {
+					Alert("parsing [%s:%d] : '%s %s %s' expects <BINARY STRING> as argument, but %s\n",
+					      file, linenum, args[0], args[1], args[2], err);
+					err_code |= ERR_ALERT | ERR_FATAL;
+					goto out;
+				}
+				tcpcheck->expect_regex = NULL;
+				tcpcheck->inverse = inverse;
+
+				LIST_ADDQ(&curproxy->tcpcheck_rules, &tcpcheck->list);
+			}
+			else if (strcmp(ptr_arg, "string") == 0) {
+				if (!*(args[cur_arg + 1])) {
+					Alert("parsing [%s:%d] : '%s %s %s' expects <string> as an argument.\n",
+					      file, linenum, args[0], args[1], ptr_arg);
+					err_code |= ERR_ALERT | ERR_FATAL;
+					goto out;
+				}
+				struct tcpcheck_rule *tcpcheck;
+
+				tcpcheck = (struct tcpcheck_rule *)calloc(1, sizeof(*tcpcheck));
+
+				tcpcheck->action = TCPCHK_ACT_EXPECT;
+				tcpcheck->string_len = strlen(args[cur_arg + 1]);
+				tcpcheck->string = strdup(args[cur_arg + 1]);
+				tcpcheck->expect_regex = NULL;
+				tcpcheck->inverse = inverse;
+
+				LIST_ADDQ(&curproxy->tcpcheck_rules, &tcpcheck->list);
+			}
+			else if (strcmp(ptr_arg, "rstring") == 0) {
+				if (!*(args[cur_arg + 1])) {
+					Alert("parsing [%s:%d] : '%s %s %s' expects <regex> as an argument.\n",
+					      file, linenum, args[0], args[1], ptr_arg);
+					err_code |= ERR_ALERT | ERR_FATAL;
+					goto out;
+				}
+				struct tcpcheck_rule *tcpcheck;
+
+				tcpcheck = (struct tcpcheck_rule *)calloc(1, sizeof(*tcpcheck));
+
+				tcpcheck->action = TCPCHK_ACT_EXPECT;
+				tcpcheck->string_len = 0;
+				tcpcheck->string = NULL;
+				tcpcheck->expect_regex = calloc(1, sizeof(regex_t));
+				if (regcomp(tcpcheck->expect_regex, args[cur_arg + 1], REG_EXTENDED) != 0) {
+					Alert("parsing [%s:%d] : '%s %s %s' : bad regular expression '%s'.\n",
+					      file, linenum, args[0], args[1], ptr_arg, args[cur_arg + 1]);
+					err_code |= ERR_ALERT | ERR_FATAL;
+					goto out;
+				}
+				tcpcheck->inverse = inverse;
+
+				LIST_ADDQ(&curproxy->tcpcheck_rules, &tcpcheck->list);
+			}
+			else {
+				Alert("parsing [%s:%d] : '%s %s' only supports [!] 'binary', 'string', 'rstring', found '%s'.\n",
+				      file, linenum, args[0], args[1], ptr_arg);
+				err_code |= ERR_ALERT | ERR_FATAL;
+				goto out;
+			}
+		}
+		else {
+			Alert("parsing [%s:%d] : '%s' only supports 'send' or 'expect'.\n", file, linenum, args[0]);
+			err_code |= ERR_ALERT | ERR_FATAL;
+			goto out;
+		}
+	}
 	else if (!strcmp(args[0], "monitor")) {
 		if (curproxy == &defproxy) {
 			Alert("parsing [%s:%d] : '%s' not allowed in 'defaults' section.\n", file, linenum, args[0]);
diff --git a/src/checks.c b/src/checks.c
index 56f3537..10aa41a 100644
--- a/src/checks.c
+++ b/src/checks.c
@@ -52,6 +52,8 @@
 #include <proto/task.h>
 
 static int httpchk_expect(struct server *s, int done);
+static int tcpcheck_get_step_id(struct server *);
+static void tcpcheck_main(struct connection *);
 
 static const struct check_status check_statuses[HCHK_STATUS_SIZE] = {
 	[HCHK_STATUS_UNKNOWN]	= { SRV_CHK_UNKNOWN,                   "UNK",     "Unknown" },
@@ -834,6 +836,7 @@
 {
 	struct check *check = conn->owner;
 	const char *err_msg;
+	struct chunk *chk;
 
 	if (check->result != SRV_CHK_UNKNOWN)
 		return;
@@ -850,20 +853,36 @@
 	 * socket error possibly collected above. This is useful to know the
 	 * exact step of the L6 layer (eg: SSL handshake).
 	 */
+	chk = get_trash_chunk();
+
+	if (check->type == PR_O2_TCPCHK_CHK) {
+		chunk_printf(chk, " at step %d of tcp-check", tcpcheck_get_step_id(check->server));
+		/* we were looking for a string */
+		if (check->current_step && check->current_step->action == TCPCHK_ACT_EXPECT) {
+			if (check->current_step->string)
+				chunk_appendf(chk, " (string '%s')", check->current_step->string);
+			else if (check->current_step->expect_regex)
+				chunk_appendf(chk, " (expect regex)");
+		}
+		else if (check->current_step && check->current_step->action == TCPCHK_ACT_SEND) {
+			chunk_appendf(chk, " (send)");
+		}
+	}
+
 	if (conn->err_code) {
 		if (errno && errno != EAGAIN)
-			chunk_printf(&trash, "%s (%s)", conn_err_code_str(conn), strerror(errno));
+			chunk_printf(&trash, "%s (%s)%s", conn_err_code_str(conn), strerror(errno), chk->str);
 		else
-			chunk_printf(&trash, "%s", conn_err_code_str(conn));
+			chunk_printf(&trash, "%s%s", conn_err_code_str(conn), chk->str);
 		err_msg = trash.str;
 	}
 	else {
 		if (errno && errno != EAGAIN) {
-			chunk_printf(&trash, "%s", strerror(errno));
+			chunk_printf(&trash, "%s%s", strerror(errno), chk->str);
 			err_msg = trash.str;
 		}
 		else {
-			err_msg = NULL;
+			err_msg = chk->str;
 		}
 	}
 
@@ -933,6 +952,11 @@
 	if (!check->type)
 		goto out_wakeup;
 
+	if (check->type == PR_O2_TCPCHK_CHK) {
+		tcpcheck_main(conn);
+		return;
+	}
+
 	if (check->bo->o) {
 		conn->xprt->snd_buf(conn, check->bo, MSG_DONTWAIT | MSG_NOSIGNAL);
 		if (conn->flags & CO_FL_ERROR) {
@@ -985,6 +1009,11 @@
 
 	if (conn->flags & (CO_FL_HANDSHAKE | CO_FL_WAIT_RD))
 		return;
+
+	if (check->type == PR_O2_TCPCHK_CHK) {
+		tcpcheck_main(conn);
+		return;
+	}
 
 	/* Warning! Linux returns EAGAIN on SO_ERROR if data are still available
 	 * but the connection was closed on the remote end. Fortunately, recv still
@@ -1481,11 +1510,17 @@
 		check->bo->p = check->bo->data;
 		check->bo->o = 0;
 
-	       /* prepare the check buffer
-	        * This should not be used if check is the secondary agent check
-	        * of a server as s->proxy->check_req will relate to the
-	        * configuration of the primary check */
-	       if (check->type && check != &s->agent) {
+		/* tcpcheck send/expect initialisation */
+		if (check->type == PR_O2_TCPCHK_CHK)
+			check->current_step = NULL;
+
+		/* prepare the check buffer.
+		 * This should not be used if check is the secondary agent check
+		 * of a server as s->proxy->check_req will relate to the
+		 * configuration of the primary check. Similarly, tcp-check uses
+		 * its own strings.
+		 */
+		if (check->type && check->type != PR_O2_TCPCHK_CHK && check != &s->agent) {
 			bo_putblk(check->bo, s->proxy->check_req, s->proxy->check_len);
 
 			/* we want to check if this host replies to HTTP or SSLv3 requests
@@ -1863,6 +1898,278 @@
 	return 1;
 }
 
+/*
+ * return the id of a step in a send/expect session
+ */
+static int tcpcheck_get_step_id(struct server *s)
+{
+	struct tcpcheck_rule *cur = NULL, *next = NULL;
+	int i = 0;
+
+	cur = s->check.current_step;
+
+	/* no step => first step */
+	if (cur == NULL)
+		return 1;
+
+	/* increment i until current step */
+	list_for_each_entry(next, &s->proxy->tcpcheck_rules, list) {
+		if (next->list.p == &cur->list)
+			break;
+		++i;
+	}
+
+	return i;
+}
+
+static void tcpcheck_main(struct connection *conn)
+{
+	char *contentptr;
+	unsigned int contentlen;
+	struct list *head = NULL;
+	struct tcpcheck_rule *cur = NULL;
+	int done = 0, ret = 0;
+
+	struct check *check = conn->owner;
+	struct server *s = check->server;
+	struct task *t = check->task;
+
+	/* don't do anything until the connection is established */
+	if (!(conn->flags & CO_FL_CONNECTED)) {
+		/* update expire time, should be done by process_chk */
+		/* we allow up to min(inter, timeout.connect) for a connection
+		 * to establish but only when timeout.check is set
+		 * as it may be to short for a full check otherwise
+		 */
+		while (tick_is_expired(t->expire, now_ms)) {
+			int t_con;
+
+			t_con = tick_add(t->expire, s->proxy->timeout.connect);
+			t->expire = tick_add(t->expire, MS_TO_TICKS(check->inter));
+
+			if (s->proxy->timeout.check)
+				t->expire = tick_first(t->expire, t_con);
+		}
+		return;
+	}
+
+	/* here, we know that the connection is established */
+	if (check->result & (SRV_CHK_FAILED | SRV_CHK_PASSED)) {
+		goto out_end_tcpcheck;
+	}
+
+	/* head is be the first element of the double chained list */
+	head = &s->proxy->tcpcheck_rules;
+
+	/* no step means first step
+	 * initialisation */
+	if (check->current_step == NULL) {
+		check->bo->p = check->bo->data;
+		check->bo->o = 0;
+		check->bi->p = check->bi->data;
+		check->bi->i = 0;
+		cur = check->current_step = LIST_ELEM(head->n, struct tcpcheck_rule *, list);
+		t->expire = tick_add(now_ms, MS_TO_TICKS(check->inter));
+		if (s->proxy->timeout.check)
+			t->expire = tick_add_ifset(now_ms, s->proxy->timeout.check);
+	}
+	/* keep on processing step */
+	else {
+		cur = check->current_step;
+	}
+
+	while (&cur->list != head) {
+		if (check->current_step->action & TCPCHK_ACT_SEND) {
+			/* reset the read buffer */
+			if (*check->bi->data != '\0') {
+				*check->bi->data = '\0';
+				check->bi->i = 0;
+			}
+
+			if (conn->flags & (CO_FL_SOCK_WR_SH | CO_FL_DATA_WR_SH)) {
+				conn->flags |= CO_FL_ERROR;
+				chk_report_conn_err(conn, 0, 0);
+				goto out_end_tcpcheck;
+			}
+
+			if (conn->flags & (CO_FL_HANDSHAKE | CO_FL_WAIT_WR))
+				return;
+
+			/* disable reading for now */
+			//if (conn->flags & (CO_FL_WAIT_RD | CO_FL_DATA_RD_ENA))
+			__conn_data_stop_recv(conn);
+
+			bo_putblk(check->bo, check->current_step->string, check->current_step->string_len);
+			*check->bo->p = '\0'; /* to make gdb output easier to read */
+
+			if (check->bo->o) {
+				conn->xprt->snd_buf(conn, check->bo, MSG_DONTWAIT | MSG_NOSIGNAL);
+				if (conn->flags & CO_FL_ERROR) {
+					chk_report_conn_err(conn, errno, 0);
+					__conn_data_stop_both(conn);
+					goto out_end_tcpcheck;
+				}
+			}
+			cur = (struct tcpcheck_rule *)cur->list.n;
+			check->current_step = cur;
+
+			if (check->bo->o)
+				goto out_incomplete;
+
+			__conn_data_stop_send(conn);   /* nothing more to write */
+		} /* end 'send' */
+		else if (check->current_step->action & TCPCHK_ACT_EXPECT) {
+			if (unlikely(check->result & SRV_CHK_FAILED))
+				goto out_end_tcpcheck;
+
+			if (conn->flags & (CO_FL_HANDSHAKE | CO_FL_WAIT_RD))
+				return;
+
+			conn->xprt->rcv_buf(conn, check->bi, buffer_total_space(check->bi));
+
+			if (conn->flags & (CO_FL_ERROR | CO_FL_SOCK_RD_SH | CO_FL_DATA_RD_SH)) {
+				done = 1;
+				if ((conn->flags & CO_FL_ERROR) && !check->bi->i) {
+					/* Report network errors only if we got no other data. Otherwise
+					 * we'll let the upper layers decide whether the response is OK
+					 * or not. It is very common that an RST sent by the server is
+					 * reported as an error just after the last data chunk.
+					 */
+					chk_report_conn_err(conn, errno, 0);
+					goto out_end_tcpcheck;
+				}
+			}
+
+			/* Intermediate or complete response received.
+			 * Terminate string in check->bi->data buffer.
+			 */
+			if (check->bi->i < check->bi->size) {
+				check->bi->data[check->bi->i] = '\0';
+			}
+			else {
+				check->bi->data[check->bi->i - 1] = '\0';
+				done = 1; /* buffer full, don't wait for more data */
+			}
+
+			contentptr = check->bi->data;
+			contentlen = check->bi->i;
+
+			/* Check that response body is not empty... */
+			if (*contentptr == '\0') {
+				if (!done)
+					return;
+
+				/* empty response */
+				chunk_printf(&trash, "TCPCHK got an empty response at step %d",
+						tcpcheck_get_step_id(s));
+				set_server_check_status(check, HCHK_STATUS_L7RSP, trash.str);
+
+				goto out_end_tcpcheck;
+			}
+
+			if (!done && (cur->string != NULL) && (check->bi->i < cur->string_len) )
+				goto wait_more_data;
+
+tcpcheck_expect:
+			if (cur->string != NULL)
+				ret = my_memmem(contentptr, contentlen, cur->string, cur->string_len) != NULL;
+			else if (cur->expect_regex != NULL)
+				ret = regexec(cur->expect_regex, contentptr, MAX_MATCH, pmatch, 0) == 0;
+
+			if (!ret && !done)
+				goto wait_more_data;
+
+			/* matched */
+			if (ret) {
+				/* matched but we did not want to => ERROR */
+				if (cur->inverse) {
+					/* we were looking for a string */
+					if (cur->string != NULL) {
+						chunk_printf(&trash, "TCPCHK matched unwanted content '%s' at step %d",
+								cur->string, tcpcheck_get_step_id(s));
+					}
+					else {
+					/* we were looking for a regex */
+						chunk_printf(&trash, "TCPCHK matched unwanted content (regex) at step %d",
+								tcpcheck_get_step_id(s));
+					}
+					set_server_check_status(check, HCHK_STATUS_L7RSP, trash.str);
+					goto out_end_tcpcheck;
+				}
+				/* matched and was supposed to => OK, next step */
+				else {
+					cur = (struct tcpcheck_rule*)cur->list.n;
+					check->current_step = cur;
+					if (check->current_step->action & TCPCHK_ACT_EXPECT)
+						goto tcpcheck_expect;
+					__conn_data_stop_recv(conn);
+				}
+			}
+			else {
+			/* not matched */
+				/* not matched and was not supposed to => OK, next step */
+				if (cur->inverse) {
+					cur = (struct tcpcheck_rule*)cur->list.n;
+					check->current_step = cur;
+					if (check->current_step->action & TCPCHK_ACT_EXPECT)
+						goto tcpcheck_expect;
+					__conn_data_stop_recv(conn);
+				}
+				/* not matched but was supposed to => ERROR */
+				else {
+					/* we were looking for a string */
+					if (cur->string != NULL) {
+						chunk_printf(&trash, "TCPCHK did not match content '%s' at step %d",
+								cur->string, tcpcheck_get_step_id(s));
+					}
+					else {
+					/* we were looking for a regex */
+						chunk_printf(&trash, "TCPCHK did not match content (regex) at step %d",
+								tcpcheck_get_step_id(s));
+					}
+					set_server_check_status(check, HCHK_STATUS_L7RSP, trash.str);
+					goto out_end_tcpcheck;
+				}
+			}
+		} /* end expect */
+	} /* end loop over double chained step list */
+
+	set_server_check_status(check, HCHK_STATUS_L7OKD, "(tcp-check)");
+	goto out_end_tcpcheck;
+
+ wait_more_data:
+	__conn_data_poll_recv(conn);
+	return;
+
+ out_incomplete:
+	return;
+
+ out_end_tcpcheck:
+	/* collect possible new errors */
+	if (conn->flags & CO_FL_ERROR)
+		chk_report_conn_err(conn, 0, 0);
+
+	/* Close the connection... We absolutely want to perform a hard close
+	 * and reset the connection if some data are pending, otherwise we end
+	 * up with many TIME_WAITs and eat all the source port range quickly.
+	 * To avoid sending RSTs all the time, we first try to drain pending
+	 * data.
+	 */
+	if (conn->xprt && conn->xprt->shutw)
+		conn->xprt->shutw(conn, 0);
+
+	check->current_step = NULL;
+
+	if (check->result & SRV_CHK_FAILED)
+		conn->flags |= CO_FL_ERROR;
+
+	__conn_data_stop_both(conn);
+	task_wakeup(t, TASK_WOKEN_IO);
+
+	return;
+}
+
+
 /*
  * Local variables:
  *  c-indent-level: 8
diff --git a/src/proxy.c b/src/proxy.c
index 37bda48..f0863d6 100644
--- a/src/proxy.c
+++ b/src/proxy.c
@@ -454,6 +454,7 @@
 	LIST_INIT(&p->conf.bind);
 	LIST_INIT(&p->conf.listeners);
 	LIST_INIT(&p->conf.args.list);
+	LIST_INIT(&p->tcpcheck_rules);
 
 	/* Timeouts are defined as -1 */
 	proxy_reset_timeouts(p);