diff options
Diffstat (limited to 'smtpd/proxy.c')
-rw-r--r-- | smtpd/proxy.c | 387 |
1 files changed, 387 insertions, 0 deletions
diff --git a/smtpd/proxy.c b/smtpd/proxy.c new file mode 100644 index 00000000..fdaf4f27 --- /dev/null +++ b/smtpd/proxy.c @@ -0,0 +1,387 @@ +/* + * Copyright (c) 2017 Antoine Kaufmann <toni@famkaufmann.info> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "includes.h" + +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/tree.h> +#include <sys/un.h> + +#include <err.h> +#include <errno.h> +#include <event.h> +#include <imsg.h> +#include <inttypes.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "log.h" +#include "smtpd.h" + + +/* + * The PROXYv2 protocol is described here: + * http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt + */ + +#define PROXY_CLOCAL 0x0 +#define PROXY_CPROXY 0x1 +#define PROXY_AF_UNSPEC 0x0 +#define PROXY_AF_INET 0x1 +#define PROXY_AF_INET6 0x2 +#define PROXY_AF_UNIX 0x3 +#define PROXY_TF_UNSPEC 0x0 +#define PROXY_TF_STREAM 0x1 +#define PROXY_TF_DGRAM 0x2 + +#define PROXY_SESSION_TIMEOUT 300 + +static const uint8_t pv2_signature[] = { + 0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, + 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A +}; + +struct proxy_hdr_v2 { + uint8_t sig[12]; + uint8_t ver_cmd; + uint8_t fam; + uint16_t len; +} __attribute__((packed)); + + +struct proxy_addr_ipv4 { + uint32_t src_addr; + uint32_t dst_addr; + uint16_t src_port; + uint16_t dst_port; +} __attribute__((packed)); + +struct proxy_addr_ipv6 { + uint8_t src_addr[16]; + uint8_t dst_addr[16]; + uint16_t src_port; + uint16_t dst_port; +} __attribute__((packed)); + +struct proxy_addr_unix { + uint8_t src_addr[108]; + uint8_t dst_addr[108]; +} __attribute__((packed)); + +union proxy_addr { + struct proxy_addr_ipv4 ipv4; + struct proxy_addr_ipv6 ipv6; + struct proxy_addr_unix un; +} __attribute__((packed)); + +struct proxy_session { + struct listener *l; + struct io *io; + + uint64_t id; + int fd; + uint16_t header_len; + uint16_t header_total; + uint16_t addr_len; + uint16_t addr_total; + + struct sockaddr_storage ss; + struct proxy_hdr_v2 hdr; + union proxy_addr addr; + + void (*cb_accepted)(struct listener *, int, + const struct sockaddr_storage *, struct io *); + void (*cb_dropped)(struct listener *, int, + const struct sockaddr_storage *); +}; + +static void proxy_io(struct io *, int, void *); +static void proxy_error(struct proxy_session *, const char *, const char *); +static int proxy_header_validate(struct proxy_session *); +static int proxy_translate_ss(struct proxy_session *); + +int +proxy_session(struct listener *listener, int sock, + const struct sockaddr_storage *ss, + void (*accepted)(struct listener *, int, + const struct sockaddr_storage *, struct io *), + void (*dropped)(struct listener *, int, + const struct sockaddr_storage *)); + +int +proxy_session(struct listener *listener, int sock, + const struct sockaddr_storage *ss, + void (*accepted)(struct listener *, int, + const struct sockaddr_storage *, struct io *), + void (*dropped)(struct listener *, int, + const struct sockaddr_storage *)) +{ + struct proxy_session *s; + + if ((s = calloc(1, sizeof(*s))) == NULL) + return (-1); + + if ((s->io = io_new()) == NULL) { + free(s); + return (-1); + } + + s->id = generate_uid(); + s->l = listener; + s->fd = sock; + s->header_len = 0; + s->addr_len = 0; + s->ss = *ss; + s->cb_accepted = accepted; + s->cb_dropped = dropped; + + io_set_callback(s->io, proxy_io, s); + io_set_fd(s->io, sock); + io_set_timeout(s->io, PROXY_SESSION_TIMEOUT * 1000); + io_set_read(s->io); + + log_info("%016"PRIx64" smtp event=proxy address=%s", + s->id, ss_to_text(&s->ss)); + + return 0; +} + +static void +proxy_io(struct io *io, int evt, void *arg) +{ + struct proxy_session *s = arg; + struct proxy_hdr_v2 *h = &s->hdr; + uint8_t *buf; + size_t len, off; + + switch (evt) { + + case IO_DATAIN: + buf = io_data(io); + len = io_datalen(io); + + if (s->header_len < sizeof(s->hdr)) { + /* header is incomplete */ + off = sizeof(s->hdr) - s->header_len; + off = (len < off ? len : off); + memcpy((uint8_t *) &s->hdr + s->header_len, buf, off); + + s->header_len += off; + buf += off; + len -= off; + io_drop(s->io, off); + + if (s->header_len < sizeof(s->hdr)) { + /* header is still not complete */ + return; + } + + if (proxy_header_validate(s) != 0) + return; + } + + if (s->addr_len < s->addr_total) { + /* address is incomplete */ + off = s->addr_total - s->addr_len; + off = (len < off ? len : off); + memcpy((uint8_t *) &s->addr + s->addr_len, buf, off); + + s->header_len += off; + s->addr_len += off; + buf += off; + len -= off; + io_drop(s->io, off); + + if (s->addr_len < s->addr_total) { + /* address is still not complete */ + return; + } + } + + if (s->header_len < s->header_total) { + /* additional parameters not complete */ + /* these are ignored for now, but we still need to drop + * the bytes from the buffer */ + off = s->header_total - s->header_len; + off = (len < off ? len : off); + + s->header_len += off; + io_drop(s->io, off); + + if (s->header_len < s->header_total) + /* not complete yet */ + return; + } + + switch(h->ver_cmd & 0xF) { + case PROXY_CLOCAL: + /* local address, no need to modify ss */ + break; + + case PROXY_CPROXY: + if (proxy_translate_ss(s) != 0) + return; + break; + + default: + proxy_error(s, "protocol error", "unknown command"); + return; + } + + s->cb_accepted(s->l, s->fd, &s->ss, s->io); + /* we passed off s->io, so it does not need to be freed here */ + free(s); + break; + + case IO_TIMEOUT: + proxy_error(s, "timeout", NULL); + break; + + case IO_DISCONNECTED: + proxy_error(s, "disconnected", NULL); + break; + + case IO_ERROR: + proxy_error(s, "IO error", io_error(io)); + break; + + default: + fatalx("proxy_io()"); + } + +} + +static void +proxy_error(struct proxy_session *s, const char *reason, const char *extra) +{ + if (extra) + log_info("proxy %p event=closed address=%s reason=\"%s (%s)\"", + s, ss_to_text(&s->ss), reason, extra); + else + log_info("proxy %p event=closed address=%s reason=\"%s\"", + s, ss_to_text(&s->ss), reason); + + s->cb_dropped(s->l, s->fd, &s->ss); + io_free(s->io); + free(s); +} + +static int +proxy_header_validate(struct proxy_session *s) +{ + struct proxy_hdr_v2 *h = &s->hdr; + + if (memcmp(h->sig, pv2_signature, + sizeof(pv2_signature)) != 0) { + proxy_error(s, "protocol error", "invalid signature"); + return (-1); + } + + if ((h->ver_cmd >> 4) != 2) { + proxy_error(s, "protocol error", "invalid version"); + return (-1); + } + + switch (h->fam) { + case (PROXY_AF_UNSPEC << 4 | PROXY_TF_UNSPEC): + s->addr_total = 0; + break; + + case (PROXY_AF_INET << 4 | PROXY_TF_STREAM): + s->addr_total = sizeof(s->addr.ipv4); + break; + + case (PROXY_AF_INET6 << 4 | PROXY_TF_STREAM): + s->addr_total = sizeof(s->addr.ipv6); + break; + + case (PROXY_AF_UNIX << 4 | PROXY_TF_STREAM): + s->addr_total = sizeof(s->addr.un); + break; + + default: + proxy_error(s, "protocol error", "unsupported address family"); + return (-1); + } + + s->header_total = ntohs(h->len); + if (s->header_total > UINT16_MAX - sizeof(struct proxy_hdr_v2)) { + proxy_error(s, "protocol error", "header too long"); + return (-1); + } + s->header_total += sizeof(struct proxy_hdr_v2); + + if (s->header_total < sizeof(struct proxy_hdr_v2) + s->addr_total) { + proxy_error(s, "protocol error", "address info too short"); + return (-1); + } + + return 0; +} + +static int +proxy_translate_ss(struct proxy_session *s) +{ + struct sockaddr_in *sin = (struct sockaddr_in *) &s->ss; + struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *) &s->ss; + struct sockaddr_un *sun = (struct sockaddr_un *) &s->ss; + size_t sun_len; + + switch (s->hdr.fam) { + case (PROXY_AF_UNSPEC << 4 | PROXY_TF_UNSPEC): + /* unspec: only supported for local */ + proxy_error(s, "address translation", "UNSPEC family not " + "supported for PROXYing"); + return (-1); + + case (PROXY_AF_INET << 4 | PROXY_TF_STREAM): + memset(&s->ss, 0, sizeof(s->ss)); + sin->sin_family = AF_INET; + sin->sin_port = s->addr.ipv4.src_port; + sin->sin_addr.s_addr = s->addr.ipv4.src_addr; + break; + + case (PROXY_AF_INET6 << 4 | PROXY_TF_STREAM): + memset(&s->ss, 0, sizeof(s->ss)); + sin6->sin6_family = AF_INET6; + sin6->sin6_port = s->addr.ipv6.src_port; + memcpy(sin6->sin6_addr.s6_addr, s->addr.ipv6.src_addr, + sizeof(s->addr.ipv6.src_addr)); + break; + + case (PROXY_AF_UNIX << 4 | PROXY_TF_STREAM): + memset(&s->ss, 0, sizeof(s->ss)); + sun_len = strnlen(s->addr.un.src_addr, + sizeof(s->addr.un.src_addr)); + if (sun_len > sizeof(sun->sun_path)) { + proxy_error(s, "address translation", "Unix socket path" + " longer than supported"); + return (-1); + } + sun->sun_family = AF_UNIX; + memcpy(sun->sun_path, s->addr.un.src_addr, sun_len); + break; + + default: + fatalx("proxy_translate_ss()"); + } + + return 0; +} |