Use resources for the greylister too.
[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("no cleanup needed");
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                     now = time(0);
197                     tcbdbput(tmp_db, "@@cleanup@@", strlen("@@cleanup@@"), &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("database cleanup finished: database was corrupted, create a new one");
219             unlink(path);
220         } else if (replace) {
221             info("database cleanup finished: before %u entries, after %d entries",
222                    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("database cleanup finished: nothing to do, %u entries", new_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     res->db = awl_db;
247     return &res->db;
248 }
249
250
251 static bool greylist_initialize(greylist_config_t *config,
252                                 const char *directory, const char *prefix)
253 {
254     char path[PATH_MAX];
255
256     if (config->client_awl) {
257         snprintf(path, sizeof(path), "%s/%swhitelist.db", directory, prefix);
258         info("loading auto-whitelist database");
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     info("loading greylist database");
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 char *sender, const char *c_addr,
369                          const char *c_name, const char *rcpt)
370 {
371 #define INCR_AWL                                              \
372     aent.count++;                                             \
373     aent.last = now;                                          \
374     debug("whitelist entry for %.*s updated, count %d",       \
375           c_addrlen, c_addr, aent.count);                     \
376     tcbdbput(awl_db, c_addr, c_addrlen, &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, c_addrlen = strlen(c_addr);
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, c_addrlen, &len);
393         if (res && len == sizeof(aent)) {
394             memcpy(&aent, res, len);
395             debug("client %.*s has a whitelist entry, count is %d",
396                   c_addrlen, c_addr, 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                   c_addrlen, c_addr);
404         }
405
406         /* Whitelist if count is enough.
407          */
408         if (aent.count >= config->client_awl) {
409             debug("client %.*s whitelisted", c_addrlen, c_addr);
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, c_name, cnet, sizeof(cnet)),
424                     config->no_sender ? "" : sender_normalize(sender, sbuf, sizeof(sbuf)),
425                     config->no_recipient ? "" : rcpt);
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);
511           FILTER_PARAM_PARSE_STRING(PREFIX, prefix);
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
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, "path");
580     (void)filter_param_register(type, "prefix");
581     return 0;
582 }
583 module_init(greylist_init)