fe5188c6c4443d60b46fb07ca529c2694019badc
[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
72 static bool greylist_initialize(greylist_config_t *config,
73                                 const char *directory, const char *prefix)
74 {
75     char path[PATH_MAX];
76
77     if (config->client_awl) {
78         snprintf(path, sizeof(path), "%s/%swhitelist.db", directory, prefix);
79         config->awl_db = tcbdbnew();
80         if (!tcbdbopen(config->awl_db, path, BDBOWRITER | BDBOCREAT)) {
81             tcbdbdel(config->awl_db);
82             config->awl_db = NULL;
83             return false;
84         }
85     }
86
87     snprintf(path, sizeof(path), "%s/%sgreylist.db", directory, prefix);
88     config->obj_db = tcbdbnew();
89     if (!tcbdbopen(config->obj_db, path, BDBOWRITER | BDBOCREAT)) {
90         tcbdbdel(config->obj_db);
91         config->obj_db = NULL;
92         if (config->awl_db) {
93             tcbdbdel(config->awl_db);
94             config->awl_db = NULL;
95         }
96         return false;
97     }
98
99     return true;
100 }
101
102 static void greylist_shutdown(greylist_config_t *config)
103 {
104     if (config->awl_db) {
105         tcbdbsync(config->awl_db);
106         tcbdbdel(config->awl_db);
107         config->awl_db = NULL;
108     }
109     if (config->obj_db) {
110         tcbdbsync(config->obj_db);
111         tcbdbdel(config->obj_db);
112         config->obj_db = NULL;
113     }
114 }
115
116 static const char *sender_normalize(const char *sender, char *buf, int len)
117 {
118     const char *at = strchr(sender, '@');
119     int rpos = 0, wpos = 0, userlen;
120
121     if (!at)
122         return sender;
123
124     /* strip extension used for VERP or alike */
125     userlen = ((char *)memchr(sender, '+', at - sender) ?: at) - sender;
126
127     while (rpos < userlen) {
128         int count = 0;
129
130         while (isdigit(sender[rpos + count]) && rpos + count < userlen)
131             count++;
132         if (count && !isalnum(sender[rpos + count])) {
133             /* replace \<\d+\> with '#' */
134             wpos += m_strputc(buf + wpos, len - wpos, '#');
135             rpos += count;
136             count = 0;
137         }
138         while (isalnum(sender[rpos + count]) && rpos + count < userlen)
139             count++;
140         while (!isalnum(sender[rpos + count]) && rpos + count < userlen)
141             count++;
142         wpos += m_strncpy(buf + wpos, len - wpos, sender + rpos, count);
143         rpos += count;
144     }
145
146     wpos += m_strputc(buf + wpos, len - wpos, '#');
147     wpos += m_strcpy(buf + wpos, len - wpos, at + 1);
148     return buf;
149 }
150
151 static const char *c_net(const greylist_config_t *config,
152                          const char *c_addr, const char *c_name,
153                          char *cnet, int cnetlen)
154 {
155     char ip2[4], ip3[4];
156     const char *dot, *p;
157
158     if (config->lookup_by_host)
159         return c_addr;
160
161     if (!(dot = strchr(c_addr, '.')))
162         return c_addr;
163     if (!(dot = strchr(dot + 1, '.')))
164         return c_addr;
165
166     p = ++dot;
167     if (!(dot = strchr(dot, '.')) || dot - p > 3)
168         return c_addr;
169     m_strncpy(ip2, sizeof(ip2), p, dot - p);
170
171     p = ++dot;
172     if (!(dot = strchr(dot, '.')) || dot - p > 3)
173         return c_addr;
174     m_strncpy(ip3, sizeof(ip3), p, dot - p);
175
176     /* skip if contains the last two ip numbers in the hostname,
177        we assume it's a pool of dialup of a provider */
178     if (strstr(c_name, ip2) && strstr(c_name, ip3))
179         return c_addr;
180
181     m_strncpy(cnet, cnetlen, c_addr, dot - c_addr);
182     return cnet;
183 }
184
185 static inline bool greylist_check_awlentry(const greylist_config_t *config,
186                                            struct awl_entry *aent, time_t now)
187 {
188     return !(now - aent->last > config->max_age);
189 }
190
191 static inline bool greylist_check_object(const greylist_config_t *config,
192                                          const struct obj_entry *oent, time_t now)
193 {
194     return !(now - oent->last > config->max_age
195              || (oent->last - oent->first < config->delay
196                  && now - oent->last > config->retry_window));
197 }
198
199 static bool try_greylist(const greylist_config_t *config,
200                          const char *sender, const char *c_addr,
201                          const char *c_name, const char *rcpt)
202 {
203 #define INCR_AWL                                              \
204     aent.count++;                                             \
205     aent.last = now;                                          \
206     tcbdbput(config->awl_db, c_addr, c_addrlen, &aent,        \
207              sizeof(aent));
208
209     char sbuf[BUFSIZ], cnet[64], key[BUFSIZ];
210     const void *res;
211
212     time_t now = time(NULL);
213     struct obj_entry oent = { now, now };
214     struct awl_entry aent = { 0, 0 };
215
216     int len, klen, c_addrlen = strlen(c_addr);
217
218     /* Auto whitelist clients.
219      */
220     if (config->client_awl) {
221         res = tcbdbget3(config->awl_db, c_addr, c_addrlen, &len);
222         if (res && len == sizeof(aent)) {
223             memcpy(&aent, res, len);
224         }
225
226         if (!greylist_check_awlentry(config, &aent, now)) {
227             aent.count = 0;
228             aent.last  = 0;
229         }
230
231         /* Whitelist if count is enough.
232          */
233         if (aent.count >= config->client_awl) {
234             if (now < aent.last + 3600) {
235                 INCR_AWL
236             }
237
238             /* OK.
239              */
240             //syslog(LOG_INFO, "client whitelisted");
241             return true;
242         }
243     }
244
245     /* Lookup.
246      */
247     klen = snprintf(key, sizeof(key), "%s/%s/%s",
248                     c_net(config, c_addr, c_name, cnet, sizeof(cnet)),
249                     sender_normalize(sender, sbuf, sizeof(sbuf)), rcpt);
250     klen = MIN(klen, ssizeof(key) - 1);
251
252     res = tcbdbget3(config->obj_db, key, klen, &len);
253     if (res && len == sizeof(oent)) {
254         memcpy(&oent, res, len);
255         greylist_check_object(config, &oent, now);
256     }
257
258     /* Discard stored first-seen if it is the first retrial and
259      * it is beyong the retry window and too old entries.
260      */
261     if (!greylist_check_object(config, &oent, now)) {
262         oent.first = now;
263     }
264
265     /* Update.
266      */
267     oent.last = now;
268     tcbdbput(config->obj_db, key, klen, &oent, sizeof(oent));
269
270     /* Auto whitelist clients:
271      *  algorithm:
272      *    - on successful entry in the greylist db of a triplet:
273      *        - client not whitelisted yet ? -> increase count
274      *                                       -> withelist if count > limit
275      *        - client whitelisted already ? -> update last-seen timestamp.
276      */
277     if (oent.first + config->delay < now) {
278         if (config->client_awl) {
279             INCR_AWL
280         }
281
282         /* OK
283          */
284         //syslog(LOG_INFO, "client whitelisted");
285         return true;
286     }
287
288     /* DUNNO
289      */
290     //syslog(LOG_INFO, "client greylisted");
291     return false;
292 }
293
294
295 /* postlicyd filter declaration */
296
297 #include "filter.h"
298
299 static greylist_config_t *greylist_config_new(void)
300 {
301     const greylist_config_t g = GREYLIST_INIT;
302     greylist_config_t *config = p_new(greylist_config_t, 1);
303     *config = g;
304     return config;
305 }
306
307 static void greylist_config_delete(greylist_config_t **config)
308 {
309     if (*config) {
310         greylist_shutdown(*config);
311         p_delete(config);
312     }
313 }
314
315 static bool greylist_filter_constructor(filter_t *filter)
316 {
317     const char* path   = NULL;
318     const char* prefix = NULL;
319     greylist_config_t *config = greylist_config_new();
320
321 #define PARSE_CHECK(Expr, Str, ...)                                            \
322     if (!(Expr)) {                                                             \
323         syslog(LOG_ERR, Str, ##__VA_ARGS__);                                   \
324         greylist_config_delete(&config);                                       \
325         return false;                                                          \
326     }
327
328     foreach (filter_param_t *param, filter->params) {
329         switch (param->type) {
330           FILTER_PARAM_PARSE_STRING(PATH,   path);
331           FILTER_PARAM_PARSE_STRING(PREFIX, prefix);
332           FILTER_PARAM_PARSE_BOOLEAN(LOOKUP_BY_HOST, config->lookup_by_host);
333           FILTER_PARAM_PARSE_INT(RETRY_WINDOW, config->retry_window);
334           FILTER_PARAM_PARSE_INT(CLIENT_AWL,   config->client_awl);
335           FILTER_PARAM_PARSE_INT(DELAY,        config->delay);
336           FILTER_PARAM_PARSE_INT(MAX_AGE,      config->max_age);
337
338           default: break;
339         }
340     }}
341
342     PARSE_CHECK(path, "path to greylist db not given");
343     PARSE_CHECK(greylist_initialize(config, path, prefix ? prefix : ""),
344                 "can not load greylist database");
345
346     filter->data = config;
347     return true;
348 }
349
350 static void greylist_filter_destructor(filter_t *filter)
351 {
352     greylist_config_t *data = filter->data;
353     greylist_config_delete(&data);
354     filter->data = data;
355 }
356
357 static filter_result_t greylist_filter(const filter_t *filter,
358                                        const query_t *query)
359 {
360     const greylist_config_t *config = filter->data;
361     if (query->state != SMTP_RCPT) {
362         syslog(LOG_WARNING, "greylisting only works as smtpd_recipient_restrictions");
363         return HTK_ABORT;
364     }
365
366     return try_greylist(config, query->sender, query->client_address,
367                         query->client_name, query->recipient) ?
368            HTK_WHITELIST : HTK_GREYLIST;
369 }
370
371 static int greylist_init(void)
372 {
373     filter_type_t type =  filter_register("greylist", greylist_filter_constructor,
374                                           greylist_filter_destructor,
375                                           greylist_filter);
376     /* Hooks.
377      */
378     (void)filter_hook_register(type, "abort");
379     (void)filter_hook_register(type, "error");
380     (void)filter_hook_register(type, "greylist");
381     (void)filter_hook_register(type, "whitelist");
382
383     /* Parameters.
384      */
385     (void)filter_param_register(type, "lookup_by_host");
386     (void)filter_param_register(type, "delay");
387     (void)filter_param_register(type, "retry_window");
388     (void)filter_param_register(type, "client_awl");
389     (void)filter_param_register(type, "max_age");
390     (void)filter_param_register(type, "path");
391     (void)filter_param_register(type, "prefix");
392     return 0;
393 }
394 module_init(greylist_init)