MEDIUM: tcp-check new feature: connect
A new tcp-check rule type: connect.
It allows HAProxy to test applications which stand on multiple ports or
multiple applications load-balanced through the same backend.
diff --git a/doc/configuration.txt b/doc/configuration.txt
index b881427..524b83d 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -1220,6 +1220,7 @@
http-check send-state X - X X
http-request - X X X
http-response - X X X
+tcp-check connect - - X X
tcp-check expect - - X X
tcp-check send - - X X
tcp-check send-binary - - X X
@@ -3021,6 +3022,63 @@
ACL usage.
+tcp-check connect [params*]
+ Opens a new connection
+ May be used in sections: defaults | frontend | listen | backend
+ no | no | yes | yes
+
+ When an application lies on more than a single TCP port or when HAProxy
+ load-balance many services in a single backend, it makes sense to probe all
+ the services individually before considering a server as operational.
+
+ When there are no TCP port configured on the server line neither server port
+ directive, then the 'tcp-check connect port <port>' must be the first step
+ of the sequence.
+
+ In a tcp-check ruleset a 'connect' is required, it is also mandatory to start
+ the ruleset with a 'connect' rule. Purpose is to ensure admin know what they
+ do.
+
+ Parameters :
+ They are optional and can be used to describe how HAProxy should open and
+ use the TCP connection.
+
+ port if not set, check port or server port is used.
+ It tells HAProxy where to open the connection to.
+ <port> must be a valid TCP port source integer, from 1 to 65535.
+
+ send-proxy send a PROXY protocol string
+
+ ssl opens a ciphered connection
+
+ Examples:
+ # check HTTP and HTTPs services on a server.
+ # first open port 80 thanks to server line port directive, then
+ # tcp-check opens port 443, ciphered and run a request on it:
+ option tcp-check
+ tcp-check connect
+ tcp-check send GET\ /\ HTTP/1.0\r\n
+ tcp-check send Host:\ haproxy.1wt.eu\r\n
+ tcp-check send \r\n
+ tcp-check expect rstring (2..|3..)
+ tcp-check connect port 443 ssl
+ tcp-check send GET\ /\ HTTP/1.0\r\n
+ tcp-check send Host:\ haproxy.1wt.eu\r\n
+ tcp-check send \r\n
+ tcp-check expect rstring (2..|3..)
+ server www 10.0.0.1 check port 80
+
+ # check both POP and IMAP from a single server:
+ option tcp-check
+ tcp-check connect port 110
+ tcp-check expect string +OK\ POP3\ ready
+ tcp-check connect port 143
+ tcp-check expect string *\ OK\ IMAP4\ ready
+ server mail 10.0.0.1 check
+
+ See also : "option tcp-check", "tcp-check send", "tcp-check expect"
+
+
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
@@ -3098,8 +3156,8 @@
tcp-check expect string +OK
- See also : "option tcp-check", "tcp-check send", "http-check expect",
- tune.chksize
+ See also : "option tcp-check", "tcp-check connect", "tcp-check send",
+ "tcp-check send-binary", "http-check expect", tune.chksize
tcp-check send <data>
@@ -3116,8 +3174,8 @@
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
+ See also : "option tcp-check", "tcp-check connect", "tcp-check expect",
+ "tcp-check send-binary", tune.chksize
tcp-check send-binary <hexastring>
@@ -3141,9 +3199,8 @@
tcp-check expect binary 2b504F4e47 # +PONG
- See also : "option tcp-check", "tcp-check expect", "tcp-check send",
- tune.chksize
-
+ See also : "option tcp-check", "tcp-check connect", "tcp-check expect",
+ "tcp-check send", tune.chksize
http-send-name-header [<header>]
diff --git a/include/types/checks.h b/include/types/checks.h
index 7db68b4..83ea2b5 100644
--- a/include/types/checks.h
+++ b/include/types/checks.h
@@ -135,6 +135,7 @@
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 */
+ struct tcpcheck_rule *last_started_step;/* pointer to latest tcpcheck rule started */
int inter, fastinter, downinter; /* checks: time in milliseconds */
enum chk_result result; /* health-check result : CHK_RES_* */
int state; /* state of the check : CHK_ST_* */
@@ -160,8 +161,14 @@
enum {
TCPCHK_ACT_SEND = 0, /* send action, regular string format */
TCPCHK_ACT_EXPECT, /* expect action, either regular or binary string */
+ TCPCHK_ACT_CONNECT, /* connect action, to probe a new port */
};
+/* flags used by tcpcheck_rule->conn_opts */
+#define TCPCHK_OPT_NONE 0x0000 /* no options specified, default */
+#define TCPCHK_OPT_SEND_PROXY 0x0001 /* send proxy-protocol string */
+#define TCPCHK_OPT_SSL 0x0002 /* SSL connection */
+
struct tcpcheck_rule {
struct list list; /* list linked to from the proxy */
int action; /* action: send or expect */
@@ -171,6 +178,8 @@
int string_len; /* string lenght */
regex_t *expect_regex; /* expected */
int inverse; /* 0 = regular match, 1 = inverse match */
+ unsigned short port; /* port to connect to */
+ unsigned short conn_opts; /* options when setting up a new connection */
};
#endif /* _TYPES_CHECKS_H */
diff --git a/include/types/proxy.h b/include/types/proxy.h
index 3f2b814..af2a3ab 100644
--- a/include/types/proxy.h
+++ b/include/types/proxy.h
@@ -104,8 +104,7 @@
#define PR_O_HTTP_TUN 0x04000000 /* HTTP tunnel mode : no analysis past first request/response */
/* unassigned values : 0x05000000, 0x06000000, 0x07000000 */
#define PR_O_HTTP_MODE 0x07000000 /* MASK to retrieve the HTTP mode */
-
-/* unused: 0x08000000 */
+#define PR_O_TCPCHK_SSL 0x08000000 /* at least one TCPCHECK connect rule requires SSL */
#define PR_O_CONTSTATS 0x10000000 /* continous counters */
#define PR_O_HTTP_PROXY 0x20000000 /* Enable session to use HTTP proxy operations */
#define PR_O_DISABLE404 0x40000000 /* Disable a server on a 404 response to a health-check */
diff --git a/src/cfgparse.c b/src/cfgparse.c
index 4c52eed..9993c61 100644
--- a/src/cfgparse.c
+++ b/src/cfgparse.c
@@ -4089,7 +4089,70 @@
if (warnifnotcap(curproxy, PR_CAP_BE, file, linenum, args[0], NULL))
err_code |= ERR_WARN;
- if (strcmp(args[1], "send") == 0) {
+ if (strcmp(args[1], "connect") == 0) {
+ const char *ptr_arg;
+ int cur_arg;
+ struct tcpcheck_rule *tcpcheck;
+ struct list *l;
+
+ /* check if first rule is also a 'connect' action */
+ l = (struct list *)&curproxy->tcpcheck_rules;
+ if (l->p != l->n) {
+ tcpcheck = (struct tcpcheck_rule *)l->n;
+ if (tcpcheck && tcpcheck->action != TCPCHK_ACT_CONNECT) {
+ Alert("parsing [%s:%d] : first step MUST also be a 'connect' when there is a 'connect' step in the tcp-check ruleset.\n",
+ file, linenum);
+ err_code |= ERR_ALERT | ERR_FATAL;
+ goto out;
+ }
+ }
+
+ cur_arg = 2;
+ tcpcheck = (struct tcpcheck_rule *)calloc(1, sizeof(*tcpcheck));
+ tcpcheck->action = TCPCHK_ACT_CONNECT;
+
+ /* parsing each parameters to fill up the rule */
+ while (*(ptr_arg = args[cur_arg])) {
+ /* tcp port */
+ if (strcmp(args[cur_arg], "port") == 0) {
+ if ( (atol(args[cur_arg + 1]) > 65535) ||
+ (atol(args[cur_arg + 1]) < 1) ){
+ Alert("parsing [%s:%d] : '%s %s %s' expects a valid TCP port (from range 1 to 65535), got %s.\n",
+ file, linenum, args[0], args[1], "port", args[cur_arg + 1]);
+ err_code |= ERR_ALERT | ERR_FATAL;
+ goto out;
+ }
+ tcpcheck->port = atol(args[cur_arg + 1]);
+ cur_arg += 2;
+ }
+ /* send proxy protocol */
+ else if (strcmp(args[cur_arg], "send-proxy") == 0) {
+ tcpcheck->conn_opts |= TCPCHK_OPT_SEND_PROXY;
+ cur_arg++;
+ }
+#ifdef USE_OPENSSL
+ else if (strcmp(args[cur_arg], "ssl") == 0) {
+ curproxy->options |= PR_O_TCPCHK_SSL;
+ tcpcheck->conn_opts |= TCPCHK_OPT_SSL;
+ cur_arg++;
+ }
+#endif /* USE_OPENSSL */
+ else {
+#ifdef USE_OPENSSL
+ Alert("parsing [%s:%d] : '%s %s' expects 'port', 'send-proxy' or 'ssl' but got '%s' as argument.\n",
+#else /* USE_OPENSSL */
+ Alert("parsing [%s:%d] : '%s %s' expects 'port', 'send-proxy' or but got '%s' as argument.\n",
+#endif /* USE_OPENSSL */
+ file, linenum, args[0], args[1], args[cur_arg]);
+ err_code |= ERR_ALERT | ERR_FATAL;
+ goto out;
+ }
+
+ }
+
+ LIST_ADDQ(&curproxy->tcpcheck_rules, &tcpcheck->list);
+ }
+ else if (strcmp(args[1], "send") == 0) {
if (! *(args[2]) ) {
/* SEND string expected */
Alert("parsing [%s:%d] : '%s %s %s' expects <STRING> as argument.\n",
@@ -4235,7 +4298,7 @@
}
}
else {
- Alert("parsing [%s:%d] : '%s' only supports 'send' or 'expect'.\n", file, linenum, args[0]);
+ Alert("parsing [%s:%d] : '%s' only supports 'connect', 'send' or 'expect'.\n", file, linenum, args[0]);
err_code |= ERR_ALERT | ERR_FATAL;
goto out;
}
@@ -5191,7 +5254,7 @@
*/
if (!newsrv->check.port && !is_addr(&newsrv->check_common.addr)) {
#ifdef USE_OPENSSL
- newsrv->check.use_ssl |= newsrv->use_ssl;
+ newsrv->check.use_ssl |= (newsrv->use_ssl || (newsrv->proxy->options & PR_O_TCPCHK_SSL));
#endif
newsrv->check.send_proxy |= (newsrv->state & SRV_SEND_PROXY);
}
@@ -5215,11 +5278,40 @@
break;
}
}
+ /*
+ * We need at least a service port, a check port or the first tcp-check rule must
+ * be a 'connect' one
+ */
if (!newsrv->check.port) {
- Alert("parsing [%s:%d] : server %s has neither service port nor check port. Check has been disabled.\n",
- file, linenum, newsrv->id);
- err_code |= ERR_ALERT | ERR_FATAL;
- goto out;
+ struct tcpcheck_rule *n = NULL, *r = NULL;
+ struct list *l;
+
+ r = (struct tcpcheck_rule *)newsrv->proxy->tcpcheck_rules.n;
+ if (!r) {
+ Alert("parsing [%s:%d] : server %s has neither service port nor check port. Check has been disabled.\n",
+ file, linenum, newsrv->id);
+ err_code |= ERR_ALERT | ERR_FATAL;
+ goto out;
+ }
+ if ((r->action != TCPCHK_ACT_CONNECT) || !r->port) {
+ Alert("parsing [%s:%d] : server %s has neither service port nor check port nor tcp_check rule 'connect' with port information. Check has been disabled.\n",
+ file, linenum, newsrv->id);
+ err_code |= ERR_ALERT | ERR_FATAL;
+ goto out;
+ }
+ else {
+ /* scan the tcp-check ruleset to ensure a port has been configured */
+ l = &newsrv->proxy->tcpcheck_rules;
+ list_for_each_entry(n, l, list) {
+ r = (struct tcpcheck_rule *)n->list.p;
+ if ((r->action == TCPCHK_ACT_CONNECT) && (!r->port)) {
+ Alert("parsing [%s:%d] : server %s has neither service port nor check port, and a tcp_check rule 'connect' with no port information. Check has been disabled.\n",
+ file, linenum, newsrv->id);
+ err_code |= ERR_ALERT | ERR_FATAL;
+ goto out;
+ }
+ }
+ }
}
/* note: check type will be set during the config review phase */
diff --git a/src/checks.c b/src/checks.c
index 70b5a2e..c3051aa 100644
--- a/src/checks.c
+++ b/src/checks.c
@@ -35,6 +35,11 @@
#include <types/global.h>
+#ifdef USE_OPENSSL
+#include <types/ssl_sock.h>
+#include <proto/ssl_sock.h>
+#endif /* USE_OPENSSL */
+
#include <proto/backend.h>
#include <proto/checks.h>
#include <proto/dumpstats.h>
@@ -44,6 +49,7 @@
#include <proto/port_range.h>
#include <proto/proto_http.h>
#include <proto/proto_tcp.h>
+#include <proto/protocol.h>
#include <proto/proxy.h>
#include <proto/raw_sock.h>
#include <proto/server.h>
@@ -862,7 +868,10 @@
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 && check->current_step->action == TCPCHK_ACT_CONNECT) {
+ chunk_appendf(chk, " (connect)");
+ }
+ else 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)
@@ -1564,7 +1573,15 @@
/* we'll connect to the addr on the server */
conn->addr.to = s->addr;
+ if (check->port) {
+ set_host_port(&conn->addr.to, check->port);
+ }
+
+ if (check->type == PR_O2_TCPCHK_CHK) {
+ tcpcheck_main(conn);
+ return t;
+ }
+
- set_host_port(&conn->addr.to, check->port);
/* It can return one of :
* - SN_ERR_NONE if everything's OK
@@ -1920,7 +1937,7 @@
struct tcpcheck_rule *cur = NULL, *next = NULL;
int i = 0;
- cur = s->check.current_step;
+ cur = s->check.last_started_step;
/* no step => first step */
if (cur == NULL)
@@ -1948,8 +1965,11 @@
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)) {
+ /*
+ * don't do anything until the connection is established but if we're running
+ * first step which must be a connect
+ */
+ if (check->current_step && (!(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
@@ -2023,7 +2043,129 @@
break;
}
+ if (check->current_step->action == TCPCHK_ACT_CONNECT) {
+ struct protocol *proto;
+ struct xprt_ops *xprt;
+
+ /* mark the step as started */
+ check->last_started_step = check->current_step;
+ /* first, shut existing connection */
+ conn_force_close(conn);
+
+ /* prepare new connection */
+ /* initialization */
+ conn_init(conn);
+ conn_attach(conn, check, &check_conn_cb);
+ conn->target = &s->obj_type;
+
+ /* no client address */
+ clear_addr(&conn->addr.from);
+
+ if (is_addr(&s->check_common.addr))
+ /* we'll connect to the check addr specified on the server */
+ conn->addr.to = s->check_common.addr;
+ else
+ /* we'll connect to the addr on the server */
+ conn->addr.to = s->addr;
+
+ /* protocol */
+ proto = protocol_by_family(conn->addr.to.ss_family);
+
+ /* port */
+ if (check->current_step->port)
+ set_host_port(&conn->addr.to, check->current_step->port);
+ else if (check->port)
+ set_host_port(&conn->addr.to, check->port);
+
+#ifdef USE_OPENSSL
+ if (check->current_step->conn_opts & TCPCHK_OPT_SSL) {
+ xprt = &ssl_sock;
+ ssl_sock_prepare_srv_ctx(s, s->proxy);
+ }
+ else {
+ xprt = &raw_sock;
+ }
+#else /* USE_OPENSSL */
+ xprt = &raw_sock;
+#endif /* USE_OPENSSL */
+ conn_prepare(conn, proto, xprt);
+
+ ret = SN_ERR_INTERNAL;
+ if (proto->connect)
+ ret = proto->connect(conn, check->type, (check->type) ? 0 : 2);
+ conn->flags |= CO_FL_WAKE_DATA;
+ if (check->current_step->conn_opts & TCPCHK_OPT_SEND_PROXY) {
+ conn->send_proxy_ofs = 1;
+ conn->flags |= CO_FL_SEND_PROXY;
+ }
+
+ /* It can return one of :
+ * - SN_ERR_NONE if everything's OK
+ * - SN_ERR_SRVTO if there are no more servers
+ * - SN_ERR_SRVCL if the connection was refused by the server
+ * - SN_ERR_PRXCOND if the connection has been limited by the proxy (maxconn)
+ * - SN_ERR_RESOURCE if a system resource is lacking (eg: fd limits, ports, ...)
+ * - SN_ERR_INTERNAL for any other purely internal errors
+ * Additionnally, in the case of SN_ERR_RESOURCE, an emergency log will be emitted.
+ * Note that we try to prevent the network stack from sending the ACK during the
+ * connect() when a pure TCP check is used (without PROXY protocol).
+ */
+ switch (ret) {
+ case SN_ERR_NONE:
+ /* 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
+ */
+ t->expire = tick_add(now_ms, MS_TO_TICKS(check->inter));
+
+ if (s->proxy->timeout.check && s->proxy->timeout.connect) {
+ int t_con = tick_add(now_ms, s->proxy->timeout.connect);
+ t->expire = tick_first(t->expire, t_con);
+ }
+ break;
+ case SN_ERR_SRVTO: /* ETIMEDOUT */
+ case SN_ERR_SRVCL: /* ECONNREFUSED, ENETUNREACH, ... */
+ chunk_printf(&trash, "TCPCHK error establishing connection at step %d: %s",
+ tcpcheck_get_step_id(s), strerror(errno));
+ set_server_check_status(check, HCHK_STATUS_L4CON, trash.str);
+ goto out_end_tcpcheck;
+ case SN_ERR_PRXCOND:
+ case SN_ERR_RESOURCE:
+ case SN_ERR_INTERNAL:
+ chunk_printf(&trash, "TCPCHK error establishing connection at step %d",
+ tcpcheck_get_step_id(s));
+ set_server_check_status(check, HCHK_STATUS_SOCKERR, trash.str);
+ goto out_end_tcpcheck;
+ }
+
+ /* allow next rule */
+ cur = (struct tcpcheck_rule *)cur->list.n;
+ check->current_step = cur;
+
+ /* 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;
+ }
+
+ } /* end 'connect' */
+ else if (check->current_step->action == TCPCHK_ACT_SEND) {
+ /* mark the step as started */
+ check->last_started_step = check->current_step;
+
- if (check->current_step->action == TCPCHK_ACT_SEND) {
/* reset the read buffer */
if (*check->bi->data != '\0') {
*check->bi->data = '\0';
@@ -2076,6 +2218,10 @@
goto out_need_io;
}
+ /* mark the step as started */
+ check->last_started_step = check->current_step;
+
+
/* Intermediate or complete response received.
* Terminate string in check->bi->data buffer.
*/
@@ -2186,22 +2332,13 @@
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);
-
+ /* cleanup before leaving */
check->current_step = NULL;
if (check->result == CHK_RES_FAILED)
conn->flags |= CO_FL_ERROR;
__conn_data_stop_both(conn);
- task_wakeup(t, TASK_WOKEN_IO);
return;
}