From 9ee44efbe7474892e931cd517195cb690e765ab6 Mon Sep 17 00:00:00 2001 From: Phil Pennock Date: Tue, 8 May 2012 14:44:36 -0700 Subject: [PATCH] inetd wait mode support with -bw --- doc/doc-docbook/spec.xfpt | 17 ++ doc/doc-txt/ChangeLog | 2 + doc/doc-txt/NewStuff | 6 + doc/doc-txt/OptionLists.txt | 1 + src/src/daemon.c | 323 ++++++++++++++++++++++++------------ src/src/exim.c | 23 ++- src/src/globals.c | 2 + src/src/globals.h | 2 + src/src/log.c | 4 +- 9 files changed, 275 insertions(+), 105 deletions(-) diff --git a/doc/doc-docbook/spec.xfpt b/doc/doc-docbook/spec.xfpt index 2a01a1e5b..2202e6b75 100644 --- a/doc/doc-docbook/spec.xfpt +++ b/doc/doc-docbook/spec.xfpt @@ -3386,6 +3386,23 @@ This option acts like &%-bv%&, but verifies the address as a sender rather than a recipient address. This affects any rewriting and qualification that might happen. +.vitem &%-bw%& +.oindex "&%-bw%&" +.cindex "daemon" +.cindex "inetd" +.cindex "inetd" "wait mode" +This option runs Exim as a daemon, awaiting incoming SMTP connections, +similarly to the &%-bd%& option. All port specifications on the command-line +and in the configuration file are ignored. Queue-running may not be specified. + +In this mode, Exim expects to be passed a socket as fd 0 (stdin) which is +listening for connections. This permits the system to start up and have +inetd (or equivalent) listen on the SMTP ports, starting an Exim daemon for +each port only when the first connection is received. + +If the option is given as &%-bw%&<&'time'&> then the time is a timeout, after +which the daemon will exit, which should cause inetd to listen once more. + .vitem &%-C%&&~<&'filelist'&> .oindex "&%-C%&" .cindex "configuration file" "alternate" diff --git a/doc/doc-txt/ChangeLog b/doc/doc-txt/ChangeLog index d202cf16b..de7c17878 100644 --- a/doc/doc-txt/ChangeLog +++ b/doc/doc-txt/ChangeLog @@ -91,6 +91,8 @@ PP/20 Revert part of NM/04, it broke log_path containing %D expansions. PP/21 Defaulting "accept_8bitmime" to true, not false. +PP/22 Added -bw for inetd wait mode support. + Exim version 4.77 ----------------- diff --git a/doc/doc-txt/NewStuff b/doc/doc-txt/NewStuff index 1c8190597..432bbd220 100644 --- a/doc/doc-txt/NewStuff +++ b/doc/doc-txt/NewStuff @@ -62,6 +62,12 @@ Version 4.78 Those who disagree, or know that they are talking to mail servers that, even today, are not 8-bit clean, need to turn off this option. + 9. Exim can now be started with -bw (with an optional timeout, given as + -bw). With this, stdin at startup is a socket that is + already listening for connections. This has a more modern name of + "socket activation", but forcing the activated socket to fd 0. We're + interested in adding more support for modern variants. + Version 4.77 ------------ diff --git a/doc/doc-txt/OptionLists.txt b/doc/doc-txt/OptionLists.txt index 20aeff965..c9629688a 100644 --- a/doc/doc-txt/OptionLists.txt +++ b/doc/doc-txt/OptionLists.txt @@ -641,6 +641,7 @@ provide compatibility with Sendmail. -bV Verify version number -bv Test recipient address verification -bvs Test sender address verification +-bw + Inetd wait mode -C + Use alternate configuration file -D + Define macro for configuration file -d + Turn on debugging output diff --git a/src/src/daemon.c b/src/src/daemon.c index 27b4cb265..de794f693 100644 --- a/src/src/daemon.c +++ b/src/src/daemon.c @@ -913,12 +913,67 @@ struct passwd *pw; int *listen_sockets = NULL; int listen_socket_count = 0; ip_address_item *addresses = NULL; +time_t last_connection_time = (time_t)0; /* If any debugging options are set, turn on the D_pid bit so that all debugging lines get the pid added. */ DEBUG(D_any|D_v) debug_selector |= D_pid; +if (inetd_wait_mode) + { + int on = 1; + + listen_socket_count = 1; + listen_sockets = store_get(sizeof(int *)); + (void) close(3); + if (dup2(0, 3) == -1) + { + log_write(0, LOG_MAIN|LOG_PANIC_DIE, + "failed to dup inetd socket safely away: %s", strerror(errno)); + } + listen_sockets[0] = 3; + (void) close(0); + (void) close(1); + (void) close(2); + exim_nullstd(); + + if (debug_file == stderr) + { + /* need a call to log_write before call to open debug_file, so that + log.c:file_path has been initialised. This is unfortunate. */ + log_write(0, LOG_MAIN, "debugging Exim in inetd wait mode starting"); + + fclose(debug_file); + debug_file = NULL; + exim_nullstd(); /* re-open fd2 after we just closed it again */ + debug_logging_activate(US"-wait", NULL); + } + + DEBUG(D_any) debug_printf("running in inetd wait mode\n"); + + /* As per below, when creating sockets ourselves, we handle tcp_nodelay for + our own buffering; we assume though that inetd set the socket REUSEADDR. */ + + if (tcp_nodelay) setsockopt(3, IPPROTO_TCP, TCP_NODELAY, + (uschar *)(&on), sizeof(on)); + } + + +if (inetd_wait_mode || daemon_listen) + { + /* If any option requiring a load average to be available during the + reception of a message is set, call os_getloadavg() while we are root + for those OS for which this is necessary the first time it is called (in + order to perform an "open" on the kernel memory file). */ + + #ifdef LOAD_AVG_NEEDS_ROOT + if (queue_only_load >= 0 || smtp_load_reserve >= 0 || + (deliver_queue_load_max >= 0 && deliver_drop_privilege)) + (void)os_getloadavg(); + #endif + } + /* Do the preparation for setting up a listener on one or more interfaces, and possible on various ports. This is controlled by the combination of @@ -987,7 +1042,7 @@ The preparation code decodes options and sets up the relevant data. We do this first, so that we can return non-zero if there are any syntax errors, and also write to stderr. */ -if (daemon_listen) +else if (daemon_listen) { int *default_smtp_port; int sep; @@ -998,17 +1053,6 @@ if (daemon_listen) ip_address_item *ipa; ip_address_item **pipa; - /* If any option requiring a load average to be available during the - reception of a message is set, call os_getloadavg() while we are root - for those OS for which this is necessary the first time it is called (in - order to perform an "open" on the kernel memory file). */ - - #ifdef LOAD_AVG_NEEDS_ROOT - if (queue_only_load >= 0 || smtp_load_reserve >= 0 || - (deliver_queue_load_max >= 0 && deliver_drop_privilege)) - (void)os_getloadavg(); - #endif - /* If -oX was used, disable the writing of a pid file unless -oP was explicitly used to force it. Then scan the string given to -oX. Any items that contain neither a dot nor a colon are used to override daemon_smtp_port. @@ -1208,6 +1252,11 @@ if (daemon_listen) listen_socket_count++; listen_sockets = store_get(sizeof(int *) * listen_socket_count); + } /* daemon_listen but not inetd_wait_mode */ + +if (daemon_listen) + { + /* Do a sanity check on the max connects value just to save us from getting a huge amount of store. */ @@ -1233,7 +1282,8 @@ if (daemon_listen) /* The variable background_daemon is always false when debugging, but can also be forced false in order to keep a non-debugging daemon in the foreground. If background_daemon is true, close all open file descriptors that -we know about, but then re-open stdin, stdout, and stderr to /dev/null. +we know about, but then re-open stdin, stdout, and stderr to /dev/null. Also +do this for inetd_wait mode. This is protection against any called functions (in libraries, or in Perl, or whatever) that think they can write to stderr (or stdout). Before this @@ -1244,7 +1294,7 @@ Then disconnect from the controlling terminal, Most modern Unixes seem to have setsid() for getting rid of the controlling terminal. For any OS that doesn't, setsid() can be #defined as a no-op, or as something else. */ -if (background_daemon) +if (background_daemon || inetd_wait_mode) { log_close_all(); /* Just in case anything was logged earlier */ search_tidyup(); /* Just in case any were used in reading the config. */ @@ -1253,7 +1303,10 @@ if (background_daemon) (void)close(2); exim_nullstd(); /* Connect stdin/stdout/stderr to /dev/null */ log_stderr = NULL; /* So no attempt to copy paniclog output */ + } +if (background_daemon) + { /* If the parent process of this one has pid == 1, we are re-initializing the daemon as the result of a SIGHUP. In this case, there is no need to do anything, because the controlling terminal has long gone. Otherwise, fork, in @@ -1273,7 +1326,7 @@ if (background_daemon) /* We are now in the disconnected, daemon process (unless debugging). Set up the listening sockets if required. */ -if (daemon_listen) +if (daemon_listen && !inetd_wait_mode) { int sk; int on = 1; @@ -1513,7 +1566,25 @@ sigalrm_seen = (queue_interval > 0); /* Log the start up of a daemon - at least one of listening or queue running must be set up. */ -if (daemon_listen) +if (inetd_wait_mode) + { + uschar *p = big_buffer; + + if (inetd_wait_timeout >= 0) + sprintf(CS p, "terminating after %d seconds", inetd_wait_timeout); + else + sprintf(CS p, "with no wait timeout"); + + log_write(0, LOG_MAIN, + "exim %s daemon started: pid=%d, launched with listening socket, %s", + version_string, getpid(), big_buffer); + set_process_info("daemon: pre-listening socket"); + + /* set up the timeout logic */ + sigalrm_seen = 1; + } + +else if (daemon_listen) { int i, j; int smtp_ports = 0; @@ -1631,122 +1702,166 @@ for (;;) pid_t pid; /* This code is placed first in the loop, so that it gets obeyed at the - start, before the first wait. This causes the first queue-runner to be - started immediately. */ + start, before the first wait, for the queue-runner case, so that the first + one can be started immediately. + + The other option is that we have an inetd wait timeout specified to -bw. */ if (sigalrm_seen) { - DEBUG(D_any) debug_printf("SIGALRM received\n"); + if (inetd_wait_timeout > 0) + { + time_t resignal_interval = inetd_wait_timeout; + + if (last_connection_time == (time_t)0) + { + DEBUG(D_any) + debug_printf("inetd wait timeout expired, but still not seen first message, ignoring\n"); + } + else + { + time_t now = time(NULL); + if (now == (time_t)-1) + { + DEBUG(D_any) debug_printf("failed to get time: %s\n", strerror(errno)); + } + else + { + if ((now - last_connection_time) >= inetd_wait_timeout) + { + DEBUG(D_any) + debug_printf("inetd wait timeout %d expired, ending daemon\n", + inetd_wait_timeout); + log_write(0, LOG_MAIN, "exim %s daemon terminating, inetd wait timeout reached.\n", + version_string); + exit(EXIT_SUCCESS); + } + else + { + resignal_interval -= (now - last_connection_time); + } + } + } - /* Do a full queue run in a child process, if required, unless we already - have enough queue runners on the go. If we are not running as root, a - re-exec is required. */ + sigalrm_seen = FALSE; + alarm(resignal_interval); + } - if (queue_interval > 0 && - (queue_run_max <= 0 || queue_run_count < queue_run_max)) + else { - if ((pid = fork()) == 0) - { - int sk; + DEBUG(D_any) debug_printf("SIGALRM received\n"); - DEBUG(D_any) debug_printf("Starting queue-runner: pid %d\n", - (int)getpid()); + /* Do a full queue run in a child process, if required, unless we already + have enough queue runners on the go. If we are not running as root, a + re-exec is required. */ - /* Disable debugging if it's required only for the daemon process. We - leave the above message, because it ties up with the "child ended" - debugging messages. */ + if (queue_interval > 0 && + (queue_run_max <= 0 || queue_run_count < queue_run_max)) + { + if ((pid = fork()) == 0) + { + int sk; - if (debug_daemon) debug_selector = 0; + DEBUG(D_any) debug_printf("Starting queue-runner: pid %d\n", + (int)getpid()); - /* Close any open listening sockets in the child */ + /* Disable debugging if it's required only for the daemon process. We + leave the above message, because it ties up with the "child ended" + debugging messages. */ - for (sk = 0; sk < listen_socket_count; sk++) - (void)close(listen_sockets[sk]); + if (debug_daemon) debug_selector = 0; - /* Reset SIGHUP and SIGCHLD in the child in both cases. */ + /* Close any open listening sockets in the child */ - signal(SIGHUP, SIG_DFL); - signal(SIGCHLD, SIG_DFL); + for (sk = 0; sk < listen_socket_count; sk++) + (void)close(listen_sockets[sk]); - /* Re-exec if privilege has been given up, unless deliver_drop_ - privilege is set. Reset SIGALRM before exec(). */ + /* Reset SIGHUP and SIGCHLD in the child in both cases. */ - if (geteuid() != root_uid && !deliver_drop_privilege) - { - uschar opt[8]; - uschar *p = opt; - uschar *extra[5]; - int extracount = 1; + signal(SIGHUP, SIG_DFL); + signal(SIGCHLD, SIG_DFL); - signal(SIGALRM, SIG_DFL); - *p++ = '-'; - *p++ = 'q'; - if (queue_2stage) *p++ = 'q'; - if (queue_run_first_delivery) *p++ = 'i'; - if (queue_run_force) *p++ = 'f'; - if (deliver_force_thaw) *p++ = 'f'; - if (queue_run_local) *p++ = 'l'; - *p = 0; - extra[0] = opt; - - /* If -R or -S were on the original command line, ensure they get - passed on. */ - - if (deliver_selectstring != NULL) - { - extra[extracount++] = deliver_selectstring_regex? US"-Rr" : US"-R"; - extra[extracount++] = deliver_selectstring; - } + /* Re-exec if privilege has been given up, unless deliver_drop_ + privilege is set. Reset SIGALRM before exec(). */ - if (deliver_selectstring_sender != NULL) + if (geteuid() != root_uid && !deliver_drop_privilege) { - extra[extracount++] = deliver_selectstring_sender_regex? - US"-Sr" : US"-S"; - extra[extracount++] = deliver_selectstring_sender; + uschar opt[8]; + uschar *p = opt; + uschar *extra[5]; + int extracount = 1; + + signal(SIGALRM, SIG_DFL); + *p++ = '-'; + *p++ = 'q'; + if (queue_2stage) *p++ = 'q'; + if (queue_run_first_delivery) *p++ = 'i'; + if (queue_run_force) *p++ = 'f'; + if (deliver_force_thaw) *p++ = 'f'; + if (queue_run_local) *p++ = 'l'; + *p = 0; + extra[0] = opt; + + /* If -R or -S were on the original command line, ensure they get + passed on. */ + + if (deliver_selectstring != NULL) + { + extra[extracount++] = deliver_selectstring_regex? US"-Rr" : US"-R"; + extra[extracount++] = deliver_selectstring; + } + + if (deliver_selectstring_sender != NULL) + { + extra[extracount++] = deliver_selectstring_sender_regex? + US"-Sr" : US"-S"; + extra[extracount++] = deliver_selectstring_sender; + } + + /* Overlay this process with a new execution. */ + + (void)child_exec_exim(CEE_EXEC_PANIC, FALSE, NULL, TRUE, extracount, + extra[0], extra[1], extra[2], extra[3], extra[4]); + + /* Control never returns here. */ } - /* Overlay this process with a new execution. */ - - (void)child_exec_exim(CEE_EXEC_PANIC, FALSE, NULL, TRUE, extracount, - extra[0], extra[1], extra[2], extra[3], extra[4]); + /* No need to re-exec; SIGALRM remains set to the default handler */ - /* Control never returns here. */ + queue_run(NULL, NULL, FALSE); + _exit(EXIT_SUCCESS); } - /* No need to re-exec; SIGALRM remains set to the default handler */ - - queue_run(NULL, NULL, FALSE); - _exit(EXIT_SUCCESS); - } - - if (pid < 0) - { - log_write(0, LOG_MAIN|LOG_PANIC, "daemon: fork of queue-runner " - "process failed: %s", strerror(errno)); - log_close_all(); - } - else - { - int i; - for (i = 0; i < queue_run_max; ++i) + if (pid < 0) { - if (queue_pid_slots[i] <= 0) + log_write(0, LOG_MAIN|LOG_PANIC, "daemon: fork of queue-runner " + "process failed: %s", strerror(errno)); + log_close_all(); + } + else + { + int i; + for (i = 0; i < queue_run_max; ++i) { - queue_pid_slots[i] = pid; - queue_run_count++; - break; + if (queue_pid_slots[i] <= 0) + { + queue_pid_slots[i] = pid; + queue_run_count++; + break; + } } + DEBUG(D_any) debug_printf("%d queue-runner process%s running\n", + queue_run_count, (queue_run_count == 1)? "" : "es"); } - DEBUG(D_any) debug_printf("%d queue-runner process%s running\n", - queue_run_count, (queue_run_count == 1)? "" : "es"); } - } - /* Reset the alarm clock */ + /* Reset the alarm clock */ - sigalrm_seen = FALSE; - alarm(queue_interval); - } + sigalrm_seen = FALSE; + alarm(queue_interval); + } + + } /* sigalrm_seen */ /* Sleep till a connection happens if listening, and handle the connection if @@ -1886,8 +2001,12 @@ for (;;) /* If select/accept succeeded, deal with the connection. */ if (accept_socket >= 0) + { + if (inetd_wait_timeout) + last_connection_time = time(NULL); handle_smtp_call(listen_sockets, listen_socket_count, accept_socket, (struct sockaddr *)&accepted); + } } } diff --git a/src/src/exim.c b/src/src/exim.c index 90ecd0629..b53d6ca9b 100644 --- a/src/src/exim.c +++ b/src/src/exim.c @@ -2060,6 +2060,24 @@ for (i = 1; i < argc; i++) show_whats_supported(stdout); } + /* -bw: inetd wait mode, accept a listening socket as stdin */ + + else if (*argrest == 'w') + { + inetd_wait_mode = TRUE; + background_daemon = FALSE; + daemon_listen = TRUE; + if (*(++argrest) != '\0') + { + inetd_wait_timeout = readconf_readtime(argrest, 0, FALSE); + if (inetd_wait_timeout <= 0) + { + fprintf(stderr, "exim: bad time value %s: abandoned\n", argv[i]); + exit(EXIT_FAILURE); + } + } + } + else badarg = TRUE; break; @@ -3222,6 +3240,9 @@ if (( daemon_listen && queue_interval == 0 ) || ( + inetd_wait_mode && queue_interval >= 0 + ) || + ( list_options && (checking || smtp_input || extract_recipients || filter_test != FTEST_NONE || bi_option) @@ -4406,7 +4427,7 @@ returns. We leave this till here so that the originator_ fields are available for incoming messages via the daemon. The daemon cannot be run in mua_wrapper mode. */ -if (daemon_listen || queue_interval > 0) +if (daemon_listen || inetd_wait_mode || queue_interval > 0) { if (mua_wrapper) { diff --git a/src/src/globals.c b/src/src/globals.c index af0c14b02..b68544e54 100644 --- a/src/src/globals.c +++ b/src/src/globals.c @@ -659,6 +659,8 @@ uschar *hosts_connection_nolog = NULL; int ignore_bounce_errors_after = 10*7*24*60*60; /* 10 weeks */ BOOL ignore_fromline_local = FALSE; uschar *ignore_fromline_hosts = NULL; +BOOL inetd_wait_mode = FALSE; +int inetd_wait_timeout = -1; uschar *interface_address = NULL; int interface_port = -1; BOOL is_inetd = FALSE; diff --git a/src/src/globals.h b/src/src/globals.h index f9540785c..d267a52b7 100644 --- a/src/src/globals.h +++ b/src/src/globals.h @@ -424,6 +424,8 @@ extern uschar *hosts_treat_as_local; /* For routing */ extern int ignore_bounce_errors_after; /* Keep them for this time. */ extern BOOL ignore_fromline_local; /* Local SMTP ignore fromline */ extern uschar *ignore_fromline_hosts; /* Hosts permitted to send "From " */ +extern BOOL inetd_wait_mode; /* Whether running in inetd wait mode */ +extern int inetd_wait_timeout; /* Timeout for inetd wait mode */ extern BOOL is_inetd; /* True for inetd calls */ extern uschar *iterate_item; /* Item from iterate list */ diff --git a/src/src/log.c b/src/src/log.c index bfd1fef86..75285c136 100644 --- a/src/src/log.c +++ b/src/src/log.c @@ -1306,7 +1306,7 @@ misconfiguration. The first use of this is in ACL logic, "control = debug/tag=foo/opts=+expand" which can be combined with conditions, etc, to activate extra logging only -for certain sources. */ +for certain sources. The second use is inetd wait mode debug preservation. */ void debug_logging_activate(uschar *tag_name, uschar *opts) @@ -1316,7 +1316,7 @@ int fd = -1; if (debug_file) { debug_printf("DEBUGGING ACTIVATED FROM WITHIN CONFIG.\n" - "DEBUG: Tag=\"%s\" Opts=\"%s\"\n", tag_name, opts); + "DEBUG: Tag=\"%s\" Opts=\"%s\"\n", tag_name, opts ? opts : US""); return; } -- 2.25.1