diff options
Diffstat (limited to 'smtpd/smtp.c')
-rw-r--r-- | smtpd/smtp.c | 387 |
1 files changed, 387 insertions, 0 deletions
diff --git a/smtpd/smtp.c b/smtpd/smtp.c new file mode 100644 index 00000000..602fd0d6 --- /dev/null +++ b/smtpd/smtp.c @@ -0,0 +1,387 @@ +/* $OpenBSD: smtp.c,v 1.166 2019/08/10 16:07:01 gilles Exp $ */ + +/* + * Copyright (c) 2008 Gilles Chehade <gilles@poolp.org> + * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org> + * Copyright (c) 2009 Jacek Masiulaniec <jacekm@dobremiasto.net> + * + * 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/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> + +#include <err.h> +#include <errno.h> +#include <event.h> +#include <grp.h> /* needed for setgroups */ +#include <imsg.h> +#include <netdb.h> +#include <pwd.h> +#include <signal.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include <openssl/ssl.h> + +#include "smtpd.h" +#include "log.h" +#include "ssl.h" + +static void smtp_setup_events(void); +static void smtp_pause(void); +static void smtp_resume(void); +static void smtp_accept(int, short, void *); +static void smtp_dropped(struct listener *, int, const struct sockaddr_storage *); +static int smtp_enqueue(void); +static int smtp_can_accept(void); +static void smtp_setup_listeners(void); +static int smtp_sni_callback(SSL *, int *, void *); + +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 *)); + +static void smtp_accepted(struct listener *, int, const struct sockaddr_storage *, struct io *); + + +#define SMTP_FD_RESERVE 5 +#define getdtablecount() 0 + +static size_t sessions; +static size_t maxsessions; + +void +smtp_imsg(struct mproc *p, struct imsg *imsg) +{ + switch (imsg->hdr.type) { + case IMSG_SMTP_CHECK_SENDER: + case IMSG_SMTP_EXPAND_RCPT: + case IMSG_SMTP_LOOKUP_HELO: + case IMSG_SMTP_AUTHENTICATE: + case IMSG_FILTER_SMTP_PROTOCOL: + case IMSG_FILTER_SMTP_DATA_BEGIN: + smtp_session_imsg(p, imsg); + return; + + case IMSG_SMTP_MESSAGE_COMMIT: + case IMSG_SMTP_MESSAGE_CREATE: + case IMSG_SMTP_MESSAGE_OPEN: + case IMSG_QUEUE_ENVELOPE_SUBMIT: + case IMSG_QUEUE_ENVELOPE_COMMIT: + smtp_session_imsg(p, imsg); + return; + + case IMSG_QUEUE_SMTP_SESSION: + m_compose(p, IMSG_QUEUE_SMTP_SESSION, 0, 0, smtp_enqueue(), + imsg->data, imsg->hdr.len - sizeof imsg->hdr); + return; + + case IMSG_CTL_SMTP_SESSION: + m_compose(p, IMSG_CTL_SMTP_SESSION, imsg->hdr.peerid, 0, + smtp_enqueue(), NULL, 0); + return; + + case IMSG_CTL_PAUSE_SMTP: + log_debug("debug: smtp: pausing listening sockets"); + smtp_pause(); + env->sc_flags |= SMTPD_SMTP_PAUSED; + return; + + case IMSG_CTL_RESUME_SMTP: + log_debug("debug: smtp: resuming listening sockets"); + env->sc_flags &= ~SMTPD_SMTP_PAUSED; + smtp_resume(); + return; + } + + errx(1, "smtp_imsg: unexpected %s imsg", imsg_to_str(imsg->hdr.type)); +} + +void +smtp_postfork(void) +{ + smtp_setup_listeners(); +} + +void +smtp_postprivdrop(void) +{ +} + +void +smtp_configure(void) +{ + smtp_setup_events(); +} + +static void +smtp_setup_listeners(void) +{ + struct listener *l; + int opt; + + TAILQ_FOREACH(l, env->sc_listeners, entry) { + if ((l->fd = socket(l->ss.ss_family, SOCK_STREAM, 0)) == -1) { + if (errno == EAFNOSUPPORT) { + log_warn("smtpd: socket"); + continue; + } + fatal("smtpd: socket"); + } + opt = 1; +#ifdef SO_REUSEADDR + if (setsockopt(l->fd, SOL_SOCKET, SO_REUSEADDR, &opt, + sizeof(opt)) == -1) + fatal("smtpd: setsockopt"); +#else + if (setsockopt(l->fd, SOL_SOCKET, SO_REUSEPORT, &opt, + sizeof(opt)) < 0) + fatal("smtpd: setsockopt"); +#endif +#ifdef IPV6_V6ONLY + /* + * If using IPv6, bind only to IPv6 if possible. + * This avoids ambiguities with IPv4-mapped IPv6 addresses. + */ + if (l->ss.ss_family == AF_INET6) + if (setsockopt(l->fd, IPPROTO_IPV6, IPV6_V6ONLY, &opt, + sizeof(opt)) < 0) + fatal("smtpd: setsockopt"); +#endif + if (bind(l->fd, (struct sockaddr *)&l->ss, SS_LEN(&l->ss)) == -1) + fatal("smtpd: bind"); + } +} + +static void +smtp_setup_events(void) +{ + struct listener *l; + struct pki *pki; + SSL_CTX *ssl_ctx; + void *iter; + const char *k; + + TAILQ_FOREACH(l, env->sc_listeners, entry) { + log_debug("debug: smtp: listen on %s port %d flags 0x%01x" + " pki \"%s\"" + " ca \"%s\"", ss_to_text(&l->ss), ntohs(l->port), + l->flags, l->pki_name, l->ca_name); + + io_set_nonblocking(l->fd); + if (listen(l->fd, SMTPD_BACKLOG) == -1) + fatal("listen"); + event_set(&l->ev, l->fd, EV_READ|EV_PERSIST, smtp_accept, l); + + if (!(env->sc_flags & SMTPD_SMTP_PAUSED)) + event_add(&l->ev, NULL); + } + + iter = NULL; + while (dict_iter(env->sc_pki_dict, &iter, &k, (void **)&pki)) { + if (!ssl_setup((SSL_CTX **)&ssl_ctx, pki, smtp_sni_callback, + env->sc_tls_ciphers)) + fatal("smtp_setup_events: ssl_setup failure"); + dict_xset(env->sc_ssl_dict, k, ssl_ctx); + } + + purge_config(PURGE_PKI_KEYS); + + maxsessions = (getdtablesize() - getdtablecount()) / 2 - SMTP_FD_RESERVE; + log_debug("debug: smtp: will accept at most %zu clients", maxsessions); +} + +static void +smtp_pause(void) +{ + struct listener *l; + + if (env->sc_flags & (SMTPD_SMTP_DISABLED|SMTPD_SMTP_PAUSED)) + return; + + TAILQ_FOREACH(l, env->sc_listeners, entry) + event_del(&l->ev); +} + +static void +smtp_resume(void) +{ + struct listener *l; + + if (env->sc_flags & (SMTPD_SMTP_DISABLED|SMTPD_SMTP_PAUSED)) + return; + + TAILQ_FOREACH(l, env->sc_listeners, entry) + event_add(&l->ev, NULL); +} + +static int +smtp_enqueue(void) +{ + struct listener *listener = env->sc_sock_listener; + int fd[2]; + + /* + * Some enqueue requests buffered in IMSG may still arrive even after + * call to smtp_pause() because enqueue listener is not a real socket + * and thus cannot be paused properly. + */ + if (env->sc_flags & SMTPD_SMTP_PAUSED) + return (-1); + + if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, fd)) + return (-1); + + if ((smtp_session(listener, fd[0], &listener->ss, env->sc_hostname, NULL)) == -1) { + close(fd[0]); + close(fd[1]); + return (-1); + } + + sessions++; + stat_increment("smtp.session", 1); + stat_increment("smtp.session.local", 1); + + return (fd[1]); +} + +static void +smtp_accept(int fd, short event, void *p) +{ + struct listener *listener = p; + struct sockaddr_storage ss; + socklen_t len; + int sock; + + if (env->sc_flags & SMTPD_SMTP_PAUSED) + fatalx("smtp_session: unexpected client"); + + if (!smtp_can_accept()) { + log_warnx("warn: Disabling incoming SMTP connections: " + "Client limit reached"); + goto pause; + } + + len = sizeof(ss); + if ((sock = accept(fd, (struct sockaddr *)&ss, &len)) == -1) { + if (errno == ENFILE || errno == EMFILE) { + log_warn("warn: Disabling incoming SMTP connections"); + goto pause; + } + if (errno == EINTR || errno == EWOULDBLOCK || + errno == ECONNABORTED) + return; + fatal("smtp_accept"); + } + + if (listener->flags & F_PROXY) { + io_set_nonblocking(sock); + if (proxy_session(listener, sock, &ss, + smtp_accepted, smtp_dropped) == -1) { + close(sock); + return; + } + return; + } + + smtp_accepted(listener, sock, &ss, NULL); + return; + +pause: + smtp_pause(); + env->sc_flags |= SMTPD_SMTP_DISABLED; + return; +} + +static int +smtp_can_accept(void) +{ + if (sessions + 1 == maxsessions) + return 0; + return (getdtablesize() - getdtablecount() - SMTP_FD_RESERVE >= 2); +} + +void +smtp_collect(void) +{ + sessions--; + stat_decrement("smtp.session", 1); + + if (!smtp_can_accept()) + return; + + if (env->sc_flags & SMTPD_SMTP_DISABLED) { + log_warnx("warn: smtp: " + "fd exhaustion over, re-enabling incoming connections"); + env->sc_flags &= ~SMTPD_SMTP_DISABLED; + smtp_resume(); + } +} + +static int +smtp_sni_callback(SSL *ssl, int *ad, void *arg) +{ + const char *sn; + void *ssl_ctx; + + sn = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); + if (sn == NULL) + return SSL_TLSEXT_ERR_NOACK; + ssl_ctx = dict_get(env->sc_ssl_dict, sn); + if (ssl_ctx == NULL) + return SSL_TLSEXT_ERR_NOACK; + SSL_set_SSL_CTX(ssl, ssl_ctx); + return SSL_TLSEXT_ERR_OK; +} + +static void +smtp_accepted(struct listener *listener, int sock, const struct sockaddr_storage *ss, struct io *io) +{ + int ret; + + ret = smtp_session(listener, sock, ss, NULL, io); + if (ret == -1) { + log_warn("warn: Failed to create SMTP session"); + close(sock); + return; + } + io_set_nonblocking(sock); + + sessions++; + stat_increment("smtp.session", 1); + if (listener->ss.ss_family == AF_LOCAL) + stat_increment("smtp.session.local", 1); + if (listener->ss.ss_family == AF_INET) + stat_increment("smtp.session.inet4", 1); + if (listener->ss.ss_family == AF_INET6) + stat_increment("smtp.session.inet6", 1); +} + +static void +smtp_dropped(struct listener *listener, int sock, const struct sockaddr_storage *ss) +{ + close(sock); + sessions--; +} |