/* $OpenBSD: grey.c,v 1.67 2023/03/08 04:43:06 guenther Exp $ */ /* * Copyright (c) 2004-2006 Bob Beck. All rights reserved. * * 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 #include "grey.h" #include "sync.h" extern time_t passtime, greyexp, whiteexp, trapexp; extern struct syslog_data sdata; extern struct passwd *pw; extern u_short cfg_port; extern pid_t jail_pid; extern FILE *trapcfg; extern FILE *grey; extern int debug; extern int syncsend; extern int greyback[2]; /* From netinet/in.h, but only _KERNEL_ gets them. */ #define satosin(sa) ((struct sockaddr_in *)(sa)) #define satosin6(sa) ((struct sockaddr_in6 *)(sa)) void configure_spamd(char **, u_int, FILE *); int configure_pf(char **, int); char *dequotetolower(const char *); void readsuffixlists(void); void freeaddrlists(void); int addwhiteaddr(char *); int addtrapaddr(char *); int db_addrstate(DB *, char *); int greyscan(char *); int trapcheck(DB *, char *); int twupdate(char *, char *, char *, char *, char *); int twread(char *); int greyreader(void); void greyscanner(void); u_int whitecount, whitealloc; u_int trapcount, trapalloc; char **whitelist; char **traplist; char *traplist_name = "spamd-greytrap"; char *traplist_msg = "\"Your address %A has mailed to spamtraps here\\n\""; pid_t db_pid = -1; int pfdev; struct db_change { SLIST_ENTRY(db_change) entry; char * key; void * data; size_t dsiz; int act; }; #define DBC_ADD 1 #define DBC_DEL 2 /* db pending changes list */ SLIST_HEAD(, db_change) db_changes = SLIST_HEAD_INITIALIZER(db_changes); struct mail_addr { SLIST_ENTRY(mail_addr) entry; char addr[MAX_MAIL]; }; /* list of suffixes that must match TO: */ SLIST_HEAD(, mail_addr) match_suffix = SLIST_HEAD_INITIALIZER(match_suffix); char *alloweddomains_file = PATH_SPAMD_ALLOWEDDOMAINS; char *low_prio_mx_ip; time_t startup; static char *pargv[11]= { "pfctl", "-p", "/dev/pf", "-q", "-t", "spamd-white", "-T", "replace", "-f", "-", NULL }; /* If the parent gets a signal, kill off the children and exit */ static void sig_term_chld(int sig) { if (db_pid != -1) kill(db_pid, SIGTERM); if (jail_pid != -1) kill(jail_pid, SIGTERM); _exit(1); } /* * Greatly simplified version from spamd_setup.c - only * sends one blacklist to an already open stream. Has no need * to collapse cidr ranges since these are only ever single * host hits. */ void configure_spamd(char **addrs, u_int count, FILE *sdc) { u_int i; /* XXX - doesn't support IPV6 yet */ fprintf(sdc, "%s;", traplist_name); if (count != 0) { fprintf(sdc, "%s;inet;%u", traplist_msg, count); for (i = 0; i < count; i++) fprintf(sdc, ";%s/32", addrs[i]); } fputc('\n', sdc); if (fflush(sdc) == EOF) syslog_r(LOG_DEBUG, &sdata, "configure_spamd: fflush failed (%m)"); } int configure_pf(char **addrs, int count) { FILE *pf = NULL; int i, pdes[2], status; pid_t pid; char *fdpath; struct sigaction sa; sigfillset(&sa.sa_mask); sa.sa_flags = SA_RESTART; sa.sa_handler = sig_term_chld; if (debug) fprintf(stderr, "configure_pf - device on fd %d\n", pfdev); /* Because /dev/fd/ only contains device nodes for 0-63 */ if (pfdev < 1 || pfdev > 63) return(-1); if (asprintf(&fdpath, "/dev/fd/%d", pfdev) == -1) return(-1); pargv[2] = fdpath; if (pipe(pdes) != 0) { syslog_r(LOG_INFO, &sdata, "pipe failed (%m)"); free(fdpath); fdpath = NULL; return(-1); } signal(SIGCHLD, SIG_DFL); switch (pid = fork()) { case -1: syslog_r(LOG_INFO, &sdata, "fork failed (%m)"); free(fdpath); fdpath = NULL; close(pdes[0]); close(pdes[1]); sigaction(SIGCHLD, &sa, NULL); return(-1); case 0: /* child */ close(pdes[1]); if (pdes[0] != STDIN_FILENO) { dup2(pdes[0], STDIN_FILENO); close(pdes[0]); } execvp(PATH_PFCTL, pargv); syslog_r(LOG_ERR, &sdata, "can't exec %s:%m", PATH_PFCTL); _exit(1); } /* parent */ free(fdpath); fdpath = NULL; close(pdes[0]); pf = fdopen(pdes[1], "w"); if (pf == NULL) { syslog_r(LOG_INFO, &sdata, "fdopen failed (%m)"); close(pdes[1]); sigaction(SIGCHLD, &sa, NULL); return(-1); } for (i = 0; i < count; i++) if (addrs[i] != NULL) fprintf(pf, "%s/32\n", addrs[i]); fclose(pf); waitpid(pid, &status, 0); if (WIFEXITED(status) && WEXITSTATUS(status) != 0) syslog_r(LOG_ERR, &sdata, "%s returned status %d", PATH_PFCTL, WEXITSTATUS(status)); else if (WIFSIGNALED(status)) syslog_r(LOG_ERR, &sdata, "%s died on signal %d", PATH_PFCTL, WTERMSIG(status)); sigaction(SIGCHLD, &sa, NULL); return(0); } char * dequotetolower(const char *addr) { static char buf[MAX_MAIL]; char *cp; if (*addr == '<') addr++; (void) strlcpy(buf, addr, sizeof(buf)); cp = strrchr(buf, '>'); if (cp != NULL && cp[1] == '\0') *cp = '\0'; cp = buf; while (*cp != '\0') { *cp = tolower((unsigned char)*cp); cp++; } return(buf); } void readsuffixlists(void) { FILE *fp; char *buf; size_t len; struct mail_addr *m; while (!SLIST_EMPTY(&match_suffix)) { m = SLIST_FIRST(&match_suffix); SLIST_REMOVE_HEAD(&match_suffix, entry); free(m); } if ((fp = fopen(alloweddomains_file, "r")) != NULL) { while ((buf = fgetln(fp, &len))) { /* strip white space-characters */ while (len > 0 && isspace((unsigned char)buf[len-1])) len--; while (len > 0 && isspace((unsigned char)*buf)) { buf++; len--; } if (len == 0) continue; /* jump over comments and blank lines */ if (*buf == '#' || *buf == '\n') continue; if (buf[len-1] == '\n') len--; if ((len + 1) > sizeof(m->addr)) { syslog_r(LOG_ERR, &sdata, "line too long in %s - file ignored", alloweddomains_file); goto bad; } if ((m = malloc(sizeof(struct mail_addr))) == NULL) goto bad; memcpy(m->addr, buf, len); m->addr[len]='\0'; syslog_r(LOG_ERR, &sdata, "got suffix %s", m->addr); SLIST_INSERT_HEAD(&match_suffix, m, entry); } } return; bad: while (!SLIST_EMPTY(&match_suffix)) { m = SLIST_FIRST(&match_suffix); SLIST_REMOVE_HEAD(&match_suffix, entry); free(m); } } void freeaddrlists(void) { int i; if (whitelist != NULL) for (i = 0; i < whitecount; i++) { free(whitelist[i]); whitelist[i] = NULL; } whitecount = 0; if (traplist != NULL) { for (i = 0; i < trapcount; i++) { free(traplist[i]); traplist[i] = NULL; } } trapcount = 0; } /* validate, then add to list of addrs to whitelist */ int addwhiteaddr(char *addr) { struct addrinfo hints, *res; char ch; memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_INET; /*for now*/ hints.ai_socktype = SOCK_DGRAM; /*dummy*/ hints.ai_protocol = IPPROTO_UDP; /*dummy*/ hints.ai_flags = AI_NUMERICHOST; if (getaddrinfo(addr, NULL, &hints, &res) != 0) return(-1); /* Check spamd blacklists in main process. */ if (send(greyback[0], res->ai_addr, res->ai_addr->sa_len, 0) == -1) { syslog_r(LOG_ERR, &sdata, "%s: send: %m", __func__); } else { if (recv(greyback[0], &ch, sizeof(ch), 0) == 1) { if (ch == '1') { syslog_r(LOG_DEBUG, &sdata, "%s blacklisted, removing from whitelist", addr); freeaddrinfo(res); return(-1); } } } if (whitecount == whitealloc) { char **tmp; tmp = reallocarray(whitelist, whitealloc + 1024, sizeof(char *)); if (tmp == NULL) { freeaddrinfo(res); return(-1); } whitelist = tmp; whitealloc += 1024; } whitelist[whitecount] = strdup(addr); if (whitelist[whitecount] == NULL) { freeaddrinfo(res); return(-1); } whitecount++; freeaddrinfo(res); return(0); } /* validate, then add to list of addrs to traplist */ int addtrapaddr(char *addr) { struct addrinfo hints, *res; memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_INET; /*for now*/ hints.ai_socktype = SOCK_DGRAM; /*dummy*/ hints.ai_protocol = IPPROTO_UDP; /*dummy*/ hints.ai_flags = AI_NUMERICHOST; if (getaddrinfo(addr, NULL, &hints, &res) == 0) { if (trapcount == trapalloc) { char **tmp; tmp = reallocarray(traplist, trapalloc + 1024, sizeof(char *)); if (tmp == NULL) { freeaddrinfo(res); return(-1); } traplist = tmp; trapalloc += 1024; } traplist[trapcount] = strdup(addr); if (traplist[trapcount] == NULL) { freeaddrinfo(res); return(-1); } trapcount++; freeaddrinfo(res); } else return(-1); return(0); } static int queue_change(char *key, char *data, size_t dsiz, int act) { struct db_change *dbc; if ((dbc = malloc(sizeof(*dbc))) == NULL) { syslog_r(LOG_DEBUG, &sdata, "malloc failed (queue change)"); return(-1); } if ((dbc->key = strdup(key)) == NULL) { syslog_r(LOG_DEBUG, &sdata, "malloc failed (queue change)"); free(dbc); return(-1); } if ((dbc->data = malloc(dsiz)) == NULL) { syslog_r(LOG_DEBUG, &sdata, "malloc failed (queue change)"); free(dbc->key); free(dbc); return(-1); } memcpy(dbc->data, data, dsiz); dbc->dsiz = dsiz; dbc->act = act; syslog_r(LOG_DEBUG, &sdata, "queueing %s of %s", ((act == DBC_ADD) ? "add" : "deletion"), dbc->key); SLIST_INSERT_HEAD(&db_changes, dbc, entry); return(0); } static int do_changes(DB *db) { DBT dbk, dbd; struct db_change *dbc; int ret = 0; while (!SLIST_EMPTY(&db_changes)) { dbc = SLIST_FIRST(&db_changes); switch (dbc->act) { case DBC_ADD: memset(&dbk, 0, sizeof(dbk)); dbk.size = strlen(dbc->key); dbk.data = dbc->key; memset(&dbd, 0, sizeof(dbd)); dbd.size = dbc->dsiz; dbd.data = dbc->data; if (db->put(db, &dbk, &dbd, 0)) { db->sync(db, 0); syslog_r(LOG_ERR, &sdata, "can't add %s to spamd db (%m)", dbc->key); ret = -1; } db->sync(db, 0); break; case DBC_DEL: memset(&dbk, 0, sizeof(dbk)); dbk.size = strlen(dbc->key); dbk.data = dbc->key; if (db->del(db, &dbk, 0)) { syslog_r(LOG_ERR, &sdata, "can't delete %s from spamd db (%m)", dbc->key); ret = -1; } break; default: syslog_r(LOG_ERR, &sdata, "Unrecognized db change"); ret = -1; } free(dbc->key); dbc->key = NULL; free(dbc->data); dbc->data = NULL; dbc->act = 0; dbc->dsiz = 0; SLIST_REMOVE_HEAD(&db_changes, entry); free(dbc); } return(ret); } /* -1=error, 0=notfound, 1=TRAPPED, 2=WHITE */ int db_addrstate(DB *db, char *key) { DBT dbk, dbd; struct gdata gd; memset(&dbk, 0, sizeof(dbk)); dbk.size = strlen(key); dbk.data = key; memset(&dbd, 0, sizeof(dbd)); switch (db->get(db, &dbk, &dbd, 0)) { case 1: /* not found */ return (0); case 0: if (gdcopyin(&dbd, &gd) != -1) return (gd.pcount == -1 ? 1 : 2); /* FALLTHROUGH */ default: /* error */ return (-1); } } int greyscan(char *dbname) { HASHINFO hashinfo; DBT dbk, dbd; DB *db; struct gdata gd; int r; char *a = NULL; size_t asiz = 0; time_t now = time(NULL); /* walk db, expire, and whitelist */ memset(&hashinfo, 0, sizeof(hashinfo)); db = dbopen(dbname, O_EXLOCK|O_RDWR, 0600, DB_HASH, &hashinfo); if (db == NULL) { syslog_r(LOG_INFO, &sdata, "dbopen failed (%m)"); return(-1); } memset(&dbk, 0, sizeof(dbk)); memset(&dbd, 0, sizeof(dbd)); for (r = db->seq(db, &dbk, &dbd, R_FIRST); !r; r = db->seq(db, &dbk, &dbd, R_NEXT)) { if ((dbk.size < 1) || gdcopyin(&dbd, &gd) == -1) { syslog_r(LOG_ERR, &sdata, "bogus entry in spamd database"); goto bad; } if (asiz < dbk.size + 1) { char *tmp; tmp = reallocarray(a, dbk.size, 2); if (tmp == NULL) goto bad; a = tmp; asiz = dbk.size * 2; } memset(a, 0, asiz); memcpy(a, dbk.data, dbk.size); if (gd.expire <= now && gd.pcount != -2) { /* get rid of entry */ if (queue_change(a, NULL, 0, DBC_DEL) == -1) goto bad; } else if (gd.pcount == -1) { /* this is a greytrap hit */ if ((addtrapaddr(a) == -1) && (queue_change(a, NULL, 0, DBC_DEL) == -1)) goto bad; } else if (gd.pcount >= 0 && gd.pass <= now) { int tuple = 0; char *cp; int state; /* * if not already TRAPPED, * add address to whitelist * add an address-keyed entry to db */ cp = strchr(a, '\n'); if (cp != NULL) { tuple = 1; *cp = '\0'; } state = db_addrstate(db, a); if (state != 1 && addwhiteaddr(a) == -1) { if (cp != NULL) *cp = '\n'; if (queue_change(a, NULL, 0, DBC_DEL) == -1) goto bad; } if (tuple && state <= 0) { if (cp != NULL) *cp = '\0'; /* re-add entry, keyed only by ip */ gd.expire = now + whiteexp; dbd.size = sizeof(gd); dbd.data = &gd; if (queue_change(a, (void *) &gd, sizeof(gd), DBC_ADD) == -1) goto bad; syslog_r(LOG_DEBUG, &sdata, "whitelisting %s in %s", a, dbname); } if (debug) fprintf(stderr, "whitelisted %s\n", a); } } (void) do_changes(db); db->close(db); db = NULL; configure_pf(whitelist, whitecount); configure_spamd(traplist, trapcount, trapcfg); freeaddrlists(); free(a); a = NULL; return(0); bad: (void) do_changes(db); db->close(db); db = NULL; freeaddrlists(); free(a); a = NULL; return(-1); } int trapcheck(DB *db, char *to) { int i, j, smatch = 0; DBT dbk, dbd; struct mail_addr *m; char * trap; size_t s; trap = dequotetolower(to); if (!SLIST_EMPTY(&match_suffix)) { s = strlen(trap); SLIST_FOREACH(m, &match_suffix, entry) { j = s - strlen(m->addr); if ((j >= 0) && (strcasecmp(trap+j, m->addr) == 0)) smatch = 1; } if (!smatch) /* no suffixes match, so trap it */ return (0); } memset(&dbk, 0, sizeof(dbk)); dbk.size = strlen(trap); dbk.data = trap; memset(&dbd, 0, sizeof(dbd)); i = db->get(db, &dbk, &dbd, 0); if (i == -1) return (-1); if (i) /* didn't exist - so this doesn't match a known spamtrap */ return (1); else /* To: address is a spamtrap, so add as a greytrap entry */ return (0); } int twupdate(char *dbname, char *what, char *ip, char *source, char *expires) { /* we got a TRAP or WHITE update from someone else */ HASHINFO hashinfo; DBT dbk, dbd; DB *db; struct gdata gd; time_t now, expire; int r, spamtrap; now = time(NULL); /* expiry times have to be in the future */ expire = strtonum(expires, now, sizeof(time_t) == sizeof(int) ? INT_MAX : LLONG_MAX, NULL); if (expire == 0) return(-1); if (strcmp(what, "TRAP") == 0) spamtrap = 1; else if (strcmp(what, "WHITE") == 0) spamtrap = 0; else return(-1); memset(&hashinfo, 0, sizeof(hashinfo)); db = dbopen(dbname, O_EXLOCK|O_RDWR, 0600, DB_HASH, &hashinfo); if (db == NULL) return(-1); memset(&dbk, 0, sizeof(dbk)); dbk.size = strlen(ip); dbk.data = ip; memset(&dbd, 0, sizeof(dbd)); r = db->get(db, &dbk, &dbd, 0); if (r == -1) goto bad; if (r) { /* new entry */ memset(&gd, 0, sizeof(gd)); gd.first = now; gd.pcount = spamtrap ? -1 : 0; gd.expire = expire; memset(&dbk, 0, sizeof(dbk)); dbk.size = strlen(ip); dbk.data = ip; memset(&dbd, 0, sizeof(dbd)); dbd.size = sizeof(gd); dbd.data = &gd; r = db->put(db, &dbk, &dbd, 0); db->sync(db, 0); if (r) goto bad; if (debug) fprintf(stderr, "added %s %s\n", spamtrap ? "trap entry for" : "", ip); syslog_r(LOG_DEBUG, &sdata, "new %s from %s for %s, expires %s", what, source, ip, expires); } else { /* existing entry */ if (gdcopyin(&dbd, &gd) == -1) { /* whatever this is, it doesn't belong */ db->del(db, &dbk, 0); db->sync(db, 0); goto bad; } if (spamtrap) { gd.pcount = -1; gd.bcount++; } else gd.pcount++; memset(&dbk, 0, sizeof(dbk)); dbk.size = strlen(ip); dbk.data = ip; memset(&dbd, 0, sizeof(dbd)); dbd.size = sizeof(gd); dbd.data = &gd; r = db->put(db, &dbk, &dbd, 0); db->sync(db, 0); if (r) goto bad; if (debug) fprintf(stderr, "updated %s\n", ip); } db->close(db); return(0); bad: db->close(db); return(-1); } int greyupdate(char *dbname, char *helo, char *ip, char *from, char *to, int sync, char *cip) { HASHINFO hashinfo; DBT dbk, dbd; DB *db; char *key = NULL; char *lookup; struct gdata gd; time_t now, expire; int r, spamtrap; now = time(NULL); /* open with lock, find record, update, close, unlock */ memset(&hashinfo, 0, sizeof(hashinfo)); db = dbopen(dbname, O_EXLOCK|O_RDWR, 0600, DB_HASH, &hashinfo); if (db == NULL) return(-1); if (asprintf(&key, "%s\n%s\n%s\n%s", ip, helo, from, to) == -1) goto bad; r = trapcheck(db, to); switch (r) { case 1: /* do not trap */ spamtrap = 0; lookup = key; expire = greyexp; break; case 0: /* trap */ spamtrap = 1; lookup = ip; expire = trapexp; syslog_r(LOG_DEBUG, &sdata, "Trapping %s for tuple %s", ip, key); break; default: goto bad; break; } memset(&dbk, 0, sizeof(dbk)); dbk.size = strlen(lookup); dbk.data = lookup; memset(&dbd, 0, sizeof(dbd)); r = db->get(db, &dbk, &dbd, 0); if (r == -1) goto bad; if (r) { /* new entry */ if (sync && low_prio_mx_ip && (strcmp(cip, low_prio_mx_ip) == 0) && ((startup + 60) < now)) { /* we haven't seen a greylist entry for this tuple, * and yet the connection was to a low priority MX * which we know can't be hit first if the client * is adhering to the RFC's - soo.. kill it! */ spamtrap = 1; lookup = ip; expire = trapexp; syslog_r(LOG_DEBUG, &sdata, "Trapping %s for trying %s first for tuple %s", ip, low_prio_mx_ip, key); } memset(&gd, 0, sizeof(gd)); gd.first = now; gd.bcount = 1; gd.pcount = spamtrap ? -1 : 0; gd.pass = now + expire; gd.expire = now + expire; memset(&dbk, 0, sizeof(dbk)); dbk.size = strlen(lookup); dbk.data = lookup; memset(&dbd, 0, sizeof(dbd)); dbd.size = sizeof(gd); dbd.data = &gd; r = db->put(db, &dbk, &dbd, 0); db->sync(db, 0); if (r) goto bad; if (debug) fprintf(stderr, "added %s %s\n", spamtrap ? "greytrap entry for" : "", lookup); syslog_r(LOG_DEBUG, &sdata, "new %sentry %s from %s to %s, helo %s", spamtrap ? "greytrap " : "", ip, from, to, helo); } else { /* existing entry */ if (gdcopyin(&dbd, &gd) == -1) { /* whatever this is, it doesn't belong */ db->del(db, &dbk, 0); db->sync(db, 0); goto bad; } gd.bcount++; gd.pcount = spamtrap ? -1 : 0; if (gd.first + passtime < now) gd.pass = now; memset(&dbk, 0, sizeof(dbk)); dbk.size = strlen(lookup); dbk.data = lookup; memset(&dbd, 0, sizeof(dbd)); dbd.size = sizeof(gd); dbd.data = &gd; r = db->put(db, &dbk, &dbd, 0); db->sync(db, 0); if (r) goto bad; if (debug) fprintf(stderr, "updated %s\n", lookup); } free(key); key = NULL; db->close(db); db = NULL; /* Entry successfully update, sent out sync message */ if (syncsend && sync) { if (spamtrap) { syslog_r(LOG_DEBUG, &sdata, "sync_trap %s", ip); sync_trapped(now, now + expire, ip); } else sync_update(now, helo, ip, from, to); } return(0); bad: free(key); key = NULL; db->close(db); db = NULL; return(-1); } int twread(char *buf) { if ((strncmp(buf, "WHITE:", 6) == 0) || (strncmp(buf, "TRAP:", 5) == 0)) { char **ap, *argv[5]; int argc = 0; for (ap = argv; ap < &argv[4] && (*ap = strsep(&buf, ":")) != NULL;) { if (**ap != '\0') ap++; argc++; } *ap = NULL; if (argc != 4) return (-1); twupdate(PATH_SPAMD_DB, argv[0], argv[1], argv[2], argv[3]); return (0); } else return (-1); } int greyreader(void) { char cip[32], ip[32], helo[MAX_MAIL], from[MAX_MAIL], to[MAX_MAIL]; char *buf; size_t len; int state, sync; struct addrinfo hints, *res; memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_INET; /*for now*/ hints.ai_socktype = SOCK_DGRAM; /*dummy*/ hints.ai_protocol = IPPROTO_UDP; /*dummy*/ hints.ai_flags = AI_NUMERICHOST; state = 0; sync = 1; if (grey == NULL) { syslog_r(LOG_ERR, &sdata, "No greylist pipe stream!\n"); return (-1); } /* grab trap suffixes */ readsuffixlists(); while ((buf = fgetln(grey, &len))) { if (buf[len - 1] == '\n') buf[len - 1] = '\0'; else /* all valid lines end in \n */ continue; if (strlen(buf) < 4) continue; if (strcmp(buf, "SYNC") == 0) { sync = 0; continue; } switch (state) { case 0: if (twread(buf) == 0) { state = 0; break; } if (strncmp(buf, "HE:", 3) != 0) { if (strncmp(buf, "CO:", 3) == 0) strlcpy(cip, buf+3, sizeof(cip)); state = 0; break; } strlcpy(helo, buf+3, sizeof(helo)); state = 1; break; case 1: if (strncmp(buf, "IP:", 3) != 0) break; strlcpy(ip, buf+3, sizeof(ip)); if (getaddrinfo(ip, NULL, &hints, &res) == 0) { freeaddrinfo(res); state = 2; } else state = 0; break; case 2: if (strncmp(buf, "FR:", 3) != 0) { state = 0; break; } strlcpy(from, buf+3, sizeof(from)); state = 3; break; case 3: if (strncmp(buf, "TO:", 3) != 0) { state = 0; break; } strlcpy(to, buf+3, sizeof(to)); if (debug) fprintf(stderr, "Got Grey HELO %s, IP %s from %s to %s\n", helo, ip, from, to); greyupdate(PATH_SPAMD_DB, helo, ip, from, to, sync, cip); sync = 1; state = 0; break; } } return (0); } void greyscanner(void) { for (;;) { if (greyscan(PATH_SPAMD_DB) == -1) syslog_r(LOG_NOTICE, &sdata, "scan of %s failed", PATH_SPAMD_DB); sleep(DB_SCAN_INTERVAL); } } static void drop_privs(void) { /* * lose root, continue as non-root user */ 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)) { syslog_r(LOG_ERR, &sdata, "failed to drop privs (%m)"); exit(1); } } void check_spamd_db(void) { HASHINFO hashinfo; int i = -1; DB *db; /* check to see if /var/db/spamd exists, if not, create it */ memset(&hashinfo, 0, sizeof(hashinfo)); db = dbopen(PATH_SPAMD_DB, O_EXLOCK|O_RDWR, 0600, DB_HASH, &hashinfo); if (db == NULL) { switch (errno) { case ENOENT: i = open(PATH_SPAMD_DB, O_RDWR|O_CREAT, 0644); if (i == -1) { syslog_r(LOG_ERR, &sdata, "create %s failed (%m)", PATH_SPAMD_DB); exit(1); } /* if we are dropping privs, chown to that user */ if (pw && (fchown(i, pw->pw_uid, pw->pw_gid) == -1)) { syslog_r(LOG_ERR, &sdata, "chown %s failed (%m)", PATH_SPAMD_DB); exit(1); } close(i); return; break; default: syslog_r(LOG_ERR, &sdata, "open of %s failed (%m)", PATH_SPAMD_DB); exit(1); } } db->sync(db, 0); db->close(db); } int greywatcher(void) { struct sigaction sa; drop_privs(); if (unveil(PATH_SPAMD_DB, "rw") == -1) { syslog_r(LOG_ERR, &sdata, "unveil failed (%m)"); exit(1); } if (unveil(alloweddomains_file, "r") == -1) { syslog_r(LOG_ERR, &sdata, "unveil failed (%m)"); exit(1); } if (unveil(PATH_PFCTL, "x") == -1) { syslog_r(LOG_ERR, &sdata, "unveil failed (%m)"); exit(1); } if (pledge("stdio rpath wpath inet flock proc exec", NULL) == -1) { syslog_r(LOG_ERR, &sdata, "pledge failed (%m)"); exit(1); } startup = time(NULL); db_pid = fork(); switch (db_pid) { case -1: syslog_r(LOG_ERR, &sdata, "fork failed (%m)"); exit(1); case 0: /* * child, talks to jailed spamd over greypipe, * updates db. has no access to pf. */ close(pfdev); setproctitle("(%s update)", PATH_SPAMD_DB); if (greyreader() == -1) { syslog_r(LOG_ERR, &sdata, "greyreader failed (%m)"); _exit(1); } _exit(0); } fclose(grey); /* * parent, scans db periodically for changes and updates * pf whitelist table accordingly. */ sigfillset(&sa.sa_mask); sa.sa_flags = SA_RESTART; sa.sa_handler = sig_term_chld; sigaction(SIGTERM, &sa, NULL); sigaction(SIGHUP, &sa, NULL); sigaction(SIGCHLD, &sa, NULL); sigaction(SIGINT, &sa, NULL); setproctitle("(pf update)"); greyscanner(); exit(1); }