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