/* * Copyright (c) 2019 Markus Hennecke * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "dnsbl.h" #include "log.h" int run = 1; int reconfig = 0; regex_t rx_spamd_connect; #define CONFFILE "/etc/mail/spamd-dnsbld.conf" #define SPAMD_USER "_spamd" #define PATH_SPAMDB "/usr/sbin/spamdb" #define MAXLINELEN 8192 #define RX_SPAMLOG "spamd\\[[0-9]+\\]: " \ "([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})" \ ": connected \\([0-9]+/[0-9]+\\)" extern int h_errno; static char *rx_error_string(int, regex_t *); static void sighandler(int); static bool lookup_blacklist(const char *, in_addr_t); static bool handle_async_lookup(struct config *, in_addr_t); static void handle_logfile(struct config *); static void usage(void); __dead void usage(void) { fprintf(stderr, "usage: %s [-dhnv] [-f configfile]\n", getprogname()); exit(1); } char * rx_error_string(int rc, regex_t *rx) { static char error[1024]; regerror(rc, rx, error, sizeof(error)); return error; } struct spamlist * spamlist_new(const char *name, const char *dns) { struct spamlist *l = calloc(1, sizeof(struct spamlist)); if (l == NULL) fatal(NULL); l->name = strdup(name); if (l->name == NULL) fatal(NULL); l->dns = strdup(dns); if (l->dns == NULL) fatal(NULL); return l; } void spamlist_free(struct spamlist *l) { if (l) { free(l->name); free(l->dns); free(l); } } struct config * config_new(void) { struct config *c = calloc(1, sizeof(struct config)); if (c == NULL) fatal(NULL); TAILQ_INIT(&c->lists); return c; } void config_free(struct config *c) { if (c) { struct spamlist *l; while ((l = TAILQ_FIRST(&c->lists)) != NULL) { TAILQ_REMOVE(&c->lists, l, entry); spamlist_free(l); } if (c->log) fclose(c->log); free(c->logfile); free(c); } } void config_add_spamlist(struct config *c, const char *name, const char *dns) { struct spamlist *l = spamlist_new(name, dns); TAILQ_INSERT_TAIL(&c->lists, l, entry); } void sighandler(int sig) { switch (sig) { case SIGCHLD: { wait(NULL); break; } case SIGHUP: reconfig = 1; break; case SIGTERM: case SIGINT: run = 0; break; default: break; } } bool lookup_blacklist(const char *bl, in_addr_t addr) { char *revipbl; const uint32_t hoaddr = ntohl(addr); const uint8_t *a = (const uint8_t *)&hoaddr; struct in_addr **ra; if (asprintf(&revipbl, "%d.%d.%d.%d.%s", a[0], a[1], a[2], a[3], bl) == -1) fatal(NULL); log_debug("Looking up: %s", revipbl); struct hostent *info = gethostbyname2(revipbl, AF_INET); if (info == NULL) { if (h_errno == HOST_NOT_FOUND) return false; log_warnx("gethostbyname2: %s", hstrerror(h_errno)); return false; } for (ra = (struct in_addr **)info->h_addr_list; *ra; ++ra) { const uint32_t saddr = (*ra)->s_addr; return (((saddr & 0xff) == 127) && (((saddr >> 8) & 0xff) == 0)); } return false; } bool handle_async_lookup(struct config *cfg, in_addr_t addr) { pid_t pid = fork(); if (pid == 0) { // Child signal(SIGCHLD, SIG_DFL); signal(SIGHUP, SIG_DFL); struct passwd *pw = cfg->pw; if (setgroups(1, &pw->pw_gid) || setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) || setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid)) fatal("can't drop privileges"); struct spamlist *l; struct in_addr ia; ia.s_addr = addr; const char *ip = inet_ntoa(ia); TAILQ_FOREACH(l, &cfg->lists, entry) { if (lookup_blacklist(l->dns, addr)) { log_info("%s didn't like %s", l->name, ip); execl(PATH_SPAMDB, "spamdb", "-t", "-a", ip, NULL); fatal("execl"); } } log_info("%s was clean", inet_ntoa(ia)); if (cfg->autowhitelist) { execl(PATH_SPAMDB, "spamdb", "-a", ip, NULL); fatal("execl"); } exit(0); } else if (pid > 0) { // Parent return true; } else { fatal(NULL); } } void handle_logfile(struct config *cfg) { char line[MAXLINELEN]; while ((fgets(line, MAXLINELEN, cfg->log)) != NULL) { regmatch_t match[2]; int rc = regexec(&rx_spamd_connect, line, 2, match, 0); switch (rc) { case REG_NOMATCH: break; case 0: { const regmatch_t *m = &match[1]; const size_t len = m->rm_eo - m->rm_so; char *ip = strndup(line + m->rm_so, len); handle_async_lookup(cfg, inet_addr(ip)); free(ip); break; } default: log_warn("regexec: %s", rx_error_string(rc, &rx_spamd_connect)); break; } } if (feof(cfg->log)) clearerr(cfg->log); else if (ferror(cfg->log)) fatal(NULL); } bool config_check(struct config *cfg) { struct stat sb; if (cfg->logfile == NULL) { cfg->logfile = strdup("/var/log/spamd"); if (cfg->logfile == NULL) fatal(NULL); } if (strncmp("/var/log/", cfg->logfile, 9) != 0) { log_warnx("spamd logfile %s not under /var/log/", cfg->logfile); return false; } if (stat(cfg->logfile, &sb) == -1) { log_warnx("unable to stat logfile %s", cfg->logfile); return false; } if (!(sb.st_mode & S_IFREG)) { log_warnx("logfile %s is no regular file", cfg->logfile); return false; } if (! TAILQ_FIRST(&cfg->lists)) { log_warnx("no dns blacklists configured"); return false; } return true; } struct config * config_reload(struct config *cfg) { struct config *n = config_new(); if (!config_parse(n, cfg->config_file) && !config_check(n)) { log_warnx("Unable to reload config file %s", cfg->config_file); config_free(n); return NULL; } /* This will close the opened log and remove the events from the * kqueue. */ config_free(cfg); return n; } int main(int argc, char **argv) { int rc, ch; int queue, nev; const char *conffile = CONFFILE; struct kevent ev[2]; bool noaction = false; int debug = 0, verbose = 0; struct config *cfg, *ncfg; struct passwd *pw; int logdest = LOG_TO_STDERR; if (geteuid() != 0) errx(1, "Need root privileges to run."); if ((pw = getpwnam(SPAMD_USER)) == NULL) errx(1, "unknown user %s", SPAMD_USER); while ((ch = getopt(argc, argv, "df:nv")) != -1) { switch (ch) { case 'd': debug = 2; break; case 'f': conffile = optarg; break; case 'n': noaction = true; break; case 'v': verbose++; break; default: usage(); /* NOT REACHED */ } } argc -= optind; argv += optind; setprogname("spamd-dnsbld"); if (!debug) logdest |= LOG_TO_SYSLOG; log_init(logdest, verbose, LOG_DAEMON); cfg = config_new(); if (! config_parse(cfg, conffile)) errx(1, "Unable to parse config file"); if (noaction) { fprintf(stderr, "configuration OK\n"); exit(0); } rc = regcomp(&rx_spamd_connect, RX_SPAMLOG, REG_EXTENDED); if (rc != 0) errx(1, "%s", rx_error_string(rc, &rx_spamd_connect)); logdest = debug ? LOG_TO_STDERR : LOG_TO_SYSLOG; log_init(logdest, verbose, LOG_DAEMON); if (!debug) { if (daemon(0, 0) == -1) fatal("daemon"); } signal(SIGCHLD, sighandler); signal(SIGHUP, sighandler); signal(SIGTERM, sighandler); signal(SIGINT, sighandler); if ((queue = kqueue()) == -1) fatal("kqueue"); if (unveil(conffile, "r") == -1) fatal("unveil"); if (unveil(PATH_SPAMDB, "x") == -1) fatal("unveil"); if (unveil("/var/log", "r") == -1) fatal("unveil"); if (pledge("stdio dns proc exec rpath id", NULL) == -1) fatal("pledge"); reconfig: cfg->pw = pw; if ((cfg->log = fopen(cfg->logfile, "r")) == NULL) fatal("%s", cfg->logfile); if (fseek(cfg->log, 0L, SEEK_END) < 0) fatal("fseek"); add_events: EV_SET(&ev[0], fileno(cfg->log), EVFILT_READ, EV_ENABLE | EV_ADD | EV_CLEAR, 0, 0, NULL); EV_SET(&ev[1], fileno(cfg->log), EVFILT_VNODE, EV_ENABLE | EV_ADD | EV_CLEAR, NOTE_DELETE | NOTE_RENAME, 0, NULL); if (kevent(queue, ev, 2, NULL, 0, NULL) < 0) fatal(NULL); log_info("Starting event loop..."); struct kevent ke; while (run) { nev = kevent(queue, NULL, 0, &ke, 1, NULL); if (nev == -1) { if (errno != EINTR) { log_warn("kevent"); break; } } if (nev > 0) { if (ke.filter == EVFILT_READ) { handle_logfile(cfg); } else if (ke.filter == EVFILT_VNODE) { if (ke.fflags & NOTE_RENAME) { log_info("%s renamed...", cfg->logfile); cfg->log = freopen(cfg->logfile, "r", cfg->log); if (cfg->log == NULL) fatal("freopen"); goto add_events; } else if (ke.fflags & NOTE_DELETE) { log_info("%s deleted...", cfg->logfile); cfg->log = freopen(cfg->logfile, "r", cfg->log); if (cfg->log == NULL) fatal("freopen"); goto add_events; } } } if (reconfig) { reconfig = 0; ncfg = config_reload(cfg); if (ncfg) { log_info("Config file reloaded successfully"); cfg = ncfg; goto reconfig; } } } close(queue); regfree(&rx_spamd_connect); config_free(cfg); // Wait up to 2 seconds for child processes before exiting alarm(2); while (wait(NULL) != -1); alarm(0); return 0; }