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