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