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 /sql/proxy_protocol.cc | |
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.
Diffstat (limited to 'sql/proxy_protocol.cc')
-rw-r--r-- | sql/proxy_protocol.cc | 491 |
1 files changed, 491 insertions, 0 deletions
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; +} + |