aboutsummaryrefslogtreecommitdiffstats
path: root/smtpd/proxy.c
diff options
context:
space:
mode:
Diffstat (limited to 'smtpd/proxy.c')
-rw-r--r--smtpd/proxy.c387
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;
+}