123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471 |
- /*
- * Copyright (c) 2019 Markus Hennecke <markus-hennecke@markus-hennecke.de>
- *
- * 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 <arpa/inet.h>
- #include <sys/socket.h>
- #include <sys/stat.h>
- #include <sys/types.h>
- #include <sys/event.h>
- #include <sys/time.h>
- #include <sys/wait.h>
- #include <err.h>
- #include <errno.h>
- #include <fcntl.h>
- #include <netdb.h>
- #include <pwd.h>
- #include <regex.h>
- #include <signal.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <syslog.h>
- #include <unistd.h>
- #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;
- }
|