Home | History | Annotate | Line # | Download | only in postscreen
      1 /*	$NetBSD: postscreen_dnsbl.c,v 1.5 2025/02/25 19:15:48 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 allow+denylist 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     const char *safe_dnsbl;
    237 
    238     /*
    239      * Parse the required DNSBL domain name, the optional reply filter and
    240      * the optional reply weight factor.
    241      */
    242 #define DO_GRIPE	1
    243 
    244     /* Negative weight means allowlist. */
    245     if ((weight_text = split_at(saved_site, '*')) != 0) {
    246 	if (sscanf(weight_text, "%d%c", &weight, &junk) != 1)
    247 	    msg_fatal("bad DNSBL weight factor \"%s\" in \"%s\"",
    248 		      weight_text, site);
    249     } else {
    250 	weight = 1;
    251     }
    252     /* Reply filter. */
    253     if ((pattern_text = split_at(saved_site, '=')) != 0) {
    254 	byte_codes = vstring_alloc(100);
    255 	if ((parse_err = ip_match_parse(byte_codes, pattern_text)) != 0)
    256 	    msg_fatal("bad DNSBL filter syntax: %s", parse_err);
    257     }
    258     if (valid_hostname(saved_site, DO_GRIPE) == 0)
    259 	msg_fatal("bad DNSBL domain name \"%s\" in \"%s\"",
    260 		  saved_site, site);
    261 
    262     if (msg_verbose > 1)
    263 	msg_info("%s: \"%s\" -> domain=\"%s\" pattern=\"%s\" weight=%d",
    264 		 myname, site, saved_site, pattern_text ? pattern_text :
    265 		 "null", weight);
    266 
    267     /*
    268      * Look up or create the (filter, weight) list head for this DNSBL domain
    269      * name.
    270      */
    271     if ((head = (PSC_DNSBL_HEAD *)
    272 	 htable_find(dnsbl_site_cache, saved_site)) == 0) {
    273 	head = (PSC_DNSBL_HEAD *) mymalloc(sizeof(*head));
    274 	ht = htable_enter(dnsbl_site_cache, saved_site, (void *) head);
    275 	/* Translate the DNSBL name into a safe name if available. */
    276 	if (psc_dnsbl_reply == 0
    277 	    || (safe_dnsbl = dict_get(psc_dnsbl_reply, saved_site)) == 0)
    278 	    safe_dnsbl = ht->key;
    279 	head->safe_dnsbl = mystrdup(safe_dnsbl);
    280 	if (psc_dnsbl_reply && psc_dnsbl_reply->error)
    281 	    msg_fatal("%s:%s lookup error", psc_dnsbl_reply->type,
    282 		      psc_dnsbl_reply->name);
    283 	head->first = 0;
    284     }
    285 
    286     /*
    287      * Append the new (filter, weight) node to the list for this DNSBL domain
    288      * name.
    289      */
    290     new_site = (PSC_DNSBL_SITE *) mymalloc(sizeof(*new_site));
    291     new_site->filter = (pattern_text ? mystrdup(pattern_text) : 0);
    292     new_site->byte_codes = (byte_codes ? ip_match_save(byte_codes) : 0);
    293     new_site->weight = weight;
    294     new_site->next = head->first;
    295     head->first = new_site;
    296 
    297     myfree(saved_site);
    298     if (byte_codes)
    299 	vstring_free(byte_codes);
    300 }
    301 
    302 /* psc_dnsbl_match - match DNSBL reply filter */
    303 
    304 static int psc_dnsbl_match(const char *filter, ARGV *reply)
    305 {
    306     char    addr_buf[MAI_HOSTADDR_STRSIZE];
    307     char  **cpp;
    308 
    309     /*
    310      * Run the replies through the pattern-matching engine.
    311      */
    312     for (cpp = reply->argv; *cpp != 0; cpp++) {
    313 	if (inet_pton(AF_INET, *cpp, addr_buf) != 1)
    314 	    msg_warn("address conversion error for %s -- ignoring this reply",
    315 		     *cpp);
    316 	if (ip_match_execute(filter, addr_buf))
    317 	    return (1);
    318     }
    319     return (0);
    320 }
    321 
    322 /* psc_dnsbl_retrieve - retrieve blocklist score, decrement reference count */
    323 
    324 int     psc_dnsbl_retrieve(const char *client_addr, const char **dnsbl_name,
    325 			           int dnsbl_index, int *dnsbl_ttl)
    326 {
    327     const char *myname = "psc_dnsbl_retrieve";
    328     PSC_DNSBL_SCORE *score;
    329     int     result_score;
    330     int     result_ttl;
    331 
    332     /*
    333      * Sanity check.
    334      */
    335     if ((score = (PSC_DNSBL_SCORE *)
    336 	 htable_find(dnsbl_score_cache, client_addr)) == 0)
    337 	msg_panic("%s: no blocklist score for %s", myname, client_addr);
    338 
    339     /*
    340      * Disable callbacks.
    341      */
    342     PSC_CALL_BACK_CANCEL(score, dnsbl_index);
    343 
    344     /*
    345      * Reads are destructive.
    346      */
    347     result_score = score->total;
    348     *dnsbl_name = score->dnsbl_name;
    349     result_ttl = (result_score > 0) ? score->fail_ttl : score->pass_ttl;
    350     /* As with dnsblog(8), a value < 0 means no reply TTL. */
    351     if (result_ttl < var_psc_dnsbl_min_ttl)
    352 	result_ttl = var_psc_dnsbl_min_ttl;
    353     if (result_ttl > var_psc_dnsbl_max_ttl)
    354 	result_ttl = var_psc_dnsbl_max_ttl;
    355     *dnsbl_ttl = result_ttl;
    356     if (msg_verbose)
    357 	msg_info("%s: addr=%s score=%d ttl=%d",
    358 		 myname, client_addr, result_score, result_ttl);
    359     score->refcount -= 1;
    360     if (score->refcount < 1) {
    361 	if (msg_verbose > 1)
    362 	    msg_info("%s: delete blocklist score for %s", myname, client_addr);
    363 	htable_delete(dnsbl_score_cache, client_addr, myfree);
    364     }
    365     return (result_score);
    366 }
    367 
    368 /* psc_dnsbl_receive - receive DNSBL reply, update blocklist score */
    369 
    370 static void psc_dnsbl_receive(int event, void *context)
    371 {
    372     const char *myname = "psc_dnsbl_receive";
    373     VSTREAM *stream = (VSTREAM *) context;
    374     PSC_DNSBL_SCORE *score;
    375     PSC_DNSBL_HEAD *head;
    376     PSC_DNSBL_SITE *site;
    377     ARGV   *reply_argv;
    378     int     request_id;
    379     int     dnsbl_ttl;
    380 
    381     PSC_CLEAR_EVENT_REQUEST(vstream_fileno(stream), psc_dnsbl_receive, context);
    382 
    383     /*
    384      * Receive the DNSBL lookup result.
    385      *
    386      * This is preliminary code to explore the field. Later, DNSBL lookup will
    387      * be handled by an UDP-based DNS client that is built directly into some
    388      * Postfix daemon.
    389      *
    390      * Don't bother looking up the blocklist score when the client IP address is
    391      * not listed at the DNSBL.
    392      *
    393      * Don't panic when the blocklist score no longer exists. It may be deleted
    394      * when the client triggers a "drop" action after pregreet, when the
    395      * client does not pregreet and the DNSBL reply arrives late, or when the
    396      * client triggers a "drop" action after hanging up.
    397      */
    398     if (event == EVENT_READ
    399 	&& attr_scan(stream,
    400 		     ATTR_FLAG_STRICT,
    401 		     RECV_ATTR_STR(MAIL_ATTR_RBL_DOMAIN, reply_dnsbl),
    402 		     RECV_ATTR_STR(MAIL_ATTR_ACT_CLIENT_ADDR, reply_client),
    403 		     RECV_ATTR_INT(MAIL_ATTR_LABEL, &request_id),
    404 		     RECV_ATTR_STR(MAIL_ATTR_RBL_ADDR, reply_addr),
    405 		     RECV_ATTR_INT(MAIL_ATTR_TTL, &dnsbl_ttl),
    406 		     ATTR_TYPE_END) == 5
    407 	&& (score = (PSC_DNSBL_SCORE *)
    408 	    htable_find(dnsbl_score_cache, STR(reply_client))) != 0
    409 	&& score->request_id == request_id) {
    410 
    411 	/*
    412 	 * Run this response past all applicable DNSBL filters and update the
    413 	 * blocklist score for this client IP address.
    414 	 *
    415 	 * Don't panic when the DNSBL domain name is not found. The DNSBLOG
    416 	 * server may be messed up.
    417 	 */
    418 	if (msg_verbose > 1)
    419 	    msg_info("%s: client=\"%s\" score=%d domain=\"%s\" reply=\"%d %s\"",
    420 		     myname, STR(reply_client), score->total,
    421 		     STR(reply_dnsbl), dnsbl_ttl, STR(reply_addr));
    422 	head = (PSC_DNSBL_HEAD *)
    423 	    htable_find(dnsbl_site_cache, STR(reply_dnsbl));
    424 	if (head == 0) {
    425 	    /* Bogus domain. Do nothing. */
    426 	} else if (*STR(reply_addr) != 0) {
    427 	    /* DNS reputation record(s) found. */
    428 	    reply_argv = 0;
    429 	    for (site = head->first; site != 0; site = site->next) {
    430 		if (site->byte_codes == 0
    431 		    || psc_dnsbl_match(site->byte_codes, reply_argv ? reply_argv :
    432 			 (reply_argv = argv_split(STR(reply_addr), " ")))) {
    433 		    if (score->dnsbl_name == 0
    434 			|| score->dnsbl_weight < site->weight) {
    435 			score->dnsbl_name = head->safe_dnsbl;
    436 			score->dnsbl_weight = site->weight;
    437 		    }
    438 		    score->total += site->weight;
    439 		    if (msg_verbose > 1)
    440 			msg_info("%s: filter=\"%s\" weight=%d score=%d",
    441 			       myname, site->filter ? site->filter : "null",
    442 				 site->weight, score->total);
    443 		}
    444 		/* As with dnsblog(8), a value < 0 means no reply TTL. */
    445 		if (site->weight > 0) {
    446 		    if (score->fail_ttl < 0 || score->fail_ttl > dnsbl_ttl)
    447 			score->fail_ttl = dnsbl_ttl;
    448 		} else {
    449 		    if (score->pass_ttl < 0 || score->pass_ttl > dnsbl_ttl)
    450 			score->pass_ttl = dnsbl_ttl;
    451 		}
    452 	    }
    453 	    if (reply_argv != 0)
    454 		argv_free(reply_argv);
    455 	} else {
    456 	    /* No DNS reputation record found. */
    457 	    for (site = head->first; site != 0; site = site->next) {
    458 		/* As with dnsblog(8), a value < 0 means no reply TTL. */
    459 		if (site->weight > 0) {
    460 		    if (score->pass_ttl < 0 || score->pass_ttl > dnsbl_ttl)
    461 			score->pass_ttl = dnsbl_ttl;
    462 		} else {
    463 		    if (score->fail_ttl < 0 || score->fail_ttl > dnsbl_ttl)
    464 			score->fail_ttl = dnsbl_ttl;
    465 		}
    466 	    }
    467 	}
    468 
    469 	/*
    470 	 * Notify the requestor(s) that the result is ready to be picked up.
    471 	 * If this call isn't made, clients have to sit out the entire
    472 	 * pre-handshake delay.
    473 	 */
    474 	score->pending_lookups -= 1;
    475 	if (score->pending_lookups == 0)
    476 	    PSC_CALL_BACK_NOTIFY(score, PSC_NULL_EVENT);
    477     } else if (event == EVENT_TIME) {
    478 	msg_warn("dnsblog reply timeout %ds for %s",
    479 		 var_psc_dnsbl_tmout, (char *) vstream_context(stream));
    480     }
    481     /* Here, score may be a null pointer. */
    482     vstream_fclose(stream);
    483 }
    484 
    485 /* psc_dnsbl_request  - send dnsbl query, increment reference count */
    486 
    487 int     psc_dnsbl_request(const char *client_addr,
    488 			          void (*callback) (int, void *),
    489 			          void *context)
    490 {
    491     const char *myname = "psc_dnsbl_request";
    492     int     fd;
    493     VSTREAM *stream;
    494     HTABLE_INFO **ht;
    495     PSC_DNSBL_SCORE *score;
    496     HTABLE_INFO *hash_node;
    497     static int request_count;
    498 
    499     /*
    500      * Some spambots make several connections at nearly the same time,
    501      * causing their pregreet delays to overlap. Such connections can share
    502      * the efforts of DNSBL lookup.
    503      *
    504      * We store a reference-counted DNSBL score under its client IP address. We
    505      * increment the reference count with each score request, and decrement
    506      * the reference count with each score retrieval.
    507      *
    508      * Do not notify the requestor NOW when the DNS replies are already in.
    509      * Reason: we must not make a backwards call while we are still in the
    510      * middle of executing the corresponding forward call. Instead we create
    511      * a zero-delay timer request and call the notification function from
    512      * there.
    513      *
    514      * psc_dnsbl_request() could instead return a result value to indicate that
    515      * the DNSBL score is already available, but that would complicate the
    516      * caller with two different notification code paths: one asynchronous
    517      * code path via the callback invocation, and one synchronous code path
    518      * via the psc_dnsbl_request() result value. That would be a source of
    519      * future bugs.
    520      */
    521     if ((hash_node = htable_locate(dnsbl_score_cache, client_addr)) != 0) {
    522 	score = (PSC_DNSBL_SCORE *) hash_node->value;
    523 	score->refcount += 1;
    524 	PSC_CALL_BACK_EXTEND(hash_node, score);
    525 	PSC_CALL_BACK_ENTER(score, callback, context);
    526 	if (msg_verbose > 1)
    527 	    msg_info("%s: reuse blocklist score for %s refcount=%d pending=%d",
    528 		     myname, client_addr, score->refcount,
    529 		     score->pending_lookups);
    530 	if (score->pending_lookups == 0)
    531 	    event_request_timer(callback, context, EVENT_NULL_DELAY);
    532 	return (PSC_CALL_BACK_INDEX_OF_LAST(score));
    533     }
    534     if (msg_verbose > 1)
    535 	msg_info("%s: create blocklist score for %s", myname, client_addr);
    536     score = (PSC_DNSBL_SCORE *) mymalloc(sizeof(*score));
    537     score->request_id = request_count++;
    538     score->dnsbl_name = 0;
    539     score->dnsbl_weight = 0;
    540     /* As with dnsblog(8), a value < 0 means no reply TTL. */
    541     score->pass_ttl = -1;
    542     score->fail_ttl = -1;
    543     score->total = 0;
    544     score->refcount = 1;
    545     score->pending_lookups = 0;
    546     PSC_CALL_BACK_INIT(score);
    547     PSC_CALL_BACK_ENTER(score, callback, context);
    548     (void) htable_enter(dnsbl_score_cache, client_addr, (void *) score);
    549 
    550     /*
    551      * Send a query to all DNSBL servers. Later, DNSBL lookup will be done
    552      * with an UDP-based DNS client that is built directly into Postfix code.
    553      * We therefore do not optimize the maximum out of this temporary
    554      * implementation.
    555      */
    556     for (ht = dnsbl_site_list; *ht; ht++) {
    557 	if ((fd = LOCAL_CONNECT(psc_dnsbl_service, NON_BLOCKING, 1)) < 0) {
    558 	    msg_warn("%s: connect to %s service: %m",
    559 		     myname, psc_dnsbl_service);
    560 	    continue;
    561 	}
    562 	stream = vstream_fdopen(fd, O_RDWR);
    563 	vstream_control(stream,
    564 			CA_VSTREAM_CTL_CONTEXT(ht[0]->key),
    565 			CA_VSTREAM_CTL_END);
    566 	attr_print(stream, ATTR_FLAG_NONE,
    567 		   SEND_ATTR_STR(MAIL_ATTR_RBL_DOMAIN, ht[0]->key),
    568 		   SEND_ATTR_STR(MAIL_ATTR_ACT_CLIENT_ADDR, client_addr),
    569 		   SEND_ATTR_INT(MAIL_ATTR_LABEL, score->request_id),
    570 		   ATTR_TYPE_END);
    571 	if (vstream_fflush(stream) != 0) {
    572 	    msg_warn("%s: error sending to %s service: %m",
    573 		     myname, psc_dnsbl_service);
    574 	    vstream_fclose(stream);
    575 	    continue;
    576 	}
    577 	PSC_READ_EVENT_REQUEST(vstream_fileno(stream), psc_dnsbl_receive,
    578 			       (void *) stream, var_psc_dnsbl_tmout);
    579 	score->pending_lookups += 1;
    580     }
    581     return (PSC_CALL_BACK_INDEX_OF_LAST(score));
    582 }
    583 
    584 /* psc_dnsbl_init - initialize */
    585 
    586 void    psc_dnsbl_init(void)
    587 {
    588     const char *myname = "psc_dnsbl_init";
    589     ARGV   *dnsbl_site = argv_split(var_psc_dnsbl_sites, CHARS_COMMA_SP);
    590     char  **cpp;
    591 
    592     /*
    593      * Sanity check.
    594      */
    595     if (dnsbl_site_cache != 0)
    596 	msg_panic("%s: called more than once", myname);
    597 
    598     /*
    599      * pre-compute the DNSBLOG socket name.
    600      */
    601     psc_dnsbl_service = concatenate(MAIL_CLASS_PRIVATE, "/",
    602 				    var_dnsblog_service, (char *) 0);
    603 
    604     /*
    605      * Prepare for quick iteration when sending out queries to all DNSBL
    606      * servers, and for quick lookup when a reply arrives from a specific
    607      * DNSBL server.
    608      */
    609     dnsbl_site_cache = htable_create(13);
    610     for (cpp = dnsbl_site->argv; *cpp; cpp++)
    611 	psc_dnsbl_add_site(*cpp);
    612     argv_free(dnsbl_site);
    613     dnsbl_site_list = htable_list(dnsbl_site_cache);
    614 
    615     /*
    616      * The per-client blocklist score.
    617      */
    618     dnsbl_score_cache = htable_create(13);
    619 
    620     /*
    621      * Space for ad-hoc DNSBLOG server request/reply parameters.
    622      */
    623     reply_client = vstring_alloc(100);
    624     reply_dnsbl = vstring_alloc(100);
    625     reply_addr = vstring_alloc(100);
    626 }
    627