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