[PATCH v4] imapd.c: imapoptions: implement idle timeout
ellie timoney
ellie at fastmail.com
Mon Oct 3 20:02:39 EDT 2016
This version of the patch is now on master. Thanks very much for
putting it together :)
I've opened https://github.com/cyrusimap/cyrus-imapd/issues/42 for
backporting it to 2.5 -- expecting to do it soon, but making sure it
doesn't get forgotten if I get distracted!
Cheers,
ellie
On Thu, Sep 22, 2016, at 02:26 AM, Philipp Gesang wrote:
> Use the value of the configuration variable “timeout” as an upper
> limit in minutes for idle connections. To allow further
> customization, add a new configuration option “imapidletimeout”
> which, if greater than zero, will be used instead. The value
> defaults to zero (not set).
>
> RFC 2177 recommends that a client re-issue the IDLE command at
> least every 29 minutes if it wishes to continue, otherwise the
> server is free to treat the client as disconnected.
>
> The rationale is that sometimes connections aren’t properly
> reset. Currently, a connection is not collected if it was in IDLE
> state at that point. If this happens repeatedly, imapd keeps
> accumulating dead connections which can cause DOS. This patch
> solves the problem by forcing imapd to stop idling after
> exceeding the configured timeout.
> ---
> imap/imapd.c | 73
> ++++++++++++++++++++++++++++++++++++++++++++++++++++++---
> lib/imapoptions | 5 ++++
> 2 files changed, 75 insertions(+), 3 deletions(-)
>
> diff --git a/imap/imapd.c b/imap/imapd.c
> index e2bd237..745110c 100644
> --- a/imap/imapd.c
> +++ b/imap/imapd.c
> @@ -58,6 +58,9 @@
> #include <sys/wait.h>
> #include <netinet/in.h>
> #include <arpa/inet.h>
> +#include <time.h>
> +#include <stdbool.h>
> +#include <errno.h>
>
> #include <sasl/sasl.h>
>
> @@ -3055,6 +3058,25 @@ static void cmd_id(char *tag)
> imapd_id.did_id = 1;
> }
>
> +static bool deadline_exceeded(const struct timespec *d)
> +{
> + struct timespec now;
> +
> + if (d->tv_sec <= 0) {
> + /* No deadline configured */
> + return false;
> + }
> +
> + errno = 0;
> + if (clock_gettime(CLOCK_MONOTONIC, &now) == -1) {
> + syslog(LOG_ERR, "clock_gettime (%d %m): error reading clock",
> errno);
> + return false;
> + }
> +
> + return now.tv_sec > d->tv_sec ||
> + (now.tv_sec == d->tv_sec && now.tv_nsec > d->tv_nsec);
> +}
> +
> /*
> * Perform an IDLE command
> */
> @@ -3064,6 +3086,26 @@ static void cmd_idle(char *tag)
> int flags;
> static struct buf arg;
> static int idle_period = -1;
> + static time_t idle_timeout = -1;
> + struct timespec deadline = { 0, 0 };
> +
> + if (idle_timeout == -1) {
> + idle_timeout = config_getint(IMAPOPT_IMAPIDLETIMEOUT);
> + if (idle_timeout <= 0) {
> + idle_timeout = config_getint(IMAPOPT_TIMEOUT);
> + }
> + idle_timeout *= 60; /* unit is minutes */
> + }
> +
> + if (idle_timeout > 0) {
> + errno = 0;
> + if (clock_gettime(CLOCK_MONOTONIC, &deadline) == -1) {
> + syslog(LOG_ERR, "clock_gettime (%d %m): error reading
> clock",
> + errno);
> + } else {
> + deadline.tv_sec += idle_timeout;
> + }
> + }
>
> if (!backend_current) { /* Local mailbox */
>
> @@ -3080,6 +3122,13 @@ static void cmd_idle(char *tag)
>
> index_release(imapd_index);
> while ((flags = idle_wait(imapd_in->fd))) {
> + if (deadline_exceeded(&deadline)) {
> + syslog(LOG_DEBUG, "timeout for user '%s' while idling",
> + imapd_userid);
> + shut_down(0);
> + break;
> + }
> +
> if (flags & IDLE_INPUT) {
> /* Get continuation data */
> c = getword(imapd_in, &arg);
> @@ -3112,7 +3161,8 @@ static void cmd_idle(char *tag)
> idle_stop(index_mboxname(imapd_index));
> }
> else { /* Remote mailbox */
> - int done = 0, shutdown = 0;
> + int done = 0;
> + enum { shutdown_skip, shutdown_bye, shutdown_silent } shutdown =
> shutdown_skip;
> char buf[2048];
>
> /* get polling period */
> @@ -3144,6 +3194,14 @@ static void cmd_idle(char *tag)
>
> /* Pipe updates to client while waiting for end of command */
> while (!done) {
> + if (deadline_exceeded(&deadline)) {
> + syslog(LOG_DEBUG,
> + "timeout for user '%s' while idling on remote
> mailbox",
> + imapd_userid);
> + shutdown = shutdown_silent;
> + goto done;
> + }
> +
> /* Flush any buffered output */
> prot_flush(imapd_out);
>
> @@ -3152,7 +3210,8 @@ static void cmd_idle(char *tag)
> (shutdown_file(buf, sizeof(buf)) ||
> (imapd_userid &&
> userdeny(imapd_userid, config_ident, buf,
> sizeof(buf))))) {
> - shutdown = done = 1;
> + done = 1;
> + shutdown = shutdown_bye;
> goto done;
> }
>
> @@ -3176,12 +3235,20 @@ static void cmd_idle(char *tag)
> pipe_until_tag(backend_current, tag, 0);
> }
>
> - if (shutdown) {
> + switch (shutdown) {
> + case shutdown_bye:
> + ;
> char *p;
>
> for (p = buf; *p == '['; p++); /* can't have [ be first char
> */
> prot_printf(imapd_out, "* BYE [ALERT] %s\r\n", p);
> + /* fallthrough */
> + case shutdown_silent:
> shut_down(0);
> + break;
> + case shutdown_skip:
> + default:
> + break;
> }
> }
>
> diff --git a/lib/imapoptions b/lib/imapoptions
> index 2b26241..b4e238c 100644
> --- a/lib/imapoptions
> +++ b/lib/imapoptions
> @@ -2115,6 +2115,11 @@ product version in the capabilities */
> allow a bit of leeway for clients that try to NOOP every 30
> minutes. */
>
> +{ "imapidletimeout", 0, INT }
> +/* Timeout for idling clients (RFC 2177) in minutes. If set to
> + zero (the default) or less, the value of "timeout" will be
> + used instead. */
> +
> { "tls_ca_file", NULL, STRING, "2.5.0", "tls_client_ca_file" }
> /* Deprecated in favor of \fItls_client_ca_file\fR. */
>
More information about the Cyrus-devel
mailing list