[RFC PATCH v2] imapd.c: imapoptions: implement idle timeout

Philipp Gesang philipp.gesang at intra2net.com
Wed Sep 7 04:54:01 EDT 2016


Add a new configuration option “imapidletimeout” which, if
greater than zero, specifies an upper limit in seconds for idle
connections. 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    | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---
 lib/imapoptions |  4 ++++
 2 files changed, 66 insertions(+), 3 deletions(-)

diff --git a/imap/imapd.c b/imap/imapd.c
index 8897412..5efe973 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;
+    }
+
+    if (clock_gettime(CLOCK_MONOTONIC_COARSE, &now) == -1) {
+        syslog(LOG_ERR, "clock_gettime (%d %s): error reading clock",
+               errno, strerror(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,22 @@ 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) {
+        errno = 0;
+        if (clock_gettime(CLOCK_MONOTONIC_COARSE, &deadline) == -1) {
+            syslog(LOG_ERR, "clock_gettime (%d %s): error reading clock",
+                   errno, strerror(errno));
+        } else {
+            deadline.tv_sec += idle_timeout;
+        }
+    }
 
     if (!backend_current) {  /* Local mailbox */
 
@@ -3080,6 +3118,11 @@ static void cmd_idle(char *tag)
 
         index_release(imapd_index);
         while ((flags = idle_wait(imapd_in->fd))) {
+            if (deadline_exceeded(&deadline)) {
+                shut_down(0);
+                break;
+            }
+
             if (flags & IDLE_INPUT) {
                 /* Get continuation data */
                 c = getword(imapd_in, &arg);
@@ -3112,7 +3155,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 +3188,11 @@ static void cmd_idle(char *tag)
 
         /* Pipe updates to client while waiting for end of command */
         while (!done) {
+            if (deadline_exceeded(&deadline)) {
+                shutdown = shutdown_silent;
+                goto done_shutdown;
+            }
+
             /* Flush any buffered output */
             prot_flush(imapd_out);
 
@@ -3152,7 +3201,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 +3226,21 @@ static void cmd_idle(char *tag)
             pipe_until_tag(backend_current, tag, 0);
         }
 
-        if (shutdown) {
+      done_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 c53a446..bbd8f95 100644
--- a/lib/imapoptions
+++ b/lib/imapoptions
@@ -892,6 +892,10 @@ Blank lines and lines beginning with ``#'' are ignored.
    idled is not enabled or cannot be contacted.  The minimum value is
    1.  A value of 0 will disable IDLE. */
 
+{ "imapidletimeout", 0, INT }
+/* Timeout for idling clients (RFC 2177) in seconds. A value of zero (the
+   default) means no timeout is enforced. */
+
 { "imapidresponse", 1, SWITCH }
 /* If enabled, the server responds to an ID command with a parameter
    list containing: version, vendor, support-url, os, os-version,


More information about the Cyrus-devel mailing list