Use resources for the greylister too.
[apps/pfixtools.git] / postlicyd / greylist.c
index 88ec6c7..a7fedc6 100644 (file)
 
 #include "common.h"
 #include "str.h"
+#include "resources.h"
 
 
 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 * 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 +79,174 @@ 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, "@@cleanup@@", strlen("@@cleanup@@"), &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("no cleanup needed");
+        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));
+                    now = time(0);
+                    tcbdbput(tmp_db, "@@cleanup@@", strlen("@@cleanup@@"), &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("database cleanup finished: database was corrupted, create a new one");
+            unlink(path);
+        } else if (replace) {
+            info("database cleanup finished: before %u entries, after %d entries",
+                   old_count, new_count);
+            unlink(path);
+            if (rename(tmppath, path) != 0) {
+                UNIXERR("rename");
+                resource_release("greylist", path);
+                return NULL;
+            }
+        } else {
+            unlink(tmppath);
+            info("database cleanup finished: nothing to do, %u entries", new_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;
+    }
+
+    res->db = awl_db;
+    return &res->db;
+}
+
 
 static bool greylist_initialize(greylist_config_t *config,
                                 const char *directory, const char *prefix)
@@ -74,40 +255,42 @@ 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;
+        info("loading auto-whitelist database");
+        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;
         }
+        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;
+    info("loading greylist database");
+    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,6 +363,7 @@ 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)
@@ -187,8 +371,9 @@ static bool try_greylist(const greylist_config_t *config,
 #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",       \
+          c_addrlen, c_addr, aent.count);                     \
+    tcbdbput(awl_db, c_addr, c_addrlen, &aent, sizeof(aent));
 
     char sbuf[BUFSIZ], cnet[64], key[BUFSIZ];
     const void *res;
@@ -198,25 +383,36 @@ static bool try_greylist(const greylist_config_t *config,
     struct awl_entry aent = { 0, 0 };
 
     int len, klen, c_addrlen = strlen(c_addr);
+    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, c_addrlen, &len);
         if (res && len == sizeof(aent)) {
             memcpy(&aent, res, len);
+            debug("client %.*s has a whitelist entry, count is %d",
+                  c_addrlen, c_addr, aent.count);
+        }
+
+        if (!greylist_check_awlentry(config, &aent, now)) {
+            aent.count = 0;
+            aent.last  = 0;
+            debug("client %.*s whitelist entry too old",
+                  c_addrlen, c_addr);
         }
 
         /* Whitelist if count is enough.
          */
-        if (aent.count > config->client_awl) {
+        if (aent.count >= config->client_awl) {
+            debug("client %.*s whitelisted", c_addrlen, c_addr);
             if (now < aent.last + 3600) {
                 INCR_AWL
             }
 
             /* OK.
              */
-            //syslog(LOG_INFO, "client whitelisted");
             return true;
         }
     }
@@ -225,26 +421,32 @@ static bool try_greylist(const greylist_config_t *config,
      */
     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);
+                    config->no_sender ? "" : sender_normalize(sender, sbuf, sizeof(sbuf)),
+                    config->no_recipient ? "" : rcpt);
     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:
@@ -254,19 +456,18 @@ 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
         }
 
         /* OK
          */
-        //syslog(LOG_INFO, "client whitelisted");
         return true;
     }
 
     /* DUNNO
      */
-    //syslog(LOG_INFO, "client greylisted");
     return false;
 }
 
@@ -299,32 +500,22 @@ 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);
+          FILTER_PARAM_PARSE_STRING(PREFIX, prefix);
+          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);
 
           default: break;
         }
@@ -346,11 +537,16 @@ 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;
-    if (query->state != SMTP_RCPT) {
-        syslog(LOG_WARNING, "greylisting only works as smtpd_recipient_restrictions");
+    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;
     }
 
@@ -363,7 +559,7 @@ 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");
@@ -374,9 +570,12 @@ static int greylist_init(void)
     /* 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, "path");
     (void)filter_param_register(type, "prefix");
     return 0;