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