/*++ /* NAME /* postscreen_dnsbl 3 /* SUMMARY /* postscreen DNSBL support /* SYNOPSIS /* #include /* /* void psc_dnsbl_init(void) /* /* int psc_dnsbl_request(client_addr, callback, context) /* char *client_addr; /* void (*callback)(int, char *); /* char *context; /* /* int psc_dnsbl_retrieve(client_addr, dnsbl_name, dnsbl_index, /* dnsbl_ttl) /* char *client_addr; /* const char **dnsbl_name; /* int dnsbl_index; /* int *dnsbl_ttl; /* DESCRIPTION /* This module implements preliminary support for DNSBL lookups. /* Multiple requests for the same information are handled with /* reference counts. /* /* psc_dnsbl_init() initializes this module, and must be called /* once before any of the other functions in this module. /* /* psc_dnsbl_request() requests a blocklist score for the /* specified client IP address and increments the reference /* count. The request completes in the background. The client /* IP address must be in inet_ntop(3) output format. The /* callback argument specifies a function that is called when /* the requested result is available. The context is passed /* on to the callback function. The callback should ignore its /* first argument (it exists for compatibility with Postfix /* generic event infrastructure). /* The result value is the index for the psc_dnsbl_retrieve() /* call. /* /* psc_dnsbl_retrieve() retrieves the result score and reply /* TTL requested with psc_dnsbl_request(), and decrements the /* reference count. The reply TTL value is clamped to /* postscreen_dnsbl_min_ttl and postscreen_dnsbl_max_ttl. It /* is an error to retrieve a score without requesting it first. /* LICENSE /* .ad /* .fi /* The Secure Mailer license must be distributed with this software. /* AUTHOR(S) /* Wietse Venema /* IBM T.J. Watson Research /* P.O. Box 704 /* Yorktown Heights, NY 10598, USA /* /* Wietse Venema /* Google, Inc. /* 111 8th Avenue /* New York, NY 10011, USA /*--*/ /* System library. */ #include #include /* AF_INET */ #include /* inet_pton() */ #include /* inet_pton() */ #include /* sscanf */ #include /* Utility library. */ #include #include #include #include #include #include #include #include #include #include #include #include /* Global library. */ #include #include /* Application-specific. */ #include /* * Talking to the DNSBLOG service. */ static char *psc_dnsbl_service; /* * Per-DNSBL filters and weights. * * The postscreen_dnsbl_sites parameter specifies zero or more DNSBL domains. * We provide multiple access methods, one for quick iteration when sending * queries to all DNSBL servers, and one for quick location when receiving a * reply from one DNSBL server. * * Each DNSBL domain can be specified more than once, each time with a * different (filter, weight) pair. We group (filter, weight) pairs in a * linked list under their DNSBL domain name. The list head has a reference * to a "safe name" for the DNSBL, in case the name includes a password. */ static HTABLE *dnsbl_site_cache; /* indexed by DNSBNL domain */ static HTABLE_INFO **dnsbl_site_list; /* flattened cache */ typedef struct { const char *safe_dnsbl; /* from postscreen_dnsbl_reply_map */ struct PSC_DNSBL_SITE *first; /* list of (filter, weight) tuples */ } PSC_DNSBL_HEAD; typedef struct PSC_DNSBL_SITE { char *filter; /* printable filter (default: null) */ char *byte_codes; /* encoded filter (default: null) */ int weight; /* reply weight (default: 1) */ struct PSC_DNSBL_SITE *next; /* linked list */ } PSC_DNSBL_SITE; /* * Per-client DNSBL scores. * * Some SMTP clients make parallel connections. This can trigger parallel * blocklist score requests when the pre-handshake delays of the connections * overlap. * * We combine requests for the same score under the client IP address in a * single reference-counted entry. The reference count goes up with each * request for a score, and it goes down with each score retrieval. Each * score has one or more requestors that need to be notified when the result * is ready, so that postscreen can terminate a pre-handshake delay when all * pre-handshake tests are completed. */ static HTABLE *dnsbl_score_cache; /* indexed by client address */ typedef struct { void (*callback) (int, void *); /* generic call-back routine */ void *context; /* generic call-back argument */ } PSC_CALL_BACK_ENTRY; typedef struct { const char *dnsbl_name; /* DNSBL with largest contribution */ int dnsbl_weight; /* weight of largest contribution */ int total; /* combined allow+denylist score */ int fail_ttl; /* combined reply TTL */ int pass_ttl; /* combined reply TTL */ int refcount; /* score reference count */ int pending_lookups; /* nr of DNS requests in flight */ int request_id; /* duplicate suppression */ /* Call-back table support. */ int index; /* next table index */ int limit; /* last valid index */ PSC_CALL_BACK_ENTRY table[1]; /* actually a bunch */ } PSC_DNSBL_SCORE; #define PSC_CALL_BACK_INIT(sp) do { \ (sp)->limit = 0; \ (sp)->index = 0; \ } while (0) #define PSC_CALL_BACK_INDEX_OF_LAST(sp) ((sp)->index - 1) #define PSC_CALL_BACK_CANCEL(sp, idx) do { \ PSC_CALL_BACK_ENTRY *_cb_; \ if ((idx) < 0 || (idx) >= (sp)->index) \ msg_panic("%s: index %d must be >= 0 and < %d", \ myname, (idx), (sp)->index); \ _cb_ = (sp)->table + (idx); \ event_cancel_timer(_cb_->callback, _cb_->context); \ _cb_->callback = 0; \ _cb_->context = 0; \ } while (0) #define PSC_CALL_BACK_EXTEND(hp, sp) do { \ if ((sp)->index >= (sp)->limit) { \ int _count_ = ((sp)->limit ? (sp)->limit * 2 : 5); \ (hp)->value = myrealloc((void *) (sp), sizeof(*(sp)) + \ _count_ * sizeof((sp)->table)); \ (sp) = (PSC_DNSBL_SCORE *) (hp)->value; \ (sp)->limit = _count_; \ } \ } while (0) #define PSC_CALL_BACK_ENTER(sp, fn, ctx) do { \ PSC_CALL_BACK_ENTRY *_cb_ = (sp)->table + (sp)->index++; \ _cb_->callback = (fn); \ _cb_->context = (ctx); \ } while (0) #define PSC_CALL_BACK_NOTIFY(sp, ev) do { \ PSC_CALL_BACK_ENTRY *_cb_; \ for (_cb_ = (sp)->table; _cb_ < (sp)->table + (sp)->index; _cb_++) \ if (_cb_->callback != 0) \ _cb_->callback((ev), _cb_->context); \ } while (0) #define PSC_NULL_EVENT (0) /* * Per-request state. * * This implementation stores the client IP address and DNSBL domain in the * DNSBLOG query/reply stream. This simplifies code, and allows the DNSBLOG * server to produce more informative logging. */ static VSTRING *reply_client; /* client address in DNSBLOG reply */ static VSTRING *reply_dnsbl; /* domain in DNSBLOG reply */ static VSTRING *reply_addr; /* address list in DNSBLOG reply */ /* psc_dnsbl_add_site - add DNSBL site information */ static void psc_dnsbl_add_site(const char *site) { const char *myname = "psc_dnsbl_add_site"; char *saved_site = mystrdup(site); VSTRING *byte_codes = 0; PSC_DNSBL_HEAD *head; PSC_DNSBL_SITE *new_site; char junk; const char *weight_text; char *pattern_text; int weight; HTABLE_INFO *ht; char *parse_err; const char *safe_dnsbl; /* * Parse the required DNSBL domain name, the optional reply filter and * the optional reply weight factor. */ #define DO_GRIPE 1 /* Negative weight means allowlist. */ if ((weight_text = split_at(saved_site, '*')) != 0) { if (sscanf(weight_text, "%d%c", &weight, &junk) != 1) msg_fatal("bad DNSBL weight factor \"%s\" in \"%s\"", weight_text, site); } else { weight = 1; } /* Reply filter. */ if ((pattern_text = split_at(saved_site, '=')) != 0) { byte_codes = vstring_alloc(100); if ((parse_err = ip_match_parse(byte_codes, pattern_text)) != 0) msg_fatal("bad DNSBL filter syntax: %s", parse_err); } if (valid_hostname(saved_site, DO_GRIPE) == 0) msg_fatal("bad DNSBL domain name \"%s\" in \"%s\"", saved_site, site); if (msg_verbose > 1) msg_info("%s: \"%s\" -> domain=\"%s\" pattern=\"%s\" weight=%d", myname, site, saved_site, pattern_text ? pattern_text : "null", weight); /* * Look up or create the (filter, weight) list head for this DNSBL domain * name. */ if ((head = (PSC_DNSBL_HEAD *) htable_find(dnsbl_site_cache, saved_site)) == 0) { head = (PSC_DNSBL_HEAD *) mymalloc(sizeof(*head)); ht = htable_enter(dnsbl_site_cache, saved_site, (void *) head); /* Translate the DNSBL name into a safe name if available. */ if (psc_dnsbl_reply == 0 || (safe_dnsbl = dict_get(psc_dnsbl_reply, saved_site)) == 0) safe_dnsbl = ht->key; head->safe_dnsbl = mystrdup(safe_dnsbl); if (psc_dnsbl_reply && psc_dnsbl_reply->error) msg_fatal("%s:%s lookup error", psc_dnsbl_reply->type, psc_dnsbl_reply->name); head->first = 0; } /* * Append the new (filter, weight) node to the list for this DNSBL domain * name. */ new_site = (PSC_DNSBL_SITE *) mymalloc(sizeof(*new_site)); new_site->filter = (pattern_text ? mystrdup(pattern_text) : 0); new_site->byte_codes = (byte_codes ? ip_match_save(byte_codes) : 0); new_site->weight = weight; new_site->next = head->first; head->first = new_site; myfree(saved_site); if (byte_codes) vstring_free(byte_codes); } /* psc_dnsbl_match - match DNSBL reply filter */ static int psc_dnsbl_match(const char *filter, ARGV *reply) { char addr_buf[MAI_HOSTADDR_STRSIZE]; char **cpp; /* * Run the replies through the pattern-matching engine. */ for (cpp = reply->argv; *cpp != 0; cpp++) { if (inet_pton(AF_INET, *cpp, addr_buf) != 1) msg_warn("address conversion error for %s -- ignoring this reply", *cpp); if (ip_match_execute(filter, addr_buf)) return (1); } return (0); } /* psc_dnsbl_retrieve - retrieve blocklist score, decrement reference count */ int psc_dnsbl_retrieve(const char *client_addr, const char **dnsbl_name, int dnsbl_index, int *dnsbl_ttl) { const char *myname = "psc_dnsbl_retrieve"; PSC_DNSBL_SCORE *score; int result_score; int result_ttl; /* * Sanity check. */ if ((score = (PSC_DNSBL_SCORE *) htable_find(dnsbl_score_cache, client_addr)) == 0) msg_panic("%s: no blocklist score for %s", myname, client_addr); /* * Disable callbacks. */ PSC_CALL_BACK_CANCEL(score, dnsbl_index); /* * Reads are destructive. */ result_score = score->total; *dnsbl_name = score->dnsbl_name; result_ttl = (result_score > 0) ? score->fail_ttl : score->pass_ttl; /* As with dnsblog(8), a value < 0 means no reply TTL. */ if (result_ttl < var_psc_dnsbl_min_ttl) result_ttl = var_psc_dnsbl_min_ttl; if (result_ttl > var_psc_dnsbl_max_ttl) result_ttl = var_psc_dnsbl_max_ttl; *dnsbl_ttl = result_ttl; if (msg_verbose) msg_info("%s: addr=%s score=%d ttl=%d", myname, client_addr, result_score, result_ttl); score->refcount -= 1; if (score->refcount < 1) { if (msg_verbose > 1) msg_info("%s: delete blocklist score for %s", myname, client_addr); htable_delete(dnsbl_score_cache, client_addr, myfree); } return (result_score); } /* psc_dnsbl_receive - receive DNSBL reply, update blocklist score */ static void psc_dnsbl_receive(int event, void *context) { const char *myname = "psc_dnsbl_receive"; VSTREAM *stream = (VSTREAM *) context; PSC_DNSBL_SCORE *score; PSC_DNSBL_HEAD *head; PSC_DNSBL_SITE *site; ARGV *reply_argv; int request_id; int dnsbl_ttl; PSC_CLEAR_EVENT_REQUEST(vstream_fileno(stream), psc_dnsbl_receive, context); /* * Receive the DNSBL lookup result. * * This is preliminary code to explore the field. Later, DNSBL lookup will * be handled by an UDP-based DNS client that is built directly into some * Postfix daemon. * * Don't bother looking up the blocklist score when the client IP address is * not listed at the DNSBL. * * Don't panic when the blocklist score no longer exists. It may be deleted * when the client triggers a "drop" action after pregreet, when the * client does not pregreet and the DNSBL reply arrives late, or when the * client triggers a "drop" action after hanging up. */ if (event == EVENT_READ && attr_scan(stream, ATTR_FLAG_STRICT, RECV_ATTR_STR(MAIL_ATTR_RBL_DOMAIN, reply_dnsbl), RECV_ATTR_STR(MAIL_ATTR_ACT_CLIENT_ADDR, reply_client), RECV_ATTR_INT(MAIL_ATTR_LABEL, &request_id), RECV_ATTR_STR(MAIL_ATTR_RBL_ADDR, reply_addr), RECV_ATTR_INT(MAIL_ATTR_TTL, &dnsbl_ttl), ATTR_TYPE_END) == 5 && (score = (PSC_DNSBL_SCORE *) htable_find(dnsbl_score_cache, STR(reply_client))) != 0 && score->request_id == request_id) { /* * Run this response past all applicable DNSBL filters and update the * blocklist score for this client IP address. * * Don't panic when the DNSBL domain name is not found. The DNSBLOG * server may be messed up. */ if (msg_verbose > 1) msg_info("%s: client=\"%s\" score=%d domain=\"%s\" reply=\"%d %s\"", myname, STR(reply_client), score->total, STR(reply_dnsbl), dnsbl_ttl, STR(reply_addr)); head = (PSC_DNSBL_HEAD *) htable_find(dnsbl_site_cache, STR(reply_dnsbl)); if (head == 0) { /* Bogus domain. Do nothing. */ } else if (*STR(reply_addr) != 0) { /* DNS reputation record(s) found. */ reply_argv = 0; for (site = head->first; site != 0; site = site->next) { if (site->byte_codes == 0 || psc_dnsbl_match(site->byte_codes, reply_argv ? reply_argv : (reply_argv = argv_split(STR(reply_addr), " ")))) { if (score->dnsbl_name == 0 || score->dnsbl_weight < site->weight) { score->dnsbl_name = head->safe_dnsbl; score->dnsbl_weight = site->weight; } score->total += site->weight; if (msg_verbose > 1) msg_info("%s: filter=\"%s\" weight=%d score=%d", myname, site->filter ? site->filter : "null", site->weight, score->total); } /* As with dnsblog(8), a value < 0 means no reply TTL. */ if (site->weight > 0) { if (score->fail_ttl < 0 || score->fail_ttl > dnsbl_ttl) score->fail_ttl = dnsbl_ttl; } else { if (score->pass_ttl < 0 || score->pass_ttl > dnsbl_ttl) score->pass_ttl = dnsbl_ttl; } } if (reply_argv != 0) argv_free(reply_argv); } else { /* No DNS reputation record found. */ for (site = head->first; site != 0; site = site->next) { /* As with dnsblog(8), a value < 0 means no reply TTL. */ if (site->weight > 0) { if (score->pass_ttl < 0 || score->pass_ttl > dnsbl_ttl) score->pass_ttl = dnsbl_ttl; } else { if (score->fail_ttl < 0 || score->fail_ttl > dnsbl_ttl) score->fail_ttl = dnsbl_ttl; } } } /* * Notify the requestor(s) that the result is ready to be picked up. * If this call isn't made, clients have to sit out the entire * pre-handshake delay. */ score->pending_lookups -= 1; if (score->pending_lookups == 0) PSC_CALL_BACK_NOTIFY(score, PSC_NULL_EVENT); } else if (event == EVENT_TIME) { msg_warn("dnsblog reply timeout %ds for %s", var_psc_dnsbl_tmout, (char *) vstream_context(stream)); } /* Here, score may be a null pointer. */ vstream_fclose(stream); } /* psc_dnsbl_request - send dnsbl query, increment reference count */ int psc_dnsbl_request(const char *client_addr, void (*callback) (int, void *), void *context) { const char *myname = "psc_dnsbl_request"; int fd; VSTREAM *stream; HTABLE_INFO **ht; PSC_DNSBL_SCORE *score; HTABLE_INFO *hash_node; static int request_count; /* * Some spambots make several connections at nearly the same time, * causing their pregreet delays to overlap. Such connections can share * the efforts of DNSBL lookup. * * We store a reference-counted DNSBL score under its client IP address. We * increment the reference count with each score request, and decrement * the reference count with each score retrieval. * * Do not notify the requestor NOW when the DNS replies are already in. * Reason: we must not make a backwards call while we are still in the * middle of executing the corresponding forward call. Instead we create * a zero-delay timer request and call the notification function from * there. * * psc_dnsbl_request() could instead return a result value to indicate that * the DNSBL score is already available, but that would complicate the * caller with two different notification code paths: one asynchronous * code path via the callback invocation, and one synchronous code path * via the psc_dnsbl_request() result value. That would be a source of * future bugs. */ if ((hash_node = htable_locate(dnsbl_score_cache, client_addr)) != 0) { score = (PSC_DNSBL_SCORE *) hash_node->value; score->refcount += 1; PSC_CALL_BACK_EXTEND(hash_node, score); PSC_CALL_BACK_ENTER(score, callback, context); if (msg_verbose > 1) msg_info("%s: reuse blocklist score for %s refcount=%d pending=%d", myname, client_addr, score->refcount, score->pending_lookups); if (score->pending_lookups == 0) event_request_timer(callback, context, EVENT_NULL_DELAY); return (PSC_CALL_BACK_INDEX_OF_LAST(score)); } if (msg_verbose > 1) msg_info("%s: create blocklist score for %s", myname, client_addr); score = (PSC_DNSBL_SCORE *) mymalloc(sizeof(*score)); score->request_id = request_count++; score->dnsbl_name = 0; score->dnsbl_weight = 0; /* As with dnsblog(8), a value < 0 means no reply TTL. */ score->pass_ttl = -1; score->fail_ttl = -1; score->total = 0; score->refcount = 1; score->pending_lookups = 0; PSC_CALL_BACK_INIT(score); PSC_CALL_BACK_ENTER(score, callback, context); (void) htable_enter(dnsbl_score_cache, client_addr, (void *) score); /* * Send a query to all DNSBL servers. Later, DNSBL lookup will be done * with an UDP-based DNS client that is built directly into Postfix code. * We therefore do not optimize the maximum out of this temporary * implementation. */ for (ht = dnsbl_site_list; *ht; ht++) { if ((fd = LOCAL_CONNECT(psc_dnsbl_service, NON_BLOCKING, 1)) < 0) { msg_warn("%s: connect to %s service: %m", myname, psc_dnsbl_service); continue; } stream = vstream_fdopen(fd, O_RDWR); vstream_control(stream, CA_VSTREAM_CTL_CONTEXT(ht[0]->key), CA_VSTREAM_CTL_END); attr_print(stream, ATTR_FLAG_NONE, SEND_ATTR_STR(MAIL_ATTR_RBL_DOMAIN, ht[0]->key), SEND_ATTR_STR(MAIL_ATTR_ACT_CLIENT_ADDR, client_addr), SEND_ATTR_INT(MAIL_ATTR_LABEL, score->request_id), ATTR_TYPE_END); if (vstream_fflush(stream) != 0) { msg_warn("%s: error sending to %s service: %m", myname, psc_dnsbl_service); vstream_fclose(stream); continue; } PSC_READ_EVENT_REQUEST(vstream_fileno(stream), psc_dnsbl_receive, (void *) stream, var_psc_dnsbl_tmout); score->pending_lookups += 1; } return (PSC_CALL_BACK_INDEX_OF_LAST(score)); } /* psc_dnsbl_init - initialize */ void psc_dnsbl_init(void) { const char *myname = "psc_dnsbl_init"; ARGV *dnsbl_site = argv_split(var_psc_dnsbl_sites, CHARS_COMMA_SP); char **cpp; /* * Sanity check. */ if (dnsbl_site_cache != 0) msg_panic("%s: called more than once", myname); /* * pre-compute the DNSBLOG socket name. */ psc_dnsbl_service = concatenate(MAIL_CLASS_PRIVATE, "/", var_dnsblog_service, (char *) 0); /* * Prepare for quick iteration when sending out queries to all DNSBL * servers, and for quick lookup when a reply arrives from a specific * DNSBL server. */ dnsbl_site_cache = htable_create(13); for (cpp = dnsbl_site->argv; *cpp; cpp++) psc_dnsbl_add_site(*cpp); argv_free(dnsbl_site); dnsbl_site_list = htable_list(dnsbl_site_cache); /* * The per-client blocklist score. */ dnsbl_score_cache = htable_create(13); /* * Space for ad-hoc DNSBLOG server request/reply parameters. */ reply_client = vstring_alloc(100); reply_dnsbl = vstring_alloc(100); reply_addr = vstring_alloc(100); }