diff --git a/include/mysql_com.h b/include/mysql_com.h index 1b796ce402490..42b378677dcb7 100644 --- a/include/mysql_com.h +++ b/include/mysql_com.h @@ -488,6 +488,8 @@ typedef struct st_net { unsigned char compress; my_bool pkt_nr_can_be_reset; my_bool using_proxy_protocol; + /* proxy protocol: real client address has connect errors to reset on login */ + my_bool have_proxy_protocol_connect_errors; /* Pointer to query object in query cache, do not equal NULL (0) for queries in cache that have not stored its results yet diff --git a/mysql-test/main/mysql_client_test.result b/mysql-test/main/mysql_client_test.result index 4c7b20314c05d..7403a1bb5a332 100644 --- a/mysql-test/main/mysql_client_test.result +++ b/mysql-test/main/mysql_client_test.result @@ -8,6 +8,8 @@ SET @old_general_log= @@global.general_log; SET @old_slow_query_log= @@global.slow_query_log; call mtr.add_suppression(" Error reading file './client_test_db/test_frm_bug.frm'"); call mtr.add_suppression(" IP address .* could not be resolved"); +call mtr.add_suppression("Aborted connection .* host: '192.0.2.50'"); +call mtr.add_suppression("Aborted connection .* host: 'santa.claus.ipv6.example.com'"); ok # cat MYSQL_TMP_DIR/test_wl4435.out.log diff --git a/mysql-test/main/mysql_client_test.test b/mysql-test/main/mysql_client_test.test index f99aecace80d3..a227e95c984df 100644 --- a/mysql-test/main/mysql_client_test.test +++ b/mysql-test/main/mysql_client_test.test @@ -15,6 +15,8 @@ SET @old_slow_query_log= @@global.slow_query_log; call mtr.add_suppression(" Error reading file './client_test_db/test_frm_bug.frm'"); call mtr.add_suppression(" IP address .* could not be resolved"); +call mtr.add_suppression("Aborted connection .* host: '192.0.2.50'"); +call mtr.add_suppression("Aborted connection .* host: 'santa.claus.ipv6.example.com'"); # We run with different binaries for normal and --embedded-server # diff --git a/sql/net_serv.cc b/sql/net_serv.cc index 35b2a24a2d2c9..38a4c0269ce61 100644 --- a/sql/net_serv.cc +++ b/sql/net_serv.cc @@ -164,6 +164,7 @@ my_bool my_net_init(NET *net, Vio *vio, void *thd, uint my_flags) net->last_errno=0; net->pkt_nr_can_be_reset= 0; net->using_proxy_protocol= 0; + net->have_proxy_protocol_connect_errors= 0; net->thread_specific_malloc= MY_TEST(my_flags & MY_THREAD_SPECIFIC); net->thd= 0; #ifdef MYSQL_SERVER @@ -219,6 +220,7 @@ void net_end(NET *net) my_free(net->buff); net->buff=0; net->using_proxy_protocol= 0; + net->have_proxy_protocol_connect_errors= 0; DBUG_VOID_RETURN; } @@ -958,11 +960,15 @@ static handle_proxy_header_result handle_proxy_header(NET *net) /* proxy header indicates LOCAL connection, no action necessary */ return RETRY; /* Change peer address in THD and ACL structures.*/ - uint host_errors; + uint host_errors= 0; net->using_proxy_protocol= 1; - return (handle_proxy_header_result)thd_set_peer_addr(thd, - &(peer_info.peer_addr), NULL, peer_info.port, - false, &host_errors); + handle_proxy_header_result res= + (handle_proxy_header_result) thd_set_peer_addr(thd, &(peer_info.peer_addr), + NULL, peer_info.port, false, &host_errors); + /* Record the real client's connect errors for the login-success reset. */ + if (host_errors) + net->have_proxy_protocol_connect_errors= 1; + return res; #endif } diff --git a/sql/sql_connect.cc b/sql/sql_connect.cc index f7712b9abc1dc..ed866ab0b3921 100644 --- a/sql/sql_connect.cc +++ b/sql/sql_connect.cc @@ -1024,6 +1024,9 @@ static int check_connection(THD *thd) uint connect_errors= 0; int auth_rc; NET *net= &thd->net; + /* Socket peer IP address, the proxy host under PROXY protocol */ + char ip[NI_MAXHOST]; + ip[0]= 0; DBUG_PRINT("info", ("New connection received on %s", vio_description(net->vio))); @@ -1035,7 +1038,6 @@ static int check_connection(THD *thd) if (!thd->main_security_ctx.host) // If TCP/IP connection { my_bool peer_rc; - char ip[NI_MAXHOST]; uint16 peer_port; peer_rc= vio_peer_addr(net->vio, ip, &peer_port, NI_MAXHOST); @@ -1153,14 +1155,16 @@ static int check_connection(THD *thd) } auth_rc= acl_authenticate(thd, 0); - if (auth_rc == 0 && connect_errors != 0) + if (auth_rc == 0) { /* - A client connection from this IP was successful, - after some previous failures. - Reset the connection error counter. + Successful login resets connect errors, for both the socket peer and, + under PROXY protocol, the real client - each by its own error count. */ - reset_host_connect_errors(thd->main_security_ctx.ip); + if (connect_errors) + reset_host_connect_errors(ip); + if (net->have_proxy_protocol_connect_errors) + reset_host_connect_errors(thd->main_security_ctx.ip); } return auth_rc; diff --git a/tests/mysql_client_test.c b/tests/mysql_client_test.c index dba0b4c63e703..989777fcbaf87 100644 --- a/tests/mysql_client_test.c +++ b/tests/mysql_client_test.c @@ -38,6 +38,9 @@ #include "mysql_client_fw.c" #ifndef _WIN32 #include +#include +#include +#include #endif #include "my_valgrind.h" @@ -20855,6 +20858,227 @@ static void test_proxy_header_dbug_remote_connection() } +/* + Open a raw TCP connection and let the handshake fail (server reads EOF) to + add one max_connect_errors error. With a header it counts against 'client_ip'; + with NULL no header is sent, so against the socket peer (the proxy host). +*/ +static void proxy_send_handshake_error(const char *client_ip) +{ + struct addrinfo hints, *ai= NULL, *p; + char portbuf[20], header[128]; + int connected= 0; + my_socket s= INVALID_SOCKET; + + snprintf(portbuf, sizeof(portbuf), "%u", opt_port); + memset(&hints, 0, sizeof(hints)); + hints.ai_family= AF_UNSPEC; + hints.ai_socktype= SOCK_STREAM; + DIE_UNLESS(getaddrinfo(opt_host ? opt_host : "localhost", portbuf, + &hints, &ai) == 0); + + for (p= ai; p; p= p->ai_next) + { + s= socket(p->ai_family, p->ai_socktype, p->ai_protocol); + if (s == INVALID_SOCKET) + continue; + if (connect(s, p->ai_addr, (int) p->ai_addrlen) == 0) + { + connected= 1; + break; + } + closesocket(s); + s= INVALID_SOCKET; + } + freeaddrinfo(ai); + DIE_UNLESS(connected); + + if (client_ip) + { + snprintf(header, sizeof(header), + "PROXY TCP4 %s 127.0.0.1 12345 %u\r\n", + client_ip, opt_port); + DIE_UNLESS(send(s, header, strlen(header), 0) > 0); + } + /* Half-close our write side so the server's handshake read hits EOF. */ + DIE_UNLESS(shutdown(s, IF_WIN(SD_SEND,SHUT_WR)) == 0); + /* Drain until the server closes, ensuring the error has been accounted. */ + { + char buf[256]; + while (recv(s, buf, sizeof(buf), 0) > 0) + ; + } + closesocket(s); +} + +/* + Connect through PROXY protocol, advertising 'client_ip' as the client. + On success returns the connection (caller closes it); on failure returns NULL + and stores the client error code in *out_errno. +*/ +static MYSQL *proxy_connect_as(const char *client_ip, const char *user, + const char *passwd, unsigned int *out_errno) +{ + MYSQL *m= mysql_client_init(NULL); + char header[128]; + int proto= MYSQL_PROTOCOL_TCP; + int v6= strchr(client_ip, ':') != NULL; + DIE_UNLESS(m != NULL); + snprintf(header, sizeof(header), "PROXY %s %s %s 12345 %u\r\n", + v6 ? "TCP6" : "TCP4", client_ip, v6 ? "::1" : "127.0.0.1", opt_port); + mysql_optionsv(m, MARIADB_OPT_PROXY_HEADER, header, strlen(header)); + mysql_optionsv(m, MYSQL_OPT_PROTOCOL, &proto); + if (!mysql_real_connect(m, opt_host, user, passwd, NULL, opt_port, NULL, 0)) + { + if (out_errno) + *out_errno= mysql_errno(m); + mysql_close(m); + return NULL; + } + if (out_errno) + *out_errno= 0; + return m; +} + +/* + MDEV-25817: with PROXY protocol a successful login must reset the proxied + client's connect-error counter; the reset used to be gated on the proxy + host's count, so it never fired. +*/ +static void test_proxy_header_connect_errors_reset() +{ + const char *client_ip= "192.0.2.50"; + char query[256]; + unsigned int conn_errno= 0; + int rc, i; + MYSQL *m; + + myheader("test_proxy_header_connect_errors_reset"); + + rc= mysql_query(mysql, + "SET @saved_max_connect_errors= @@global.max_connect_errors"); + myquery(rc); + rc= mysql_query(mysql, "SET @@global.max_connect_errors=3"); + myquery(rc); + rc= mysql_query(mysql, "FLUSH HOSTS"); + myquery(rc); + + snprintf(query, sizeof(query), + "CREATE USER 'u'@'%s' IDENTIFIED BY 'password'", client_ip); + rc= mysql_query(mysql, query); + myquery(rc); + + /* max_connect_errors handshake errors block the host. */ + for (i= 0; i < 3; i++) + proxy_send_handshake_error(client_ip); + m= proxy_connect_as(client_ip, "u", "password", &conn_errno); + DIE_UNLESS(m == NULL && conn_errno == ER_HOST_IS_BLOCKED); + + rc= mysql_query(mysql, "FLUSH HOSTS"); + myquery(rc); + + /* Two handshake errors, still below max_connect_errors (3). */ + proxy_send_handshake_error(client_ip); + proxy_send_handshake_error(client_ip); + + /* Below the limit: must connect, and this success must reset the counter. */ + m= proxy_connect_as(client_ip, "u", "password", &conn_errno); + DIE_UNLESS(m != NULL); + mysql_close(m); + + /* Two more: with the reset count is 2 (<3, connectable); without it 4 + (>=3, blocked). */ + proxy_send_handshake_error(client_ip); + proxy_send_handshake_error(client_ip); + + m= proxy_connect_as(client_ip, "u", "password", &conn_errno); + DIE_UNLESS(m != NULL); /* ER_HOST_IS_BLOCKED with unfixed MDEV-25817 */ + mysql_close(m); + + snprintf(query, sizeof(query), "DROP USER 'u'@'%s'", client_ip); + rc= mysql_query(mysql, query); + myquery(rc); + rc= mysql_query(mysql, + "SET global max_connect_errors=@saved_max_connect_errors"); + myquery(rc); + rc= mysql_query(mysql, "FLUSH HOSTS"); + myquery(rc); +} + +/* + A successful proxied login must also reset the proxy host (socket peer), not + only the proxied client - else connections that never finish the handshake + (e.g. port checks) could block the proxy and lock out everyone behind it. + Debug injection fakes the socket peer to a non-loopback address (2001:db8::6:6, + which is in proxy_protocol_networks of both .opt files) and name resolution so + it is accounted. +*/ +static void test_proxy_header_proxy_host_connect_errors_reset() +{ +#ifndef DBUG_OFF + const char *proxied_client= "2001:db8::6:7"; /* in getaddrinfo_fake_good_ipv6 */ + unsigned int conn_errno= 0; + int rc, i; + MYSQL *m; + + myheader("test_proxy_header_proxy_host_connect_errors_reset"); + + rc= mysql_query(mysql, "SET @save_dbug= @@global.debug_dbug"); + myquery(rc); + rc= mysql_query(mysql, "SET GLOBAL debug_dbug='+d,vio_peer_addr_fake_ipv6," + "getnameinfo_fake_ipv6,getaddrinfo_fake_good_ipv6'"); + myquery(rc); + rc= mysql_query(mysql, + "SET @save_max_connect_errors= @@global.max_connect_errors"); + myquery(rc); + rc= mysql_query(mysql, "SET @@global.max_connect_errors=3"); + myquery(rc); + rc= mysql_query(mysql, "FLUSH HOSTS"); + myquery(rc); + + rc= mysql_query(mysql, + "CREATE USER 'u'@'santa.claus.ipv6.example.com' IDENTIFIED BY 'password'"); + myquery(rc); + + /* Errors with no header count against the socket peer (proxy host); + max_connect_errors of them block it. */ + for (i= 0; i < 3; i++) + proxy_send_handshake_error(NULL); + m= proxy_connect_as(proxied_client, "u", "password", &conn_errno); + DIE_UNLESS(m == NULL && conn_errno == ER_HOST_IS_BLOCKED); + + rc= mysql_query(mysql, "FLUSH HOSTS"); + myquery(rc); + + /* Two proxy-host handshake errors, below max_connect_errors (3). */ + proxy_send_handshake_error(NULL); + proxy_send_handshake_error(NULL); + + /* Success: client has no errors, so this must reset the proxy host's two. */ + m= proxy_connect_as(proxied_client, "u", "password", &conn_errno); + DIE_UNLESS(m != NULL); + mysql_close(m); + + /* Two more: with the reset proxy host is at 2 (<3); without it 4 (>=3). */ + proxy_send_handshake_error(NULL); + proxy_send_handshake_error(NULL); + + m= proxy_connect_as(proxied_client, "u", "password", &conn_errno); + DIE_UNLESS(m != NULL); /* ER_HOST_IS_BLOCKED if proxy host not reset */ + mysql_close(m); + + rc= mysql_query(mysql, "DROP USER 'u'@'santa.claus.ipv6.example.com'"); + myquery(rc); + rc= mysql_query(mysql, + "SET GLOBAL max_connect_errors= @save_max_connect_errors"); + myquery(rc); + rc= mysql_query(mysql, "SET GLOBAL debug_dbug= @save_dbug"); + myquery(rc); + rc= mysql_query(mysql, "FLUSH HOSTS"); + myquery(rc); +#endif /* !DBUG_OFF */ +} + static void test_proxy_header() { myheader("test_proxy_header"); @@ -20865,6 +21089,8 @@ static void test_proxy_header() test_proxy_header_ignore(); test_proxy_header_limits(); test_proxy_header_dbug_remote_connection(); + test_proxy_header_connect_errors_reset(); + test_proxy_header_proxy_host_connect_errors_reset(); }