From 41dd192cc3a36bdc707e7bef659df82a574a353c Mon Sep 17 00:00:00 2001 From: Vladislav Vaintroub Date: Fri, 26 Jun 2026 21:47:23 +0200 Subject: [PATCH] MDEV-25817 proxy protocol: successful login does not reset connect errors With proxy protocol thd_set_peer_addr() runs twice (proxy host, then the real client from the proxy header). Connect errors are accounted against the real client, but check_connection() incorrectly uses condition on the proxy host's count, rather than real client's address. Fix: reset both the proxy host, and real client's connect errors on successful connection. Added tests for incomplete handshake, and reset behavior, under proxy protocol, for both real client errors, and proxy host errors. --- include/mysql_com.h | 2 + mysql-test/main/mysql_client_test.result | 2 + mysql-test/main/mysql_client_test.test | 2 + sql/net_serv.cc | 14 +- sql/sql_connect.cc | 16 +- tests/mysql_client_test.c | 226 +++++++++++++++++++++++ 6 files changed, 252 insertions(+), 10 deletions(-) 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(); }