[RFC PATCH v2] imapd.c: imapoptions: implement idle timeout
ellie timoney
ellie at fastmail.com
Wed Sep 7 21:07:54 EDT 2016
> + if (clock_gettime(CLOCK_MONOTONIC_COARSE, &now) == -1) {
> + syslog(LOG_ERR, "clock_gettime (%d %s): error reading clock",
> + errno, strerror(errno));
> + return false;
> + }
I'm curious about the choice of struct timespec/clock_gettime() vs
time_t/time() -- considering we're operating at seconds (or maybe
minutes) granularity anyway?
I can kind of see a point about the ability to request a monotonic
clock, such that some sysadmin changing the system time doesn't
prematurely timeout a bunch of idle connections. Though that doesn't
seem likely to be a real problem in practice (a: don't do that, b: the
clients will just reconnect in a bit anyway).
And I'm having mixed feelings about CLOCK_MONOTONIC_COARSE, which Linux
documents as "Linux-specific". I know we have some Solaris users out
there (because they very kindly send us patches every time us
Linux-using developers break it for them!)
Also, I'm curious about the explicit %s=>strerror(errno) in the syslog
lines, instead of just doing something like: syslog(LOG_ERR,
"clock_gettime: %m"). Not sure if this is a case of you not knowing
about %m, or you knowing something I don't about %m...
> + if (deadline_exceeded(&deadline)) {
> + shutdown = shutdown_silent;
> + goto done_shutdown;
> + }
I notice this bit is explicitly skipping past terminating IDLE on the
backend? I guess we specifically didn't receive an idle termination from
the client in this case, so passing one on to the backend would be
misleading, but I wonder.
Anyway, the patch applies cleanly, builds fine for me, and passes our
current tests. I'll try and put together a couple of Cassandane tests
for the new behaviour and see what shakes out of that.
Cheers,
elli
On Wed, Sep 7, 2016, at 06:54 PM, Philipp Gesang wrote:
> 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