X-Git-Url: http://git.madism.org/?p=apps%2Fpfixtools.git;a=blobdiff_plain;f=postlicyd%2Fgreylist.c;h=6bc65db2a5ce3058cc768e6ffd6be50254c6b072;hp=108b02b246f2dbabd4110e27963771b366545c4a;hb=41b82fa1b8fbad44a1cc7671bd614b016b2c38b8;hpb=7d041574a564b98145fc2235bd3c8676a1689911 diff --git a/postlicyd/greylist.c b/postlicyd/greylist.c index 108b02b..6bc65db 100644 --- a/postlicyd/greylist.c +++ b/postlicyd/greylist.c @@ -37,23 +37,37 @@ #include "common.h" #include "str.h" +#include "resources.h" +static const static_str_t static_cleanup = { "@@cleanup@@", 11 }; typedef struct greylist_config_t { unsigned lookup_by_host : 1; + unsigned no_sender : 1; + unsigned no_recipient : 1; int delay; int retry_window; int client_awl; + int max_age; + int cleanup_period; - TCBDB *awl_db; - TCBDB *obj_db; + char *awlfilename; + TCBDB **awl_db; + char *objfilename; + TCBDB **obj_db; } greylist_config_t; #define GREYLIST_INIT { .lookup_by_host = false, \ + .no_sender = false, \ + .no_recipient = false, \ .delay = 300, \ - .retry_window = 2 * 24 * 2600, \ + .retry_window = 2 * 24 * 3600, \ .client_awl = 5, \ + .max_age = 35 * 3600, \ + .cleanup_period = 86400, \ + .awlfilename = NULL, \ .awl_db = NULL, \ + .objfilename = NULL, \ .obj_db = NULL } struct awl_entry { @@ -66,6 +80,175 @@ struct obj_entry { time_t last; }; +typedef struct greylist_resource_t { + TCBDB *db; +} greylist_resource_t; + + +static void greylist_resource_wipe(greylist_resource_t *res) +{ + if (res->db) { + tcbdbsync(res->db); + tcbdbdel(res->db); + } + p_delete(&res); +} + +static inline bool greylist_check_awlentry(const greylist_config_t *config, + struct awl_entry *aent, time_t now) +{ + return !(config->max_age > 0 && now - aent->last > config->max_age); +} + +static inline bool greylist_check_object(const greylist_config_t *config, + const struct obj_entry *oent, time_t now) +{ + return !((config->max_age > 0 && now - oent->last > config->max_age) + || (oent->last - oent->first < config->delay + && now - oent->last > config->retry_window)); +} + +typedef bool (*db_entry_checker_t)(const greylist_config_t *, const void *, time_t); + +static inline bool greylist_db_need_cleanup(const greylist_config_t *config, TCBDB *db) +{ + int len = 0; + time_t now = time(NULL); + const time_t *last_cleanup = tcbdbget3(db, static_cleanup.str, static_cleanup.len, &len); + if (last_cleanup == NULL) { + debug("No last cleanup time"); + } else { + debug("Last cleanup time %u, (ie %us ago)", + (uint32_t)*last_cleanup, (uint32_t)(now - *last_cleanup)); + } + return last_cleanup == NULL + || len != sizeof(*last_cleanup) + || (now - *last_cleanup) >= config->cleanup_period; +} + +static TCBDB **greylist_db_get(const greylist_config_t *config, const char *path, + size_t entry_len, db_entry_checker_t check) +{ + TCBDB *awl_db, *tmp_db; + time_t now = time(NULL); + + greylist_resource_t *res = resource_get("greylist", path); + if (res == NULL) { + res = p_new(greylist_resource_t, 1); + resource_set("greylist", path, res, (resource_destructor_t)greylist_resource_wipe); + } + + /* Open the database and check if cleanup is needed + */ + awl_db = res->db; + res->db = NULL; + if (awl_db == NULL) { + awl_db = tcbdbnew(); + if (!tcbdbopen(awl_db, path, BDBOWRITER | BDBOCREAT)) { + err("can not open database: %s", tcbdberrmsg(tcbdbecode(awl_db))); + tcbdbdel(awl_db); + resource_release("greylist", path); + return NULL; + } + } + if (!greylist_db_need_cleanup(config, awl_db) || config->max_age <= 0) { + info("%s loaded: no cleanup needed", path); + res->db = awl_db; + return &res->db; + } else { + tcbdbsync(awl_db); + tcbdbdel(awl_db); + } + + /* Rebuild a new database after removing too old entries. + */ + if (config->max_age > 0) { + uint32_t old_count = 0; + uint32_t new_count = 0; + bool replace = false; + bool trashable = false; + char tmppath[PATH_MAX]; + snprintf(tmppath, PATH_MAX, "%s.tmp", path); + + awl_db = tcbdbnew(); + if (tcbdbopen(awl_db, path, BDBOREADER)) { + tmp_db = tcbdbnew(); + if (tcbdbopen(tmp_db, tmppath, BDBOWRITER | BDBOCREAT | BDBOTRUNC)) { + BDBCUR *cur = tcbdbcurnew(awl_db); + TCXSTR *key, *value; + + key = tcxstrnew(); + value = tcxstrnew(); + if (tcbdbcurfirst(cur)) { + replace = true; + do { + tcxstrclear(key); + tcxstrclear(value); + (void)tcbdbcurrec(cur, key, value); + + if ((size_t)tcxstrsize(value) == entry_len + && check(config, tcxstrptr(value), now)) { + tcbdbput(tmp_db, tcxstrptr(key), tcxstrsize(key), + tcxstrptr(value), entry_len); + ++new_count; + } + ++old_count; + } while (tcbdbcurnext(cur)); + tcbdbput(tmp_db, static_cleanup.str, static_cleanup.len, &now, sizeof(now)); + } + tcxstrdel(key); + tcxstrdel(value); + tcbdbcurdel(cur); + tcbdbsync(tmp_db); + } else { + warn("cannot run database cleanup: can't open destination database: %s", + tcbdberrmsg(tcbdbecode(awl_db))); + } + tcbdbdel(tmp_db); + } else { + int ecode = tcbdbecode(awl_db); + warn("can not open database: %s", tcbdberrmsg(ecode)); + trashable = ecode != TCENOPERM && ecode != TCEOPEN && ecode != TCENOFILE && ecode != TCESUCCESS; + } + tcbdbdel(awl_db); + + /** Cleanup successful, replace the old database with the new one. + */ + if (trashable) { + info("%s cleanup: database was corrupted, create a new one", path); + unlink(path); + } else if (replace) { + info("%s cleanup: done in %us, before %u, after %u entries", + path, (uint32_t)(time(0) - now), old_count, new_count); + unlink(path); + if (rename(tmppath, path) != 0) { + UNIXERR("rename"); + resource_release("greylist", path); + return NULL; + } + } else { + unlink(tmppath); + info("%s cleanup: done in %us, nothing to do, %u entries", + path, (uint32_t)(time(0) - now), old_count); + } + } + + /* Effectively open the database. + */ + res->db = NULL; + awl_db = tcbdbnew(); + if (!tcbdbopen(awl_db, path, BDBOWRITER | BDBOCREAT)) { + err("can not open database: %s", tcbdberrmsg(tcbdbecode(awl_db))); + tcbdbdel(awl_db); + resource_release("greylist", path); + return NULL; + } + + info("%s loaded", path); + res->db = awl_db; + return &res->db; +} + static bool greylist_initialize(greylist_config_t *config, const char *directory, const char *prefix) @@ -74,40 +257,40 @@ static bool greylist_initialize(greylist_config_t *config, if (config->client_awl) { snprintf(path, sizeof(path), "%s/%swhitelist.db", directory, prefix); - config->awl_db = tcbdbnew(); - if (!tcbdbopen(config->awl_db, path, BDBOWRITER | BDBOCREAT)) { - tcbdbdel(config->awl_db); - config->awl_db = NULL; + config->awl_db = greylist_db_get(config, path, + sizeof(struct awl_entry), + (db_entry_checker_t)(greylist_check_awlentry)); + if (config->awl_db == NULL) { + return false; } - return false; + config->awlfilename = m_strdup(path); } snprintf(path, sizeof(path), "%s/%sgreylist.db", directory, prefix); - config->obj_db = tcbdbnew(); - if (!tcbdbopen(config->obj_db, path, BDBOWRITER | BDBOCREAT)) { - tcbdbdel(config->obj_db); - config->obj_db = NULL; - if (config->awl_db) { - tcbdbdel(config->awl_db); - config->awl_db = NULL; + config->obj_db = greylist_db_get(config, path, + sizeof(struct obj_entry), + (db_entry_checker_t)(greylist_check_object)); + if (config->obj_db == NULL) { + if (config->awlfilename) { + resource_release("greylist", config->awlfilename); + p_delete(&config->awlfilename); } return false; } + config->objfilename = m_strdup(path); return true; } static void greylist_shutdown(greylist_config_t *config) { - if (config->awl_db) { - tcbdbsync(config->awl_db); - tcbdbdel(config->awl_db); - config->awl_db = NULL; + if (config->awlfilename) { + resource_release("greylist", config->awlfilename); + p_delete(&config->awlfilename); } - if (config->obj_db) { - tcbdbsync(config->obj_db); - tcbdbdel(config->obj_db); - config->obj_db = NULL; + if (config->objfilename) { + resource_release("greylist", config->objfilename); + p_delete(&config->objfilename); } } @@ -180,15 +363,17 @@ static const char *c_net(const greylist_config_t *config, return cnet; } + static bool try_greylist(const greylist_config_t *config, - const char *sender, const char *c_addr, - const char *c_name, const char *rcpt) + const static_str_t *sender, const static_str_t *c_addr, + const static_str_t *c_name, const static_str_t *rcpt) { #define INCR_AWL \ aent.count++; \ aent.last = now; \ - tcbdbput(config->awl_db, c_addr, c_addrlen, &aent, \ - sizeof(aent)); + debug("whitelist entry for %.*s updated, count %d", \ + (int)c_addr->len, c_addr->str, aent.count); \ + tcbdbput(awl_db, c_addr->str, c_addr->len, &aent, sizeof(aent)); char sbuf[BUFSIZ], cnet[64], key[BUFSIZ]; const void *res; @@ -197,19 +382,31 @@ static bool try_greylist(const greylist_config_t *config, struct obj_entry oent = { now, now }; struct awl_entry aent = { 0, 0 }; - int len, klen, c_addrlen = strlen(c_addr); + int len, klen; + TCBDB * const awl_db = config->awl_db ? *(config->awl_db) : NULL; + TCBDB * const obj_db = config->obj_db ? *(config->obj_db) : NULL; /* Auto whitelist clients. */ if (config->client_awl) { - res = tcbdbget3(config->awl_db, c_addr, c_addrlen, &len); + res = tcbdbget3(awl_db, c_addr->str, c_addr->len, &len); if (res && len == sizeof(aent)) { memcpy(&aent, res, len); + debug("client %.*s has a whitelist entry, count is %d", + (int)c_addr->len, c_addr->str, aent.count); + } + + if (!greylist_check_awlentry(config, &aent, now)) { + aent.count = 0; + aent.last = 0; + debug("client %.*s whitelist entry too old", + (int)c_addr->len, c_addr->str); } /* Whitelist if count is enough. */ - if (aent.count > config->client_awl) { + if (aent.count >= config->client_awl) { + debug("client %.*s whitelisted", (int)c_addr->len, c_addr->str); if (now < aent.last + 3600) { INCR_AWL } @@ -223,27 +420,33 @@ static bool try_greylist(const greylist_config_t *config, /* Lookup. */ klen = snprintf(key, sizeof(key), "%s/%s/%s", - c_net(config, c_addr, c_name, cnet, sizeof(cnet)), - sender_normalize(sender, sbuf, sizeof(sbuf)), rcpt); + c_net(config, c_addr->str, c_name->str, cnet, sizeof(cnet)), + config->no_sender ? "" : sender_normalize(sender->str, sbuf, sizeof(sbuf)), + config->no_recipient ? "" : rcpt->str); klen = MIN(klen, ssizeof(key) - 1); - res = tcbdbget3(config->obj_db, key, klen, &len); + res = tcbdbget3(obj_db, key, klen, &len); if (res && len == sizeof(oent)) { memcpy(&oent, res, len); + debug("found a greylist entry for %.*s", klen, key); } /* Discard stored first-seen if it is the first retrial and - * it is beyong the retry window. + * it is beyong the retry window and too old entries. */ - if (oent.last - oent.first < config->delay - && now - oent.first > config->retry_window) { + if (!greylist_check_object(config, &oent, now)) { oent.first = now; + debug("invalid retry for %.*s: %s", klen, key, + (config->max_age > 0 && now - oent.last > config->max_age) ? + "too old entry" + : (oent.last - oent.first < config->delay ? + "retry too early" : "retry too late" )); } /* Update. */ oent.last = now; - tcbdbput(config->obj_db, key, klen, &oent, sizeof(oent)); + tcbdbput(obj_db, key, klen, &oent, sizeof(oent)); /* Auto whitelist clients: * algorithm: @@ -253,6 +456,7 @@ static bool try_greylist(const greylist_config_t *config, * - client whitelisted already ? -> update last-seen timestamp. */ if (oent.first + config->delay < now) { + debug("valid retry for %.*s", klen, key); if (config->client_awl) { INCR_AWL } @@ -296,32 +500,23 @@ static bool greylist_filter_constructor(filter_t *filter) #define PARSE_CHECK(Expr, Str, ...) \ if (!(Expr)) { \ - syslog(LOG_ERR, Str, ##__VA_ARGS__); \ + err(Str, ##__VA_ARGS__); \ greylist_config_delete(&config); \ return false; \ } foreach (filter_param_t *param, filter->params) { switch (param->type) { - case ATK_PATH: - path = param->value; - break; - - case ATK_PREFIX: - prefix = param->value; - break; - - case ATK_LOOKUP_BY_HOST: - config->lookup_by_host = (atoi(param->value) != 0); - break; - - case ATK_RETRY_WINDOW: - config->retry_window = atoi(param->value); - break; - - case ATK_CLIENT_AWL: - config->client_awl = atoi(param->value); - break; + FILTER_PARAM_PARSE_STRING(PATH, path, false); + FILTER_PARAM_PARSE_STRING(PREFIX, prefix, false); + FILTER_PARAM_PARSE_BOOLEAN(LOOKUP_BY_HOST, config->lookup_by_host); + FILTER_PARAM_PARSE_BOOLEAN(NO_SENDER, config->no_sender); + FILTER_PARAM_PARSE_BOOLEAN(NO_RECIPIENT, config->no_recipient); + FILTER_PARAM_PARSE_INT(RETRY_WINDOW, config->retry_window); + FILTER_PARAM_PARSE_INT(CLIENT_AWL, config->client_awl); + FILTER_PARAM_PARSE_INT(DELAY, config->delay); + FILTER_PARAM_PARSE_INT(MAX_AGE, config->max_age); + FILTER_PARAM_PARSE_INT(CLEANUP_PERIOD, config->cleanup_period); default: break; } @@ -343,31 +538,46 @@ static void greylist_filter_destructor(filter_t *filter) } static filter_result_t greylist_filter(const filter_t *filter, - const query_t *query) + const query_t *query, + filter_context_t *context) { const greylist_config_t *config = filter->data; - return try_greylist(config, query->sender, query->client_address, - query->client_name, query->recipient) ? - HTK_MATCH : HTK_FAIL; + if (!config->no_recipient && query->state != SMTP_RCPT) { + warn("greylisting on recipient only works as smtpd_recipient_restrictions"); + return HTK_ABORT; + } + if (!config->no_sender && query->state < SMTP_MAIL) { + warn("greylisting on sender must be performed after (or at) MAIL TO"); + return HTK_ABORT; + } + + return try_greylist(config, &query->sender, &query->client_address, + &query->client_name, &query->recipient) ? + HTK_WHITELIST : HTK_GREYLIST; } static int greylist_init(void) { filter_type_t type = filter_register("greylist", greylist_filter_constructor, greylist_filter_destructor, - greylist_filter); + greylist_filter, NULL, NULL); /* Hooks. */ + (void)filter_hook_register(type, "abort"); (void)filter_hook_register(type, "error"); - (void)filter_hook_register(type, "fail"); - (void)filter_hook_register(type, "match"); + (void)filter_hook_register(type, "greylist"); + (void)filter_hook_register(type, "whitelist"); /* Parameters. */ (void)filter_param_register(type, "lookup_by_host"); + (void)filter_param_register(type, "no_sender"); + (void)filter_param_register(type, "no_recipient"); (void)filter_param_register(type, "delay"); (void)filter_param_register(type, "retry_window"); (void)filter_param_register(type, "client_awl"); + (void)filter_param_register(type, "max_age"); + (void)filter_param_register(type, "cleanup_period"); (void)filter_param_register(type, "path"); (void)filter_param_register(type, "prefix"); return 0;