diff options
Diffstat (limited to '')
-rw-r--r-- | src/lib-master/master-service-haproxy.c | 689 |
1 files changed, 689 insertions, 0 deletions
diff --git a/src/lib-master/master-service-haproxy.c b/src/lib-master/master-service-haproxy.c new file mode 100644 index 0000000..c36935d --- /dev/null +++ b/src/lib-master/master-service-haproxy.c @@ -0,0 +1,689 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "llist.h" +#include "ioloop.h" +#include "str-sanitize.h" +#include "master-service-private.h" +#include "master-service-settings.h" + +#define HAPROXY_V1_MAX_HEADER_SIZE (108) + +#define PP2_TYPE_ALPN 0x01 +#define PP2_TYPE_AUTHORITY 0x02 +#define PP2_TYPE_CRC32C 0x03 +#define PP2_TYPE_NOOP 0x04 +#define PP2_TYPE_SSL 0x20 +#define PP2_SUBTYPE_SSL_VERSION 0x21 +#define PP2_SUBTYPE_SSL_CN 0x22 +#define PP2_SUBTYPE_SSL_CIPHER 0x23 +#define PP2_SUBTYPE_SSL_SIG_ALG 0x24 +#define PP2_SUBTYPE_SSL_KEY_ALG 0x25 +#define PP2_TYPE_NETNS 0x30 + +#define PP2_CLIENT_SSL 0x01 +#define PP2_CLIENT_CERT_CONN 0x02 +#define PP2_CLIENT_CERT_SESS 0x04 + +enum haproxy_version_t { + HAPROXY_VERSION_1, + HAPROXY_VERSION_2, +}; + +enum { + HAPROXY_CMD_LOCAL = 0x00, + HAPROXY_CMD_PROXY = 0x01 +}; + +enum { + HAPROXY_AF_UNSPEC = 0x00, + HAPROXY_AF_INET = 0x01, + HAPROXY_AF_INET6 = 0x02, + HAPROXY_AF_UNIX = 0x03 +}; + +enum { + HAPROXY_SOCK_UNSPEC = 0x00, + HAPROXY_SOCK_STREAM = 0x01, + HAPROXY_SOCK_DGRAM = 0x02 +}; + +static const char haproxy_v2sig[12] = + "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A"; + +struct haproxy_header_v2 { + uint8_t sig[12]; + uint8_t ver_cmd; + uint8_t fam; + uint16_t len; +}; + +struct haproxy_data_v2 { + union { + struct { /* for TCP/UDP over IPv4, len = 12 */ + uint32_t src_addr; + uint32_t dst_addr; + uint16_t src_port; + uint16_t dst_port; + } ip4; + struct { /* for TCP/UDP over IPv6, len = 36 */ + uint8_t src_addr[16]; + uint8_t dst_addr[16]; + uint16_t src_port; + uint16_t dst_port; + } ip6; + struct { /* for AF_UNIX sockets, len = 216 */ + uint8_t src_addr[108]; + uint8_t dst_addr[108]; + } unx; + } addr; +}; + +#define SIZEOF_PP2_TLV (1U+2U) +struct haproxy_pp2_tlv { + uint8_t type; + uint16_t len; + const unsigned char *data; +}; + +#define SIZEOF_PP2_TLV_SSL (1U+4U) +struct haproxy_pp2_tlv_ssl { + uint8_t client; + uint32_t verify; + + size_t len; + const unsigned char *data; +}; + +struct master_service_haproxy_conn { + struct master_service_connection conn; + + pool_t pool; + + struct master_service_haproxy_conn *prev, *next; + + struct master_service *service; + + struct io *io; + struct timeout *to; +}; + +static void +master_service_haproxy_conn_free(struct master_service_haproxy_conn *hpconn) +{ + struct master_service *service = hpconn->service; + + DLLIST_REMOVE(&service->haproxy_conns, hpconn); + + io_remove(&hpconn->io); + timeout_remove(&hpconn->to); + pool_unref(&hpconn->pool); +} + +static void +master_service_haproxy_conn_failure(struct master_service_haproxy_conn *hpconn) +{ + struct master_service *service = hpconn->service; + struct master_service_connection conn = hpconn->conn; + + master_service_haproxy_conn_free(hpconn); + master_service_client_connection_handled(service, &conn); +} + +static void +master_service_haproxy_conn_success(struct master_service_haproxy_conn *hpconn) +{ + struct master_service *service = hpconn->service; + struct master_service_connection conn = hpconn->conn; + + master_service_haproxy_conn_free(hpconn); + master_service_client_connection_callback(service, &conn); +} + +static void +master_service_haproxy_timeout(struct master_service_haproxy_conn *hpconn) +{ + i_error("haproxy: Client timed out (rip=%s)", + net_ip2addr(&hpconn->conn.remote_ip)); + master_service_haproxy_conn_failure(hpconn); +} + +static int +master_service_haproxy_recv(int fd, void *buf, size_t len, int flags) +{ + ssize_t ret; + + do { + ret = recv(fd, buf, len, flags); + } while (ret < 0 && errno == EINTR); + + if (ret < 0 && errno == EAGAIN) + return 0; + if (ret <= 0) { + if (ret == 0) + errno = ECONNRESET; + return -1; + } + + return ret; +} + +static int get_ssl_tlv(const unsigned char *kvdata, size_t dlen, + struct haproxy_pp2_tlv_ssl *kv) +{ + if (dlen < SIZEOF_PP2_TLV_SSL) + return -1; + kv->client = kvdata[0]; + /* spec does not specify the endianess of this field */ + kv->verify = cpu32_to_cpu_unaligned(kvdata+1); + kv->data = kvdata+SIZEOF_PP2_TLV_SSL; + kv->len = dlen - SIZEOF_PP2_TLV_SSL; + return 0; +} + +static int get_tlv(const unsigned char *kvdata, size_t dlen, + struct haproxy_pp2_tlv *kv) +{ + if (dlen < SIZEOF_PP2_TLV) + return -1; + + /* spec says + uint8_t type + uint8_t len_hi + uint8_t len_lo + so we combine the hi and lo here. */ + kv->type = kvdata[0]; + kv->len = (kvdata[1]<<8)+kvdata[2]; + kv->data = kvdata + SIZEOF_PP2_TLV; + + if (kv->len + SIZEOF_PP2_TLV > dlen) + return -1; + + return 0; +} + +static int +master_service_haproxy_parse_ssl_tlv(struct master_service_haproxy_conn *hpconn, + const struct haproxy_pp2_tlv_ssl *ssl_kv, + const char **error_r) +{ + hpconn->conn.proxy.ssl = (ssl_kv->client & (PP2_CLIENT_SSL)) != 0; + + /* try parse some more */ + for(size_t i = 0; i < ssl_kv->len;) { + struct haproxy_pp2_tlv kv; + if (get_tlv(ssl_kv->data + i, ssl_kv->len - i, &kv) < 0) { + *error_r = t_strdup_printf("get_tlv(%zu) failed: " + "Truncated data", i); + return -1; + } + i += SIZEOF_PP2_TLV + kv.len; + switch(kv.type) { + /* we don't care about these */ + case PP2_SUBTYPE_SSL_CIPHER: + case PP2_SUBTYPE_SSL_SIG_ALG: + case PP2_SUBTYPE_SSL_KEY_ALG: + break; + case PP2_SUBTYPE_SSL_CN: + hpconn->conn.proxy.cert_common_name = + p_strndup(hpconn->pool, kv.data, kv.len); + break; + } + } + return 0; +} + +static int +master_service_haproxy_parse_tlv(struct master_service_haproxy_conn *hpconn, + const unsigned char *buf, size_t blen, + const char **error_r) +{ + for(size_t i = 0; i < blen;) { + struct haproxy_pp2_tlv kv; + struct haproxy_pp2_tlv_ssl ssl_kv; + + if (get_tlv(buf + i, blen - i, &kv) < 0) { + *error_r = t_strdup_printf("get_tlv(%zu) failed: " + "Truncated data", i); + return -1; + } + + /* skip unsupported values */ + switch(kv.type) { + case PP2_TYPE_ALPN: + hpconn->conn.proxy.alpn_size = kv.len; + hpconn->conn.proxy.alpn = + p_memdup(hpconn->pool, kv.data, kv.len); + break; + case PP2_TYPE_AUTHORITY: + /* store hostname somewhere */ + hpconn->conn.proxy.hostname = + p_strndup(hpconn->pool, kv.data, kv.len); + break; + case PP2_TYPE_SSL: + if (get_ssl_tlv(kv.data, kv.len, &ssl_kv) < 0) { + *error_r = t_strdup_printf("get_ssl_tlv(%zu) failed: " + "Truncated data", i); + return -1; + } + if (master_service_haproxy_parse_ssl_tlv(hpconn, &ssl_kv, error_r)<0) + return -1; + break; + } + i += SIZEOF_PP2_TLV + kv.len; + } + return 0; +} + +static int +master_service_haproxy_read(struct master_service_haproxy_conn *hpconn) +{ + /* reasonable max size for haproxy data */ + unsigned char rbuf[1500]; + const char *error; + static union { + unsigned char v1_data[HAPROXY_V1_MAX_HEADER_SIZE]; + struct { + const struct haproxy_header_v2 hdr; + const struct haproxy_data_v2 data; + } v2; + } buf; + struct ip_addr *real_remote_ip = &hpconn->conn.remote_ip; + int fd = hpconn->conn.fd; + struct ip_addr local_ip, remote_ip; + in_port_t local_port, remote_port; + size_t size,i,want; + ssize_t ret; + enum haproxy_version_t version; + + /* the protocol specification explicitly states that the protocol header + must be sent as one TCP frame, meaning that we will get it in full + with the first recv() call. + */ + i_zero(&buf); + i_zero(&rbuf); + + /* see if there is a HAPROXY protocol command waiting */ + if ((ret = master_service_haproxy_recv(fd, &buf, sizeof(buf), MSG_PEEK))<=0) { + if (ret < 0) + i_info("haproxy: Client disconnected (rip=%s): %m", + net_ip2addr(real_remote_ip)); + return ret; + /* see if there is a haproxy command, 8 is used later on as well */ + } else if (ret >= 8 && memcmp(buf.v1_data, "PROXY", 5) == 0) { + /* fine */ + version = HAPROXY_VERSION_1; + } else if ((size_t)ret >= sizeof(buf.v2.hdr) && + memcmp(buf.v2.hdr.sig, haproxy_v2sig, sizeof(haproxy_v2sig)) == 0) { + want = ntohs(buf.v2.hdr.len) + sizeof(buf.v2.hdr); + if (want > sizeof(rbuf)) { + i_error("haproxy: Client disconnected: Too long header (rip=%s)", + net_ip2addr(real_remote_ip)); + return -1; + } + + if ((ret = master_service_haproxy_recv(fd, rbuf, want, MSG_WAITALL))<=0) { + if (ret < 0) + i_info("haproxy: Client disconnected (rip=%s): %m", + net_ip2addr(real_remote_ip)); + return ret; + } + + if (ret != (ssize_t)want) { + i_info("haproxy: Client disconnected: Failed to read full header (rip=%s)", + net_ip2addr(real_remote_ip)); + return -1; + } + memcpy(&buf, rbuf, sizeof(buf)); + version = HAPROXY_VERSION_2; + } else { + /* it wasn't haproxy data */ + i_error("haproxy: Client disconnected: " + "Failed to read valid HAproxy data (rip=%s)", + net_ip2addr(real_remote_ip)); + return -1; + } + + /* don't update true connection data until we succeed */ + local_ip = hpconn->conn.local_ip; + remote_ip = hpconn->conn.remote_ip; + local_port = hpconn->conn.local_port; + remote_port = hpconn->conn.remote_port; + + /* protocol version 2 */ + if (version == HAPROXY_VERSION_2) { + const struct haproxy_header_v2 *hdr = &buf.v2.hdr; + const struct haproxy_data_v2 *data = &buf.v2.data; + size_t hdr_len; + + i_assert(ret >= (ssize_t)sizeof(buf.v2.hdr)); + + if ((hdr->ver_cmd & 0xf0) != 0x20) { + i_error("haproxy: Client disconnected: " + "Unsupported protocol version (version=%02x, rip=%s)", + (hdr->ver_cmd & 0xf0) >> 4, + net_ip2addr(real_remote_ip)); + return -1; + } + + hdr_len = ntohs(hdr->len); + size = sizeof(*hdr) + hdr_len; + /* keep tab of how much address data there really is because + because TLVs begin after that. */ + i = 0; + + if (ret < (ssize_t)size) { + i_error("haproxy(v2): Client disconnected: " + "Protocol payload length does not match header " + "(got=%zu, expect=%zu, rip=%s)", + (size_t)ret, size, net_ip2addr(real_remote_ip)); + return -1; + } + + i += sizeof(*hdr); + + switch (hdr->ver_cmd & 0x0f) { + case HAPROXY_CMD_LOCAL: + /* keep local connection address for LOCAL */ + /*i_debug("haproxy(v2): Local connection (rip=%s)", + net_ip2addr(real_remote_ip));*/ + i = size; /* we should skip all the remaining data which can be present in PROXY protocol */ + break; + case HAPROXY_CMD_PROXY: + if ((hdr->fam & 0x0f) != HAPROXY_SOCK_STREAM) { + /* UDP makes no sense currently */ + i_error("haproxy(v2): Client disconnected: " + "Not using TCP (type=%02x, rip=%s)", + (hdr->fam & 0x0f), net_ip2addr(real_remote_ip)); + return -1; + } + switch ((hdr->fam & 0xf0) >> 4) { + case HAPROXY_AF_INET: + /* IPv4 */ + if (hdr_len < sizeof(data->addr.ip4)) { + i_error("haproxy(v2): Client disconnected: " + "IPv4 data is incomplete (rip=%s)", + net_ip2addr(real_remote_ip)); + return -1; + } + local_ip.family = AF_INET; + local_ip.u.ip4.s_addr = data->addr.ip4.dst_addr; + local_port = ntohs(data->addr.ip4.dst_port); + remote_ip.family = AF_INET; + remote_ip.u.ip4.s_addr = data->addr.ip4.src_addr; + remote_port = ntohs(data->addr.ip4.src_port); + i += sizeof(data->addr.ip4); + break; + case HAPROXY_AF_INET6: + /* IPv6 */ + if (hdr_len < sizeof(data->addr.ip6)) { + i_error("haproxy(v2): Client disconnected: " + "IPv6 data is incomplete (rip=%s)", + net_ip2addr(real_remote_ip)); + return -1; + } + local_ip.family = AF_INET6; + memcpy(&local_ip.u.ip6.s6_addr, data->addr.ip6.dst_addr, 16); + local_port = ntohs(data->addr.ip6.dst_port); + remote_ip.family = AF_INET6; + memcpy(&remote_ip.u.ip6.s6_addr, data->addr.ip6.src_addr, 16); + remote_port = ntohs(data->addr.ip6.src_port); + i += sizeof(data->addr.ip6); + break; + case HAPROXY_AF_UNSPEC: + case HAPROXY_AF_UNIX: + /* unsupported; ignored */ + i_error("haproxy(v2): Unsupported address family " + "(family=%02x, rip=%s)", (hdr->fam & 0xf0) >> 4, + net_ip2addr(real_remote_ip)); + break; + default: + /* unsupported; error */ + i_error("haproxy(v2): Client disconnected: " + "Unknown address family " + "(family=%02x, rip=%s)", (hdr->fam & 0xf0) >> 4, + net_ip2addr(real_remote_ip)); + return -1; + } + break; + default: + i_error("haproxy(v2): Client disconnected: " + "Invalid command (cmd=%02x, rip=%s)", + (hdr->ver_cmd & 0x0f), + net_ip2addr(real_remote_ip)); + return -1; /* not a supported command */ + } + + if (i > size) { + i_error("haproxy(v2): Client disconnected: " + "Invalid header size (size=%zu, tlv offset=%zu)", + size, i); + return -1; /* not a supported command */ + } + if (master_service_haproxy_parse_tlv(hpconn, rbuf+i, size-i, &error) < 0) { + i_error("haproxy(v2): Client disconnected: " + "Invalid TLV: %s (cmd=%02x, rip=%s)", + error, + (hdr->ver_cmd & 0x0f), + net_ip2addr(real_remote_ip)); + return -1; + } + /* protocol version 1 (soon obsolete) */ + } else if (version == HAPROXY_VERSION_1) { + unsigned char *data = buf.v1_data, *end; + const char *const *fields; + unsigned int family = 0; + + i_assert(ret >= 8); + + /* find end of header line */ + end = memchr(data, '\r', ret - 1); + if (end == NULL || end[1] != '\n') + return -1; + *end = '\0'; + size = end + 2 - data; + + /* magic */ + fields = t_strsplit((char *)data, " "); + i_assert(strcmp(*fields, "PROXY") == 0); + fields++; + + /* protocol */ + if (*fields == NULL) { + i_error("haproxy(v1): Client disconnected: " + "Field for proxied protocol is missing " + "(rip=%s)", net_ip2addr(real_remote_ip)); + return -1; + } + if (strcmp(*fields, "TCP4") == 0) { + family = AF_INET; + } else if (strcmp(*fields, "TCP6") == 0) { + family = AF_INET6; + } else if (strcmp(*fields, "UNKNOWN") == 0) { + family = 0; + } else { + i_error("haproxy(v1): Client disconnected: " + "Unknown proxied protocol " + "(protocol=`%s', rip=%s)", str_sanitize(*fields, 64), + net_ip2addr(real_remote_ip)); + return -1; + } + fields++; + + if (family != 0) { + /* remote address */ + if (*fields == NULL) { + i_error("haproxy(v1): Client disconnected: " + "Field for proxied remote address is missing " + "(rip=%s)", net_ip2addr(real_remote_ip)); + return -1; + } + if (net_addr2ip(*fields, &remote_ip) < 0 || + remote_ip.family != family) { + i_error("haproxy(v1): Client disconnected: " + "Proxied remote address is invalid " + "(address=`%s', rip=%s)", str_sanitize(*fields, 64), + net_ip2addr(real_remote_ip)); + return -1; + } + fields++; + + /* local address */ + if (*fields == NULL) { + i_error("haproxy(v1): Client disconnected: " + "Field for proxied local address is missing " + "(rip=%s)", net_ip2addr(real_remote_ip)); + return -1; + } + if (net_addr2ip(*fields, &local_ip) < 0 || + local_ip.family != family) { + i_error("haproxy(v1): Client disconnected: " + "Proxied local address is invalid " + "(address=`%s', rip=%s)", str_sanitize(*fields, 64), + net_ip2addr(real_remote_ip)); + return -1; + } + fields++; + + /* remote port */ + if (*fields == NULL) { + i_error("haproxy(v1): Client disconnected: " + "Field for proxied local port is missing " + "(rip=%s)", net_ip2addr(real_remote_ip)); + return -1; + } + if (net_str2port(*fields, &remote_port) < 0) { + i_error("haproxy(v1): Client disconnected: " + "Proxied remote port is invalid " + "(port=`%s', rip=%s)", str_sanitize(*fields, 64), + net_ip2addr(real_remote_ip)); + return -1; + } + fields++; + + /* local port */ + if (*fields == NULL) { + i_error("haproxy(v1): Client disconnected: " + "Field for proxied local port is missing " + "(rip=%s)", net_ip2addr(real_remote_ip)); + return -1; + } + if (net_str2port(*fields, &local_port) < 0) { + i_error("haproxy(v1): Client disconnected: " + "Proxied local port is invalid " + "(port=`%s', rip=%s)", str_sanitize(*fields, 64), + net_ip2addr(real_remote_ip)); + return -1; + } + fields++; + + if (*fields != NULL) { + i_error("haproxy(v1): Client disconnected: " + "Header line has spurius extra field " + "(field=`%s', rip=%s)", str_sanitize(*fields, 64), + net_ip2addr(real_remote_ip)); + return -1; + } + } + i_assert(size <= sizeof(buf)); + + if ((ret = master_service_haproxy_recv(fd, &buf, size, 0))<=0) { + if (ret < 0) + i_info("haproxy: Client disconnected (rip=%s): %m", + net_ip2addr(real_remote_ip)); + return ret; + } else if (ret != (ssize_t)size) { + i_error("haproxy: Client disconnected: " + "Failed to read full header (rip=%s)", + net_ip2addr(real_remote_ip)); + return -1; + } + /* invalid protocol */ + } else { + i_unreached(); + } + + /* assign data from proxy */ + hpconn->conn.local_ip = local_ip; + hpconn->conn.remote_ip = remote_ip; + hpconn->conn.local_port = local_port; + hpconn->conn.remote_port = remote_port; + hpconn->conn.proxied = TRUE; + + return 1; +} + +static void +master_service_haproxy_input(struct master_service_haproxy_conn *hpconn) +{ + int ret; + + if ((ret = master_service_haproxy_read(hpconn)) <= 0) { + if (ret < 0) + master_service_haproxy_conn_failure(hpconn); + } else { + master_service_haproxy_conn_success(hpconn); + } +} + +static bool +master_service_haproxy_conn_is_trusted(struct master_service *service, + struct master_service_connection *conn) +{ + const char *const *net; + struct ip_addr net_ip; + unsigned int bits; + + if (service->set->haproxy_trusted_networks == NULL) + return FALSE; + + net = t_strsplit_spaces(service->set->haproxy_trusted_networks, ", "); + for (; *net != NULL; net++) { + if (net_parse_range(*net, &net_ip, &bits) < 0) { + i_error("haproxy_trusted_networks: " + "Invalid network '%s'", *net); + break; + } + + if (net_is_in_network(&conn->real_remote_ip, &net_ip, bits)) + return TRUE; + } + return FALSE; +} + +void master_service_haproxy_new(struct master_service *service, + struct master_service_connection *conn) +{ + struct master_service_haproxy_conn *hpconn; + pool_t pool; + + if (!master_service_haproxy_conn_is_trusted(service, conn)) { + i_warning("haproxy: Client not trusted (rip=%s)", + net_ip2addr(&conn->real_remote_ip)); + master_service_client_connection_handled(service, conn); + return; + } + + pool = pool_alloconly_create("haproxy connection", 128); + hpconn = p_new(pool, struct master_service_haproxy_conn, 1); + hpconn->pool = pool; + hpconn->conn = *conn; + hpconn->service = service; + DLLIST_PREPEND(&service->haproxy_conns, hpconn); + + hpconn->io = io_add(conn->fd, IO_READ, + master_service_haproxy_input, hpconn); + hpconn->to = timeout_add(service->set->haproxy_timeout*1000, + master_service_haproxy_timeout, hpconn); +} + +void master_service_haproxy_abort(struct master_service *service) +{ + while (service->haproxy_conns != NULL) { + int fd = service->haproxy_conns->conn.fd; + + master_service_haproxy_conn_free(service->haproxy_conns); + i_close_fd(&fd); + } +} + |