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