Edit

IABSD.fr/src/libexec/spamd/grey.c

Branch :

  • Show log

    Commit

  • Author : mestre
    Date : 2018-10-25 06:42:35
    Hash : 800fd717
    Message : When spamd(8) runs in greylist mode in the parent process (which runs greywatcher()) we know that the only files that it will ever access are PATH_SPAMD_DB in rw mode, alloweddomains_file in r and that it will need to execute PATH_PFCTL so we can unveil(2) them with those permissions. OK deraadt@ millert@ beck@

  • libexec/spamd/grey.c
  • /*	$OpenBSD: grey.c,v 1.66 2018/10/25 06:42:35 mestre 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 */
    /* ARGSUSED */
    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);
    }