diff options
Diffstat (limited to '')
-rw-r--r-- | src/anvil/anvil.c | 1047 |
1 files changed, 1047 insertions, 0 deletions
diff --git a/src/anvil/anvil.c b/src/anvil/anvil.c new file mode 100644 index 0000000..884be28 --- /dev/null +++ b/src/anvil/anvil.c @@ -0,0 +1,1047 @@ +/*++ +/* NAME +/* anvil 8 +/* SUMMARY +/* Postfix session count and request rate control +/* SYNOPSIS +/* \fBanvil\fR [generic Postfix daemon options] +/* DESCRIPTION +/* The Postfix \fBanvil\fR(8) server maintains statistics about +/* client connection counts or client request rates. This +/* information can be used to defend against clients that +/* hammer a server with either too many simultaneous sessions, +/* or with too many successive requests within a configurable +/* time interval. This server is designed to run under control +/* by the Postfix \fBmaster\fR(8) server. +/* +/* In the following text, \fBident\fR specifies a (service, +/* client) combination. The exact syntax of that information +/* is application-dependent; the \fBanvil\fR(8) server does +/* not care. +/* CONNECTION COUNT/RATE CONTROL +/* .ad +/* .fi +/* To register a new connection send the following request to +/* the \fBanvil\fR(8) server: +/* +/* .nf +/* \fBrequest=connect\fR +/* \fBident=\fIstring\fR +/* .fi +/* +/* The \fBanvil\fR(8) server answers with the number of +/* simultaneous connections and the number of connections per +/* unit time for the (service, client) combination specified +/* with \fBident\fR: +/* +/* .nf +/* \fBstatus=0\fR +/* \fBcount=\fInumber\fR +/* \fBrate=\fInumber\fR +/* .fi +/* +/* To register a disconnect event send the following request +/* to the \fBanvil\fR(8) server: +/* +/* .nf +/* \fBrequest=disconnect\fR +/* \fBident=\fIstring\fR +/* .fi +/* +/* The \fBanvil\fR(8) server replies with: +/* +/* .nf +/* \fBstatus=0\fR +/* .fi +/* MESSAGE RATE CONTROL +/* .ad +/* .fi +/* To register a message delivery request send the following +/* request to the \fBanvil\fR(8) server: +/* +/* .nf +/* \fBrequest=message\fR +/* \fBident=\fIstring\fR +/* .fi +/* +/* The \fBanvil\fR(8) server answers with the number of message +/* delivery requests per unit time for the (service, client) +/* combination specified with \fBident\fR: +/* +/* .nf +/* \fBstatus=0\fR +/* \fBrate=\fInumber\fR +/* .fi +/* RECIPIENT RATE CONTROL +/* .ad +/* .fi +/* To register a recipient request send the following request +/* to the \fBanvil\fR(8) server: +/* +/* .nf +/* \fBrequest=recipient\fR +/* \fBident=\fIstring\fR +/* .fi +/* +/* The \fBanvil\fR(8) server answers with the number of recipient +/* addresses per unit time for the (service, client) combination +/* specified with \fBident\fR: +/* +/* .nf +/* \fBstatus=0\fR +/* \fBrate=\fInumber\fR +/* .fi +/* TLS SESSION NEGOTIATION RATE CONTROL +/* .ad +/* .fi +/* The features described in this section are available with +/* Postfix 2.3 and later. +/* +/* To register a request for a new (i.e. not cached) TLS session +/* send the following request to the \fBanvil\fR(8) server: +/* +/* .nf +/* \fBrequest=newtls\fR +/* \fBident=\fIstring\fR +/* .fi +/* +/* The \fBanvil\fR(8) server answers with the number of new +/* TLS session requests per unit time for the (service, client) +/* combination specified with \fBident\fR: +/* +/* .nf +/* \fBstatus=0\fR +/* \fBrate=\fInumber\fR +/* .fi +/* +/* To retrieve new TLS session request rate information without +/* updating the counter information, send: +/* +/* .nf +/* \fBrequest=newtls_report\fR +/* \fBident=\fIstring\fR +/* .fi +/* +/* The \fBanvil\fR(8) server answers with the number of new +/* TLS session requests per unit time for the (service, client) +/* combination specified with \fBident\fR: +/* +/* .nf +/* \fBstatus=0\fR +/* \fBrate=\fInumber\fR +/* .fi +/* AUTH RATE CONTROL +/* .ad +/* .fi +/* To register an AUTH request send the following request +/* to the \fBanvil\fR(8) server: +/* +/* .nf +/* \fBrequest=auth\fR +/* \fBident=\fIstring\fR +/* .fi +/* +/* The \fBanvil\fR(8) server answers with the number of auth +/* requests per unit time for the (service, client) combination +/* specified with \fBident\fR: +/* +/* .nf +/* \fBstatus=0\fR +/* \fBrate=\fInumber\fR +/* .fi +/* SECURITY +/* .ad +/* .fi +/* The \fBanvil\fR(8) server does not talk to the network or to local +/* users, and can run chrooted at fixed low privilege. +/* +/* The \fBanvil\fR(8) server maintains an in-memory table with +/* information about recent clients requests. No persistent +/* state is kept because standard system library routines are +/* not sufficiently robust for update-intensive applications. +/* +/* Although the in-memory state is kept only temporarily, this +/* may require a lot of memory on systems that handle connections +/* from many remote clients. To reduce memory usage, reduce +/* the time unit over which state is kept. +/* DIAGNOSTICS +/* Problems and transactions are logged to \fBsyslogd\fR(8) +/* or \fBpostlogd\fR(8). +/* +/* Upon exit, and every \fBanvil_status_update_time\fR +/* seconds, the server logs the maximal count and rate values measured, +/* together with (service, client) information and the time of day +/* associated with those events. +/* In order to avoid unnecessary overhead, no measurements +/* are done for activity that isn't concurrency limited or +/* rate limited. +/* BUGS +/* Systems behind network address translating routers or proxies +/* appear to have the same client address and can run into connection +/* count and/or rate limits falsely. +/* +/* In this preliminary implementation, a count (or rate) limited server +/* process can have only one remote client at a time. If a +/* server process reports +/* multiple simultaneous clients, state is kept only for the last +/* reported client. +/* +/* The \fBanvil\fR(8) server automatically discards client +/* request information after it expires. To prevent the +/* \fBanvil\fR(8) server from discarding client request rate +/* information too early or too late, a rate limited service +/* should always register connect/disconnect events even when +/* it does not explicitly limit them. +/* CONFIGURATION PARAMETERS +/* .ad +/* .fi +/* On low-traffic mail systems, changes to \fBmain.cf\fR are +/* picked up automatically as \fBanvil\fR(8) processes run for +/* only a limited amount of time. On other mail systems, use +/* the command "\fBpostfix reload\fR" to speed up a change. +/* +/* The text below provides only a parameter summary. See +/* \fBpostconf\fR(5) for more details including examples. +/* .IP "\fBanvil_rate_time_unit (60s)\fR" +/* The time unit over which client connection rates and other rates +/* are calculated. +/* .IP "\fBanvil_status_update_time (600s)\fR" +/* How frequently the \fBanvil\fR(8) connection and rate limiting server +/* logs peak usage information. +/* .IP "\fBconfig_directory (see 'postconf -d' output)\fR" +/* The default location of the Postfix main.cf and master.cf +/* configuration files. +/* .IP "\fBdaemon_timeout (18000s)\fR" +/* How much time a Postfix daemon process may take to handle a +/* request before it is terminated by a built-in watchdog timer. +/* .IP "\fBipc_timeout (3600s)\fR" +/* The time limit for sending or receiving information over an internal +/* communication channel. +/* .IP "\fBmax_idle (100s)\fR" +/* The maximum amount of time that an idle Postfix daemon process waits +/* for an incoming connection before terminating voluntarily. +/* .IP "\fBmax_use (100)\fR" +/* The maximal number of incoming connections that a Postfix daemon +/* process will service before terminating voluntarily. +/* .IP "\fBprocess_id (read-only)\fR" +/* The process ID of a Postfix command or daemon process. +/* .IP "\fBprocess_name (read-only)\fR" +/* The process name of a Postfix command or daemon process. +/* .IP "\fBsyslog_facility (mail)\fR" +/* The syslog facility of Postfix logging. +/* .IP "\fBsyslog_name (see 'postconf -d' output)\fR" +/* A prefix that is prepended to the process name in syslog +/* records, so that, for example, "smtpd" becomes "prefix/smtpd". +/* .PP +/* Available in Postfix 3.3 and later: +/* .IP "\fBservice_name (read-only)\fR" +/* The master.cf service name of a Postfix daemon process. +/* SEE ALSO +/* smtpd(8), Postfix SMTP server +/* postconf(5), configuration parameters +/* master(5), generic daemon options +/* README FILES +/* .ad +/* .fi +/* Use "\fBpostconf readme_directory\fR" or +/* "\fBpostconf html_directory\fR" to locate this information. +/* .na +/* .nf +/* TUNING_README, performance tuning +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* HISTORY +/* .ad +/* .fi +/* The anvil service is available in Postfix 2.2 and later. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA +/*--*/ + +/* System library. */ + +#include <sys_defs.h> +#include <sys/time.h> +#include <limits.h> + +/* Utility library. */ + +#include <msg.h> +#include <mymalloc.h> +#include <htable.h> +#include <stringops.h> +#include <events.h> + +/* Global library. */ + +#include <mail_conf.h> +#include <mail_params.h> +#include <mail_version.h> +#include <mail_proto.h> +#include <anvil_clnt.h> + +/* Server skeleton. */ + +#include <mail_server.h> + +/* Application-specific. */ + + /* + * Configuration parameters. + */ +int var_anvil_time_unit; +int var_anvil_stat_time; + + /* + * Global dynamic state. + */ +static HTABLE *anvil_remote_map; /* indexed by service+ remote client */ + + /* + * Remote connection state, one instance for each (service, client) pair. + */ +typedef struct { + char *ident; /* lookup key */ + int count; /* connection count */ + int rate; /* connection rate */ + int mail; /* message rate */ + int rcpt; /* recipient rate */ + int ntls; /* new TLS session rate */ + int auth; /* AUTH request rate */ + time_t start; /* time of first rate sample */ +} ANVIL_REMOTE; + + /* + * Local server state, one instance per anvil client connection. This allows + * us to clean up remote connection state when a local server goes away + * without cleaning up. + */ +typedef struct { + ANVIL_REMOTE *anvil_remote; /* XXX should be list */ +} ANVIL_LOCAL; + + /* + * The following operations are implemented as macros with recognizable + * names so that we don't lose sight of what the code is trying to do. + * + * Related operations are defined side by side so that the code implementing + * them isn't pages apart. + */ + +/* Create new (service, client) state. */ + +#define ANVIL_REMOTE_FIRST_CONN(remote, id) \ + do { \ + (remote)->ident = mystrdup(id); \ + (remote)->count = 1; \ + (remote)->rate = 1; \ + (remote)->mail = 0; \ + (remote)->rcpt = 0; \ + (remote)->ntls = 0; \ + (remote)->auth = 0; \ + (remote)->start = event_time(); \ + } while(0) + +/* Destroy unused (service, client) state. */ + +#define ANVIL_REMOTE_FREE(remote) \ + do { \ + myfree((remote)->ident); \ + myfree((void *) (remote)); \ + } while(0) + +/* Reset or update rate information for existing (service, client) state. */ + +#define ANVIL_REMOTE_RSET_RATE(remote, _start) \ + do { \ + (remote)->rate = 0; \ + (remote)->mail = 0; \ + (remote)->rcpt = 0; \ + (remote)->ntls = 0; \ + (remote)->auth = 0; \ + (remote)->start = _start; \ + } while(0) + +#define ANVIL_REMOTE_INCR_RATE(remote, _what) \ + do { \ + time_t _now = event_time(); \ + if ((remote)->start + var_anvil_time_unit < _now) \ + ANVIL_REMOTE_RSET_RATE((remote), _now); \ + if ((remote)->_what < INT_MAX) \ + (remote)->_what += 1; \ + } while(0) + +/* Update existing (service, client) state. */ + +#define ANVIL_REMOTE_NEXT_CONN(remote) \ + do { \ + ANVIL_REMOTE_INCR_RATE((remote), rate); \ + if ((remote)->count == 0) \ + event_cancel_timer(anvil_remote_expire, (void *) remote); \ + (remote)->count++; \ + } while(0) + +#define ANVIL_REMOTE_INCR_MAIL(remote) ANVIL_REMOTE_INCR_RATE((remote), mail) + +#define ANVIL_REMOTE_INCR_RCPT(remote) ANVIL_REMOTE_INCR_RATE((remote), rcpt) + +#define ANVIL_REMOTE_INCR_NTLS(remote) ANVIL_REMOTE_INCR_RATE((remote), ntls) + +#define ANVIL_REMOTE_INCR_AUTH(remote) ANVIL_REMOTE_INCR_RATE((remote), auth) + +/* Drop connection from (service, client) state. */ + +#define ANVIL_REMOTE_DROP_ONE(remote) \ + do { \ + if ((remote) && (remote)->count > 0) { \ + if (--(remote)->count == 0) \ + event_request_timer(anvil_remote_expire, (void *) remote, \ + var_anvil_time_unit); \ + } \ + } while(0) + +/* Create local server state. */ + +#define ANVIL_LOCAL_INIT(local) \ + do { \ + (local)->anvil_remote = 0; \ + } while(0) + +/* Add remote connection to local server. */ + +#define ANVIL_LOCAL_ADD_ONE(local, remote) \ + do { \ + /* XXX allow multiple remote clients per local server. */ \ + if ((local)->anvil_remote) \ + ANVIL_REMOTE_DROP_ONE((local)->anvil_remote); \ + (local)->anvil_remote = (remote); \ + } while(0) + +/* Test if this remote connection is listed for this local server. */ + +#define ANVIL_LOCAL_REMOTE_LINKED(local, remote) \ + ((local)->anvil_remote == (remote)) + +/* Drop specific remote connection from local server. */ + +#define ANVIL_LOCAL_DROP_ONE(local, remote) \ + do { \ + /* XXX allow multiple remote clients per local server. */ \ + if ((local)->anvil_remote == (remote)) \ + (local)->anvil_remote = 0; \ + } while(0) + +/* Drop all remote connections from local server. */ + +#define ANVIL_LOCAL_DROP_ALL(stream, local) \ + do { \ + /* XXX allow multiple remote clients per local server. */ \ + if ((local)->anvil_remote) \ + anvil_remote_disconnect((stream), (local)->anvil_remote->ident); \ + } while (0) + + /* + * Lookup table to map request names to action routines. + */ +typedef struct { + const char *name; + void (*action) (VSTREAM *, const char *); +} ANVIL_REQ_TABLE; + + /* + * Run-time statistics for maximal connection counts and event rates. These + * store the peak resource usage, remote connection, and time. Absent a + * query interface, this information is logged at process exit time and at + * configurable intervals. + */ +typedef struct { + int value; /* peak value */ + char *ident; /* lookup key */ + time_t when; /* time of peak value */ +} ANVIL_MAX; + +static ANVIL_MAX max_conn_count; /* peak connection count */ +static ANVIL_MAX max_conn_rate; /* peak connection rate */ +static ANVIL_MAX max_mail_rate; /* peak message rate */ +static ANVIL_MAX max_rcpt_rate; /* peak recipient rate */ +static ANVIL_MAX max_ntls_rate; /* peak new TLS session rate */ +static ANVIL_MAX max_auth_rate; /* peak AUTH request rate */ + +static int max_cache_size; /* peak cache size */ +static time_t max_cache_time; /* time of peak size */ + +/* Update/report peak usage. */ + +#define ANVIL_MAX_UPDATE(_max, _value, _ident) \ + do { \ + _max.value = _value; \ + if (_max.ident == 0) { \ + _max.ident = mystrdup(_ident); \ + } else if (!STREQ(_max.ident, _ident)) { \ + myfree(_max.ident); \ + _max.ident = mystrdup(_ident); \ + } \ + _max.when = event_time(); \ + } while (0) + +#define ANVIL_MAX_RATE_REPORT(_max, _name) \ + do { \ + if (_max.value > 0) { \ + msg_info("statistics: max " _name " rate %d/%ds for (%s) at %.15s", \ + _max.value, var_anvil_time_unit, \ + _max.ident, ctime(&_max.when) + 4); \ + _max.value = 0; \ + } \ + } while (0); + +#define ANVIL_MAX_COUNT_REPORT(_max, _name) \ + do { \ + if (_max.value > 0) { \ + msg_info("statistics: max " _name " count %d for (%s) at %.15s", \ + _max.value, _max.ident, ctime(&_max.when) + 4); \ + _max.value = 0; \ + } \ + } while (0); + + /* + * Silly little macros. + */ +#define STR(x) vstring_str(x) +#define STREQ(x,y) (strcmp((x), (y)) == 0) + +/* anvil_remote_expire - purge expired connection state */ + +static void anvil_remote_expire(int unused_event, void *context) +{ + ANVIL_REMOTE *anvil_remote = (ANVIL_REMOTE *) context; + const char *myname = "anvil_remote_expire"; + + if (msg_verbose) + msg_info("%s %s", myname, anvil_remote->ident); + + if (anvil_remote->count != 0) + msg_panic("%s: bad connection count: %d", + myname, anvil_remote->count); + + htable_delete(anvil_remote_map, anvil_remote->ident, + (void (*) (void *)) 0); + ANVIL_REMOTE_FREE(anvil_remote); + + if (msg_verbose) + msg_info("%s: anvil_remote_map used=%ld", + myname, (long) anvil_remote_map->used); +} + +/* anvil_remote_lookup - dump address status */ + +static void anvil_remote_lookup(VSTREAM *client_stream, const char *ident) +{ + ANVIL_REMOTE *anvil_remote; + const char *myname = "anvil_remote_lookup"; + + if (msg_verbose) + msg_info("%s fd=%d stream=0x%lx ident=%s", + myname, vstream_fileno(client_stream), + (unsigned long) client_stream, ident); + + /* + * Look up remote client information. + */ + if ((anvil_remote = + (ANVIL_REMOTE *) htable_find(anvil_remote_map, ident)) == 0) { + attr_print_plain(client_stream, ATTR_FLAG_NONE, + SEND_ATTR_INT(ANVIL_ATTR_STATUS, ANVIL_STAT_OK), + SEND_ATTR_INT(ANVIL_ATTR_COUNT, 0), + SEND_ATTR_INT(ANVIL_ATTR_RATE, 0), + SEND_ATTR_INT(ANVIL_ATTR_MAIL, 0), + SEND_ATTR_INT(ANVIL_ATTR_RCPT, 0), + SEND_ATTR_INT(ANVIL_ATTR_NTLS, 0), + SEND_ATTR_INT(ANVIL_ATTR_AUTH, 0), + ATTR_TYPE_END); + } else { + + /* + * Do not report stale information. + */ + if (anvil_remote->start != 0 + && anvil_remote->start + var_anvil_time_unit < event_time()) + ANVIL_REMOTE_RSET_RATE(anvil_remote, 0); + attr_print_plain(client_stream, ATTR_FLAG_NONE, + SEND_ATTR_INT(ANVIL_ATTR_STATUS, ANVIL_STAT_OK), + SEND_ATTR_INT(ANVIL_ATTR_COUNT, anvil_remote->count), + SEND_ATTR_INT(ANVIL_ATTR_RATE, anvil_remote->rate), + SEND_ATTR_INT(ANVIL_ATTR_MAIL, anvil_remote->mail), + SEND_ATTR_INT(ANVIL_ATTR_RCPT, anvil_remote->rcpt), + SEND_ATTR_INT(ANVIL_ATTR_NTLS, anvil_remote->ntls), + SEND_ATTR_INT(ANVIL_ATTR_AUTH, anvil_remote->auth), + ATTR_TYPE_END); + } +} + +/* anvil_remote_conn_update - instantiate or update connection info */ + +static ANVIL_REMOTE *anvil_remote_conn_update(VSTREAM *client_stream, const char *ident) +{ + ANVIL_REMOTE *anvil_remote; + ANVIL_LOCAL *anvil_local; + const char *myname = "anvil_remote_conn_update"; + + if (msg_verbose) + msg_info("%s fd=%d stream=0x%lx ident=%s", + myname, vstream_fileno(client_stream), + (unsigned long) client_stream, ident); + + /* + * Look up remote connection count information. Update remote connection + * rate information. Simply reset the counter every var_anvil_time_unit + * seconds. This is easier than maintaining a moving average and it gives + * a quicker response to tresspassers. + */ + if ((anvil_remote = + (ANVIL_REMOTE *) htable_find(anvil_remote_map, ident)) == 0) { + anvil_remote = (ANVIL_REMOTE *) mymalloc(sizeof(*anvil_remote)); + ANVIL_REMOTE_FIRST_CONN(anvil_remote, ident); + htable_enter(anvil_remote_map, ident, (void *) anvil_remote); + if (max_cache_size < anvil_remote_map->used) { + max_cache_size = anvil_remote_map->used; + max_cache_time = event_time(); + } + } else { + ANVIL_REMOTE_NEXT_CONN(anvil_remote); + } + + /* + * Record this connection under the local server information, so that we + * can clean up all its connection state when the local server goes away. + */ + if ((anvil_local = (ANVIL_LOCAL *) vstream_context(client_stream)) == 0) { + anvil_local = (ANVIL_LOCAL *) mymalloc(sizeof(*anvil_local)); + ANVIL_LOCAL_INIT(anvil_local); + vstream_control(client_stream, + CA_VSTREAM_CTL_CONTEXT((void *) anvil_local), + CA_VSTREAM_CTL_END); + } + ANVIL_LOCAL_ADD_ONE(anvil_local, anvil_remote); + if (msg_verbose) + msg_info("%s: anvil_local 0x%lx", + myname, (unsigned long) anvil_local); + + return (anvil_remote); +} + +/* anvil_remote_connect - report connection event, query address status */ + +static void anvil_remote_connect(VSTREAM *client_stream, const char *ident) +{ + ANVIL_REMOTE *anvil_remote; + + /* + * Update or instantiate connection info. + */ + anvil_remote = anvil_remote_conn_update(client_stream, ident); + + /* + * Respond to the local server. + */ + attr_print_plain(client_stream, ATTR_FLAG_NONE, + SEND_ATTR_INT(ANVIL_ATTR_STATUS, ANVIL_STAT_OK), + SEND_ATTR_INT(ANVIL_ATTR_COUNT, anvil_remote->count), + SEND_ATTR_INT(ANVIL_ATTR_RATE, anvil_remote->rate), + ATTR_TYPE_END); + + /* + * Update peak statistics. + */ + if (anvil_remote->rate > max_conn_rate.value) + ANVIL_MAX_UPDATE(max_conn_rate, anvil_remote->rate, anvil_remote->ident); + if (anvil_remote->count > max_conn_count.value) + ANVIL_MAX_UPDATE(max_conn_count, anvil_remote->count, anvil_remote->ident); +} + +/* anvil_remote_mail - register message delivery request */ + +static void anvil_remote_mail(VSTREAM *client_stream, const char *ident) +{ + ANVIL_REMOTE *anvil_remote; + + /* + * Be prepared for "postfix reload" after "connect". + */ + if ((anvil_remote = + (ANVIL_REMOTE *) htable_find(anvil_remote_map, ident)) == 0) + anvil_remote = anvil_remote_conn_update(client_stream, ident); + + /* + * Update message delivery request rate and respond to local server. + */ + ANVIL_REMOTE_INCR_MAIL(anvil_remote); + attr_print_plain(client_stream, ATTR_FLAG_NONE, + SEND_ATTR_INT(ANVIL_ATTR_STATUS, ANVIL_STAT_OK), + SEND_ATTR_INT(ANVIL_ATTR_RATE, anvil_remote->mail), + ATTR_TYPE_END); + + /* + * Update peak statistics. + */ + if (anvil_remote->mail > max_mail_rate.value) + ANVIL_MAX_UPDATE(max_mail_rate, anvil_remote->mail, anvil_remote->ident); +} + +/* anvil_remote_rcpt - register recipient address event */ + +static void anvil_remote_rcpt(VSTREAM *client_stream, const char *ident) +{ + ANVIL_REMOTE *anvil_remote; + + /* + * Be prepared for "postfix reload" after "connect". + */ + if ((anvil_remote = + (ANVIL_REMOTE *) htable_find(anvil_remote_map, ident)) == 0) + anvil_remote = anvil_remote_conn_update(client_stream, ident); + + /* + * Update recipient address rate and respond to local server. + */ + ANVIL_REMOTE_INCR_RCPT(anvil_remote); + attr_print_plain(client_stream, ATTR_FLAG_NONE, + SEND_ATTR_INT(ANVIL_ATTR_STATUS, ANVIL_STAT_OK), + SEND_ATTR_INT(ANVIL_ATTR_RATE, anvil_remote->rcpt), + ATTR_TYPE_END); + + /* + * Update peak statistics. + */ + if (anvil_remote->rcpt > max_rcpt_rate.value) + ANVIL_MAX_UPDATE(max_rcpt_rate, anvil_remote->rcpt, anvil_remote->ident); +} + +/* anvil_remote_auth - register auth request event */ + +static void anvil_remote_auth(VSTREAM *client_stream, const char *ident) +{ + ANVIL_REMOTE *anvil_remote; + + /* + * Be prepared for "postfix reload" after "connect". + */ + if ((anvil_remote = + (ANVIL_REMOTE *) htable_find(anvil_remote_map, ident)) == 0) + anvil_remote = anvil_remote_conn_update(client_stream, ident); + + /* + * Update recipient address rate and respond to local server. + */ + ANVIL_REMOTE_INCR_AUTH(anvil_remote); + attr_print_plain(client_stream, ATTR_FLAG_NONE, + SEND_ATTR_INT(ANVIL_ATTR_STATUS, ANVIL_STAT_OK), + SEND_ATTR_INT(ANVIL_ATTR_RATE, anvil_remote->auth), + ATTR_TYPE_END); + + /* + * Update peak statistics. + */ + if (anvil_remote->auth > max_auth_rate.value) + ANVIL_MAX_UPDATE(max_auth_rate, anvil_remote->auth, anvil_remote->ident); +} + +/* anvil_remote_newtls - register newtls event */ + +static void anvil_remote_newtls(VSTREAM *client_stream, const char *ident) +{ + ANVIL_REMOTE *anvil_remote; + + /* + * Be prepared for "postfix reload" after "connect". + */ + if ((anvil_remote = + (ANVIL_REMOTE *) htable_find(anvil_remote_map, ident)) == 0) + anvil_remote = anvil_remote_conn_update(client_stream, ident); + + /* + * Update newtls rate and respond to local server. + */ + ANVIL_REMOTE_INCR_NTLS(anvil_remote); + attr_print_plain(client_stream, ATTR_FLAG_NONE, + SEND_ATTR_INT(ANVIL_ATTR_STATUS, ANVIL_STAT_OK), + SEND_ATTR_INT(ANVIL_ATTR_RATE, anvil_remote->ntls), + ATTR_TYPE_END); + + /* + * Update peak statistics. + */ + if (anvil_remote->ntls > max_ntls_rate.value) + ANVIL_MAX_UPDATE(max_ntls_rate, anvil_remote->ntls, anvil_remote->ident); +} + +/* anvil_remote_newtls_stat - report newtls stats */ + +static void anvil_remote_newtls_stat(VSTREAM *client_stream, const char *ident) +{ + ANVIL_REMOTE *anvil_remote; + int rate; + + /* + * Be prepared for "postfix reload" after "connect". + */ + if ((anvil_remote = + (ANVIL_REMOTE *) htable_find(anvil_remote_map, ident)) == 0) { + rate = 0; + } + + /* + * Do not report stale information. + */ + else { + if (anvil_remote->start != 0 + && anvil_remote->start + var_anvil_time_unit < event_time()) + ANVIL_REMOTE_RSET_RATE(anvil_remote, 0); + rate = anvil_remote->ntls; + } + + /* + * Respond to local server. + */ + attr_print_plain(client_stream, ATTR_FLAG_NONE, + SEND_ATTR_INT(ANVIL_ATTR_STATUS, ANVIL_STAT_OK), + SEND_ATTR_INT(ANVIL_ATTR_RATE, rate), + ATTR_TYPE_END); +} + +/* anvil_remote_disconnect - report disconnect event */ + +static void anvil_remote_disconnect(VSTREAM *client_stream, const char *ident) +{ + ANVIL_REMOTE *anvil_remote; + ANVIL_LOCAL *anvil_local; + const char *myname = "anvil_remote_disconnect"; + + if (msg_verbose) + msg_info("%s fd=%d stream=0x%lx ident=%s", + myname, vstream_fileno(client_stream), + (unsigned long) client_stream, ident); + + /* + * Update local and remote info if this remote connection is listed for + * this local server. + */ + if ((anvil_local = (ANVIL_LOCAL *) vstream_context(client_stream)) != 0 + && (anvil_remote = + (ANVIL_REMOTE *) htable_find(anvil_remote_map, ident)) != 0 + && ANVIL_LOCAL_REMOTE_LINKED(anvil_local, anvil_remote)) { + ANVIL_REMOTE_DROP_ONE(anvil_remote); + ANVIL_LOCAL_DROP_ONE(anvil_local, anvil_remote); + } + if (msg_verbose) + msg_info("%s: anvil_local 0x%lx", + myname, (unsigned long) anvil_local); + + /* + * Respond to the local server. + */ + attr_print_plain(client_stream, ATTR_FLAG_NONE, + SEND_ATTR_INT(ANVIL_ATTR_STATUS, ANVIL_STAT_OK), + ATTR_TYPE_END); +} + +/* anvil_service_done - clean up */ + +static void anvil_service_done(VSTREAM *client_stream, char *unused_service, + char **unused_argv) +{ + ANVIL_LOCAL *anvil_local; + const char *myname = "anvil_service_done"; + + if (msg_verbose) + msg_info("%s fd=%d stream=0x%lx", + myname, vstream_fileno(client_stream), + (unsigned long) client_stream); + + /* + * Look up the local server, and get rid of any remote connection state + * that we still have for this local server. Do not destroy remote client + * status information before it expires. + */ + if ((anvil_local = (ANVIL_LOCAL *) vstream_context(client_stream)) != 0) { + if (msg_verbose) + msg_info("%s: anvil_local 0x%lx", + myname, (unsigned long) anvil_local); + ANVIL_LOCAL_DROP_ALL(client_stream, anvil_local); + myfree((void *) anvil_local); + } else if (msg_verbose) + msg_info("client socket not found for fd=%d", + vstream_fileno(client_stream)); +} + +/* anvil_status_dump - log and reset extreme usage */ + +static void anvil_status_dump(char *unused_name, char **unused_argv) +{ + ANVIL_MAX_RATE_REPORT(max_conn_rate, "connection"); + ANVIL_MAX_COUNT_REPORT(max_conn_count, "connection"); + ANVIL_MAX_RATE_REPORT(max_mail_rate, "message"); + ANVIL_MAX_RATE_REPORT(max_rcpt_rate, "recipient"); + ANVIL_MAX_RATE_REPORT(max_ntls_rate, "newtls"); + ANVIL_MAX_RATE_REPORT(max_auth_rate, "auth"); + + if (max_cache_size > 0) { + msg_info("statistics: max cache size %d at %.15s", + max_cache_size, ctime(&max_cache_time) + 4); + max_cache_size = 0; + } +} + +/* anvil_status_update - log and reset extreme usage periodically */ + +static void anvil_status_update(int unused_event, void *context) +{ + anvil_status_dump((char *) 0, (char **) 0); + event_request_timer(anvil_status_update, context, var_anvil_stat_time); +} + +/* anvil_service - perform service for client */ + +static void anvil_service(VSTREAM *client_stream, char *unused_service, char **argv) +{ + static VSTRING *request; + static VSTRING *ident; + static const ANVIL_REQ_TABLE request_table[] = { + ANVIL_REQ_CONN, anvil_remote_connect, + ANVIL_REQ_MAIL, anvil_remote_mail, + ANVIL_REQ_RCPT, anvil_remote_rcpt, + ANVIL_REQ_NTLS, anvil_remote_newtls, + ANVIL_REQ_DISC, anvil_remote_disconnect, + ANVIL_REQ_NTLS_STAT, anvil_remote_newtls_stat, + ANVIL_REQ_AUTH, anvil_remote_auth, + ANVIL_REQ_LOOKUP, anvil_remote_lookup, + 0, 0, + }; + const ANVIL_REQ_TABLE *rp; + + /* + * Sanity check. This service takes no command-line arguments. + */ + if (argv[0]) + msg_fatal("unexpected command-line argument: %s", argv[0]); + + /* + * Initialize. + */ + if (request == 0) { + request = vstring_alloc(10); + ident = vstring_alloc(10); + } + + /* + * This routine runs whenever a client connects to the socket dedicated + * to the client connection rate management service. All + * connection-management stuff is handled by the common code in + * multi_server.c. + */ + if (msg_verbose) + msg_info("--- start request ---"); + if (attr_scan_plain(client_stream, + ATTR_FLAG_MISSING | ATTR_FLAG_STRICT, + RECV_ATTR_STR(ANVIL_ATTR_REQ, request), + RECV_ATTR_STR(ANVIL_ATTR_IDENT, ident), + ATTR_TYPE_END) == 2) { + for (rp = request_table; /* see below */ ; rp++) { + if (rp->name == 0) { + msg_warn("unrecognized request: \"%s\", ignored", STR(request)); + attr_print_plain(client_stream, ATTR_FLAG_NONE, + SEND_ATTR_INT(ANVIL_ATTR_STATUS, ANVIL_STAT_FAIL), + ATTR_TYPE_END); + break; + } + if (STREQ(rp->name, STR(request))) { + rp->action(client_stream, STR(ident)); + break; + } + } + vstream_fflush(client_stream); + } else { + /* Note: invokes anvil_service_done() */ + multi_server_disconnect(client_stream); + } + if (msg_verbose) + msg_info("--- end request ---"); +} + +/* post_jail_init - post-jail initialization */ + +static void post_jail_init(char *unused_name, char **unused_argv) +{ + + /* + * Dump and reset extreme usage every so often. + */ + event_request_timer(anvil_status_update, (void *) 0, var_anvil_stat_time); + + /* + * Initial client state tables. + */ + anvil_remote_map = htable_create(1000); + + /* + * Do not limit the number of client requests. + */ + var_use_limit = 0; + + /* + * Don't exit before the sampling interval ends. + */ + if (var_idle_limit < var_anvil_time_unit) + var_idle_limit = var_anvil_time_unit; +} + +MAIL_VERSION_STAMP_DECLARE; + +/* post_accept - announce our protocol */ + +static void post_accept(VSTREAM *stream, char *unused_name, + char **unused_argv, HTABLE *unused_table) +{ + + /* + * Announce the protocol. + */ + attr_print_plain(stream, ATTR_FLAG_NONE, + SEND_ATTR_STR(MAIL_ATTR_PROTO, MAIL_ATTR_PROTO_ANVIL), + ATTR_TYPE_END); + (void) vstream_fflush(stream); +} + +/* main - pass control to the multi-threaded skeleton */ + +int main(int argc, char **argv) +{ + static const CONFIG_TIME_TABLE time_table[] = { + VAR_ANVIL_TIME_UNIT, DEF_ANVIL_TIME_UNIT, &var_anvil_time_unit, 1, 0, + VAR_ANVIL_STAT_TIME, DEF_ANVIL_STAT_TIME, &var_anvil_stat_time, 1, 0, + 0, + }; + + /* + * Fingerprint executables and core dumps. + */ + MAIL_VERSION_STAMP_ALLOCATE; + + multi_server_main(argc, argv, anvil_service, + CA_MAIL_SERVER_TIME_TABLE(time_table), + CA_MAIL_SERVER_POST_INIT(post_jail_init), + CA_MAIL_SERVER_POST_ACCEPT(post_accept), + CA_MAIL_SERVER_SOLITARY, + CA_MAIL_SERVER_PRE_DISCONN(anvil_service_done), + CA_MAIL_SERVER_EXIT(anvil_status_dump), + 0); +} |