Mercurial > dnsbl
view src/dnsbl.cpp @ 0:96a9758165cd original
Initial revision
author | carl |
---|---|
date | Tue, 20 Apr 2004 20:02:29 -0700 |
parents | |
children | bd0b1a153f67 |
line wrap: on
line source
/* Copyright (c) 2004 Carl Byington - 510 Software Group, released under the GPL version 2 or any later version at your choice available at http://www.fsf.org/licenses/gpl.txt Based on a sample milter Copyright (c) 2000-2003 Sendmail, Inc. and its suppliers. Inspired by the DCC by Rhyolite Software -p port The port through which the MTA will connect to this milter. -t sec The timeout value. TODO: 1) Add "include-dcc NAME fn" to read a dcc whiteclnt file looking for many substitute mail-host domain, and add the equivalent "env_from domain black" into the NAME mapping. That allows clients to use just the DCC for white/blacklisting, but the backup mx machines can use dnsbl and get the same effect. */ // from sendmail sample #include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sysexits.h> #include <unistd.h> // needed for socket io #include <sys/ioctl.h> #include <net/if.h> #include <arpa/inet.h> #include <netinet/in.h> #include <netinet/tcp.h> #include <netdb.h> #include <sys/socket.h> // needed for thread #include <pthread.h> // needed for std c++ collections #include <set> #include <map> #include <list> // for the dns resolver #include <netinet/in.h> #include <arpa/nameser.h> #include <resolv.h> // misc stuff needed here #include <ctype.h> #include <fstream> #include <syslog.h> using namespace std; extern "C" { #include "libmilter/mfapi.h" sfsistat mlfi_connect(SMFICTX *ctx, char *hostname, _SOCK_ADDR *hostaddr); sfsistat mlfi_envfrom(SMFICTX *ctx, char **argv); sfsistat mlfi_envrcpt(SMFICTX *ctx, char **argv); sfsistat mlfi_eom_or_abort(SMFICTX *ctx); sfsistat mlfi_close(SMFICTX *ctx); } #ifndef bool # define bool int # define TRUE 1 # define FALSE 0 #endif /* ! bool */ struct ltstr { bool operator()(char* s1, char* s2) const { return strcmp(s1, s2) < 0; } }; struct DNSBL { char *suffix; // blacklist suffix like blackholes.five-ten-sg.com char *message; // error message with one or two %s operators for the ip address replacement DNSBL(char *s, char *m); }; DNSBL::DNSBL(char *s, char *m) { suffix = s; message = m; } typedef DNSBL * DNSBLP; typedef list<DNSBLP> DNSBLL; typedef DNSBLL * DNSBLLP; typedef map<char *, char *, ltstr> string_map; typedef map<char *, string_map *, ltstr> from_map; typedef map<char *, DNSBLP, ltstr> dnsblp_map; typedef map<char *, DNSBLLP, ltstr> dnsbllp_map; typedef set<char *, ltstr> string_set; typedef list<char *> string_list; struct CONFIG { // the only mutable stuff once it has been loaded from the config file int reference_count; // protected by the global config_mutex // all the rest is constant after loading from the config file time_t load_time; string_list config_files; dnsblp_map dnsbls; dnsbllp_map dnsblls; from_map env_from; string_map env_to_dnsbll; // map recipient to a named dnsbll string_map env_to_chkfrom; // map recipient to a named from map CONFIG(); ~CONFIG(); }; CONFIG::CONFIG() { reference_count = 0; load_time = 0; } CONFIG::~CONFIG() { for (dnsblp_map::iterator i=dnsbls.begin(); i!=dnsbls.end(); i++) { DNSBLP d = (*i).second; delete d; } for (dnsbllp_map::iterator i=dnsblls.begin(); i!=dnsblls.end(); i++) { DNSBLLP d = (*i).second; delete d; } for (from_map::iterator i=env_from.begin(); i!=env_from.end(); i++) { string_map *d = (*i).second; delete d; } } static string_set all_strings; // owns all the strings, only modified by the config loader thread static CONFIG * config = NULL; // protected by the config_mutex static pthread_mutex_t config_mutex; static pthread_mutex_t syslog_mutex; static pthread_mutex_t resolve_mutex; //////////////////////////////////////////////// // predefined names // #define DEFAULT "default" #define WHITE "white" #define BLACK "black" //////////////////////////////////////////////// // mail filter private data, held for us by sendmail // enum status {oksofar, // not rejected yet white, // whitelisted by envelope from black, // blacklisted by envelope from or to reject}; // rejected by a dns list struct mlfiPriv { CONFIG *pc; // global context with our maps int ip; // ip4 address of the smtp client char *mailaddr; // envelope from value bool authenticated; // client authenticated? if so, suppress all dnsbl checks map<DNSBLP, status> checked; // status from those lists mlfiPriv(); ~mlfiPriv(); }; mlfiPriv::mlfiPriv() { pthread_mutex_lock(&config_mutex); pc = config; pc->reference_count++; pthread_mutex_unlock(&config_mutex); ip = 0; mailaddr = NULL; } mlfiPriv::~mlfiPriv() { pthread_mutex_lock(&config_mutex); pc->reference_count--; pthread_mutex_unlock(&config_mutex); if (mailaddr) free(mailaddr); } #define MLFIPRIV ((struct mlfiPriv *) smfi_getpriv(ctx)) //////////////////////////////////////////////// // syslog a message // static void my_syslog(char *text); static void my_syslog(char *text) { pthread_mutex_lock(&syslog_mutex); openlog("dnsbl", LOG_PID, LOG_MAIL); syslog(LOG_NOTICE, "%s", text); closelog(); pthread_mutex_unlock(&syslog_mutex); } //////////////////////////////////////////////// // register a global string // static char* register_string(char *name); static char* register_string(char *name) { string_set::iterator i = all_strings.find(name); if (i != all_strings.end()) return *i; char *x = strdup(name); all_strings.insert(x); return x; } static char* next_token(char *delim); static char* next_token(char *delim) { char *name = strtok(NULL, delim); if (!name) return name; return register_string(name); } //////////////////////////////////////////////// // lookup an email address in the env_from or env_to maps // static char* lookup1(char *email, string_map map); static char* lookup1(char *email, string_map map) { string_map::iterator i = map.find(email); if (i != map.end()) return (*i).second; char *x = strchr(email, '@'); if (!x) return DEFAULT; x++; i = map.find(x); if (i != map.end()) return (*i).second; return DEFAULT; } //////////////////////////////////////////////// // lookup an email address in the env_from or env_to maps // this email address is passed in from sendmail, and will // always be enclosed in <>. It may have mixed case, just // as the mail client sent it. // static char* lookup(char* email, string_map map); static char* lookup(char* email, string_map map) { int n = strlen(email)-2; if (n < 1) return DEFAULT; // malformed char *key = strdup(email+1); key[n] = '\0'; for (int i=0; i<n; i++) key[i] = tolower(key[i]); char *rc = lookup1(key, map); free(key); return rc; } //////////////////////////////////////////////// // find the dnsbl with a specific name // static DNSBLP find_dnsbl(CONFIG &dc, char *name); static DNSBLP find_dnsbl(CONFIG &dc, char *name) { dnsblp_map::iterator i = dc.dnsbls.find(name); if (i == dc.dnsbls.end()) return NULL; return (*i).second; } //////////////////////////////////////////////// // find the dnsbll with a specific name // static DNSBLLP find_dnsbll(CONFIG &dc, char *name); static DNSBLLP find_dnsbll(CONFIG &dc, char *name) { dnsbllp_map::iterator i = dc.dnsblls.find(name); if (i == dc.dnsblls.end()) return NULL; return (*i).second; } //////////////////////////////////////////////// // find the envfrom map with a specific name // static string_map* find_from_map(CONFIG &dc, char *name); static string_map* find_from_map(CONFIG &dc, char *name) { from_map::iterator i = dc.env_from.find(name); if (i == dc.env_from.end()) return NULL; return (*i).second; } static string_map& really_find_from_map(CONFIG &dc, char *name); static string_map& really_find_from_map(CONFIG &dc, char *name) { string_map *sm = find_from_map(dc, name); if (!sm) { sm = new string_map; dc.env_from[name] = sm; } return *sm; } //////////////////////////////////////////////// // check a single dnsbl - we don't try very hard, just // using the default resolver retry settings. If we cannot // get an answer, we just accept the mail. The caller // must ensure thread safety. // static status check_single(int ip, DNSBL &bl); static status check_single(int ip, DNSBL &bl) { // make a dns question const u_char *src = (const u_char *)&ip; if (src[0] == 127) return oksofar; // don't do dns lookups on localhost char question[NS_MAXDNAME]; snprintf(question, sizeof(question), "%u.%u.%u.%u.%s.", src[3], src[2], src[1], src[0], bl.suffix); // ask the question u_char answer[NS_PACKETSZ]; int length = res_search(question, ns_c_in, ns_t_a, answer, sizeof(answer)); if (length < 0) return oksofar; // error in getting answer // parse the answer ns_msg handle; ns_rr rr; if (ns_initparse(answer, length, &handle) != 0) return oksofar; int rrnum = 0; while (ns_parserr(&handle, ns_s_an, rrnum++, &rr) == 0) { if (ns_rr_type(rr) == ns_t_a) { // we see an A record, implies blacklisted ip address return reject; } } return oksofar; } //////////////////////////////////////////////// // check the dnsbls specified for this recipient // static status check_dnsbl(mlfiPriv &priv, DNSBLLP dnsbllp, DNSBLP &rejectlist); static status check_dnsbl(mlfiPriv &priv, DNSBLLP dnsbllp, DNSBLP &rejectlist) { if (priv.authenticated) return oksofar; if (!dnsbllp) return oksofar; DNSBLL &dnsbll = *dnsbllp; for (DNSBLL::iterator i=dnsbll.begin(); i!=dnsbll.end(); i++) { DNSBLP dp = *i; // non null by construction status st; map<DNSBLP, status>::iterator f = priv.checked.find(dp); if (f == priv.checked.end()) { // have not checked this list yet pthread_mutex_lock(&resolve_mutex); st = check_single(priv.ip, *dp); pthread_mutex_unlock(&resolve_mutex); rejectlist = dp; priv.checked[dp] = st; } else { st = (*f).second; rejectlist = (*f).first; } if (st == reject) return st; } return oksofar; } //////////////////////////////////////////////// // start of sendmail milter interfaces // sfsistat mlfi_connect(SMFICTX *ctx, char *hostname, _SOCK_ADDR *hostaddr) { // allocate some private memory mlfiPriv *priv = new mlfiPriv; if (hostaddr->sa_family == AF_INET) { priv->ip = ((struct sockaddr_in *)hostaddr)->sin_addr.s_addr; } // save the private data smfi_setpriv(ctx, (void*)priv); // continue processing return SMFIS_CONTINUE; } sfsistat mlfi_envfrom(SMFICTX *ctx, char **from) { mlfiPriv &priv = *MLFIPRIV; priv.mailaddr = strdup(from[0]); priv.authenticated = (smfi_getsymval(ctx, "{auth_authen}") != NULL); return SMFIS_CONTINUE; } sfsistat mlfi_envrcpt(SMFICTX *ctx, char **rcpt) { DNSBLP rejectlist = NULL; // list that caused the reject status st = oksofar; mlfiPriv &priv = *MLFIPRIV; CONFIG &dc = *priv.pc; char *rcptaddr = rcpt[0]; char *dnsname = lookup(rcptaddr, dc.env_to_dnsbll); char *fromname = lookup(rcptaddr, dc.env_to_chkfrom); if ((strcmp(dnsname, BLACK) == 0) || (strcmp(fromname, BLACK) == 0)) { st = black; // two options to blacklist this recipient } else if (strcmp(fromname, WHITE) == 0) { st = white; } else { // check an env_from map string_map *sm = find_from_map(dc, fromname); if (sm != NULL) { fromname = lookup(priv.mailaddr, *sm); // returns default if name not in map if (strcmp(fromname, BLACK) == 0) { st = black; // blacklist this envelope from value } if (strcmp(fromname, WHITE) == 0) { st = white; // blacklist this envelope from value } } } if ((st == oksofar) && (strcmp(dnsname, WHITE) != 0)) { // check dns lists st = check_dnsbl(priv, find_dnsbll(dc, dnsname), rejectlist); } if (st == reject) { // reject the recipient based on some dnsbl char adr[sizeof "255.255.255.255"]; adr[0] = '\0'; const char *rc = inet_ntop(AF_INET, (const u_char *)&priv.ip, adr, sizeof(adr)); char buf[2000]; snprintf(buf, sizeof(buf), rejectlist->message, adr, adr); smfi_setreply(ctx, "550", "5.7.1", buf); return SMFIS_REJECT; } else if (st == black) { // reject the recipient based on blacklisting either from or to smfi_setreply(ctx, "550", "5.7.1", "no such user"); return SMFIS_REJECT; } else { // accept the recipient return SMFIS_CONTINUE; } } sfsistat mlfi_eom_or_abort(SMFICTX *ctx) { mlfiPriv &priv = *MLFIPRIV; if (priv.mailaddr) { free(priv.mailaddr); priv.mailaddr = NULL; } return SMFIS_CONTINUE; } sfsistat mlfi_close(SMFICTX *ctx) { mlfiPriv *priv = MLFIPRIV; if (!priv) return SMFIS_CONTINUE; delete priv; smfi_setpriv(ctx, NULL); return SMFIS_CONTINUE; } struct smfiDesc smfilter = { "DNSBL", // filter name SMFI_VERSION, // version code -- do not change SMFIF_DELRCPT, // flags mlfi_connect, // connection info filter NULL, // SMTP HELO command filter mlfi_envfrom, // envelope sender filter mlfi_envrcpt, // envelope recipient filter NULL, // header filter NULL, // end of header NULL, // body block filter mlfi_eom_or_abort, // end of message mlfi_eom_or_abort, // message aborted mlfi_close, // connection cleanup }; static void dumpit(char *name, string_map map); static void dumpit(char *name, string_map map) { fprintf(stderr, "\n"); for (string_map::iterator i=map.begin(); i!=map.end(); i++) { fprintf(stderr, "%s %s->%s\n", name, (*i).first, (*i).second); } } static void dumpit(from_map map); static void dumpit(from_map map) { fprintf(stderr, "\n"); for (from_map::iterator i=map.begin(); i!=map.end(); i++) { fprintf(stderr, "envfrom map %s\n", (*i).first); string_map *sm = (*i).second; dumpit("envelope from", *sm); } } static void dumpit(); static void dumpit() { CONFIG &dc = *config; fprintf(stderr, "dnsbls\n"); for (dnsblp_map::iterator i=dc.dnsbls.begin(); i!=dc.dnsbls.end(); i++) { fprintf(stderr, "%s %s %s\n", (*i).first, (*i).second->suffix, (*i).second->message); } fprintf(stderr, "dnsbl_lists\n"); for (dnsbllp_map::iterator i=dc.dnsblls.begin(); i!=dc.dnsblls.end(); i++) { char *name = (*i).first; DNSBLL &dl = *((*i).second); fprintf(stderr, "%s", name); for (DNSBLL::iterator j=dl.begin(); j!=dl.end(); j++) { DNSBL &d = **j; fprintf(stderr, " %s", d.suffix); } fprintf(stderr, "\n"); } } //////////////////////////////////////////////// // load a single config file // static void load_conf(CONFIG &dc, char *fn); static void load_conf(CONFIG &dc, char *fn) { dc.config_files.push_back(fn); map<char*, int, ltstr> commands; enum {dummy, dnsbl, dnsbll, envfrom, envto, include}; commands["dnsbl" ] = dnsbl; commands["dnsbl_list"] = dnsbll; commands["env_from" ] = envfrom; commands["env_to" ] = envto; commands["include" ] = include; const int LINE_SIZE = 2000; ifstream is(fn); if (is.fail()) return; char line[LINE_SIZE]; char orig[LINE_SIZE]; char *delim = " \t"; int curline = 0; while (!is.eof()) { is.getline(line, LINE_SIZE); snprintf(orig, sizeof(orig), "%s", line); curline++; int n = strlen(line); for (int i=0; i<n; i++) line[i] = tolower(line[i]); char *cmd = strtok(line, delim); if (cmd && (cmd[0] != '#') && (cmd[0] != '\0')) { // have a decent command bool processed = false; switch (commands[cmd]) { case dnsbl: { // have a new dnsbl to use char *name = next_token(delim); if (!name) break; // no name name if (find_dnsbl(dc, name)) break; // duplicate entry char *suff = strtok(NULL, delim); if (!suff) break; // no dns suffic char *msg = suff + strlen(suff); if ((msg - line) >= strlen(orig)) break; // line ended with the dns suffix msg = strchr(msg+1, '\''); if (!msg) break; // no reply message template msg++; // move over the leading ' if ((msg - line) >= strlen(orig)) break; // line ended with the leading quote char *last = strchr(msg, '\''); if (!last) break; // no trailing quote *last = '\0'; // make it a null terminator dc.dnsbls[name] = new DNSBL(register_string(suff), register_string(msg)); processed = true; } break; case dnsbll: { // define a new combination of dnsbls char *name = next_token(delim); if (!name) break; if (find_dnsbll(dc, name)) break; // duplicate entry char *list = next_token(delim); if (!list || (*list == '\0') || (*list == '#')) break; DNSBLLP d = new DNSBLL; DNSBLP p = find_dnsbl(dc, list); if (p) d->push_back(p); while (true) { list = next_token(delim); if (!list || (*list == '\0') || (*list == '#')) break; DNSBLP p = find_dnsbl(dc, list); if (p) d->push_back(p); } dc.dnsblls[name] = d; processed = true; } break; case envfrom: { // add an entry into the named string_map char *name = next_token(delim); if (!name) break; char *from = next_token(delim); if (!from) break; char *list = next_token(delim); if (!list) break; if ((strcmp(list, WHITE) == 0) || (strcmp(list, BLACK) == 0)) { string_map &fm = really_find_from_map(dc, name); fm[from] = list; processed = true; } else { // list may be the name of a previously defined from_map string_map *m = find_from_map(dc, list); if (m && (strcmp(list,name) != 0)) { string_map &pm = *m; string_map &fm = really_find_from_map(dc, name); fm.insert(pm.begin(), pm.end()); processed = true; } } } break; case envto: { // define the dnsbl_list and env_from maps to use for this recipient char *to = next_token(delim); if (!to) break; char *list = next_token(delim); if (!list) break; char *from = next_token(delim); if (!from) break; dc.env_to_dnsbll[to] = list; dc.env_to_chkfrom[to] = from; processed = true; } break; case include: { char *fn = next_token(delim); if (fn) { bool ok = true; for (string_list::iterator i=dc.config_files.begin(); i!=dc.config_files.end(); i++) { char *f = *i; if (strcmp(f, fn) == 0) { my_syslog("recursive include file detected"); ok = false; break; } } if (ok) { load_conf(dc, fn); processed = true; } } } break; default: { } break; } if (!processed) { pthread_mutex_lock(&syslog_mutex); openlog("dnsbl", LOG_PID, LOG_MAIL); syslog(LOG_ERR, "ignoring file %s line %d : %s\n", fn, curline, orig); closelog(); pthread_mutex_unlock(&syslog_mutex); } } } is.close(); } //////////////////////////////////////////////// // reload the config // static CONFIG* new_conf(); static CONFIG* new_conf() { my_syslog("loading new configuration"); CONFIG *newc = new CONFIG; load_conf(*newc, "dnsbl.conf"); newc->load_time = time(NULL); return newc; } //////////////////////////////////////////////// // thread to watch the old config files for changes // and reload when needed. we also cleanup old // configs whose reference count has gone to zero. // static void* config_loader(void *arg); static void* config_loader(void *arg) { typedef set<CONFIG *> configp_set; configp_set old_configs; while (true) { sleep(180); // look for modifications every 3 minutes CONFIG &dc = *config; time_t then = dc.load_time; struct stat st; bool reload = false; for (string_list::iterator i=dc.config_files.begin(); i!=dc.config_files.end(); i++) { char *fn = *i; if (stat(fn, &st)) reload = true; // file disappeared else if (st.st_mtime > then) reload = true; // file modified if (reload) break; } if (reload) { CONFIG *newc = new_conf(); // replace the global config pointer pthread_mutex_lock(&config_mutex); CONFIG *old = config; config = newc; pthread_mutex_unlock(&config_mutex); // dumpit(env_from); // dumpit("envelope to dnsbl", env_to_dnsbl); // dumpit("envelope to check from", env_to_chkfrom); // dumpit(); if (old) old_configs.insert(old); } // now look for old configs with zero ref counts for (configp_set::iterator i=old_configs.begin(); i!=old_configs.end(); ) { CONFIG *old = *i; if (!old->reference_count) { delete old; // destructor does all the work old_configs.erase(i++); } else i++; } } } static void usage(char *prog); static void usage(char *prog) { fprintf(stderr, "Usage: %s -p socket-addr [-t timeout]\n", prog); fprintf(stderr, "where socket-addr is for the connection to sendmail and should be one of\n"); fprintf(stderr, " inet:port@local-ip-address\n"); fprintf(stderr, " local:local-domain-socket-file-name\n"); } int main(int argc, char**argv) { bool setconn = FALSE; int c; const char *args = "p:s:h"; extern char *optarg; // Process command line options while ((c = getopt(argc, argv, args)) != -1) { switch (c) { case 'p': if (optarg == NULL || *optarg == '\0') { fprintf(stderr, "Illegal conn: %s\n", optarg); exit(EX_USAGE); } if (smfi_setconn(optarg) == MI_FAILURE) { fprintf(stderr, "smfi_setconn failed\n"); exit(EX_SOFTWARE); } if (strncasecmp(optarg, "unix:", 5) == 0) unlink(optarg + 5); else if (strncasecmp(optarg, "local:", 6) == 0) unlink(optarg + 6); setconn = TRUE; break; case 't': if (optarg == NULL || *optarg == '\0') { fprintf(stderr, "Illegal timeout: %s\n", optarg); exit(EX_USAGE); } if (smfi_settimeout(atoi(optarg)) == MI_FAILURE) { fprintf(stderr, "smfi_settimeout failed\n"); exit(EX_SOFTWARE); } break; case 'h': default: usage(argv[0]); exit(EX_USAGE); } } if (!setconn) { fprintf(stderr, "%s: Missing required -p argument\n", argv[0]); usage(argv[0]); exit(EX_USAGE); } if (smfi_register(smfilter) == MI_FAILURE) { fprintf(stderr, "smfi_register failed\n"); exit(EX_UNAVAILABLE); } // switch to background mode if (daemon(1,0) < 0) { fprintf(stderr, "daemon() call failed\n"); exit(EX_UNAVAILABLE); } // initialize the thread sync objects pthread_mutex_init(&config_mutex, 0); pthread_mutex_init(&syslog_mutex, 0); pthread_mutex_init(&resolve_mutex, 0); // load the initial config config = new_conf(); // only create threads after the fork() in daemon pthread_t tid; if (pthread_create(&tid, 0, config_loader, 0)) my_syslog("failed to create config loader thread"); if (pthread_detach(tid)) my_syslog("failed to detach config loader thread"); // write the pid const char *pidpath = "/var/run/dnsbl.pid"; unlink(pidpath); FILE *f = fopen(pidpath, "w"); if (f) { fprintf(f, "-%d\n", (u_int)getpgrp()); fclose(f); } int rc = smfi_main(); }