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