Asynchronous DNS queries on iplist.
[apps/pfixtools.git] / postlicyd / iplist.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  * Copyright © 2008 Florent Bruneau
35  */
36
37 #include <arpa/inet.h>
38 #include <netinet/in.h>
39 #include <sys/mman.h>
40
41 #include "common.h"
42 #include "iplist.h"
43 #include "str.h"
44 #include "file.h"
45 #include "array.h"
46 #include "rbl.h"
47
48 #define IPv4_BITS        5
49 #define IPv4_PREFIX(ip)  ((uint32_t)(ip) >> IPv4_BITS)
50 #define IPv4_SUFFIX(ip)  ((uint32_t)(ip) & ((1 << IPv4_BITS) - 1))
51 #define NODE(db, i)      ((db)->tree + (i))
52 #ifndef DEBUG
53 #define DEBUG(...)
54 #endif
55
56 /* Implementation */
57
58 enum {
59     BALANCED    = 0,
60     LEFT_HEAVY  = 1,
61     RIGHT_HEAVY = 2,
62 };
63
64 struct rbldb_t {
65     A(uint16_t) ips[1 << 16];
66 };
67 ARRAY(rbldb_t)
68
69 static int get_o(const char *s, const char **out)
70 {
71     int res = 0;
72
73     if (*s < '0' || *s > '9')
74         return -1;
75
76     res = *s++ - '0';
77     if (*s < '0' || *s > '9')
78         goto ok;
79
80     res = res * 10 + *s++ - '0';
81     if (*s < '0' || *s > '9')
82         goto ok;
83
84     res = res * 10 + *s++ - '0';
85     if (!(*s < '0' || *s > '9') || res < 100)
86         return -1;
87
88   ok:
89     *out = s;
90     return res;
91 }
92
93 static int parse_ipv4(const char *s, const char **out, uint32_t *ip)
94 {
95     int o;
96
97     o = get_o(s, &s);
98     if ((o & ~0xff) || *s++ != '.')
99         return -1;
100     *ip = o << 24;
101
102     o = get_o(s, &s);
103     if ((o & ~0xff) || *s++ != '.')
104         return -1;
105     *ip |= o << 16;
106
107     o = get_o(s, &s);
108     if ((o & ~0xff) || *s++ != '.')
109         return -1;
110     *ip |= o << 8;
111
112     o = get_o(s, &s);
113     if (o & ~0xff)
114         return -1;
115     *ip |= o;
116
117     *out = s;
118     return 0;
119 }
120
121 rbldb_t *rbldb_create(const char *file, bool lock)
122 {
123     rbldb_t *db;
124     file_map_t map;
125     const char *p, *end;
126     uint32_t ips = 0;
127
128     if (!file_map_open(&map, file, false)) {
129         return NULL;
130     }
131
132     p   = map.map;
133     end = map.end;
134     while (end > p && end[-1] != '\n') {
135         --end;
136     }
137     if (end != map.end) {
138         warn("file %s miss a final \\n, ignoring last line",
139              file);
140     }
141
142     db = p_new(rbldb_t, 1);
143     while (p < end) {
144         uint32_t ip;
145
146         while (*p == ' ' || *p == '\t' || *p == '\r')
147             p++;
148
149         if (parse_ipv4(p, &p, &ip) < 0) {
150             p = (char *)memchr(p, '\n', end - p) + 1;
151         } else {
152             array_add(db->ips[ip >> 16], ip & 0xffff);
153             ++ips;
154         }
155     }
156     file_map_close(&map);
157
158     /* Lookup may perform serveral I/O, so avoid swap.
159      */
160     for (int i = 0 ; i < 1 << 16 ; ++i) {
161         array_adjust(db->ips[i]);
162         if (lock && !array_lock(db->ips[i])) {
163             UNIXERR("mlock");
164         }
165         if (db->ips[i].len) {
166 #       define QSORT_TYPE uint16_t
167 #       define QSORT_BASE db->ips[i].data
168 #       define QSORT_NELT db->ips[i].len
169 #       define QSORT_LT(a,b) *a < *b
170 #       include "qsort.c"
171         }
172     }
173
174     info("rbl %s loaded, %d IPs", file, ips);
175     return db;
176 }
177
178 static void rbldb_wipe(rbldb_t *db)
179 {
180     for (int i = 0 ; i < 1 << 16 ; ++i) {
181         array_wipe(db->ips[i]);
182     }
183 }
184
185 void rbldb_delete(rbldb_t **db)
186 {
187     if (*db) {
188         rbldb_wipe(*db);
189         p_delete(&(*db));
190     }
191 }
192
193 uint32_t rbldb_stats(const rbldb_t *rbl)
194 {
195     uint32_t ips = 0;
196     for (int i = 0 ; i < 1 << 16 ; ++i) {
197         ips += array_len(rbl->ips[i]);
198     }
199     return ips;
200 }
201
202 bool rbldb_ipv4_lookup(const rbldb_t *db, uint32_t ip)
203 {
204     const uint16_t hip = ip >> 16;
205     const uint16_t lip = ip & 0xffff;
206     int l = 0, r = db->ips[hip].len;
207
208     while (l < r) {
209         int i = (r + l) / 2;
210
211         if (array_elt(db->ips[hip], i) == lip)
212             return true;
213
214         if (lip < array_elt(db->ips[hip], i)) {
215             r = i;
216         } else {
217             l = i + 1;
218         }
219     }
220     return false;
221 }
222
223
224 /* postlicyd filter declaration */
225
226 #include "filter.h"
227
228 typedef struct iplist_filter_t {
229     PA(rbldb_t) rbls;
230     A(int)      weights;
231     A(char)     hosts;
232     A(int)      host_offsets;
233     A(int)      host_weights;
234
235     int32_t     hard_threshold;
236     int32_t     soft_threshold;
237 } iplist_filter_t;
238
239 typedef struct iplist_async_data_t {
240     A(rbl_result_t) results;
241     int awaited;
242     uint32_t sum;
243     bool error;
244 } iplist_async_data_t;
245
246 static filter_type_t filter_type = FTK_UNKNOWN;
247
248 static iplist_filter_t *iplist_filter_new(void)
249 {
250     return p_new(iplist_filter_t, 1);
251 }
252
253 static void iplist_filter_delete(iplist_filter_t **rbl)
254 {
255     if (*rbl) {
256         array_deep_wipe((*rbl)->rbls, rbldb_delete);
257         array_wipe((*rbl)->weights);
258         array_wipe((*rbl)->hosts);
259         array_wipe((*rbl)->host_offsets);
260         array_wipe((*rbl)->host_weights);
261         p_delete(rbl);
262     }
263 }
264
265
266 static bool iplist_filter_constructor(filter_t *filter)
267 {
268     iplist_filter_t *data = iplist_filter_new();
269
270 #define PARSE_CHECK(Expr, Str, ...)                                            \
271     if (!(Expr)) {                                                             \
272         err(Str, ##__VA_ARGS__);                                               \
273         iplist_filter_delete(&data);                                              \
274         return false;                                                          \
275     }
276
277     data->hard_threshold = 1;
278     data->soft_threshold = 1;
279     foreach (filter_param_t *param, filter->params) {
280         switch (param->type) {
281           /* file parameter is:
282            *  [no]lock:weight:filename
283            *  valid options are:
284            *    - lock:   memlock the database in memory.
285            *    - nolock: don't memlock the database in memory [default].
286            *    - \d+:    a number describing the weight to give to the match
287            *              the given list [mandatory]
288            *  the file pointed by filename MUST be a valid ip list issued from
289            *  the rsync (or equivalent) service of a (r)bl.
290            */
291           case ATK_FILE: case ATK_RBLDNS: {
292             bool lock = false;
293             int  weight = 0;
294             rbldb_t *rbl = NULL;
295             const char *current = param->value;
296             const char *p = m_strchrnul(param->value, ':');
297             char *next = NULL;
298             for (int i = 0 ; i < 3 ; ++i) {
299                 PARSE_CHECK(i == 2 || *p,
300                             "file parameter must contains a locking state "
301                             "and a weight option");
302                 switch (i) {
303                   case 0:
304                     if ((p - current) == 4 && strncmp(current, "lock", 4) == 0) {
305                         lock = true;
306                     } else if ((p - current) == 6
307                                && strncmp(current, "nolock", 6) == 0) {
308                         lock = false;
309                     } else {
310                         PARSE_CHECK(false, "illegal locking state %.*s",
311                                     (int)(p - current), current);
312                     }
313                     break;
314
315                   case 1:
316                     weight = strtol(current, &next, 10);
317                     PARSE_CHECK(next == p && weight >= 0 && weight <= 1024,
318                                 "illegal weight value %.*s",
319                                 (int)(p - current), current);
320                     break;
321
322                   case 2:
323                     rbl = rbldb_create(current, lock);
324                     PARSE_CHECK(rbl != NULL,
325                                 "cannot load rbl db from %s", current);
326                     array_add(data->rbls, rbl);
327                     array_add(data->weights, weight);
328                     break;
329                 }
330                 if (i != 2) {
331                     current = p + 1;
332                     p = m_strchrnul(current, ':');
333                 }
334             }
335           } break;
336
337           /* dns parameter.
338            *  weight:hostname.
339            * define a RBL to use through DNS resolution.
340            */
341           case ATK_DNS: {
342             int  weight = 0;
343             const char *current = param->value;
344             const char *p = m_strchrnul(param->value, ':');
345             char *next = NULL;
346             for (int i = 0 ; i < 2 ; ++i) {
347                 PARSE_CHECK(i == 1 || *p,
348                             "host parameter must contains a weight option");
349                 switch (i) {
350                   case 0:
351                     weight = strtol(current, &next, 10);
352                     PARSE_CHECK(next == p && weight >= 0 && weight <= 1024,
353                                 "illegal weight value %.*s",
354                                 (int)(p - current), current);
355                     break;
356
357                   case 1:
358                     array_add(data->host_offsets, array_len(data->hosts));
359                     array_append(data->hosts, current, strlen(current) + 1);
360                     array_add(data->host_weights, weight);
361                     break;
362                 }
363                 if (i != 1) {
364                     current = p + 1;
365                     p = m_strchrnul(current, ':');
366                 }
367             }
368           } break;
369
370           /* hard_threshold parameter is an integer.
371            *  If the matching score is greater or equal than this threshold,
372            *  the hook "hard_match" is called.
373            * hard_threshold = 1 means, that all matches are hard matches.
374            * default is 1;
375            */
376           FILTER_PARAM_PARSE_INT(HARD_THRESHOLD, data->hard_threshold);
377
378           /* soft_threshold parameter is an integer.
379            *  if the matching score is greater or equal than this threshold
380            *  and smaller or equal than the hard_threshold, the hook "soft_match"
381            *  is called.
382            * default is 1;
383            */
384           FILTER_PARAM_PARSE_INT(SOFT_THRESHOLD, data->soft_threshold);
385
386           default: break;
387         }
388     }}
389
390     PARSE_CHECK(data->rbls.len || data->host_offsets.len,
391                 "no file parameter in the filter %s", filter->name);
392     filter->data = data;
393     return true;
394 }
395
396 static void iplist_filter_destructor(filter_t *filter)
397 {
398     iplist_filter_t *data = filter->data;
399     iplist_filter_delete(&data);
400     filter->data = data;
401 }
402
403 static void iplist_filter_async(rbl_result_t *result, void *arg)
404 {
405     filter_context_t   *context = arg;
406     const filter_t      *filter = context->current_filter;
407     const iplist_filter_t *data = filter->data;
408     iplist_async_data_t  *async = context->contexts[filter_type];
409
410
411     if (*result != RBL_ERROR) {
412         async->error = false;
413     }
414     --async->awaited;
415
416     debug("got asynchronous request result for filter %s, rbl %d, still awaiting %d answers",
417           filter->name, result - array_ptr(async->results, 0), async->awaited);
418
419     if (async->awaited == 0) {
420         filter_result_t res = HTK_FAIL;
421         if (async->error) {
422             res = HTK_ERROR;
423         } else {
424             for (uint32_t i = 0 ; i < array_len(data->host_offsets) ; ++i) {
425                 int weight = array_elt(data->host_weights, i);
426
427                 switch (array_elt(async->results, i)) {
428                   case RBL_ASYNC:
429                     crit("no more awaited answer but result is ASYNC");
430                     abort();
431                   case RBL_FOUND:
432                     async->sum += weight;
433                     break;
434                   default:
435                     break;
436                 }
437             }
438             if (async->sum >= (uint32_t)data->hard_threshold) {
439                 res = HTK_HARD_MATCH;
440             } else if (async->sum >= (uint32_t)data->soft_threshold) {
441                 res = HTK_SOFT_MATCH;
442             }
443         }
444         debug("answering to filter %s", filter->name);
445         filter_post_async_result(context, res);
446     }
447 }
448
449 static filter_result_t iplist_filter(const filter_t *filter, const query_t *query,
450                                      filter_context_t *context)
451 {
452     uint32_t ip;
453     int32_t sum = 0;
454     const char *end = NULL;
455     const iplist_filter_t *data = filter->data;
456     bool  error = true;
457
458     if (parse_ipv4(query->client_address, &end, &ip) != 0) {
459         warn("invalid client address: %s, expected ipv4",
460              query->client_address);
461         return HTK_ERROR;
462     }
463     for (uint32_t i = 0 ; i < data->rbls.len ; ++i) {
464         const rbldb_t *rbl = array_elt(data->rbls, i);
465         int weight   = array_elt(data->weights, i);
466         if (rbldb_ipv4_lookup(rbl, ip)) {
467             sum += weight;
468             if (sum >= data->hard_threshold) {
469                 return HTK_HARD_MATCH;
470             }
471         }
472         error = false;
473     }
474     if (array_len(data->host_offsets) > 0) {
475         iplist_async_data_t* async = context->contexts[filter_type];
476         array_ensure_exact_capacity(async->results, array_len(data->host_offsets));
477         async->sum = sum;
478         async->awaited = 0;
479         for (uint32_t i = 0 ; i < data->host_offsets.len ; ++i) {
480             const char *rbl = array_ptr(data->hosts, array_elt(data->host_offsets, i));
481             if (rbl_check(rbl, ip, array_ptr(async->results, i),
482                           iplist_filter_async, context)) {
483                 error = false;
484                 ++async->awaited;
485             }
486         }
487         debug("filter %s awaiting %d asynchronous queries", filter->name, async->awaited);
488         async->error = error;
489         return HTK_ASYNC;
490     }
491     if (error) {
492         err("filter %s: all the rbl returned an error", filter->name);
493         return HTK_ERROR;
494     }
495     if (sum >= data->hard_threshold) {
496         return HTK_HARD_MATCH;
497     } else if (sum >= data->soft_threshold) {
498         return HTK_SOFT_MATCH;
499     } else {
500         return HTK_FAIL;
501     }
502 }
503
504 static void *iplist_context_constructor(void)
505 {
506     return p_new(iplist_async_data_t, 1);
507 }
508
509 static void iplist_context_destructor(void *data)
510 {
511     iplist_async_data_t *ctx = data;
512     p_delete(&ctx);
513 }
514
515 static int iplist_init(void)
516 {
517     filter_type =  filter_register("iplist", iplist_filter_constructor,
518                                    iplist_filter_destructor, iplist_filter,
519                                    iplist_context_constructor,
520                                    iplist_context_destructor);
521     /* Hooks.
522      */
523     (void)filter_hook_register(filter_type, "abort");
524     (void)filter_hook_register(filter_type, "error");
525     (void)filter_hook_register(filter_type, "fail");
526     (void)filter_hook_register(filter_type, "hard_match");
527     (void)filter_hook_register(filter_type, "soft_match");
528     (void)filter_hook_register(filter_type, "async");
529
530     /* Parameters.
531      */
532     (void)filter_param_register(filter_type, "file");
533     (void)filter_param_register(filter_type, "rbldns");
534     (void)filter_param_register(filter_type, "dns");
535     (void)filter_param_register(filter_type, "hard_threshold");
536     (void)filter_param_register(filter_type, "soft_threshold");
537     return 0;
538 }
539 module_init(iplist_init);