From: Christopher Faulet Date: Thu, 2 Apr 2020 16:07:37 +0000 (+0200) Subject: MEDIUM: checks: Implement MySQL check using tcp-check rules X-Git-Tag: v2.2-dev7~124 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=f2b3be5c27dac436183980e60cc508e9241635ee;p=thirdparty%2Fhaproxy.git MEDIUM: checks: Implement MySQL check using tcp-check rules A share tcp-check ruleset is now created to support MySQL checks. This way no extra memory is used if several backends use a MySQL check. One for the following sequence is used : ## If no extra params are set tcp-check connect default linger tcp-check expect custom ## will test the initial handshake ## If the username is defined tcp-check connect default linger tcp-check send-binary MYSQL_REQ log-format tcp-check expect custom ## will test the initial handshake tcp-check expect custom ## will test the reply to the client message The log-format hexa string MYSQL_REQ depends on 2 preset variables, the packet header containing the packet length and the sequence ID (check.header) and the username (check.username). If is also different if the "post-41" option is set or not. Expect rules relies on custom functions to check MySQL server packets. --- diff --git a/include/proto/checks.h b/include/proto/checks.h index bb275c8de2..322f9ddf36 100644 --- a/include/proto/checks.h +++ b/include/proto/checks.h @@ -77,6 +77,8 @@ int proxy_parse_smtpchk_opt(char **args, int cur_arg, struct proxy *curpx, struc const char *file, int line); int proxy_parse_pgsql_check_opt(char **args, int cur_arg, struct proxy *curpx, struct proxy *defpx, const char *file, int line); +int proxy_parse_mysql_check_opt(char **args, int cur_arg, struct proxy *curpx, struct proxy *defpx, + const char *file, int line); #endif /* _PROTO_CHECKS_H */ diff --git a/include/types/checks.h b/include/types/checks.h index 9b34cbe455..5b3fda4adc 100644 --- a/include/types/checks.h +++ b/include/types/checks.h @@ -315,6 +315,7 @@ struct tcpcheck_rule { #define TCPCHK_RULES_PGSQL_CHK 0x00000010 #define TCPCHK_RULES_REDIS_CHK 0x00000020 #define TCPCHK_RULES_SMTP_CHK 0x00000030 +#define TCPCHK_RULES_MYSQL_CHK 0x00000050 #define TCPCHK_RULES_SSL3_CHK 0x00000070 /* A list of tcp-check vars, to be registered before executing a ruleset */ diff --git a/include/types/proxy.h b/include/types/proxy.h index be5b5c9d20..6e84037370 100644 --- a/include/types/proxy.h +++ b/include/types/proxy.h @@ -171,7 +171,7 @@ enum PR_SRV_STATE_FILE { #define PR_O2_CHK_NONE 0x00000000 /* no L7 health checks configured (TCP by default) */ /* unused: 0x10000000..0x30000000 */ #define PR_O2_HTTP_CHK 0x40000000 /* use HTTP 'OPTIONS' method to check server health */ -#define PR_O2_MYSQL_CHK 0x50000000 /* use MYSQL check for server health */ +/* unused 0x50000000 */ #define PR_O2_LDAP_CHK 0x60000000 /* use LDAP check for server health */ /* unused: 0x70000000 */ #define PR_O2_LB_AGENT_CHK 0x80000000 /* use a TCP connection to obtain a metric of server health */ diff --git a/src/cfgparse-listen.c b/src/cfgparse-listen.c index e0b1bc30d6..b731a29119 100644 --- a/src/cfgparse-listen.c +++ b/src/cfgparse-listen.c @@ -2416,123 +2416,10 @@ stats_error_parsing: if (err_code & ERR_FATAL) goto out; } - 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->options2 &= ~PR_O2_CHK_ANY; - curproxy->options2 |= PR_O2_MYSQL_CHK; - - /* This is an example of a 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 - * }; - */ - - /* This is an example of a MySQL >=4.1 client Authentication packet provided by Nenad Merdanovic. - * const char mysql41_client_auth_pkt[] = { - * "\x0e\x00\x00\" // packet length - * "\x01" // packet number - * "\x00\x00\x00\x00" // client capabilities - * "\x00\x00\x00\x01" // max packet - * "\x21" // character set (UTF-8) - * char[23] // All zeroes - * "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) { - ha_alert("parsing [%s:%d] : '%s %s %s' expects 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); - - if (*(args[cur_arg+2])) { - if (!strcmp(args[cur_arg+2], "post-41")) { - packetlen = userlen + 7 + 27; - reqlen = packetlen + 9; - - free(curproxy->check_req); - curproxy->check_req = 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[5] = 0x82; // 130 - curproxy->check_req[11] = 1; - curproxy->check_req[12] = 33; - memcpy(&curproxy->check_req[36], mysqluser, userlen); - curproxy->check_req[36 + userlen + 1 + 1] = 1; - curproxy->check_req[36 + userlen + 1 + 1 + 4] = 1; - cur_arg += 3; - } else { - ha_alert("parsing [%s:%d] : keyword '%s' only supports option 'post-41'.\n", file, linenum, args[cur_arg+2]); - err_code |= ERR_ALERT | ERR_FATAL; - goto out; - } - } else { - packetlen = userlen + 7; - reqlen = packetlen + 9; - - free(curproxy->check_req); - curproxy->check_req = 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[5] = 0x80; - 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 */ - ha_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 */ - } + err_code |= proxy_parse_mysql_check_opt(args, 0, curproxy, &defproxy, file, linenum); + if (err_code & ERR_FATAL) + goto out; } else if (!strcmp(args[1], "ldap-check")) { /* use LDAP request to check servers' health */ diff --git a/src/checks.c b/src/checks.c index d0452a9f8a..0fcf91592b 100644 --- a/src/checks.c +++ b/src/checks.c @@ -1156,100 +1156,6 @@ static void __event_srv_chk_r(struct conn_stream *cs) break; } - case PR_O2_MYSQL_CHK: - if (!done && b_data(&check->bi) < 5) - goto wait_more_data; - - /* do not reset when closing, servers don't like this */ - if (conn_ctrl_ready(cs->conn)) - fdtab[cs->conn->handle.fd].linger_risk = 0; - - if (s->proxy->check_len == 0) { // old mode - if (*(b_head(&check->bi) + 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 (b_data(&check->bi) > 51) { - desc = ltrim(b_head(&check->bi) + 5, ' '); - set_server_check_status(check, 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(check, HCHK_STATUS_L7RSP, b_head(&check->bi)); - } - } - else { - /* An error message is attached in the Error packet */ - desc = ltrim(b_head(&check->bi) + 7, ' '); - set_server_check_status(check, HCHK_STATUS_L7STS, desc); - } - } else { - unsigned int first_packet_len = ((unsigned int) *b_head(&check->bi)) + - (((unsigned int) *(b_head(&check->bi) + 1)) << 8) + - (((unsigned int) *(b_head(&check->bi) + 2)) << 16); - - if (b_data(&check->bi) == first_packet_len + 4) { - /* MySQL Error packet always begin with field_count = 0xff */ - if (*(b_head(&check->bi) + 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 && b_data(&check->bi) < 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 = &b_head(&check->bi)[7]; - //ha_warning("onlyoneERR: %s\n", desc); - set_server_check_status(check, HCHK_STATUS_L7STS, desc); - } - } else if (b_data(&check->bi) > first_packet_len + 4) { - unsigned int second_packet_len = ((unsigned int) *(b_head(&check->bi) + first_packet_len + 4)) + - (((unsigned int) *(b_head(&check->bi) + first_packet_len + 5)) << 8) + - (((unsigned int) *(b_head(&check->bi) + first_packet_len + 6)) << 16); - - if (b_data(&check->bi) == 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 (*(b_head(&check->bi) + first_packet_len + 8) != '\xff') { - /* No error packet */ - /* We set the MySQL Version in description for information purpose */ - desc = &b_head(&check->bi)[5]; - //ha_warning("2packetOK: %s\n", desc); - set_server_check_status(check, HCHK_STATUS_L7OKD, desc); - } - else { - /* An error message is attached in the Error packet - * so we can display it ! :) - */ - desc = &b_head(&check->bi)[first_packet_len+11]; - //ha_warning("2packetERR: %s\n", desc); - set_server_check_status(check, HCHK_STATUS_L7STS, desc); - } - } - } - else { - if (!done) - goto wait_more_data; - - /* it seems we have a Handshake Initialization packet but without a valid length, - * it must be a protocol error - */ - desc = &b_head(&check->bi)[5]; - //ha_warning("protoerr: %s\n", desc); - set_server_check_status(check, HCHK_STATUS_L7RSP, desc); - } - } - break; - case PR_O2_LDAP_CHK: if (!done && b_data(&check->bi) < 14) goto wait_more_data; @@ -2776,6 +2682,95 @@ static void tcpcheck_onsuccess_message(struct buffer *msg, struct check *check, *(b_tail(msg)) = '\0'; } +static enum tcpcheck_eval_ret tcpcheck_mysql_expect_packet(struct check *check, struct tcpcheck_rule *rule, + unsigned int offset, int last_read) +{ + enum tcpcheck_eval_ret ret = TCPCHK_EVAL_CONTINUE; + enum healthcheck_status status; + struct buffer *msg = NULL; + struct ist desc = ist(NULL); + unsigned int err = 0, plen = 0; + + + /* 3 Bytes for the packet length and 1 byte for the sequence id */ + if (!last_read && b_data(&check->bi) < offset+4) { + if (!last_read) + goto wait_more_data; + + /* invalid length or truncated response */ + status = HCHK_STATUS_L7RSP; + goto error; + } + + plen = ((unsigned char) *b_peek(&check->bi, offset)) + + (((unsigned char) *(b_peek(&check->bi, offset+1))) << 8) + + (((unsigned char) *(b_peek(&check->bi, offset+2))) << 16); + + if (b_data(&check->bi) < offset+plen+4) { + if (!last_read) + goto wait_more_data; + + /* invalid length or truncated response */ + status = HCHK_STATUS_L7RSP; + goto error; + } + + if (*b_peek(&check->bi, offset+4) == '\xff') { + /* MySQL Error packet always begin with field_count = 0xff */ + status = HCHK_STATUS_L7STS; + err = ((unsigned char) *b_peek(&check->bi, offset+5)) + + (((unsigned char) *(b_peek(&check->bi, offset+6))) << 8); + desc = ist2(b_peek(&check->bi, offset+7), b_data(&check->bi) - offset - 7); + goto error; + } + + if (get_next_tcpcheck_rule(check->tcpcheck_rules, rule) != NULL) { + /* Not the last rule, continue */ + goto out; + } + + /* 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. + */ + set_server_check_status(check, HCHK_STATUS_L7OKD, b_peek(&check->bi, 5)); + + out: + free_trash_chunk(msg); + return ret; + + error: + ret = TCPCHK_EVAL_STOP; + check->code = err; + msg = alloc_trash_chunk(); + if (msg) + tcpcheck_onerror_message(msg, check, rule, 0, desc); + set_server_check_status(check, status, (msg ? b_head(msg) : NULL)); + goto out; + + wait_more_data: + ret = TCPCHK_EVAL_WAIT; + goto out; +} + + +static enum tcpcheck_eval_ret tcpcheck_mysql_expect_iniths(struct check *check, struct tcpcheck_rule *rule, int last_read) +{ + return tcpcheck_mysql_expect_packet(check, rule, 0, last_read); +} + +static enum tcpcheck_eval_ret tcpcheck_mysql_expect_ok(struct check *check, struct tcpcheck_rule *rule, int last_read) +{ + unsigned int hslen = 0; + + hslen = 4 + ((unsigned char) *b_head(&check->bi)) + + (((unsigned char) *(b_peek(&check->bi, 1))) << 8) + + (((unsigned char) *(b_peek(&check->bi, 2))) << 16); + + return tcpcheck_mysql_expect_packet(check, rule, hslen, last_read); +} + + /* Evaluate a TCPCHK_ACT_CONNECT rule. It returns 1 to evaluate the next rule, 0 * to wait and -1 to stop the check. */ static enum tcpcheck_eval_ret tcpcheck_eval_connect(struct check *check, struct tcpcheck_rule *rule) @@ -5535,6 +5530,240 @@ int proxy_parse_pgsql_check_opt(char **args, int cur_arg, struct proxy *curpx, s } +/* Parses the "option mysql-check" proxy keyword */ +int proxy_parse_mysql_check_opt(char **args, int cur_arg, struct proxy *curpx, struct proxy *defpx, + const char *file, int line) +{ + /* This is an example of a 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 + * }; + */ + static char mysql40_rsname[] = "*mysql40-check"; + static char mysql40_req[] = { + "%[var(check.header),hex]" /* 3 bytes for the packet length and 1 byte for the sequence ID */ + "0080" /* client capabilities */ + "000001" /* max packet */ + "%[var(check.username),hex]00" /* the username */ + "00" /* filler (always 0x00) */ + "010000" /* packet length*/ + "00" /* sequence ID */ + "01" /* COM_QUIT command */ + }; + + /* This is an example of a MySQL >=4.1 client Authentication packet provided by Nenad Merdanovic. + * const char mysql41_client_auth_pkt[] = { + * "\x0e\x00\x00\" // packet length + * "\x01" // packet number + * "\x00\x00\x00\x00" // client capabilities + * "\x00\x00\x00\x01" // max packet + * "\x21" // character set (UTF-8) + * char[23] // All zeroes + * "haproxy\x00" // username (null terminated string) + * "\x00" // filler (always 0x00) + * "\x01\x00\x00" // packet length + * "\x00" // packet number + * "\x01" // COM_QUIT command + * }; + */ + static char mysql41_rsname[] = "*mysql41-check"; + static char mysql41_req[] = { + "%[var(check.header),hex]" /* 3 bytes for the packet length and 1 byte for the sequence ID */ + "00820000" /* client capabilities */ + "00800001" /* max packet */ + "21" /* character set (UTF-8) */ + "000000000000000000000000" /* 23 bytes, al zeroes */ + "0000000000000000000000" + "%[var(check.username),hex]00" /* the username */ + "00" /* filler (always 0x00) */ + "010000" /* packet length*/ + "00" /* sequence ID */ + "01" /* COM_QUIT command */ + }; + + struct tcpcheck_ruleset *rs = NULL; + struct tcpcheck_rules *rules = &curpx->tcpcheck_rules; + struct tcpcheck_rule *chk; + struct tcpcheck_var *var = NULL; + char *mysql_rsname = "*mysql-check"; + char *mysql_req = NULL, *hdr = NULL, *user = NULL, *errmsg = NULL; + int index = 0, err_code = 0; + + if (warnifnotcap(curpx, PR_CAP_BE, file, line, args[cur_arg+1], NULL)) + err_code |= ERR_WARN; + + if (alertif_too_many_args_idx(3, 1, file, line, args, &err_code)) + goto out; + + if (rules->list && !(rules->flags & TCPCHK_RULES_SHARED)) { + ha_alert("parsing [%s:%d] : A custom tcp-check ruleset is already configured.\n", + file, line); + err_code |= ERR_ALERT | ERR_FATAL; + goto out; + } + + curpx->options2 &= ~PR_O2_CHK_ANY; + curpx->options2 |= PR_O2_TCPCHK_CHK; + + free_tcpcheck_vars(&rules->preset_vars); + rules->list = NULL; + rules->flags = 0; + + cur_arg += 2; + if (*args[cur_arg]) { + char *user; + int packetlen, userlen; + + if (strcmp(args[cur_arg], "user") != 0) { + ha_alert("parsing [%s:%d] : '%s %s' only supports optional values: 'user' (got '%s').\n", + file, line, args[0], args[1], args[cur_arg]); + goto error; + } + + if (*(args[cur_arg+1]) == 0) { + ha_alert("parsing [%s:%d] : '%s %s %s' expects as argument.\n", + file, line, args[0], args[1], args[cur_arg]); + goto error; + } + + hdr = calloc(4, sizeof(*hdr)); + user = strdup(args[cur_arg+1]); + userlen = strlen(args[cur_arg+1]); + + if (hdr == NULL || user == NULL) { + ha_alert("parsing [%s:%d] : out of memory.\n", file, line); + goto error; + } + + if (*args[cur_arg+2]) { + if (strcmp(args[cur_arg+2], "post-41") != 0) { + ha_alert("parsing [%s:%d] : keyword '%s' only supports option 'post-41' (got '%s').\n", + file, line, args[cur_arg], args[cur_arg+2]); + goto error; + } + packetlen = userlen + 7 + 27; + mysql_req = mysql41_req; + mysql_rsname = mysql41_rsname; + } + else { + packetlen = userlen + 7; + mysql_req = mysql40_req; + mysql_rsname = mysql40_rsname; + } + + hdr[0] = (unsigned char)(packetlen & 0xff); + hdr[1] = (unsigned char)((packetlen >> 8) & 0xff); + hdr[2] = (unsigned char)((packetlen >> 16) & 0xff); + hdr[3] = 1; + + var = tcpcheck_var_create("check.header"); + if (var == NULL) { + ha_alert("parsing [%s:%d] : out of memory.\n", file, line); + goto error; + } + var->data.type = SMP_T_STR; + var->data.u.str.area = hdr; + var->data.u.str.data = 4; + LIST_INIT(&var->list); + LIST_ADDQ(&rules->preset_vars, &var->list); + hdr = NULL; + var = NULL; + + var = tcpcheck_var_create("check.username"); + if (var == NULL) { + ha_alert("parsing [%s:%d] : out of memory.\n", file, line); + goto error; + } + var->data.type = SMP_T_STR; + var->data.u.str.area = user; + var->data.u.str.data = strlen(user); + LIST_INIT(&var->list); + LIST_ADDQ(&rules->preset_vars, &var->list); + user = NULL; + var = NULL; + } + + rs = tcpcheck_ruleset_lookup(mysql_rsname); + if (rs) + goto ruleset_found; + + rs = tcpcheck_ruleset_create(mysql_rsname); + if (rs == NULL) { + ha_alert("parsing [%s:%d] : out of memory.\n", file, line); + goto error; + } + + chk = parse_tcpcheck_connect((char *[]){"tcp-check", "connect", "default", "linger", ""}, + 1, curpx, &rs->rules, file, line, &errmsg); + if (!chk) { + ha_alert("parsing [%s:%d] : %s\n", file, line, errmsg); + goto error; + } + chk->index = index++; + LIST_ADDQ(&rs->rules, &chk->list); + + if (mysql_req) { + chk = parse_tcpcheck_send((char *[]){"tcp-check", "send-binary", mysql_req, "log-format", ""}, + 1, curpx, &rs->rules, file, line, &errmsg); + if (!chk) { + ha_alert("parsing [%s:%d] : %s\n", file, line, errmsg); + goto error; + } + chk->index = index++; + LIST_ADDQ(&rs->rules, &chk->list); + } + + chk = parse_tcpcheck_expect((char *[]){"tcp-check", "expect", "custom", ""}, + 1, curpx, &rs->rules, file, line, &errmsg); + if (!chk) { + ha_alert("parsing [%s:%d] : %s\n", file, line, errmsg); + goto error; + } + chk->expect.custom = tcpcheck_mysql_expect_iniths; + chk->index = index++; + LIST_ADDQ(&rs->rules, &chk->list); + + if (mysql_req) { + chk = parse_tcpcheck_expect((char *[]){"tcp-check", "expect", "custom", ""}, + 1, curpx, &rs->rules, file, line, &errmsg); + if (!chk) { + ha_alert("parsing [%s:%d] : %s\n", file, line, errmsg); + goto error; + } + chk->expect.custom = tcpcheck_mysql_expect_ok; + chk->index = index++; + LIST_ADDQ(&rs->rules, &chk->list); + } + + LIST_ADDQ(&tcpchecks_list, &rs->list); + + ruleset_found: + rules->list = &rs->rules; + rules->flags |= (TCPCHK_RULES_SHARED|TCPCHK_RULES_MYSQL_CHK); + + out: + free(errmsg); + return err_code; + + error: + free(hdr); + free(user); + free(var); + free_tcpcheck_vars(&rules->preset_vars); + tcpcheck_ruleset_release(rs); + err_code |= ERR_ALERT | ERR_FATAL; + goto out; +} + + static struct cfg_kw_list cfg_kws = {ILH, { { CFG_LISTEN, "tcp-check", proxy_parse_tcpcheck }, { 0, NULL, NULL },