src/libexec/spamd/grey.c

1136 lines
24 KiB
C

/* $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 <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <net/if.h>
#include <netinet/in.h>
#include <net/pfvar.h>
#include <ctype.h>
#include <db.h>
#include <errno.h>
#include <fcntl.h>
#include <pwd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <time.h>
#include <unistd.h>
#include <netdb.h>
#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 <spamd-white> update)");
greyscanner();
exit(1);
}