]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
[MINOR] add better support to "mysql-check"
authorHervé COMMOWICK <vr@exosec.fr>
Mon, 18 Oct 2010 13:58:36 +0000 (15:58 +0200)
committerWilly Tarreau <w@1wt.eu>
Sat, 30 Oct 2010 17:04:35 +0000 (19:04 +0200)
The MySQL check has been revamped to be able to send real MySQL data,
and to avoid Aborted connects on MySQL side.
It is however backward compatible with older version, but it is highly
recommended to use the new mode, by adding "user <username>" on the
"mysql-check" line.

The new check consists in sending two MySQL packet, one Client
Authentication packet, with "haproxy" username (by default), and one
QUIT packet, to correctly close MySQL session. We then parse the Mysql
Handshake Initialisation packet and/or Error packet. It is a basic but
useful test which does not produce error nor aborted connect on the
server.
(cherry picked from commit a1e4dcfe5718311b7653d7dabfad65c005d0439b)

doc/configuration.txt
src/cfgparse.c
src/checks.c
tests/test-sql.cfg

index c0b04bfb9c8406683a0e6bf1a8ffaad209014930..d96a4981aac92b93a149abdde2819e9070c4a49b 100644 (file)
@@ -3350,17 +3350,39 @@ no option logasap
              logging.
 
 
-option mysql-check
-  Use Mysql health checks for server testing
+option mysql-check [ user <username> ]
+  Use MySQL health checks for server testing
   May be used in sections :   defaults | frontend | listen | backend
                                  yes   |    no    |   yes  |   yes
-  Arguments : none
-
-  The check consists in parsing Mysql Handshake Initialisation packet or Error
-  packet, which is sent by MySQL server on connect. It is a basic but useful
-  test which does not produce any logging on the server. However, it does not
-  check database presence nor database consistency, nor user permission to
-  access. To do this, you can use an external check with xinetd for example.
+  Arguments :
+    user <username> This is the username which will be used when connecting
+    to MySQL server.
+
+  If you specify a username, the check consists of sending two MySQL packet,
+  one Client Authentication packet, and one QUIT packet, to correctly close
+  MySQL session. We then parse the MySQL Handshake Initialisation packet and/or
+  Error packet. It is a basic but useful test which does not produce error nor
+  aborted connect on the server. However, it requires adding an authorization
+  in the MySQL table, like this :
+
+      USE mysql;
+      INSERT INTO user (Host,User) values ('<ip_of_haproxy>','<username>');
+      FLUSH PRIVILEGES;
+
+  If you don't specify a username (it is deprecated and not recommended), the
+  check only consists in parsing the Mysql Handshake Initialisation packet or
+  Error packet, we don't send anything in this mode. It was reported that it
+  can generate lockout if check is too frequent and/or if there is not enough
+  traffic. In fact, you need in this case to check MySQL "max_connect_errors"
+  value as if a connection is established successfully within fewer than MySQL
+  "max_connect_errors" attempts after a previous connection was interrupted,
+  the error count for the host is cleared to zero. If HAProxy's server get
+  blocked, the "FLUSH HOSTS" statement is the only way to unblock it.
+
+  Remember that this does not check database presence nor database consistency.
+  To do this, you can use an external check with xinetd for example.
+
+  The check requires MySQL >=4.0, for older version, please use TCP check.
 
   Most often, an incoming MySQL server needs to see the client's IP address for
   various purposes, including IP privilege matching and connection logging.
