Implements greylist database cleanup.
[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
41
42 typedef struct greylist_config_t {
43     unsigned lookup_by_host : 1;
44     int delay;
45     int retry_window;
46     int client_awl;
47     int max_age;
48
49     TCBDB *awl_db;
50     TCBDB *obj_db;
51 } greylist_config_t;
52
53 #define GREYLIST_INIT { .lookup_by_host = false,       \
54                         .delay = 300,                  \
55                         .retry_window = 2 * 24 * 3600, \
56                         .client_awl = 5,               \
57                         .max_age = 35 * 3600,          \
58                         .awl_db = NULL,                \
59                         .obj_db = NULL }
60
61 struct awl_entry {
62     int32_t count;
63     time_t  last;
64 };
65
66 struct obj_entry {
67     time_t first;
68     time_t last;
69 };
70
71 static inline bool greylist_check_awlentry(const greylist_config_t *config,
72                                            struct awl_entry *aent, time_t now)
73 {
74     return !(config->max_age > 0 && now - aent->last > config->max_age);
75 }
76
77 static inline bool greylist_check_object(const greylist_config_t *config,
78                                          const struct obj_entry *oent, time_t now)
79 {
80     return !((config->max_age > 0 && now - oent->last > config->max_age)
81              || (oent->last - oent->first < config->delay
82                  && now - oent->last > config->retry_window));
83 }
84
85 typedef bool (*db_entry_checker_t)(const greylist_config_t *, const void *, time_t);
86
87 static TCBDB *greylist_db_get(const greylist_config_t *config,
88                               const char *path, bool cleanup,
89                               size_t entry_len, db_entry_checker_t check)
90 {
91     TCBDB *awl_db, *tmp_db;
92     time_t now = time(NULL);
93
94     /* Rebuild a new database after removing too old entries.
95      */
96     if (cleanup && config->max_age > 0) {
97         uint32_t old_count = 0;
98         uint32_t new_count = 0;
99         bool replace = false;
100         char tmppath[PATH_MAX];
101         snprintf(tmppath, PATH_MAX, "%s.tmp", path);
102
103         syslog(LOG_INFO, "database cleanup started");
104         awl_db = tcbdbnew();
105         if (tcbdbopen(awl_db, path, BDBOREADER)) {
106             tmp_db = tcbdbnew();
107             if (tcbdbopen(tmp_db, tmppath, BDBOWRITER | BDBOTRUNC)) {
108                 BDBCUR *cur = tcbdbcurnew(awl_db);
109                 TCXSTR *key, *value;
110                 key = tcxstrnew();
111                 value = tcxstrnew();
112                 if (tcbdbcurfirst(cur)) {
113                     replace = true;
114                     do {
115                         tcxstrclear(key);
116                         tcxstrclear(value);
117                         (void)tcbdbcurrec(cur, key, value);
118
119                         if ((size_t)tcxstrsize(value) == entry_len
120                             && check(config, tcxstrptr(value), now)) {
121                             tcbdbput(tmp_db, tcxstrptr(key), tcxstrsize(key),
122                                      tcxstrptr(value), entry_len);
123                             ++new_count;
124                         }
125                         ++old_count;
126                     } while (tcbdbcurnext(cur));
127                 }
128                 tcxstrdel(key);
129                 tcxstrdel(value);
130                 tcbdbcurdel(cur);
131                 tcbdbsync(tmp_db);
132             }
133             tcbdbdel(tmp_db);
134         }
135         tcbdbdel(awl_db);
136
137         /** Cleanup successful, replace the old database with the new one.
138          */
139         if (replace) {
140             unlink(path);
141             if (rename(tmppath, path) != 0) {
142                 UNIXERR("rename");
143                 return NULL;
144             }
145             syslog(LOG_INFO, "database cleanup stat: before %u entries, after %d entries",
146                    old_count, new_count);
147         }
148         syslog(LOG_INFO, "database cleanup finished");
149     }
150
151     /* Effectively open the database.
152      */
153     awl_db = tcbdbnew();
154     if (!tcbdbopen(awl_db, path, BDBOWRITER | BDBOCREAT)) {
155         tcbdbdel(awl_db);
156         return NULL;
157     }
158     return awl_db;
159 }
160
161
162 static bool greylist_initialize(greylist_config_t *config,
163                                 const char *directory, const char *prefix)
164 {
165     char path[PATH_MAX];
166
167     if (config->client_awl) {
168         snprintf(path, sizeof(path), "%s/%swhitelist.db", directory, prefix);
169         config->awl_db = greylist_db_get(config, path, true,
170                                          sizeof(struct awl_entry),
171                                          (db_entry_checker_t)(greylist_check_awlentry));
172         if (config->awl_db == NULL) {
173             return false;
174         }
175     }
176
177     snprintf(path, sizeof(path), "%s/%sgreylist.db", directory, prefix);
178     config->obj_db = greylist_db_get(config, path, true,
179                                      sizeof(struct obj_entry),
180                                      (db_entry_checker_t)(greylist_check_object));
181     if (config->obj_db == NULL) {
182         if (config->awl_db) {
183             tcbdbdel(config->awl_db);
184             config->awl_db = NULL;
185         }
186         return false;
187     }
188
189     return true;
190 }
191
192 static void greylist_shutdown(greylist_config_t *config)
193 {
194     if (config->awl_db) {
195         tcbdbsync(config->awl_db);
196         tcbdbdel(config->awl_db);
197         config->awl_db = NULL;
198     }
199     if (config->obj_db) {
200         tcbdbsync(config->obj_db);
201         tcbdbdel(config->obj_db);
202         config->obj_db = NULL;
203     }
204 }
205
206 static const char *sender_normalize(const char *sender, char *buf, int len)
207 {
208     const char *at = strchr(sender, '@');
209     int rpos = 0, wpos = 0, userlen;
210
211     if (!at)
212         return sender;
213
214     /* strip extension used for VERP or alike */
215     userlen = ((char *)memchr(sender, '+', at - sender) ?: at) - sender;
216
217     while (rpos < userlen) {
218         int count = 0;
219
220         while (isdigit(sender[rpos + count]) && rpos + count < userlen)
221             count++;
222         if (count && !isalnum(sender[rpos + count])) {
223             /* replace \<\d+\> with '#' */
224             wpos += m_strputc(buf + wpos, len - wpos, '#');
225             rpos += count;
226             count = 0;
227         }
228         while (isalnum(sender[rpos + count]) && rpos + count < userlen)
229             count++;
230         while (!isalnum(sender[rpos + count]) && rpos + count < userlen)
231             count++;
232         wpos += m_strncpy(buf + wpos, len - wpos, sender + rpos, count);
233         rpos += count;
234     }
235
236     wpos += m_strputc(buf + wpos, len - wpos, '#');
237     wpos += m_strcpy(buf + wpos, len - wpos, at + 1);
238     return buf;
239 }
240
241 static const char *c_net(const greylist_config_t *config,
242                          const char *c_addr, const char *c_name,
243                          char *cnet, int cnetlen)
244 {
245     char ip2[4], ip3[4];
246     const char *dot, *p;
247
248     if (config->lookup_by_host)
249         return c_addr;
250
251     if (!(dot = strchr(c_addr, '.')))
252         return c_addr;
253     if (!(dot = strchr(dot + 1, '.')))
254         return c_addr;
255
256     p = ++dot;
257     if (!(dot = strchr(dot, '.')) || dot - p > 3)
258         return c_addr;
259     m_strncpy(ip2, sizeof(ip2), p, dot - p);
260
261     p = ++dot;
262     if (!(dot = strchr(dot, '.')) || dot - p > 3)
263         return c_addr;
264     m_strncpy(ip3, sizeof(ip3), p, dot - p);
265
266     /* skip if contains the last two ip numbers in the hostname,
267        we assume it's a pool of dialup of a provider */
268     if (strstr(c_name, ip2) && strstr(c_name, ip3))
269         return c_addr;
270
271     m_strncpy(cnet, cnetlen, c_addr, dot - c_addr);
272     return cnet;
273 }
274
275
276 static bool try_greylist(const greylist_config_t *config,
277                          const char *sender, const char *c_addr,
278                          const char *c_name, const char *rcpt)
279 {
280 #define INCR_AWL                                              \
281     aent.count++;                                             \
282     aent.last = now;                                          \
283     tcbdbput(config->awl_db, c_addr, c_addrlen, &aent,        \
284              sizeof(aent));
285
286     char sbuf[BUFSIZ], cnet[64], key[BUFSIZ];
287     const void *res;
288
289     time_t now = time(NULL);
290     struct obj_entry oent = { now, now };
291     struct awl_entry aent = { 0, 0 };
292
293     int len, klen, c_addrlen = strlen(c_addr);
294
295     /* Auto whitelist clients.
296      */
297     if (config->client_awl) {
298         res = tcbdbget3(config->awl_db, c_addr, c_addrlen, &len);
299         if (res && len == sizeof(aent)) {
300             memcpy(&aent, res, len);
301         }
302
303         if (!greylist_check_awlentry(config, &aent, now)) {
304             aent.count = 0;
305             aent.last  = 0;
306         }
307
308         /* Whitelist if count is enough.
309          */
310         if (aent.count >= config->client_awl) {
311             if (now < aent.last + 3600) {
312                 INCR_AWL
313             }
314
315             /* OK.
316              */
317             //syslog(LOG_INFO, "client whitelisted");
318             return true;
319         }
320     }
321
322     /* Lookup.
323      */
324     klen = snprintf(key, sizeof(key), "%s/%s/%s",
325                     c_net(config, c_addr, c_name, cnet, sizeof(cnet)),
326                     sender_normalize(sender, sbuf, sizeof(sbuf)), rcpt);
327     klen = MIN(klen, ssizeof(key) - 1);
328
329     res = tcbdbget3(config->obj_db, key, klen, &len);
330     if (res && len == sizeof(oent)) {
331         memcpy(&oent, res, len);
332         greylist_check_object(config, &oent, now);
333     }
334
335     /* Discard stored first-seen if it is the first retrial and
336      * it is beyong the retry window and too old entries.
337      */
338     if (!greylist_check_object(config, &oent, now)) {
339         oent.first = now;
340     }
341
342     /* Update.
343      */
344     oent.last = now;
345     tcbdbput(config->obj_db, key, klen, &oent, sizeof(oent));
346
347     /* Auto whitelist clients:
348      *  algorithm:
349      *    - on successful entry in the greylist db of a triplet:
350      *        - client not whitelisted yet ? -> increase count
351      *                                       -> withelist if count > limit
352      *        - client whitelisted already ? -> update last-seen timestamp.
353      */
354     if (oent.first + config->delay < now) {
355         if (config->client_awl) {
356             INCR_AWL
357         }
358
359         /* OK
360          */
361         //syslog(LOG_INFO, "client whitelisted");
362         return true;
363     }
364
365     /* DUNNO
366      */
367     //syslog(LOG_INFO, "client greylisted");
368     return false;
369 }
370
371
372 /* postlicyd filter declaration */
373
374 #include "filter.h"
375
376 static greylist_config_t *greylist_config_new(void)
377 {
378     const greylist_config_t g = GREYLIST_INIT;
379     greylist_config_t *config = p_new(greylist_config_t, 1);
380     *config = g;
381     return config;
382 }
383
384 static void greylist_config_delete(greylist_config_t **config)
385 {
386     if (*config) {
387         greylist_shutdown(*config);
388         p_delete(config);
389     }
390 }
391
392 static bool greylist_filter_constructor(filter_t *filter)
393 {
394     const char* path   = NULL;
395     const char* prefix = NULL;
396     greylist_config_t *config = greylist_config_new();
397
398 #define PARSE_CHECK(Expr, Str, ...)                                            \
399     if (!(Expr)) {                                                             \
400         syslog(LOG_ERR, Str, ##__VA_ARGS__);                                   \
401         greylist_config_delete(&config);                                       \
402         return false;                                                          \
403     }
404
405     foreach (filter_param_t *param, filter->params) {
406         switch (param->type) {
407           FILTER_PARAM_PARSE_STRING(PATH,   path);
408           FILTER_PARAM_PARSE_STRING(PREFIX, prefix);
409           FILTER_PARAM_PARSE_BOOLEAN(LOOKUP_BY_HOST, config->lookup_by_host);
410           FILTER_PARAM_PARSE_INT(RETRY_WINDOW, config->retry_window);
411           FILTER_PARAM_PARSE_INT(CLIENT_AWL,   config->client_awl);
412           FILTER_PARAM_PARSE_INT(DELAY,        config->delay);
413           FILTER_PARAM_PARSE_INT(MAX_AGE,      config->max_age);
414
415           default: break;
416         }
417     }}
418
419     PARSE_CHECK(path, "path to greylist db not given");
420     PARSE_CHECK(greylist_initialize(config, path, prefix ? prefix : ""),
421                 "can not load greylist database");
422
423     filter->data = config;
424     return true;
425 }
426
427 static void greylist_filter_destructor(filter_t *filter)
428 {
429     greylist_config_t *data = filter->data;
430     greylist_config_delete(&data);
431     filter->data = data;
432 }
433
434 static filter_result_t greylist_filter(const filter_t *filter,
435                                        const query_t *query)
436 {
437     const greylist_config_t *config = filter->data;
438     if (query->state != SMTP_RCPT) {
439         syslog(LOG_WARNING, "greylisting only works as smtpd_recipient_restrictions");
440         return HTK_ABORT;
441     }
442
443     return try_greylist(config, query->sender, query->client_address,
444                         query->client_name, query->recipient) ?
445            HTK_WHITELIST : HTK_GREYLIST;
446 }
447
448 static int greylist_init(void)
449 {
450     filter_type_t type =  filter_register("greylist", greylist_filter_constructor,
451                                           greylist_filter_destructor,
452                                           greylist_filter);
453     /* Hooks.
454      */
455     (void)filter_hook_register(type, "abort");
456     (void)filter_hook_register(type, "error");
457     (void)filter_hook_register(type, "greylist");
458     (void)filter_hook_register(type, "whitelist");
459
460     /* Parameters.
461      */
462     (void)filter_param_register(type, "lookup_by_host");
463     (void)filter_param_register(type, "delay");
464     (void)filter_param_register(type, "retry_window");
465     (void)filter_param_register(type, "client_awl");
466     (void)filter_param_register(type, "max_age");
467     (void)filter_param_register(type, "path");
468     (void)filter_param_register(type, "prefix");
469     return 0;
470 }
471 module_init(greylist_init)