[PATCH v4] imapd.c: imapoptions: implement idle timeout

Philipp Gesang philipp.gesang at intra2net.com
Wed Sep 21 11:26:40 EDT 2016


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