index 617bb4113f46772a464500aaa764214d4a5f113c..58c5db86563c84e2252ba29a812b448fe7e547a1 100644 (file)
@@ -2904,6 +2904,9 @@ stats_error_parsing:
                }
                else if (!strcmp(args[1], "mysql-check")) {
                        /* use MYSQL request 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->options &= ~PR_O_HTTP_CHK;
@@ -2911,6 +2914,65 @@ stats_error_parsing:
                        curproxy->options2 &= ~PR_O2_SSL3_CHK;
                        curproxy->options2 &= ~PR_O2_LDAP_CHK;
                        curproxy->options2 |= PR_O2_MYSQL_CHK;
+
+                       /* This is an exemple of an MySQL >=4.0 client Authentication packet kindly provided by Cyril Bonte.
+                        * const char mysql40_client_auth_pkt[] = {
+                        *      "\x0e\x00\x00"  // packet length
+                        *      "\x01"          // packet number
+                        *      "\x00\x00"      // client capabilities
+                        *      "\x00\x00\x01"  // max packet
+                        *      "haproxy\x00"   // username (null terminated string)
+                        *      "\x00"          // filler (always 0x00)
+                        *      "\x01\x00\x00"  // packet length
+                        *      "\x00"          // packet number
+                        *      "\x01"          // COM_QUIT command
+                        * };
+                        */
+
+                       if (*(args[2])) {
+                               int cur_arg = 2;
+
+                               while (*(args[cur_arg])) {
+                                       if (strcmp(args[cur_arg], "user") == 0) {
+                                               char *mysqluser;
+                                               int packetlen, reqlen, userlen;
+
+                                               /* suboption header - needs additional argument for it */
+                                               if (*(args[cur_arg+1]) == 0) {
+                                                       Alert("parsing [%s:%d] : '%s %s %s' expects <username> as argument.\n",
+                                                             file, linenum, args[0], args[1], args[cur_arg]);
+                                                       err_code |= ERR_ALERT | ERR_FATAL;
+                                                       goto out;
+                                               }
+                                               mysqluser = args[cur_arg + 1];
+                                               userlen   = strlen(mysqluser);
+                                               packetlen = userlen + 7;
+                                               reqlen    = packetlen + 9;
+
+                                               free(curproxy->check_req);
+                                               curproxy->check_req = (char *)calloc(1, reqlen);
+                                               curproxy->check_len = reqlen;
+
+                                               snprintf(curproxy->check_req, 4, "%c%c%c",
+                                                       ((unsigned char) packetlen & 0xff),
+                                                       ((unsigned char) (packetlen >> 8) & 0xff),
+                                                       ((unsigned char) (packetlen >> 16) & 0xff));
+
+                                               curproxy->check_req[3] = 1;
+                                               curproxy->check_req[8] = 1;
+                                               memcpy(&curproxy->check_req[9], mysqluser, userlen);
+                                               curproxy->check_req[9 + userlen + 1 + 1]     = 1;
+                                               curproxy->check_req[9 + userlen + 1 + 1 + 4] = 1;
+                                               cur_arg += 2;
+                                       } else {
+                                               /* unknown suboption - catchall */
+                                               Alert("parsing [%s:%d] : '%s %s' only supports optional values: 'user'.\n",
+                                                     file, linenum, args[0], args[1]);
+                                               err_code |= ERR_ALERT | ERR_FATAL;
+                                               goto out;
+                                       }
+                               } /* end while loop */
+                       }
                }
                else if (!strcmp(args[1], "ldap-check")) {
                        /* use LDAP request to check servers' health */
index 4132f5e576fb1d86b554b7b6642dfe15e60aae2e..a23fd41e9100f728968fc25faf0d336959ef731f 100644 (file)
@@ -848,8 +848,9 @@ static int event_srv_chk_w(int fd)
 
 /*
  * This function is used only for server health-checks. It handles the server's
- * reply to an HTTP request or SSL HELLO. It calls set_server_check_status() to
- * update s->check_status, s->check_duration and s->result.
+ * reply to an HTTP request, SSL HELLO or MySQL client Auth. It calls
+ * set_server_check_status() to update s->check_status, s->check_duration
+ * and s->result.
 
  * The set_server_check_status function is called with HCHK_STATUS_L7OKD if
  * an HTTP server replies HTTP 2xx or 3xx (valid responses), if an SMTP server
@@ -1001,36 +1002,91 @@ static int event_srv_chk_r(int fd)
                        set_server_check_status(s, HCHK_STATUS_L7STS, desc);
        }
        else if (s->proxy->options2 & PR_O2_MYSQL_CHK) {
-               /* MySQL Error packet always begin with field_count = 0xff
-                * contrary to OK Packet who always begin whith 0x00 */
                if (!done && s->check_data_len < 5)
                        goto wait_more_data;
 
-               if (*(s->check_data + 4) != '\xff') {
-                       /* We set the MySQL Version in description for information purpose
-                        * FIXME : it can be cool to use MySQL Version for other purpose,
-                        * like mark as down old MySQL server.
-                        */
-                       if (s->check_data_len > 51) {
-                               desc = ltrim(s->check_data + 5, ' ');
-                               set_server_check_status(s, HCHK_STATUS_L7OKD, desc);
+               if (s->proxy->check_len == 0) { // old mode
+                       if (*(s->check_data + 4) != '\xff') {
+                               /* We set the MySQL Version in description for information purpose
+                                * FIXME : it can be cool to use MySQL Version for other purpose,
+                                * like mark as down old MySQL server.
+                                */
+                               if (s->check_data_len > 51) {
+                                       desc = ltrim(s->check_data + 5, ' ');
+                                       set_server_check_status(s, HCHK_STATUS_L7OKD, desc);
+                               }
+                               else {
+                                       if (!done)
+                                               goto wait_more_data;
+                                       /* it seems we have a OK packet but without a valid length,
+                                        * it must be a protocol error
+                                        */
+                                       set_server_check_status(s, HCHK_STATUS_L7RSP, s->check_data);
+                               }
+                       }
+                       else {
+                               /* An error message is attached in the Error packet */
+                               desc = ltrim(s->check_data + 7, ' ');
+                               set_server_check_status(s, HCHK_STATUS_L7STS, desc);
+                       }
+               } else {
+                       unsigned int first_packet_len = ((unsigned int) *s->check_data) +
+                                                       (((unsigned int) *(s->check_data + 1)) << 8) +
+                                                       (((unsigned int) *(s->check_data + 2)) << 16);
+
+                       if (s->check_data_len == first_packet_len + 4) {
+                               /* MySQL Error packet always begin with field_count = 0xff */
+                               if (*(s->check_data + 4) != '\xff') {
+                                       /* We have only one MySQL packet and it is a Handshake Initialization packet
+                                       * but we need to have a second packet to know if it is alright
+                                       */
+                                       if (!done && s->check_data_len < first_packet_len + 5)
+                                               goto wait_more_data;
+                               }
+                               else {
+                                       /* We have only one packet and it is an Error packet,
+                                       * an error message is attached, so we can display it
+                                       */
+                                       desc = &s->check_data[7];
+                                       //Warning("onlyoneERR: %s\n", desc);
+                                       set_server_check_status(s, HCHK_STATUS_L7STS, desc);
+                               }
+                       } else if (s->check_data_len > first_packet_len + 4) {
+                               unsigned int second_packet_len = ((unsigned int) *(s->check_data + first_packet_len + 4)) +
+                                                                (((unsigned int) *(s->check_data + first_packet_len + 5)) << 8) +
+                                                                (((unsigned int) *(s->check_data + first_packet_len + 6)) << 16);
+
+                               if (s->check_data_len == first_packet_len + 4 + second_packet_len + 4 ) {
+                                       /* We have 2 packets and that's good */
+                                       /* Check if the second packet is a MySQL Error packet or not */
+                                       if (*(s->check_data + first_packet_len + 8) != '\xff') {
+                                               /* No error packet */
+                                               /* We set the MySQL Version in description for information purpose */
+                                               desc = &s->check_data[5];
+                                               //Warning("2packetOK: %s\n", desc);
+                                               set_server_check_status(s, HCHK_STATUS_L7OKD, desc);
+                                       }
+                                       else {
+                                               /* An error message is attached in the Error packet
+                                               * so we can display it ! :)
+                                               */
+                                               desc = &s->check_data[first_packet_len+11];
+                                               //Warning("2packetERR: %s\n", desc);
+                                               set_server_check_status(s, HCHK_STATUS_L7STS, desc);
+                                       }
+                               }
                        }
                        else {
                                if (!done)
                                        goto wait_more_data;
-                               /* it seems we have a OK packet but without a valid length,
+                               /* it seems we have a Handshake Initialization packet but without a valid length,
                                 * it must be a protocol error
                                 */
-                               set_server_check_status(s, HCHK_STATUS_L7RSP, s->check_data);
+                               desc = &s->check_data[5];
+                               //Warning("protoerr: %s\n", desc);
+                               set_server_check_status(s, HCHK_STATUS_L7RSP, desc);
                        }
                }
-               else {
-                       /* An error message is attached in the Error packet,
-                        * so we can display it ! :)
-                        */
-                       desc = ltrim(s->check_data + 7, ' ');
-                       set_server_check_status(s, HCHK_STATUS_L7STS, desc);
-               }
        }
        else if (s->proxy->options2 & PR_O2_LDAP_CHK) {
                if (!done && s->check_data_len < 14)
index 52dfefd8f97d63a37622682a4671dab6dc86c246..c2aebd96d8bd00bef029b73631909c8c3e6c8ae7 100644 (file)
@@ -21,7 +21,7 @@ listen  mysql_1
         bind :3307
         mode tcp
         balance roundrobin
-        option mysql-check
+        option mysql-check user haproxy
         server  srv1 127.0.0.1:3306 check port 3306 inter 1000 fall 1
 #        server  srv2 127.0.0.2:3306 check port 3306 inter 1000 fall 1
 #        server  srv3 127.0.0.3:3306 check port 3306 inter 1000 fall 1