xref: /netbsd-src/external/ibm-public/postfix/dist/src/postscreen/postscreen_dnsbl.c (revision 4391d5e9d4f291db41e3b3ba26a01b5e51364aae)
1 /*	$NetBSD: postscreen_dnsbl.c,v 1.1.1.4 2013/09/25 19:06:33 tron Exp $	*/
2 
3 /*++
4 /* NAME
5 /*	postscreen_dnsbl 3
6 /* SUMMARY
7 /*	postscreen DNSBL support
8 /* SYNOPSIS
9 /*	#include <postscreen.h>
10 /*
11 /*	void	psc_dnsbl_init(void)
12 /*
13 /*	int	psc_dnsbl_request(client_addr, callback, context)
14 /*	char	*client_addr;
15 /*	void	(*callback)(int, char *);
16 /*	char	*context;
17 /*
18 /*	int	psc_dnsbl_retrieve(client_addr, dnsbl_name, dnsbl_index)
19 /*	char	*client_addr;
20 /*	const char **dnsbl_name;
21 /*	int	dnsbl_index;
22 /* DESCRIPTION
23 /*	This module implements preliminary support for DNSBL lookups.
24 /*	Multiple requests for the same information are handled with
25 /*	reference counts.
26 /*
27 /*	psc_dnsbl_init() initializes this module, and must be called
28 /*	once before any of the other functions in this module.
29 /*
30 /*	psc_dnsbl_request() requests a blocklist score for the
31 /*	specified client IP address and increments the reference
32 /*	count.  The request completes in the background. The client
33 /*	IP address must be in inet_ntop(3) output format.  The
34 /*	callback argument specifies a function that is called when
35 /*	the requested result is available. The context is passed
36 /*	on to the callback function. The callback should ignore its
37 /*	first argument (it exists for compatibility with Postfix
38 /*	generic event infrastructure).
39 /*	The result value is the index for the psc_dnsbl_retrieve()
40 /*	call.
41 /*
42 /*	psc_dnsbl_retrieve() retrieves the result score requested with
43 /*	psc_dnsbl_request() and decrements the reference count. It
44 /*	is an error to retrieve a score without requesting it first.
45 /* LICENSE
46 /* .ad
47 /* .fi
48 /*	The Secure Mailer license must be distributed with this software.
49 /* AUTHOR(S)
50 /*	Wietse Venema
51 /*	IBM T.J. Watson Research
52 /*	P.O. Box 704
53 /*	Yorktown Heights, NY 10598, USA
54 /*--*/
55 
56 /* System library. */
57 
58 #include <sys_defs.h>
59 #include <sys/socket.h>			/* AF_INET */
60 #include <netinet/in.h>			/* inet_pton() */
61 #include <arpa/inet.h>			/* inet_pton() */
62 #include <stdio.h>			/* sscanf */
63 
64 /* Utility library. */
65 
66 #include <msg.h>
67 #include <mymalloc.h>
68 #include <argv.h>
69 #include <htable.h>
70 #include <events.h>
71 #include <vstream.h>
72 #include <connect.h>
73 #include <split_at.h>
74 #include <valid_hostname.h>
75 #include <ip_match.h>
76 #include <myaddrinfo.h>
77 #include <stringops.h>
78 
79 /* Global library. */
80 
81 #include <mail_params.h>
82 #include <mail_proto.h>
83 
84 /* Application-specific. */
85 
86 #include <postscreen.h>
87 
88  /*
89   * Talking to the DNSBLOG service.
90   */
91 #define DNSBLOG_TIMEOUT			10
92 static char *psc_dnsbl_service;
93 
94  /*
95   * Per-DNSBL filters and weights.
96   *
97   * The postscreen_dnsbl_sites parameter specifies zero or more DNSBL domains.
98   * We provide multiple access methods, one for quick iteration when sending
99   * queries to all DNSBL servers, and one for quick location when receiving a
100   * reply from one DNSBL server.
101   *
102   * Each DNSBL domain can be specified more than once, each time with a
103   * different (filter, weight) pair. We group (filter, weight) pairs in a
104   * linked list under their DNSBL domain name. The list head has a reference
105   * to a "safe name" for the DNSBL, in case the name includes a password.
106   */
107 static HTABLE *dnsbl_site_cache;	/* indexed by DNSBNL domain */
108 static HTABLE_INFO **dnsbl_site_list;	/* flattened cache */
109 
110 typedef struct {
111     const char *safe_dnsbl;		/* from postscreen_dnsbl_reply_map */
112     struct PSC_DNSBL_SITE *first;	/* list of (filter, weight) tuples */
113 } PSC_DNSBL_HEAD;
114 
115 typedef struct PSC_DNSBL_SITE {
116     char   *filter;			/* printable filter (default: null) */
117     char   *byte_codes;			/* encoded filter (default: null) */
118     int     weight;			/* reply weight (default: 1) */
119     struct PSC_DNSBL_SITE *next;	/* linked list */
120 } PSC_DNSBL_SITE;
121 
122  /*
123   * Per-client DNSBL scores.
124   *
125   * Some SMTP clients make parallel connections. This can trigger parallel
126   * blocklist score requests when the pre-handshake delays of the connections
127   * overlap.
128   *
129   * We combine requests for the same score under the client IP address in a
130   * single reference-counted entry. The reference count goes up with each
131   * request for a score, and it goes down with each score retrieval. Each
132   * score has one or more requestors that need to be notified when the result
133   * is ready, so that postscreen can terminate a pre-handshake delay when all
134   * pre-handshake tests are completed.
135   */
136 static HTABLE *dnsbl_score_cache;	/* indexed by client address */
137 
138 typedef struct {
139     void    (*callback) (int, char *);	/* generic call-back routine */
140     char   *context;			/* generic call-back argument */
141 } PSC_CALL_BACK_ENTRY;
142 
143 typedef struct {
144     const char *dnsbl_name;		/* DNSBL with largest contribution */
145     int     dnsbl_weight;		/* weight of largest contribution */
146     int     total;			/* combined blocklist score */
147     int     refcount;			/* score reference count */
148     int     pending_lookups;		/* nr of DNS requests in flight */
149     int     request_id;			/* duplicate suppression */
150     /* Call-back table support. */
151     int     index;			/* next table index */
152     int     limit;			/* last valid index */
153     PSC_CALL_BACK_ENTRY table[1];	/* actually a bunch */
154 } PSC_DNSBL_SCORE;
155 
156 #define PSC_CALL_BACK_INIT(sp) do { \
157 	(sp)->limit = 0; \
158 	(sp)->index = 0; \
159     } while (0)
160 
161 #define PSC_CALL_BACK_INDEX_OF_LAST(sp) ((sp)->index - 1)
162 
163 #define PSC_CALL_BACK_CANCEL(sp, idx) do { \
164 	PSC_CALL_BACK_ENTRY *_cb_; \
165 	if ((idx) < 0 || (idx) >= (sp)->index) \
166 	    msg_panic("%s: index %d must be >= 0 and < %d", \
167 		      myname, (idx), (sp)->index); \
168 	_cb_ = (sp)->table + (idx); \
169 	event_cancel_timer(_cb_->callback, _cb_->context); \
170 	_cb_->callback = 0; \
171 	_cb_->context = 0; \
172     } while (0)
173 
174 #define PSC_CALL_BACK_EXTEND(hp, sp) do { \
175 	if ((sp)->index >= (sp)->limit) { \
176 	    int _count_ = ((sp)->limit ? (sp)->limit * 2 : 5); \
177 	    (hp)->value = myrealloc((char *) (sp), sizeof(*(sp)) + \
178 				    _count_ * sizeof((sp)->table)); \
179 	    (sp) = (PSC_DNSBL_SCORE *) (hp)->value; \
180 	    (sp)->limit = _count_; \
181 	} \
182     } while (0)
183 
184 #define PSC_CALL_BACK_ENTER(sp, fn, ctx) do { \
185 	PSC_CALL_BACK_ENTRY *_cb_ = (sp)->table + (sp)->index++; \
186 	_cb_->callback = (fn); \
187 	_cb_->context = (ctx); \
188     } while (0)
189 
190 #define PSC_CALL_BACK_NOTIFY(sp, ev) do { \
191 	PSC_CALL_BACK_ENTRY *_cb_; \
192 	for (_cb_ = (sp)->table; _cb_ < (sp)->table + (sp)->index; _cb_++) \
193 	    if (_cb_->callback != 0) \
194 		_cb_->callback((ev), _cb_->context); \
195     } while (0)
196 
197 #define PSC_NULL_EVENT	(0)
198 
199  /*
200   * Per-request state.
201   *
202   * This implementation stores the client IP address and DNSBL domain in the
203   * DNSBLOG query/reply stream. This simplifies code, and allows the DNSBLOG
204   * server to produce more informative logging.
205   */
206 static VSTRING *reply_client;		/* client address in DNSBLOG reply */
207 static VSTRING *reply_dnsbl;		/* domain in DNSBLOG reply */
208 static VSTRING *reply_addr;		/* adress list in DNSBLOG reply */
209 
210 /* psc_dnsbl_add_site - add DNSBL site information */
211 
212 static void psc_dnsbl_add_site(const char *site)
213 {
214     const char *myname = "psc_dnsbl_add_site";
215     char   *saved_site = mystrdup(site);
216     VSTRING *byte_codes = 0;
217     PSC_DNSBL_HEAD *head;
218     PSC_DNSBL_SITE *new_site;
219     char    junk;
220     const char *weight_text;
221     char   *pattern_text;
222     int     weight;
223     HTABLE_INFO *ht;
224     char   *parse_err;
225 
226     /*
227      * Parse the required DNSBL domain name, the optional reply filter and
228      * the optional reply weight factor.
229      */
230 #define DO_GRIPE	1
231 
232     /* Negative weight means whitelist. */
233     if ((weight_text = split_at(saved_site, '*')) != 0) {
234 	if (sscanf(weight_text, "%d%c", &weight, &junk) != 1)
235 	    msg_fatal("bad DNSBL weight factor \"%s\" in \"%s\"",
236 		      weight_text, site);
237     } else {
238 	weight = 1;
239     }
240     /* Reply filter. */
241     if ((pattern_text = split_at(saved_site, '=')) != 0) {
242 	byte_codes = vstring_alloc(100);
243 	if ((parse_err = ip_match_parse(byte_codes, pattern_text)) != 0)
244 	    msg_fatal("bad DNSBL filter syntax: %s", parse_err);
245     }
246     if (valid_hostname(saved_site, DO_GRIPE) == 0)
247 	msg_fatal("bad DNSBL domain name \"%s\" in \"%s\"",
248 		  saved_site, site);
249 
250     if (msg_verbose > 1)
251 	msg_info("%s: \"%s\" -> domain=\"%s\" pattern=\"%s\" weight=%d",
252 		 myname, site, saved_site, pattern_text ? pattern_text :
253 		 "null", weight);
254 
255     /*
256      * Look up or create the (filter, weight) list head for this DNSBL domain
257      * name.
258      */
259     if ((head = (PSC_DNSBL_HEAD *)
260 	 htable_find(dnsbl_site_cache, saved_site)) == 0) {
261 	head = (PSC_DNSBL_HEAD *) mymalloc(sizeof(*head));
262 	ht = htable_enter(dnsbl_site_cache, saved_site, (char *) head);
263 	/* Translate the DNSBL name into a safe name if available. */
264 	if (psc_dnsbl_reply == 0
265 	 || (head->safe_dnsbl = dict_get(psc_dnsbl_reply, saved_site)) == 0)
266 	    head->safe_dnsbl = ht->key;
267 	if (psc_dnsbl_reply && psc_dnsbl_reply->error)
268 	    msg_fatal("%s:%s lookup error", psc_dnsbl_reply->type,
269 		      psc_dnsbl_reply->name);
270 	head->first = 0;
271     }
272 
273     /*
274      * Append the new (filter, weight) node to the list for this DNSBL domain
275      * name.
276      */
277     new_site = (PSC_DNSBL_SITE *) mymalloc(sizeof(*new_site));
278     new_site->filter = (pattern_text ? mystrdup(pattern_text) : 0);
279     new_site->byte_codes = (byte_codes ? ip_match_save(byte_codes) : 0);
280     new_site->weight = weight;
281     new_site->next = head->first;
282     head->first = new_site;
283 
284     myfree(saved_site);
285     if (byte_codes)
286 	vstring_free(byte_codes);
287 }
288 
289 /* psc_dnsbl_match - match DNSBL reply filter */
290 
291 static int psc_dnsbl_match(const char *filter, ARGV *reply)
292 {
293     char    addr_buf[MAI_HOSTADDR_STRSIZE];
294     char  **cpp;
295 
296     /*
297      * Run the replies through the pattern-matching engine.
298      */
299     for (cpp = reply->argv; *cpp != 0; cpp++) {
300 	if (inet_pton(AF_INET, *cpp, addr_buf) != 1)
301 	    msg_warn("address conversion error for %s -- ignoring this reply",
302 		     *cpp);
303 	if (ip_match_execute(filter, addr_buf))
304 	    return (1);
305     }
306     return (0);
307 }
308 
309 /* psc_dnsbl_retrieve - retrieve blocklist score, decrement reference count */
310 
311 int     psc_dnsbl_retrieve(const char *client_addr, const char **dnsbl_name,
312 			           int dnsbl_index)
313 {
314     const char *myname = "psc_dnsbl_retrieve";
315     PSC_DNSBL_SCORE *score;
316     int     result_score;
317 
318     /*
319      * Sanity check.
320      */
321     if ((score = (PSC_DNSBL_SCORE *)
322 	 htable_find(dnsbl_score_cache, client_addr)) == 0)
323 	msg_panic("%s: no blocklist score for %s", myname, client_addr);
324 
325     /*
326      * Disable callbacks.
327      */
328     PSC_CALL_BACK_CANCEL(score, dnsbl_index);
329 
330     /*
331      * Reads are destructive.
332      */
333     result_score = score->total;
334     *dnsbl_name = score->dnsbl_name;
335     score->refcount -= 1;
336     if (score->refcount < 1) {
337 	if (msg_verbose > 1)
338 	    msg_info("%s: delete blocklist score for %s", myname, client_addr);
339 	htable_delete(dnsbl_score_cache, client_addr, myfree);
340     }
341     return (result_score);
342 }
343 
344 /* psc_dnsbl_receive - receive DNSBL reply, update blocklist score */
345 
346 static void psc_dnsbl_receive(int event, char *context)
347 {
348     const char *myname = "psc_dnsbl_receive";
349     VSTREAM *stream = (VSTREAM *) context;
350     PSC_DNSBL_SCORE *score;
351     PSC_DNSBL_HEAD *head;
352     PSC_DNSBL_SITE *site;
353     ARGV   *reply_argv;
354     int     request_id;
355 
356     PSC_CLEAR_EVENT_REQUEST(vstream_fileno(stream), psc_dnsbl_receive, context);
357 
358     /*
359      * Receive the DNSBL lookup result.
360      *
361      * This is preliminary code to explore the field. Later, DNSBL lookup will
362      * be handled by an UDP-based DNS client that is built directly into some
363      * Postfix daemon.
364      *
365      * Don't bother looking up the blocklist score when the client IP address is
366      * not listed at the DNSBL.
367      *
368      * Don't panic when the blocklist score no longer exists. It may be deleted
369      * when the client triggers a "drop" action after pregreet, when the
370      * client does not pregreet and the DNSBL reply arrives late, or when the
371      * client triggers a "drop" action after hanging up.
372      */
373     if (event == EVENT_READ
374 	&& attr_scan(stream,
375 		     ATTR_FLAG_STRICT,
376 		     ATTR_TYPE_STR, MAIL_ATTR_RBL_DOMAIN, reply_dnsbl,
377 		     ATTR_TYPE_STR, MAIL_ATTR_ACT_CLIENT_ADDR, reply_client,
378 		     ATTR_TYPE_INT, MAIL_ATTR_LABEL, &request_id,
379 		     ATTR_TYPE_STR, MAIL_ATTR_RBL_ADDR, reply_addr,
380 		     ATTR_TYPE_END) == 4
381 	&& (score = (PSC_DNSBL_SCORE *)
382 	    htable_find(dnsbl_score_cache, STR(reply_client))) != 0
383 	&& score->request_id == request_id) {
384 
385 	/*
386 	 * Run this response past all applicable DNSBL filters and update the
387 	 * blocklist score for this client IP address.
388 	 *
389 	 * Don't panic when the DNSBL domain name is not found. The DNSBLOG
390 	 * server may be messed up.
391 	 */
392 	if (msg_verbose > 1)
393 	    msg_info("%s: client=\"%s\" score=%d domain=\"%s\" reply=\"%s\"",
394 		     myname, STR(reply_client), score->total,
395 		     STR(reply_dnsbl), STR(reply_addr));
396 	if (*STR(reply_addr) != 0) {
397 	    head = (PSC_DNSBL_HEAD *)
398 		htable_find(dnsbl_site_cache, STR(reply_dnsbl));
399 	    site = (head ? head->first : (PSC_DNSBL_SITE *) 0);
400 	    for (reply_argv = 0; site != 0; site = site->next) {
401 		if (site->byte_codes == 0
402 		    || psc_dnsbl_match(site->byte_codes, reply_argv ? reply_argv :
403 			 (reply_argv = argv_split(STR(reply_addr), " ")))) {
404 		    if (score->dnsbl_name == 0
405 			|| score->dnsbl_weight < site->weight) {
406 			score->dnsbl_name = head->safe_dnsbl;
407 			score->dnsbl_weight = site->weight;
408 		    }
409 		    score->total += site->weight;
410 		    if (msg_verbose > 1)
411 			msg_info("%s: filter=\"%s\" weight=%d score=%d",
412 			       myname, site->filter ? site->filter : "null",
413 				 site->weight, score->total);
414 		}
415 	    }
416 	    if (reply_argv != 0)
417 		argv_free(reply_argv);
418 	}
419 
420 	/*
421 	 * Notify the requestor(s) that the result is ready to be picked up.
422 	 * If this call isn't made, clients have to sit out the entire
423 	 * pre-handshake delay.
424 	 */
425 	score->pending_lookups -= 1;
426 	if (score->pending_lookups == 0)
427 	    PSC_CALL_BACK_NOTIFY(score, PSC_NULL_EVENT);
428     } else if (event == EVENT_TIME) {
429 	msg_warn("dnsblog reply timeout %ds for %s",
430 		 DNSBLOG_TIMEOUT, (char *) vstream_context(stream));
431     }
432     /* Here, score may be a null pointer. */
433     vstream_fclose(stream);
434 }
435 
436 /* psc_dnsbl_request  - send dnsbl query, increment reference count */
437 
438 int     psc_dnsbl_request(const char *client_addr,
439 			          void (*callback) (int, char *),
440 			          char *context)
441 {
442     const char *myname = "psc_dnsbl_request";
443     int     fd;
444     VSTREAM *stream;
445     HTABLE_INFO **ht;
446     PSC_DNSBL_SCORE *score;
447     HTABLE_INFO *hash_node;
448     static int request_count;
449 
450     /*
451      * Some spambots make several connections at nearly the same time,
452      * causing their pregreet delays to overlap. Such connections can share
453      * the efforts of DNSBL lookup.
454      *
455      * We store a reference-counted DNSBL score under its client IP address. We
456      * increment the reference count with each score request, and decrement
457      * the reference count with each score retrieval.
458      *
459      * Do not notify the requestor NOW when the DNS replies are already in.
460      * Reason: we must not make a backwards call while we are still in the
461      * middle of executing the corresponding forward call. Instead we create
462      * a zero-delay timer request and call the notification function from
463      * there.
464      *
465      * psc_dnsbl_request() could instead return a result value to indicate that
466      * the DNSBL score is already available, but that would complicate the
467      * caller with two different notification code paths: one asynchronous
468      * code path via the callback invocation, and one synchronous code path
469      * via the psc_dnsbl_request() result value. That would be a source of
470      * future bugs.
471      */
472     if ((hash_node = htable_locate(dnsbl_score_cache, client_addr)) != 0) {
473 	score = (PSC_DNSBL_SCORE *) hash_node->value;
474 	score->refcount += 1;
475 	PSC_CALL_BACK_EXTEND(hash_node, score);
476 	PSC_CALL_BACK_ENTER(score, callback, context);
477 	if (msg_verbose > 1)
478 	    msg_info("%s: reuse blocklist score for %s refcount=%d pending=%d",
479 		     myname, client_addr, score->refcount,
480 		     score->pending_lookups);
481 	if (score->pending_lookups == 0)
482 	    event_request_timer(callback, context, EVENT_NULL_DELAY);
483 	return (PSC_CALL_BACK_INDEX_OF_LAST(score));
484     }
485     if (msg_verbose > 1)
486 	msg_info("%s: create blocklist score for %s", myname, client_addr);
487     score = (PSC_DNSBL_SCORE *) mymalloc(sizeof(*score));
488     score->request_id = request_count++;
489     score->dnsbl_name = 0;
490     score->dnsbl_weight = 0;
491     score->total = 0;
492     score->refcount = 1;
493     score->pending_lookups = 0;
494     PSC_CALL_BACK_INIT(score);
495     PSC_CALL_BACK_ENTER(score, callback, context);
496     (void) htable_enter(dnsbl_score_cache, client_addr, (char *) score);
497 
498     /*
499      * Send a query to all DNSBL servers. Later, DNSBL lookup will be done
500      * with an UDP-based DNS client that is built directly into Postfix code.
501      * We therefore do not optimize the maximum out of this temporary
502      * implementation.
503      */
504     for (ht = dnsbl_site_list; *ht; ht++) {
505 	if ((fd = LOCAL_CONNECT(psc_dnsbl_service, NON_BLOCKING, 1)) < 0) {
506 	    msg_warn("%s: connect to %s service: %m",
507 		     myname, psc_dnsbl_service);
508 	    continue;
509 	}
510 	stream = vstream_fdopen(fd, O_RDWR);
511 	vstream_control(stream,
512 			VSTREAM_CTL_CONTEXT, ht[0]->key,
513 			VSTREAM_CTL_END);
514 	attr_print(stream, ATTR_FLAG_NONE,
515 		   ATTR_TYPE_STR, MAIL_ATTR_RBL_DOMAIN, ht[0]->key,
516 		   ATTR_TYPE_STR, MAIL_ATTR_ACT_CLIENT_ADDR, client_addr,
517 		   ATTR_TYPE_INT, MAIL_ATTR_LABEL, score->request_id,
518 		   ATTR_TYPE_END);
519 	if (vstream_fflush(stream) != 0) {
520 	    msg_warn("%s: error sending to %s service: %m",
521 		     myname, psc_dnsbl_service);
522 	    vstream_fclose(stream);
523 	    continue;
524 	}
525 	PSC_READ_EVENT_REQUEST(vstream_fileno(stream), psc_dnsbl_receive,
526 			       (char *) stream, DNSBLOG_TIMEOUT);
527 	score->pending_lookups += 1;
528     }
529     return (PSC_CALL_BACK_INDEX_OF_LAST(score));
530 }
531 
532 /* psc_dnsbl_init - initialize */
533 
534 void    psc_dnsbl_init(void)
535 {
536     const char *myname = "psc_dnsbl_init";
537     ARGV   *dnsbl_site = argv_split(var_psc_dnsbl_sites, ", \t\r\n");
538     char  **cpp;
539 
540     /*
541      * Sanity check.
542      */
543     if (dnsbl_site_cache != 0)
544 	msg_panic("%s: called more than once", myname);
545 
546     /*
547      * pre-compute the DNSBLOG socket name.
548      */
549     psc_dnsbl_service = concatenate(MAIL_CLASS_PRIVATE, "/",
550 				    var_dnsblog_service, (char *) 0);
551 
552     /*
553      * Prepare for quick iteration when sending out queries to all DNSBL
554      * servers, and for quick lookup when a reply arrives from a specific
555      * DNSBL server.
556      */
557     dnsbl_site_cache = htable_create(13);
558     for (cpp = dnsbl_site->argv; *cpp; cpp++)
559 	psc_dnsbl_add_site(*cpp);
560     argv_free(dnsbl_site);
561     dnsbl_site_list = htable_list(dnsbl_site_cache);
562 
563     /*
564      * The per-client blocklist score.
565      */
566     dnsbl_score_cache = htable_create(13);
567 
568     /*
569      * Space for ad-hoc DNSBLOG server request/reply parameters.
570      */
571     reply_client = vstring_alloc(100);
572     reply_dnsbl = vstring_alloc(100);
573     reply_addr = vstring_alloc(100);
574 }
575