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