diff options
author | Vladislav Vaintroub <wlad@mariadb.com> | 2017-08-22 21:08:38 +0200 |
---|---|---|
committer | Vladislav Vaintroub <wlad@mariadb.com> | 2017-08-22 21:08:38 +0200 |
commit | 58cd69fc808cade5ec3183a56b9d16957f0da02c (patch) | |
tree | 4ba0d41e6ad569efa3826162ac3505b103c90dc4 | |
parent | d258a2bd1f8548bafc155d0ac1ed06c236d3fca2 (diff) | |
download | mariadb-git-58cd69fc808cade5ec3183a56b9d16957f0da02c.tar.gz |
MDEV-11159 Server proxy protocol support
accept proxy protocol header from client connections.
The new server variable 'proxy_protocol_networks' contains list
of networks from which proxy header is accepted.
24 files changed, 904 insertions, 58 deletions
diff --git a/include/violite.h b/include/violite.h index 5dcf27dbab1..efdcb65286f 100644 --- a/include/violite.h +++ b/include/violite.h @@ -112,6 +112,8 @@ extern void vio_set_wait_callback(void (*before_wait)(void), my_bool vio_socket_connect(Vio *vio, struct sockaddr *addr, socklen_t len, int timeout); +void vio_get_normalized_ip(const struct sockaddr *src, int src_length, struct sockaddr *dst, int *dst_length); + my_bool vio_get_normalized_ip_string(const struct sockaddr *addr, int addr_length, char *ip_string, size_t ip_string_size); diff --git a/libmysqld/CMakeLists.txt b/libmysqld/CMakeLists.txt index 6dabc5e0192..2572b79ed39 100644 --- a/libmysqld/CMakeLists.txt +++ b/libmysqld/CMakeLists.txt @@ -117,6 +117,7 @@ SET(SQL_EMBEDDED_SOURCES emb_qcache.cc libmysqld.c lib_sql.cc ../sql/ha_sequence.cc ../sql/ha_sequence.h ../sql/temporary_tables.cc ../sql/session_tracker.cc + ../sql/proxy_protocol.cc ${GEN_SOURCES} ${MYSYS_LIBWRAP_SOURCE} ) diff --git a/mysql-test/r/mysql_client_test.result b/mysql-test/r/mysql_client_test.result index 4ad07b20ab5..83ef8d442b3 100644 --- a/mysql-test/r/mysql_client_test.result +++ b/mysql-test/r/mysql_client_test.result @@ -1,6 +1,7 @@ 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"); ok # cat MYSQL_TMP_DIR/test_wl4435.out.log diff --git a/mysql-test/r/mysql_client_test_comp.result b/mysql-test/r/mysql_client_test_comp.result index b9cac467b92..096331db4f1 100644 --- a/mysql-test/r/mysql_client_test_comp.result +++ b/mysql-test/r/mysql_client_test_comp.result @@ -1,4 +1,5 @@ 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"); ok SET @@global.slow_query_log= @old_slow_query_log; diff --git a/mysql-test/r/mysql_client_test_nonblock.result b/mysql-test/r/mysql_client_test_nonblock.result index e37e2132b0c..391794f082f 100644 --- a/mysql-test/r/mysql_client_test_nonblock.result +++ b/mysql-test/r/mysql_client_test_nonblock.result @@ -1,6 +1,7 @@ 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"); ok SET @@global.general_log= @old_general_log; SET @@global.slow_query_log= @old_slow_query_log; diff --git a/mysql-test/r/mysqld--help.result b/mysql-test/r/mysqld--help.result index a80ac92fa76..45e9a670eb3 100644 --- a/mysql-test/r/mysqld--help.result +++ b/mysql-test/r/mysqld--help.result @@ -783,6 +783,14 @@ The following options may be given as the first argument: Seconds between sending progress reports to the client for time-consuming statements. Set to 0 to disable progress reporting. + --proxy-protocol-networks=name + Enable proxy protocol for these source networks. The + syntax is a comma separated list of IPv4 and IPv6 + networks. If the network doesn't contain mask, it is + considered to be a single host. "*" represents all + networks and must the only directive on the line. String + "localhost" represents non-TCP local connections (Unix + domain socket, Windows named pipe or shared memory). --query-alloc-block-size=# Allocation block size for query parsing and execution --query-cache-limit=# @@ -1437,6 +1445,7 @@ preload-buffer-size 32768 profiling-history-size 15 progress-report-time 5 protocol-version 10 +proxy-protocol-networks query-alloc-block-size 16384 query-cache-limit 1048576 query-cache-min-res-unit 4096 diff --git a/mysql-test/suite/sys_vars/r/sysvars_server_notembedded.result b/mysql-test/suite/sys_vars/r/sysvars_server_notembedded.result index 8426c6228d5..4e3461a1af7 100644 --- a/mysql-test/suite/sys_vars/r/sysvars_server_notembedded.result +++ b/mysql-test/suite/sys_vars/r/sysvars_server_notembedded.result @@ -3369,6 +3369,20 @@ NUMERIC_BLOCK_SIZE 1 ENUM_VALUE_LIST NULL READ_ONLY YES COMMAND_LINE_ARGUMENT NULL +VARIABLE_NAME PROXY_PROTOCOL_NETWORKS +SESSION_VALUE NULL +GLOBAL_VALUE +GLOBAL_VALUE_ORIGIN COMPILE-TIME +DEFAULT_VALUE +VARIABLE_SCOPE GLOBAL +VARIABLE_TYPE VARCHAR +VARIABLE_COMMENT Enable proxy protocol for these source networks. The syntax is a comma separated list of IPv4 and IPv6 networks. If the network doesn't contain mask, it is considered to be a single host. "*" represents all networks and must the only directive on the line. +NUMERIC_MIN_VALUE NULL +NUMERIC_MAX_VALUE NULL +NUMERIC_BLOCK_SIZE NULL +ENUM_VALUE_LIST NULL +READ_ONLY YES +COMMAND_LINE_ARGUMENT REQUIRED VARIABLE_NAME PROXY_USER SESSION_VALUE GLOBAL_VALUE NULL diff --git a/mysql-test/t/mysql_client_test-master.opt b/mysql-test/t/mysql_client_test-master.opt index fcaf2b69fbc..8d49abf6a10 100644 --- a/mysql-test/t/mysql_client_test-master.opt +++ b/mysql-test/t/mysql_client_test-master.opt @@ -2,3 +2,4 @@ --general-log-file=$MYSQLTEST_VARDIR/log/master.log --log-output=FILE,TABLE --max-allowed-packet=32000000 +--proxy-protocol-networks=* diff --git a/mysql-test/t/mysql_client_test.test b/mysql-test/t/mysql_client_test.test index bf5331ca4f9..260473aa0d0 100644 --- a/mysql-test/t/mysql_client_test.test +++ b/mysql-test/t/mysql_client_test.test @@ -7,6 +7,7 @@ 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"); # We run with different binaries for normal and --embedded-server # diff --git a/mysql-test/t/mysql_client_test_comp-master.opt b/mysql-test/t/mysql_client_test_comp-master.opt index 783093c900b..6c4a5e4c782 100644 --- a/mysql-test/t/mysql_client_test_comp-master.opt +++ b/mysql-test/t/mysql_client_test_comp-master.opt @@ -1,2 +1,3 @@ --loose-enable-performance-schema --max-allowed-packet=32000000 +--proxy-protocol-networks=::1/32,127.0.0.0/8,localhost diff --git a/mysql-test/t/mysql_client_test_comp.test b/mysql-test/t/mysql_client_test_comp.test index 0a6b0ba1130..13a9d4944a4 100644 --- a/mysql-test/t/mysql_client_test_comp.test +++ b/mysql-test/t/mysql_client_test_comp.test @@ -9,7 +9,7 @@ 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"); --exec echo "$MYSQL_CLIENT_TEST" > $MYSQLTEST_VARDIR/log/mysql_client_test_comp.out.log 2>&1 --exec $MYSQL_CLIENT_TEST --getopt-ll-test=25600M >> $MYSQLTEST_VARDIR/log/mysql_client_test_comp.out.log 2>&1 diff --git a/mysql-test/t/mysql_client_test_nonblock-master.opt b/mysql-test/t/mysql_client_test_nonblock-master.opt index 5775e707c5f..a39d0089562 100644 --- a/mysql-test/t/mysql_client_test_nonblock-master.opt +++ b/mysql-test/t/mysql_client_test_nonblock-master.opt @@ -1,2 +1,3 @@ --general-log --general-log-file=$MYSQLTEST_VARDIR/log/master.log --log-output=FILE,TABLE --max-allowed-packet=32000000 +--proxy-protocol-networks=::1,::ffff:127.0.0.1/97,localhost diff --git a/mysql-test/t/mysql_client_test_nonblock.test b/mysql-test/t/mysql_client_test_nonblock.test index 51263854e58..31de14e4178 100644 --- a/mysql-test/t/mysql_client_test_nonblock.test +++ b/mysql-test/t/mysql_client_test_nonblock.test @@ -6,6 +6,7 @@ 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"); # We run with different binaries for normal and --embedded-server # diff --git a/sql/CMakeLists.txt b/sql/CMakeLists.txt index 88a4e40e373..e1d1b5a32a7 100644 --- a/sql/CMakeLists.txt +++ b/sql/CMakeLists.txt @@ -154,6 +154,7 @@ SET (SQL_SOURCE sql_sequence.cc sql_sequence.h ha_sequence.h ${WSREP_SOURCES} table_cache.cc encryption.cc temporary_tables.cc + proxy_protocol.cc ${CMAKE_CURRENT_BINARY_DIR}/sql_builtin.cc ${GEN_SOURCES} ${GEN_DIGEST_SOURCES} diff --git a/sql/mysqld.cc b/sql/mysqld.cc index dad15c98920..01f754955b6 100644 --- a/sql/mysqld.cc +++ b/sql/mysqld.cc @@ -75,6 +75,7 @@ #include "wsrep_var.h" #include "wsrep_thd.h" #include "wsrep_sst.h" +#include "proxy_protocol.h" #include "sql_callback.h" #include "threadpool.h" @@ -2286,6 +2287,7 @@ void clean_up(bool print_message) my_free(const_cast<char*>(relay_log_index)); #endif free_list(opt_plugin_load_list_ptr); + cleanup_proxy_protocol_networks(); /* The following lines may never be executed as the main thread may have @@ -2682,6 +2684,9 @@ static void network_init(void) if (MYSQL_CALLBACK_ELSE(thread_scheduler, init, (), 0)) unireg_abort(1); /* purecov: inspected */ + if (set_proxy_protocol_networks(my_proxy_protocol_networks)) + unireg_abort(1); + set_ports(); if (report_port == 0) diff --git a/sql/mysqld.h b/sql/mysqld.h index 6cf5a3776a0..0b15d5ac322 100644 --- a/sql/mysqld.h +++ b/sql/mysqld.h @@ -558,6 +558,7 @@ extern MYSQL_PLUGIN_IMPORT char mysql_real_data_home[]; extern char mysql_unpacked_real_data_home[]; extern MYSQL_PLUGIN_IMPORT struct system_variables global_system_variables; extern char default_logfile_name[FN_REFLEN]; +extern char *my_proxy_protocol_networks; #define mysql_tmpdir (my_tmpdir(&mysql_tmpdir_list)) diff --git a/sql/net_serv.cc b/sql/net_serv.cc index f9635689e63..765491dc40e 100644 --- a/sql/net_serv.cc +++ b/sql/net_serv.cc @@ -45,12 +45,9 @@ #include <violite.h> #include <signal.h> #include "probes_mysql.h" - -#ifdef EMBEDDED_LIBRARY -#undef MYSQL_SERVER -#undef MYSQL_CLIENT -#define MYSQL_CLIENT -#endif /*EMBEDDED_LIBRARY */ +#include "proxy_protocol.h" +#include <sql_class.h> +#include <sql_connect.h> /* to reduce the number of ifdef's in the code @@ -118,7 +115,6 @@ extern my_bool thd_net_is_killed(); #define thd_net_is_killed() 0 #endif -#define TEST_BLOCKING 8 static my_bool net_write_buff(NET *, const uchar *, ulong); @@ -829,6 +825,57 @@ static my_bool my_net_skip_rest(NET *net, uint32 remain, thr_alarm_t *alarmed, /** + Try to parse and process proxy protocol header. + + This function is called in case MySQL packet header cannot be parsed. + It checks if proxy header was sent, and that it was send from allowed remote + host, as defined by proxy-protocol-networks parameter. + + If proxy header is parsed, then THD and ACL structures and changed to indicate + the new peer address and port. + + Note, that proxy header can only be sent either when the connection is established, + or as the client reply packet to +*/ +static int handle_proxy_header(NET *net) +{ + proxy_peer_info peer_info; + THD *thd= (THD *)net->thd; + + if (!thd || !thd->net.vio) + { + DBUG_ASSERT(0); + return 1; + } + + if (!is_proxy_protocol_allowed((sockaddr *)&(thd->net.vio->remote))) + { + /* proxy-protocol-networks variable needs to be set to allow this remote address */ + my_printf_error(ER_HOST_NOT_PRIVILEGED, "Proxy header is not accepted from %s", + MYF(0), thd->main_security_ctx.ip); + return 1; + } + + if (parse_proxy_protocol_header(net, &peer_info)) + { + /* Failed to parse proxy header*/ + my_printf_error(ER_UNKNOWN_ERROR, "Failed to parse proxy header", MYF(0)); + return 1; + } + + if (peer_info.is_local_command) + /* proxy header indicates LOCAL connection, no action necessary */ + return 0; +#ifdef EMBEDDED_LIBRARY + DBUG_ASSERT(0); + return 1; +#else + /* Change peer address in THD and ACL structures.*/ + return thd_set_peer_addr(thd, &(peer_info.peer_addr), NULL, peer_info.port, false); +#endif +} + +/** Reads one packet to net->buff + net->where_b. Long packets are handled by my_net_read(). This function reallocates the net->buff buffer if necessary. @@ -850,6 +897,9 @@ my_real_read(NET *net, size_t *complen, #ifndef NO_ALARM ALARM alarm_buff; #endif + +retry: + my_bool net_blocking=vio_is_blocking(net->vio); uint32 remain= (net->compress ? NET_HEADER_SIZE+COMP_HEADER_SIZE : NET_HEADER_SIZE); @@ -1081,6 +1131,22 @@ end: packets_out_of_order: { + if (has_proxy_protocol_header(net) + && net->thd && + ((THD *)net->thd)->get_command() == COM_CONNECT) + { + /* Proxy information found in the first 4 bytes received so far. + Read and parse proxy header , change peer ip address and port in THD. + */ + if (handle_proxy_header(net)) + { + /* error happened, message is already written. */ + len= packet_error; + goto end; + } + goto retry; + } + DBUG_PRINT("error", ("Packets out of order (Found: %d, expected %u)", (int) net->buff[net->where_b + 3], @@ -1171,6 +1237,7 @@ my_net_read_packet_reallen(NET *net, my_bool read_from_server, ulong* reallen) len+= total_length; net->where_b = save_pos; } + net->read_pos = net->buff + net->where_b; if (len != packet_error) { diff --git a/sql/proxy_protocol.cc b/sql/proxy_protocol.cc new file mode 100644 index 00000000000..616e5397ea1 --- /dev/null +++ b/sql/proxy_protocol.cc @@ -0,0 +1,491 @@ +/* Copyright (c) 2017, MariaDB + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ + +#include <my_global.h> +#include <mysql.h> +#include <mysql_com.h> +#include <mysqld_error.h> +#include <my_sys.h> +#include <m_string.h> +#include <my_net.h> +#include <violite.h> +#include <proxy_protocol.h> +#include <log.h> + +#define PROXY_PROTOCOL_V1_SIGNATURE "PROXY" +#define PROXY_PROTOCOL_V2_SIGNATURE "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A" +#define MAX_PROXY_HEADER_LEN 256 + +/* + Parse proxy protocol version 1 header (text) +*/ +static int parse_v1_header(char *hdr, size_t len, proxy_peer_info *peer_info) +{ + char address_family[MAX_PROXY_HEADER_LEN + 1]; + char client_address[MAX_PROXY_HEADER_LEN + 1]; + char server_address[MAX_PROXY_HEADER_LEN + 1]; + int client_port; + int server_port; + + int ret = sscanf(hdr, "PROXY %s %s %s %d %d", + address_family, client_address, server_address, + &client_port, &server_port); + + if (ret != 5) + { + if (ret >= 1 && !strcmp(address_family, "UNKNOWN")) + { + peer_info->is_local_command= true; + return 0; + } + return -1; + } + + if (client_port < 0 || client_port > UINT16_MAX + || server_port < 0 || server_port > UINT16_MAX) + return -1; + + if (!strcmp(address_family, "UNKNOWN")) + { + peer_info->is_local_command= true; + return 0; + } + else if (!strcmp(address_family, "TCP4")) + { + /* Initialize IPv4 peer address.*/ + peer_info->peer_addr.ss_family= AF_INET; + if (!inet_pton(AF_INET, client_address, + &((struct sockaddr_in *)(&peer_info->peer_addr))->sin_addr)) + return -1; + } + else if (!strcmp(address_family, "TCP6")) + { + /* Initialize IPv6 peer address.*/ + peer_info->peer_addr.ss_family= AF_INET6; + if (!inet_pton(AF_INET6, client_address, + &((struct sockaddr_in6 *)(&peer_info->peer_addr))->sin6_addr)) + return -1; + } + peer_info->port= client_port; + /* Check if server address is legal.*/ + char addr_bin[16]; + if (!inet_pton(peer_info->peer_addr.ss_family, + server_address, addr_bin)) + return -1; + + return 0; +} + + +/* + Parse proxy protocol V2 (binary) header +*/ +static int parse_v2_header(uchar *hdr, size_t len,proxy_peer_info *peer_info) +{ + /* V2 Signature */ + if (memcmp(hdr, PROXY_PROTOCOL_V2_SIGNATURE, 12)) + return -1; + + /* version + command */ + uint8 ver= (hdr[12] & 0xF0); + if (ver != 0x20) + return -1; /* Wrong version*/ + + uint cmd= (hdr[12] & 0xF); + + /* Address family */ + uchar fam= hdr[13]; + + if (cmd == 0) + { + /* LOCAL command*/ + peer_info->is_local_command= true; + return 0; + } + + if (cmd != 0x01) + { + /* Not PROXY COMMAND */ + return -1; + } + + struct sockaddr_in *sin= (struct sockaddr_in *)(&peer_info->peer_addr); + struct sockaddr_in6 *sin6= (struct sockaddr_in6 *)(&peer_info->peer_addr); + switch (fam) + { + case 0x11: /* TCPv4 */ + sin->sin_family= AF_INET; + memcpy(&(sin->sin_addr), hdr + 16, 4); + peer_info->port= (hdr[24] << 8) + hdr[25]; + break; + case 0x21: /* TCPv6 */ + sin6->sin6_family= AF_INET6; + memcpy(&(sin6->sin6_addr), hdr + 16, 16); + peer_info->port= (hdr[48] << 8) + hdr[49]; + break; + case 0x31: /* AF_UNIX, stream */ + peer_info->peer_addr.ss_family= AF_UNIX; + break; + default: + return -1; + } + return 0; +} + + +bool has_proxy_protocol_header(NET *net) +{ + compile_time_assert(NET_HEADER_SIZE < sizeof(PROXY_PROTOCOL_V1_SIGNATURE)); + compile_time_assert(NET_HEADER_SIZE < sizeof(PROXY_PROTOCOL_V2_SIGNATURE)); + + const uchar *preread_bytes= net->buff + net->where_b; + return !memcmp(preread_bytes, PROXY_PROTOCOL_V1_SIGNATURE, NET_HEADER_SIZE)|| + !memcmp(preread_bytes, PROXY_PROTOCOL_V2_SIGNATURE, NET_HEADER_SIZE); +} + + +/** + Try to parse proxy header. + https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt + + Whenever this function is called, client is connecting, and + we have have pre-read 4 bytes (NET_HEADER_SIZE) from the network already. + These 4 bytes did not match MySQL packet header, and (unless the client + is buggy), those bytes must be proxy header. + + @param[in] net - vio and already preread bytes from the header + @param[out] peer_info - parsed proxy header with client host and port + @return 0 in case of success, -1 if error. +*/ +int parse_proxy_protocol_header(NET *net, proxy_peer_info *peer_info) +{ + uchar hdr[MAX_PROXY_HEADER_LEN]; + size_t pos= 0; + + DBUG_ASSERT(!net->compress); + const uchar *preread_bytes= net->buff + net->where_b; + bool have_v1_header= !memcmp(preread_bytes, PROXY_PROTOCOL_V1_SIGNATURE, NET_HEADER_SIZE); + bool have_v2_header= + !have_v1_header && !memcmp(preread_bytes, PROXY_PROTOCOL_V2_SIGNATURE, NET_HEADER_SIZE); + if (!have_v1_header && !have_v2_header) + { + // not a proxy protocol header + return -1; + } + memcpy(hdr, preread_bytes, NET_HEADER_SIZE); + pos= NET_HEADER_SIZE; + Vio *vio= net->vio; + memset(peer_info, 0, sizeof (*peer_info)); + + if (have_v1_header) + { + /* Read until end of header (newline character)*/ + while(pos < sizeof(hdr)) + { + long len= (long)vio_read(vio, hdr + pos, 1); + if (len < 0) + return -1; + pos++; + if (hdr[pos-1] == '\n') + break; + } + hdr[pos]= 0; + + if (parse_v1_header((char *)hdr, pos, peer_info)) + return -1; + } + else // if (have_v2_header) + { +#define PROXY_V2_HEADER_LEN 16 + /* read off 16 bytes of the header.*/ + long len= vio_read(vio, hdr + pos, PROXY_V2_HEADER_LEN - pos); + if (len < 0) + return -1; + // 2 last bytes are the length in network byte order of the part following header + ushort trail_len= ((ushort)hdr[PROXY_V2_HEADER_LEN-2] >> 8) + hdr[PROXY_V2_HEADER_LEN-1]; + if (trail_len > sizeof(hdr) - PROXY_V2_HEADER_LEN) + return -1; + len= vio_read(vio, hdr + PROXY_V2_HEADER_LEN, trail_len); + pos= PROXY_V2_HEADER_LEN + trail_len; + if (parse_v2_header(hdr, pos, peer_info)) + return -1; + } + + if (peer_info->peer_addr.ss_family == AF_INET6) + { + /* + Normalize IPv4 compatible or mapped IPv6 addresses. + They will be treated as IPv4. + */ + sockaddr_storage tmp; + int dst_len; + memset(&tmp, 0, sizeof(tmp)); + vio_get_normalized_ip((const struct sockaddr *)&peer_info->peer_addr, + sizeof(sockaddr_storage), (struct sockaddr *)&tmp, &dst_len); + memcpy(&peer_info->peer_addr, &tmp, (size_t)dst_len); + } + return 0; +} + + +/** + CIDR address matching etc (for the proxy_protocol_networks parameter) +*/ + +/** + Subnetwork address in CIDR format, e.g + 192.168.1.0/24 or 2001:db8::/32 +*/ +struct subnet +{ + char addr[16]; /* Binary representation of the address, big endian*/ + unsigned short family; /* Address family, AF_INET or AF_INET6 */ + unsigned short bits; /* subnetwork size */ +}; + +static subnet* proxy_protocol_subnets; +size_t proxy_protocol_subnet_count; + +#define MAX_MASK_BITS(family) (family == AF_INET ? 32 : 128) + + +/** Convert IPv4 that are compat or mapped IPv4 to "normal" IPv4 */ +static int normalize_subnet(struct subnet *subnet) +{ + unsigned char *addr= (unsigned char*)subnet->addr; + if (subnet->family == AF_INET6) + { + const struct in6_addr *src_ip6=(in6_addr *)addr; + if (IN6_IS_ADDR_V4MAPPED(src_ip6) || IN6_IS_ADDR_V4COMPAT(src_ip6)) + { + /* Copy the actual IPv4 address (4 last bytes) */ + if (subnet->bits < 96) + return -1; + subnet->family= AF_INET; + memcpy(addr, addr+12, 4); + subnet->bits -= 96; + } + } + return 0; +} + +/** + Convert string representation of a subnet to subnet struct. +*/ +static int parse_subnet(char *addr_str, struct subnet *subnet) +{ + if (strchr(addr_str, ':')) + subnet->family= AF_INET6; + else if (strchr(addr_str, '.')) + subnet->family= AF_INET; + else if (!strcmp(addr_str, "localhost")) + { + subnet->family= AF_UNIX; + subnet->bits= 0; + return 0; + } + + char *pmask= strchr(addr_str, '/'); + if (!pmask) + { + subnet->bits= MAX_MASK_BITS(subnet->family); + } + else + { + *pmask= 0; + pmask++; + int b= 0; + + do + { + if (*pmask < '0' || *pmask > '9') + return -1; + b= 10 * b + *pmask - '0'; + if (b > MAX_MASK_BITS(subnet->family)) + return -1; + pmask++; + } + while (*pmask); + + subnet->bits= (unsigned short)b; + } + + if (!inet_pton(subnet->family, addr_str, subnet->addr)) + return -1; + + if (normalize_subnet(subnet)) + return -1; + + return 0; +} + +/** + Parse comma separated string subnet list into subnets array, + which is stored in 'proxy_protocol_subnets' variable + + @param[in] subnets_str : networks in CIDR format, + separated by comma and/or space + + @return 0 if success, otherwise -1 +*/ +int set_proxy_protocol_networks(const char *subnets_str) +{ + if (!subnets_str || !*subnets_str) + return 0; + + size_t max_subnets= MY_MAX(3,strlen(subnets_str)/2); + proxy_protocol_subnets= (subnet *)my_malloc(max_subnets * sizeof(subnet),MY_ZEROFILL); + + /* Check for special case '*'. */ + if (strcmp(subnets_str, "*") == 0) + { + + proxy_protocol_subnets[0].family= AF_INET; + proxy_protocol_subnets[1].family= AF_INET6; + proxy_protocol_subnets[2].family= AF_UNIX; + proxy_protocol_subnet_count= 3; + return 0; + } + + char token[256]; + const char *p= subnets_str; + for(proxy_protocol_subnet_count= 0;; proxy_protocol_subnet_count++) + { + while(*p && (*p ==',' || *p == ' ')) + p++; + if (!*p) + break; + + size_t cnt= 0; + while(*p && *p != ',' && *p != ' ' && cnt < sizeof(token)-1) + token[cnt++]= *p++; + + token[cnt++]=0; + if (cnt == sizeof(token)) + return -1; + + if (parse_subnet(token, &proxy_protocol_subnets[proxy_protocol_subnet_count])) + { + sql_print_error("Error parsing proxy_protocol_networks parameter, near '%s'",token); + return -1; + } + } + return 0; +} + +/** + Compare memory areas, in memcmp().similar fashion. + The difference to memcmp() is that size parameter is the + bit count, not byte count. +*/ +static int compare_bits(const void *s1, const void *s2, int bit_count) +{ + int result= 0; + int byte_count= bit_count / 8; + if (byte_count && (result= memcmp(s1, s2, byte_count))) + return result; + int rem= byte_count % 8; + if (rem) + { + // compare remaining bits i.e partial bytes. + unsigned char s1_bits= (((char *)s1)[byte_count]) >> (8 - rem); + unsigned char s2_bits= (((char *)s2)[byte_count]) >> (8 - rem); + if (s1_bits > s2_bits) + return 1; + if (s1_bits < s2_bits) + return -1; + } + return 0; +} + +/** + Check whether networks address matches network. +*/ +bool addr_matches_subnet(const sockaddr *sock_addr, const subnet *subnet) +{ + DBUG_ASSERT(subnet->family == AF_UNIX || + subnet->family == AF_INET || + subnet->family == AF_INET6); + + if (sock_addr->sa_family != subnet->family) + return false; + + if (subnet->family == AF_UNIX) + return true; + + void *addr= (subnet->family == AF_INET) ? + (void *)&((struct sockaddr_in *)sock_addr)->sin_addr : + (void *)&((struct sockaddr_in6 *)sock_addr)->sin6_addr; + + return (compare_bits(subnet->addr, addr, subnet->bits) == 0); +} + + +/** + Check whether proxy header from client is allowed, as per + specification in 'proxy_protocol_networks' server variable. + + The non-TCP "localhost" clients (unix socket, shared memory, pipes) + are accepted whenever 127.0.0.1 accepted in 'proxy_protocol_networks' +*/ +bool is_proxy_protocol_allowed(const sockaddr *addr) +{ + if (proxy_protocol_subnet_count == 0) + return false; + + sockaddr_storage addr_storage; + struct sockaddr *normalized_addr= (struct sockaddr *)&addr_storage; + + /* + Non-TCP addresses (unix domain socket, windows pipe and shared memory + gets tranlated to TCP4 localhost address. + + Note, that vio remote addresses are initialized with binary zeros + for these protocols (which is AF_UNSPEC everywhere). + */ + switch(addr->sa_family) + { + case AF_UNSPEC: + case AF_UNIX: + normalized_addr->sa_family= AF_UNIX; + break; + case AF_INET: + case AF_INET6: + { + int len= + (addr->sa_family == AF_INET)?sizeof(sockaddr_in):sizeof (sockaddr_in6); + int dst_len; + vio_get_normalized_ip(addr, len,normalized_addr, &dst_len); + } + break; + default: + DBUG_ASSERT(0); + } + + for (size_t i= 0; i < proxy_protocol_subnet_count; i++) + if (addr_matches_subnet(normalized_addr, &proxy_protocol_subnets[i])) + return true; + + return false; +} + + +void cleanup_proxy_protocol_networks() +{ + my_free(proxy_protocol_subnets); + proxy_protocol_subnets= 0; + proxy_protocol_subnet_count= 0; +} + diff --git a/sql/proxy_protocol.h b/sql/proxy_protocol.h new file mode 100644 index 00000000000..6f9bcf73307 --- /dev/null +++ b/sql/proxy_protocol.h @@ -0,0 +1,15 @@ +#include "my_net.h" + +struct proxy_peer_info +{ + struct sockaddr_storage peer_addr; + int port; + bool is_local_command; +}; + +extern bool has_proxy_protocol_header(NET *net); +extern int parse_proxy_protocol_header(NET *net, proxy_peer_info *peer_info); +extern bool is_proxy_protocol_allowed(const sockaddr *remote_addr); + +extern int set_proxy_protocol_networks(const char *spec); +extern void cleanup_proxy_protocol_networks(); diff --git a/sql/sql_connect.cc b/sql/sql_connect.cc index 168814bcc81..a7b31dd6abd 100644 --- a/sql/sql_connect.cc +++ b/sql/sql_connect.cc @@ -37,6 +37,7 @@ #include "sql_acl.h" // acl_getroot, NO_ACCESS, SUPER_ACL #include "sql_callback.h" #include "wsrep_mysqld.h" +#include "proxy_protocol.h" HASH global_user_stats, global_client_stats, global_table_stats; HASH global_index_stats; @@ -836,6 +837,89 @@ bool init_new_connection_handler_thread() return 0; } +int thd_set_peer_addr(THD *thd, sockaddr_storage *addr, const char *ip,uint port, bool check_proxy_networks) +{ + uint connect_errors; + thd->peer_port = port; + + char ip_string[128]; + if (!ip) + { + void *addr_data; + if (addr->ss_family == AF_UNIX) + { + /* local connection */ + my_free((void *)thd->main_security_ctx.ip); + thd->main_security_ctx.host_or_ip= thd->main_security_ctx.host = my_localhost; + thd->main_security_ctx.ip= 0; + return 0; + } + else if (addr->ss_family == AF_INET) + addr_data= &((struct sockaddr_in *)addr)->sin_addr; + else + addr_data= &((struct sockaddr_in6 *)addr)->sin6_addr; + if (!inet_ntop(addr->ss_family,addr_data, ip_string, sizeof(ip_string))) + { + DBUG_ASSERT(0); + return 1; + } + ip= ip_string; + } + + my_free((void *)thd->main_security_ctx.ip); + if (!(thd->main_security_ctx.ip = my_strdup(ip, MYF(MY_WME)))) + { + /* + No error accounting per IP in host_cache, + this is treated as a global server OOM error. + TODO: remove the need for my_strdup. + */ + statistic_increment(aborted_connects, &LOCK_status); + statistic_increment(connection_errors_internal, &LOCK_status); + return 1; /* The error is set by my_strdup(). */ + } + thd->main_security_ctx.host_or_ip = thd->main_security_ctx.ip; + if (!(specialflag & SPECIAL_NO_RESOLVE)) + { + int rc; + + rc = ip_to_hostname(addr, + thd->main_security_ctx.ip, + &thd->main_security_ctx.host, + &connect_errors); + + /* Cut very long hostnames to avoid possible overflows */ + if (thd->main_security_ctx.host) + { + if (thd->main_security_ctx.host != my_localhost) + ((char*)thd->main_security_ctx.host)[MY_MIN(strlen(thd->main_security_ctx.host), + HOSTNAME_LENGTH)] = 0; + thd->main_security_ctx.host_or_ip = thd->main_security_ctx.host; + } + + if (rc == RC_BLOCKED_HOST) + { + /* HOST_CACHE stats updated by ip_to_hostname(). */ + my_error(ER_HOST_IS_BLOCKED, MYF(0), thd->main_security_ctx.host_or_ip); + return 1; + } + } + DBUG_PRINT("info", ("Host: %s ip: %s", + (thd->main_security_ctx.host ? + thd->main_security_ctx.host : "unknown host"), + (thd->main_security_ctx.ip ? + thd->main_security_ctx.ip : "unknown ip"))); + if ((!check_proxy_networks || !is_proxy_protocol_allowed((struct sockaddr *) addr)) + && acl_check_host(thd->main_security_ctx.host, thd->main_security_ctx.ip)) + { + /* HOST_CACHE stats updated by acl_check_host(). */ + my_error(ER_HOST_NOT_PRIVILEGED, MYF(0), + thd->main_security_ctx.host_or_ip); + return 1; + } + return 0; +} + /* Perform handshake, authorize client and update thd ACL variables. @@ -865,8 +949,9 @@ static int check_connection(THD *thd) { my_bool peer_rc; char ip[NI_MAXHOST]; + uint16 peer_port; - peer_rc= vio_peer_addr(net->vio, ip, &thd->peer_port, NI_MAXHOST); + peer_rc= vio_peer_addr(net->vio, ip, &peer_port, NI_MAXHOST); /* =========================================================================== @@ -941,55 +1026,9 @@ static int check_connection(THD *thd) my_error(ER_BAD_HOST_ERROR, MYF(0)); return 1; } - if (!(thd->main_security_ctx.ip= my_strdup(ip,MYF(MY_WME)))) - { - /* - No error accounting per IP in host_cache, - this is treated as a global server OOM error. - TODO: remove the need for my_strdup. - */ - statistic_increment(aborted_connects,&LOCK_status); - statistic_increment(connection_errors_internal, &LOCK_status); - return 1; /* The error is set by my_strdup(). */ - } - thd->main_security_ctx.host_or_ip= thd->main_security_ctx.ip; - if (!(specialflag & SPECIAL_NO_RESOLVE)) - { - int rc; - - rc= ip_to_hostname(&net->vio->remote, - thd->main_security_ctx.ip, - &thd->main_security_ctx.host, - &connect_errors); - - /* Cut very long hostnames to avoid possible overflows */ - if (thd->main_security_ctx.host) - { - if (thd->main_security_ctx.host != my_localhost) - ((char*) thd->main_security_ctx.host)[MY_MIN(strlen(thd->main_security_ctx.host), - HOSTNAME_LENGTH)]= 0; - thd->main_security_ctx.host_or_ip= thd->main_security_ctx.host; - } - - if (rc == RC_BLOCKED_HOST) - { - /* HOST_CACHE stats updated by ip_to_hostname(). */ - my_error(ER_HOST_IS_BLOCKED, MYF(0), thd->main_security_ctx.host_or_ip); - return 1; - } - } - DBUG_PRINT("info",("Host: %s ip: %s", - (thd->main_security_ctx.host ? - thd->main_security_ctx.host : "unknown host"), - (thd->main_security_ctx.ip ? - thd->main_security_ctx.ip : "unknown ip"))); - if (acl_check_host(thd->main_security_ctx.host, thd->main_security_ctx.ip)) - { - /* HOST_CACHE stats updated by acl_check_host(). */ - my_error(ER_HOST_NOT_PRIVILEGED, MYF(0), - thd->main_security_ctx.host_or_ip); + + if (thd_set_peer_addr(thd, &net->vio->remote, ip, peer_port, true)) return 1; - } } else /* Hostname given means that the connection was on a socket */ { diff --git a/sql/sql_connect.h b/sql/sql_connect.h index 364be401944..03a2818b206 100644 --- a/sql/sql_connect.h +++ b/sql/sql_connect.h @@ -85,6 +85,7 @@ bool thd_init_client_charset(THD *thd, uint cs_number); bool setup_connection_thread_globals(THD *thd); bool thd_prepare_connection(THD *thd); bool thd_is_connection_alive(THD *thd); +int thd_set_peer_addr(THD *thd, sockaddr_storage *addr, const char *ip, uint port, bool check_proxy_networks); bool login_connection(THD *thd); void prepare_new_connection_state(THD* thd); diff --git a/sql/sys_vars.cc b/sql/sys_vars.cc index 86a778388ac..6c60766edd2 100644 --- a/sql/sys_vars.cc +++ b/sql/sys_vars.cc @@ -4126,6 +4126,17 @@ static Sys_var_charptr Sys_license( READ_ONLY GLOBAL_VAR(license), NO_CMD_LINE, IN_SYSTEM_CHARSET, DEFAULT(STRINGIFY_ARG(LICENSE))); +char *my_proxy_protocol_networks; +static Sys_var_charptr Sys_proxy_protocol_networks( + "proxy_protocol_networks", "Enable proxy protocol for these source " + "networks. The syntax is a comma separated list of IPv4 and IPv6 " + "networks. If the network doesn't contain mask, it is considered to be " + "a single host. \"*\" represents all networks and must the only " + "directive on the line. String \"localhost\" represents non-TCP " + "local connections (Unix domain socket, Windows named pipe or shared memory).", + READ_ONLY GLOBAL_VAR(my_proxy_protocol_networks), + CMD_LINE(REQUIRED_ARG), IN_FS_CHARSET, DEFAULT("")); + static bool check_log_path(sys_var *self, THD *thd, set_var *var) { if (!var->value) diff --git a/tests/mysql_client_test.c b/tests/mysql_client_test.c index 734f7790025..c3b932addec 100644 --- a/tests/mysql_client_test.c +++ b/tests/mysql_client_test.c @@ -33,6 +33,9 @@ */ #include "mysql_client_fw.c" +#ifndef _WIN32 +#include <arpa/inet.h> +#endif static const my_bool my_true= 1; @@ -19635,6 +19638,181 @@ static void test_mdev12579() } +typedef struct { + char sig[12]; + char ver_cmd; + char fam; + short len; + union { + struct { /* for TCP/UDP over IPv4, len = 12 */ + int src_addr; + int dst_addr; + short src_port; + short dst_port; + } ip4; + struct { /* for TCP/UDP over IPv6, len = 36 */ + char src_addr[16]; + char dst_addr[16]; + short src_port; + short dst_port; + } ip6; + struct { /* for AF_UNIX sockets, len = 216 */ + char src_addr[108]; + char dst_addr[108]; + } unx; + } addr; +} v2_proxy_header; + +#ifndef EMBEDDED_LIBRARY +static void test_proxy_header_tcp(const char *ipaddr, int port) +{ + + int rc; + MYSQL_RES *result; + int family = (strchr(ipaddr,':') == NULL)?AF_INET:AF_INET6; + char query[256]; + char text_header[256]; + char addr_bin[16]; + v2_proxy_header v2_header; + void *header_data[2]; + size_t header_lengths[2]; + int i; + + // normalize IPv4-mapped IPv6 addresses, e.g ::ffff:192.168.0.1 to 192.168.0.1 + char *normalized_addr= strncmp(ipaddr, "::ffff:", 7)?ipaddr : ipaddr + 7; + + memset(&v2_header, 0, sizeof(v2_header)); + sprintf(text_header,"PROXY %s %s %s %d 3306\r\n",family == AF_INET?"TCP4":"TCP6", ipaddr, ipaddr, port); + + inet_pton(family,ipaddr,addr_bin); + + memcpy(v2_header.sig, "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A", 12); + v2_header.ver_cmd = (0x2 << 4) | 0x1; /* Version (0x2) , Command = PROXY (0x1) */ + if(family == AF_INET) + { + v2_header.fam= 0x11; + v2_header.len= htons(12); + v2_header.addr.ip4.src_port= htons(port); + v2_header.addr.ip4.dst_port= htons(3306); + memcpy(&v2_header.addr.ip4.src_addr,addr_bin, sizeof (v2_header.addr.ip4.src_addr)); + memcpy(&v2_header.addr.ip4.dst_addr,addr_bin, sizeof (v2_header.addr.ip4.dst_addr)); + } + else + { + v2_header.fam= 0x21; + v2_header.len= htons(36); + v2_header.addr.ip6.src_port= htons(port); + v2_header.addr.ip6.dst_port= htons(3306); + memcpy(v2_header.addr.ip6.src_addr,addr_bin, sizeof (v2_header.addr.ip6.src_addr)); + memcpy(v2_header.addr.ip6.dst_addr,addr_bin, sizeof (v2_header.addr.ip6.dst_addr)); + } + + sprintf(query,"CREATE USER 'u'@'%s' IDENTIFIED BY 'password'",normalized_addr); + rc= mysql_query(mysql, query); + myquery(rc); + + header_data[0]= text_header; + header_data[1]= &v2_header; + + header_lengths[0]= strlen(text_header); + header_lengths[1]= family == AF_INET ? 28 : 52; + + for (i = 0; i < 2; i++) + { + MYSQL *m; + size_t addrlen; + MYSQL_ROW row; + m = mysql_client_init(NULL); + DIE_UNLESS(m); + mysql_optionsv(m, MARIADB_OPT_PROXY_HEADER, header_data[i], header_lengths[i]); + if (!mysql_real_connect(m, opt_host, "u", "password", NULL, opt_port, opt_unix_socket, 0)) + { + DIE_UNLESS(0); + } + rc= mysql_query(m, "select host from information_schema.processlist WHERE ID = connection_id()"); + myquery(rc); + /* get the result */ + result= mysql_store_result(m); + mytest(result); + row = mysql_fetch_row(result); + addrlen = strlen(normalized_addr); + DIE_UNLESS(strncmp(row[0], normalized_addr, addrlen) == 0); + DIE_UNLESS(atoi(row[0] + addrlen+1) == port); + mysql_close(m); + } + sprintf(query,"DROP USER 'u'@'%s'",normalized_addr); + rc = mysql_query(mysql, query); + myquery(rc); +} + + +/* Test proxy protocol with AF_UNIX (localhost) */ +static void test_proxy_header_localhost() +{ + v2_proxy_header v2_header; + void *header_data = &v2_header; + size_t header_length= 216 + 16; + MYSQL *m; + MYSQL_RES *result; + MYSQL_ROW row; + int rc; + + memset(&v2_header, 0, sizeof(v2_header)); + memcpy(v2_header.sig, "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A", 12); + v2_header.ver_cmd = (0x2 << 4) | 0x1; /* Version (0x2) , Command = PROXY (0x1) */ + v2_header.fam= 0x31; + v2_header.len= htons(216); + strcpy(v2_header.addr.unx.src_addr,"/tmp/mysql.sock"); + strcpy(v2_header.addr.unx.dst_addr,"/tmp/mysql.sock"); + rc = mysql_query(mysql, "CREATE USER 'u'@'localhost' IDENTIFIED BY 'password'"); + myquery(rc); + m = mysql_client_init(NULL); + DIE_UNLESS(m != NULL); + mysql_optionsv(m, MARIADB_OPT_PROXY_HEADER, header_data, header_length); + DIE_UNLESS(mysql_real_connect(m, opt_host, "u", "password", NULL, opt_port, opt_unix_socket, 0) == m); + DIE_UNLESS(mysql_query(m, "select host from information_schema.processlist WHERE ID = connection_id()") == 0); + /* get the result */ + result= mysql_store_result(m); + mytest(result); + row = mysql_fetch_row(result); + DIE_UNLESS(strcmp(row[0], "localhost") == 0); + mysql_close(m); + rc = mysql_query(mysql, "DROP USER 'u'@'localhost'"); + myquery(rc); +} + +/* Proxy header ignoring */ +static void test_proxy_header_ignore() +{ + MYSQL *m = mysql_client_init(NULL); + v2_proxy_header v2_header; + DIE_UNLESS(m != NULL); + mysql_optionsv(m, MARIADB_OPT_PROXY_HEADER, "PROXY UNKNOWN\r\n",15); + DIE_UNLESS(mysql_real_connect(m, opt_host, "root", "", NULL, opt_port, opt_unix_socket, 0) == m); + mysql_close(m); + + memcpy(v2_header.sig, "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A", 12); + v2_header.ver_cmd = (0x2 << 4) | 0x0; /* Version (0x2) , Command = LOCAL (0x0) */ + v2_header.fam= 0x0; /* AF_UNSPEC*/ + v2_header.len= htons(0); + m = mysql_client_init(NULL); + mysql_optionsv(m, MARIADB_OPT_PROXY_HEADER, &v2_header,16); + DIE_UNLESS(mysql_real_connect(m, opt_host, "root", "", NULL, opt_port, opt_unix_socket, 0) == m); + mysql_close(m); +} + + +static void test_proxy_header() +{ + test_proxy_header_tcp("192.168.0.1",3333); + test_proxy_header_tcp("2001:db8:85a3::8a2e:370:7334",2222); + test_proxy_header_tcp("::ffff:192.168.0.1",2222); + test_proxy_header_localhost(); + test_proxy_header_ignore(); +} + +#endif + static struct my_tests_st my_tests[]= { { "disable_query_logs", disable_query_logs }, { "test_view_sp_list_fields", test_view_sp_list_fields }, @@ -19914,6 +20092,9 @@ static struct my_tests_st my_tests[]= { { "test_big_packet", test_big_packet }, { "test_prepare_analyze", test_prepare_analyze }, { "test_mdev12579", test_mdev12579 }, +#ifndef EMBEDDED_LIBRARY + { "test_proxy_header", test_proxy_header}, +#endif { 0, 0 } }; diff --git a/vio/viosocket.c b/vio/viosocket.c index 074948badec..cc16c3698c0 100644 --- a/vio/viosocket.c +++ b/vio/viosocket.c @@ -627,7 +627,7 @@ my_socket vio_fd(Vio* vio) @param dst_length [out] actual length of the normalized IP address. */ -static void vio_get_normalized_ip(const struct sockaddr *src, +void vio_get_normalized_ip(const struct sockaddr *src, int src_length, struct sockaddr *dst, int *dst_length) |