Cleanup start/refresh.
[apps/pfixtools.git] / postlicyd / greylist.c
1 /******************************************************************************/
2 /*          pfixtools: a collection of postfix related tools                  */
3 /*          ~~~~~~~~~                                                         */
4 /*  ________________________________________________________________________  */
5 /*                                                                            */
6 /*  Redistribution and use in source and binary forms, with or without        */
7 /*  modification, are permitted provided that the following conditions        */
8 /*  are met:                                                                  */
9 /*                                                                            */
10 /*  1. Redistributions of source code must retain the above copyright         */
11 /*     notice, this list of conditions and the following disclaimer.          */
12 /*  2. Redistributions in binary form must reproduce the above copyright      */
13 /*     notice, this list of conditions and the following disclaimer in the    */
14 /*     documentation and/or other materials provided with the distribution.   */
15 /*  3. The names of its contributors may not be used to endorse or promote    */
16 /*     products derived from this software without specific prior written     */
17 /*     permission.                                                            */
18 /*                                                                            */
19 /*  THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND   */
20 /*  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE     */
21 /*  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR        */
22 /*  PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS    */
23 /*  BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR    */
24 /*  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF      */
25 /*  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS  */
26 /*  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN   */
27 /*  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)   */
28 /*  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF    */
29 /*  THE POSSIBILITY OF SUCH DAMAGE.                                           */
30 /******************************************************************************/
31
32 /*
33  * Copyright © 2007 Pierre Habouzit
34  */
35
36 #include <tcbdb.h>
37
38 #include "common.h"
39 #include "str.h"
40 #include "resources.h"
41
42
43 typedef struct greylist_config_t {
44     unsigned lookup_by_host : 1;
45     unsigned no_sender      : 1;
46     unsigned no_recipient   : 1;
47     int delay;
48     int retry_window;
49     int client_awl;
50     int max_age;
51     int cleanup_period;
52
53     char  *awlfilename;
54     TCBDB **awl_db;
55     char  *objfilename;
56     TCBDB **obj_db;
57 } greylist_config_t;
58
59 #define GREYLIST_INIT { .lookup_by_host = false,       \
60                         .no_sender = false,            \
61                         .no_recipient = false,         \
62                         .delay = 300,                  \
63                         .retry_window = 2 * 24 * 3600, \
64                         .client_awl = 5,               \
65                         .max_age = 35 * 3600,          \
66                         .cleanup_period = 86400,       \
67                         .awlfilename = NULL,           \
68                         .awl_db = NULL,                \
69                         .objfilename = NULL,           \
70                         .obj_db = NULL }
71
72 struct awl_entry {
73     int32_t count;
74     time_t  last;
75 };
76
77 struct obj_entry {
78     time_t first;
79     time_t last;
80 };
81
82 typedef struct greylist_resource_t {
83     TCBDB *db;
84 } greylist_resource_t;
85
86
87 static void greylist_resource_wipe(greylist_resource_t *res)
88 {
89     if (res->db) {
90         tcbdbsync(res->db);
91         tcbdbdel(res->db);
92     }
93     p_delete(&res);
94 }
95
96 static inline bool greylist_check_awlentry(const greylist_config_t *config,
97                                            struct awl_entry *aent, time_t now)
98 {
99     return !(config->max_age > 0 && now - aent->last > config->max_age);
100 }
101
102 static inline bool greylist_check_object(const greylist_config_t *config,
103                                          const struct obj_entry *oent, time_t now)
104 {
105     return !((config->max_age > 0 && now - oent->last > config->max_age)
106              || (oent->last - oent->first < config->delay
107                  && now - oent->last > config->retry_window));
108 }
109
110 typedef bool (*db_entry_checker_t)(const greylist_config_t *, const void *, time_t);
111
112 static inline bool greylist_db_need_cleanup(const greylist_config_t *config, TCBDB *db)
113 {
114     int len = 0;
115     time_t now = time(NULL);
116     const time_t *last_cleanup = tcbdbget3(db, "@@cleanup@@", strlen("@@cleanup@@"), &len);
117     if (last_cleanup == NULL) {
118         debug("No last cleanup time");
119     } else {
120         debug("Last cleanup time %u, (ie %us ago)",
121               (uint32_t)*last_cleanup, (uint32_t)(now - *last_cleanup));
122     }
123     return last_cleanup == NULL
124         || len != sizeof(*last_cleanup)
125         || (now - *last_cleanup) >= config->cleanup_period;
126 }
127
128 static TCBDB **greylist_db_get(const greylist_config_t *config, const char *path,
129                               size_t entry_len, db_entry_checker_t check)
130 {
131     TCBDB *awl_db, *tmp_db;
132     time_t now = time(NULL);
133
134     greylist_resource_t *res = resource_get("greylist", path);
135     if (res == NULL) {
136         res = p_new(greylist_resource_t, 1);
137         resource_set("greylist", path, res, (resource_destructor_t)greylist_resource_wipe);
138     }
139
140     /* Open the database and check if cleanup is needed
141      */
142     awl_db = res->db;
143     res->db = NULL;
144     if (awl_db == NULL) {
145         awl_db = tcbdbnew();
146         if (!tcbdbopen(awl_db, path, BDBOWRITER | BDBOCREAT)) {
147             err("can not open database: %s", tcbdberrmsg(tcbdbecode(awl_db)));
148             tcbdbdel(awl_db);
149             resource_release("greylist", path);
150             return NULL;
151         }
152     }
153     if (!greylist_db_need_cleanup(config, awl_db) || config->max_age <= 0) {
154         info("%s loaded: no cleanup needed", path);
155         res->db = awl_db;
156         return &res->db;
157     } else {
158         tcbdbsync(awl_db);
159         tcbdbdel(awl_db);
160     }
161
162     /* Rebuild a new database after removing too old entries.
163      */
164     if (config->max_age > 0) {
165         uint32_t old_count = 0;
166         uint32_t new_count = 0;
167         bool replace = false;
168         bool trashable = false;
169         char tmppath[PATH_MAX];
170         snprintf(tmppath, PATH_MAX, "%s.tmp", path);
171
172         awl_db = tcbdbnew();
173         if (tcbdbopen(awl_db, path, BDBOREADER)) {
174             tmp_db = tcbdbnew();
175             if (tcbdbopen(tmp_db, tmppath, BDBOWRITER | BDBOCREAT | BDBOTRUNC)) {
176                 BDBCUR *cur = tcbdbcurnew(awl_db);
177                 TCXSTR *key, *value;
178
179                 key = tcxstrnew();
180                 value = tcxstrnew();
181                 if (tcbdbcurfirst(cur)) {
182                     replace = true;
183                     do {
184                         tcxstrclear(key);
185                         tcxstrclear(value);
186                         (void)tcbdbcurrec(cur, key, value);
187
188                         if ((size_t)tcxstrsize(value) == entry_len
189                             && check(config, tcxstrptr(value), now)) {
190                             tcbdbput(tmp_db, tcxstrptr(key), tcxstrsize(key),
191                                      tcxstrptr(value), entry_len);
192                             ++new_count;
193                         }
194                         ++old_count;
195                     } while (tcbdbcurnext(cur));
196                     tcbdbput(tmp_db, "@@cleanup@@", strlen("@@cleanup@@"), &now, sizeof(now));
197                 }
198                 tcxstrdel(key);
199                 tcxstrdel(value);
200                 tcbdbcurdel(cur);
201                 tcbdbsync(tmp_db);
202             } else {
203                 warn("cannot run database cleanup: can't open destination database: %s",
204                      tcbdberrmsg(tcbdbecode(awl_db)));
205             }
206             tcbdbdel(tmp_db);
207         } else {
208             int ecode = tcbdbecode(awl_db);
209             warn("can not open database: %s", tcbdberrmsg(ecode));
210             trashable = ecode != TCENOPERM && ecode != TCEOPEN && ecode != TCENOFILE && ecode != TCESUCCESS;
211         }
212         tcbdbdel(awl_db);
213
214         /** Cleanup successful, replace the old database with the new one.
215          */
216         if (trashable) {
217             info("%s cleanup: database was corrupted, create a new one", path);
218             unlink(path);
219         } else if (replace) {
220             info("%s cleanup: done in %us, before %u, after %u entries",
221                  path, (uint32_t)(time(0) - now), old_count, new_count);
222             unlink(path);
223             if (rename(tmppath, path) != 0) {
224                 UNIXERR("rename");
225                 resource_release("greylist", path);
226                 return NULL;
227             }
228         } else {
229             unlink(tmppath);
230             info("%s cleanup: done in %us, nothing to do, %u entries",
231                  path, (uint32_t)(time(0) - now), old_count);
232         }
233     }
234
235     /* Effectively open the database.
236      */
237     res->db = NULL;
238     awl_db = tcbdbnew();
239     if (!tcbdbopen(awl_db, path, BDBOWRITER | BDBOCREAT)) {
240         err("can not open database: %s", tcbdberrmsg(tcbdbecode(awl_db)));
241         tcbdbdel(awl_db);
242         resource_release("greylist", path);
243         return NULL;
244     }
245
246     info("%s loaded", path);
247     res->db = awl_db;
248     return &res->db;
249 }
250
251
252 static bool greylist_initialize(greylist_config_t *config,
253                                 const char *directory, const char *prefix)
254 {
255     char path[PATH_MAX];
256
257     if (config->client_awl) {
258         snprintf(path, sizeof(path), "%s/%swhitelist.db", directory, prefix);
259         config->awl_db = greylist_db_get(config, path,
260                                          sizeof(struct awl_entry),
261                                          (db_entry_checker_t)(greylist_check_awlentry));
262         if (config->awl_db == NULL) {
263             return false;
264         }
265         config->awlfilename = m_strdup(path);
266     }
267
268     snprintf(path, sizeof(path), "%s/%sgreylist.db", directory, prefix);
269     config->obj_db = greylist_db_get(config, path,
270                                      sizeof(struct obj_entry),
271                                      (db_entry_checker_t)(greylist_check_object));
272     if (config->obj_db == NULL) {
273         if (config->awlfilename) {
274             resource_release("greylist", config->awlfilename);
275             p_delete(&config->awlfilename);
276         }
277         return false;
278     }
279     config->objfilename = m_strdup(path);
280
281     return true;
282 }
283
284 static void greylist_shutdown(greylist_config_t *config)
285 {
286     if (config->awlfilename) {
287         resource_release("greylist", config->awlfilename);
288         p_delete(&config->awlfilename);
289     }
290     if (config->objfilename) {
291         resource_release("greylist", config->objfilename);
292         p_delete(&config->objfilename);
293     }
294 }
295
296 static const char *sender_normalize(const char *sender, char *buf, int len)
297 {
298     const char *at = strchr(sender, '@');
299     int rpos = 0, wpos = 0, userlen;
300
301     if (!at)
302         return sender;
303
304     /* strip extension used for VERP or alike */
305     userlen = ((char *)memchr(sender, '+', at - sender) ?: at) - sender;
306
307     while (rpos < userlen) {
308         int count = 0;
309
310         while (isdigit(sender[rpos + count]) && rpos + count < userlen)
311             count++;
312         if (count && !isalnum(sender[rpos + count])) {
313             /* replace \<\d+\> with '#' */
314             wpos += m_strputc(buf + wpos, len - wpos, '#');
315             rpos += count;
316             count = 0;
317         }
318         while (isalnum(sender[rpos + count]) && rpos + count < userlen)
319             count++;
320         while (!isalnum(sender[rpos + count]) && rpos + count < userlen)
321             count++;
322         wpos += m_strncpy(buf + wpos, len - wpos, sender + rpos, count);
323         rpos += count;
324     }
325
326     wpos += m_strputc(buf + wpos, len - wpos, '#');
327     wpos += m_strcpy(buf + wpos, len - wpos, at + 1);
328     return buf;
329 }
330
331 static const char *c_net(const greylist_config_t *config,
332                          const char *c_addr, const char *c_name,
333                          char *cnet, int cnetlen)
334 {
335     char ip2[4], ip3[4];
336     const char *dot, *p;
337
338     if (config->lookup_by_host)
339         return c_addr;
340
341     if (!(dot = strchr(c_addr, '.')))
342         return c_addr;
343     if (!(dot = strchr(dot + 1, '.')))
344         return c_addr;
345
346     p = ++dot;
347     if (!(dot = strchr(dot, '.')) || dot - p > 3)
348         return c_addr;
349     m_strncpy(ip2, sizeof(ip2), p, dot - p);
350
351     p = ++dot;
352     if (!(dot = strchr(dot, '.')) || dot - p > 3)
353         return c_addr;
354     m_strncpy(ip3, sizeof(ip3), p, dot - p);
355
356     /* skip if contains the last two ip numbers in the hostname,
357        we assume it's a pool of dialup of a provider */
358     if (strstr(c_name, ip2) && strstr(c_name, ip3))
359         return c_addr;
360
361     m_strncpy(cnet, cnetlen, c_addr, dot - c_addr);
362     return cnet;
363 }
364
365
366 static bool try_greylist(const greylist_config_t *config,
367                          const char *sender, const char *c_addr,
368                          const char *c_name, const char *rcpt)
369 {
370 #define INCR_AWL                                              \
371     aent.count++;                                             \
372     aent.last = now;                                          \
373     debug("whitelist entry for %.*s updated, count %d",       \
374           c_addrlen, c_addr, aent.count);                     \
375     tcbdbput(awl_db, c_addr, c_addrlen, &aent, sizeof(aent));
376
377     char sbuf[BUFSIZ], cnet[64], key[BUFSIZ];
378     const void *res;
379
380     time_t now = time(NULL);
381     struct obj_entry oent = { now, now };
382     struct awl_entry aent = { 0, 0 };
383
384     int len, klen, c_addrlen = strlen(c_addr);
385     TCBDB * const awl_db = config->awl_db ? *(config->awl_db) : NULL;
386     TCBDB * const obj_db = config->obj_db ? *(config->obj_db) : NULL;
387
388     /* Auto whitelist clients.
389      */
390     if (config->client_awl) {
391         res = tcbdbget3(awl_db, c_addr, c_addrlen, &len);
392         if (res && len == sizeof(aent)) {
393             memcpy(&aent, res, len);
394             debug("client %.*s has a whitelist entry, count is %d",
395                   c_addrlen, c_addr, aent.count);
396         }
397
398         if (!greylist_check_awlentry(config, &aent, now)) {
399             aent.count = 0;
400             aent.last  = 0;
401             debug("client %.*s whitelist entry too old",
402                   c_addrlen, c_addr);
403         }
404
405         /* Whitelist if count is enough.
406          */
407         if (aent.count >= config->client_awl) {
408             debug("client %.*s whitelisted", c_addrlen, c_addr);
409             if (now < aent.last + 3600) {
410                 INCR_AWL
411             }
412
413             /* OK.
414              */
415             return true;
416         }
417     }
418
419     /* Lookup.
420      */
421     klen = snprintf(key, sizeof(key), "%s/%s/%s",
422                     c_net(config, c_addr, c_name, cnet, sizeof(cnet)),
423                     config->no_sender ? "" : sender_normalize(sender, sbuf, sizeof(sbuf)),
424                     config->no_recipient ? "" : rcpt);
425     klen = MIN(klen, ssizeof(key) - 1);
426
427     res = tcbdbget3(obj_db, key, klen, &len);
428     if (res && len == sizeof(oent)) {
429         memcpy(&oent, res, len);
430         debug("found a greylist entry for %.*s", klen, key);
431     }
432
433     /* Discard stored first-seen if it is the first retrial and
434      * it is beyong the retry window and too old entries.
435      */
436     if (!greylist_check_object(config, &oent, now)) {
437         oent.first = now;
438         debug("invalid retry for %.*s: %s", klen, key,
439               (config->max_age > 0 && now - oent.last > config->max_age) ?
440                   "too old entry"
441                 : (oent.last - oent.first < config->delay ?
442                   "retry too early" : "retry too late" ));
443     }
444
445     /* Update.
446      */
447     oent.last = now;
448     tcbdbput(obj_db, key, klen, &oent, sizeof(oent));
449
450     /* Auto whitelist clients:
451      *  algorithm:
452      *    - on successful entry in the greylist db of a triplet:
453      *        - client not whitelisted yet ? -> increase count
454      *                                       -> withelist if count > limit
455      *        - client whitelisted already ? -> update last-seen timestamp.
456      */
457     if (oent.first + config->delay < now) {
458         debug("valid retry for %.*s", klen, key);
459         if (config->client_awl) {
460             INCR_AWL
461         }
462
463         /* OK
464          */
465         return true;
466     }
467
468     /* DUNNO
469      */
470     return false;
471 }
472
473
474 /* postlicyd filter declaration */
475
476 #include "filter.h"
477
478 static greylist_config_t *greylist_config_new(void)
479 {
480     const greylist_config_t g = GREYLIST_INIT;
481     greylist_config_t *config = p_new(greylist_config_t, 1);
482     *config = g;
483     return config;
484 }
485
486 static void greylist_config_delete(greylist_config_t **config)
487 {
488     if (*config) {
489         greylist_shutdown(*config);
490         p_delete(config);
491     }
492 }
493
494 static bool greylist_filter_constructor(filter_t *filter)
495 {
496     const char* path   = NULL;
497     const char* prefix = NULL;
498     greylist_config_t *config = greylist_config_new();
499
500 #define PARSE_CHECK(Expr, Str, ...)                                            \
501     if (!(Expr)) {                                                             \
502         err(Str, ##__VA_ARGS__);                                               \
503         greylist_config_delete(&config);                                       \
504         return false;                                                          \
505     }
506
507     foreach (filter_param_t *param, filter->params) {
508         switch (param->type) {
509           FILTER_PARAM_PARSE_STRING(PATH,   path);
510           FILTER_PARAM_PARSE_STRING(PREFIX, prefix);
511           FILTER_PARAM_PARSE_BOOLEAN(LOOKUP_BY_HOST, config->lookup_by_host);
512           FILTER_PARAM_PARSE_BOOLEAN(NO_SENDER, config->no_sender);
513           FILTER_PARAM_PARSE_BOOLEAN(NO_RECIPIENT, config->no_recipient);
514           FILTER_PARAM_PARSE_INT(RETRY_WINDOW, config->retry_window);
515           FILTER_PARAM_PARSE_INT(CLIENT_AWL,   config->client_awl);
516           FILTER_PARAM_PARSE_INT(DELAY,        config->delay);
517           FILTER_PARAM_PARSE_INT(MAX_AGE,      config->max_age);
518           FILTER_PARAM_PARSE_INT(CLEANUP_PERIOD, config->cleanup_period);
519
520           default: break;
521         }
522     }}
523
524     PARSE_CHECK(path, "path to greylist db not given");
525     PARSE_CHECK(greylist_initialize(config, path, prefix ? prefix : ""),
526                 "can not load greylist database");
527
528     filter->data = config;
529     return true;
530 }
531
532 static void greylist_filter_destructor(filter_t *filter)
533 {
534     greylist_config_t *data = filter->data;
535     greylist_config_delete(&data);
536     filter->data = data;
537 }
538
539 static filter_result_t greylist_filter(const filter_t *filter,
540                                        const query_t *query,
541                                        filter_context_t *context)
542 {
543     const greylist_config_t *config = filter->data;
544     if (!config->no_recipient && query->state != SMTP_RCPT) {
545         warn("greylisting on recipient only works as smtpd_recipient_restrictions");
546         return HTK_ABORT;
547     }
548     if (!config->no_sender && query->state < SMTP_MAIL) {
549         warn("greylisting on sender must be performed after (or at) MAIL TO");
550         return HTK_ABORT;
551     }
552
553     return try_greylist(config, query->sender, query->client_address,
554                         query->client_name, query->recipient) ?
555            HTK_WHITELIST : HTK_GREYLIST;
556 }
557
558 static int greylist_init(void)
559 {
560     filter_type_t type =  filter_register("greylist", greylist_filter_constructor,
561                                           greylist_filter_destructor,
562                                           greylist_filter, NULL, NULL);
563     /* Hooks.
564      */
565     (void)filter_hook_register(type, "abort");
566     (void)filter_hook_register(type, "error");
567     (void)filter_hook_register(type, "greylist");
568     (void)filter_hook_register(type, "whitelist");
569
570     /* Parameters.
571      */
572     (void)filter_param_register(type, "lookup_by_host");
573     (void)filter_param_register(type, "no_sender");
574     (void)filter_param_register(type, "no_recipient");
575     (void)filter_param_register(type, "delay");
576     (void)filter_param_register(type, "retry_window");
577     (void)filter_param_register(type, "client_awl");
578     (void)filter_param_register(type, "max_age");
579     (void)filter_param_register(type, "cleanup_period");
580     (void)filter_param_register(type, "path");
581     (void)filter_param_register(type, "prefix");
582     return 0;
583 }
584 module_init(greylist_init)