534 lines
11 KiB
C
534 lines
11 KiB
C
/* $NetBSD: grey.c,v 1.4 2004/11/14 11:26:48 yamt Exp $ */
|
|
/* $OpenBSD: grey.c,v 1.17 2004/08/15 21:49:45 millert Exp $ */
|
|
|
|
/*
|
|
* Copyright (c) 2004 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/fcntl.h>
|
|
#include <sys/wait.h>
|
|
#include <net/if.h>
|
|
#include <netinet/in.h>
|
|
#include <net/pfvar.h>
|
|
#include <arpa/inet.h>
|
|
#include <db.h>
|
|
#include <err.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"
|
|
|
|
extern time_t passtime, greyexp, whiteexp;
|
|
extern struct syslog_data sdata;
|
|
extern struct passwd *pw;
|
|
extern pid_t jail_pid;
|
|
extern FILE * grey;
|
|
extern int debug;
|
|
|
|
size_t whitecount, whitealloc;
|
|
char **whitelist;
|
|
pid_t db_pid = -1;
|
|
int pfdev;
|
|
|
|
static char *pargv[11]= {
|
|
"pfctl", "-p", "/dev/pf", "-q", "-t",
|
|
"spamd-white", "-T", "replace", "-f" "-", NULL
|
|
};
|
|
|
|
int configure_pf(char **, int);
|
|
int addwhiteaddr(char *);
|
|
void freewhiteaddr(void);
|
|
int greyscan(char *);
|
|
int greyupdate(char *, char *, char *, char *);
|
|
int greyreader(void);
|
|
void greyscanner(void);
|
|
int greywatcher(void);
|
|
|
|
/* 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);
|
|
}
|
|
|
|
int
|
|
configure_pf(char **addrs, int count)
|
|
{
|
|
FILE *pf = NULL;
|
|
int i, pdes[2];
|
|
pid_t pid;
|
|
char *fdpath;
|
|
|
|
if (debug)
|
|
fprintf(stderr, "configure_pf - device on fd %d\n", pfdev);
|
|
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]);
|
|
signal(SIGCHLD, sig_term_chld);
|
|
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]);
|
|
signal(SIGCHLD, sig_term_chld);
|
|
return(-1);
|
|
}
|
|
for (i = 0; i < count; i++)
|
|
if (addrs[i] != NULL)
|
|
fprintf(pf, "%s/32\n", addrs[i]);
|
|
fclose(pf);
|
|
waitpid(pid, NULL, 0);
|
|
signal(SIGCHLD, sig_term_chld);
|
|
return(0);
|
|
}
|
|
|
|
void
|
|
freewhiteaddr(void)
|
|
{
|
|
int i;
|
|
|
|
if (whitelist != NULL)
|
|
for (i = 0; i < whitecount; i++) {
|
|
free(whitelist[i]);
|
|
whitelist[i] = NULL;
|
|
}
|
|
whitecount = 0;
|
|
}
|
|
|
|
/* validate, then add to list of addrs to whitelist */
|
|
int
|
|
addwhiteaddr(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 (whitecount == whitealloc) {
|
|
char **tmp;
|
|
|
|
tmp = realloc(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);
|
|
} else
|
|
return(-1);
|
|
return(0);
|
|
}
|
|
|
|
int
|
|
greyscan(char *dbname)
|
|
{
|
|
BTREEINFO btreeinfo;
|
|
DBT dbk, dbd;
|
|
DB *db;
|
|
struct gdata gd;
|
|
int r;
|
|
time_t now = time(NULL);
|
|
|
|
/* walk db, expire, and whitelist */
|
|
|
|
memset(&btreeinfo, 0, sizeof(btreeinfo));
|
|
db = dbopen(dbname, O_EXLOCK|O_RDWR, 0600, DB_BTREE, &btreeinfo);
|
|
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)) {
|
|
char a[128];
|
|
|
|
if ((dbk.size < 1) || dbd.size != sizeof(struct gdata)) {
|
|
db->close(db);
|
|
db = NULL;
|
|
return(-1);
|
|
}
|
|
memcpy(&gd, dbd.data, sizeof(gd));
|
|
if (gd.expire <= now) {
|
|
/* get rid of entry */
|
|
if (debug) {
|
|
memset(a, 0, sizeof(a));
|
|
memcpy(a, dbk.data, MIN(sizeof(a),
|
|
dbk.size));
|
|
syslog_r(LOG_DEBUG, &sdata,
|
|
"deleting %s from %s", a, dbname);
|
|
}
|
|
if (db->del(db, &dbk, 0)) {
|
|
db->close(db);
|
|
db = NULL;
|
|
return(-1);
|
|
}
|
|
db->sync(db, 0);
|
|
} else if (gd.pass <= now) {
|
|
int tuple = 0;
|
|
char *cp;
|
|
|
|
/*
|
|
* remove this tuple-keyed entry from db
|
|
* add address to whitelist
|
|
* add an address-keyed entry to db
|
|
*/
|
|
memset(a, 0, sizeof(a));
|
|
memcpy(a, dbk.data, MIN(sizeof(a) - 1, dbk.size));
|
|
cp = strchr(a, '\n');
|
|
if (cp != NULL) {
|
|
tuple = 1;
|
|
*cp = '\0';
|
|
}
|
|
if ((addwhiteaddr(a) == -1) && db->del(db, &dbk, 0)) {
|
|
db->sync(db, 0);
|
|
goto bad;
|
|
}
|
|
if (tuple) {
|
|
if (db->del(db, &dbk, 0)) {
|
|
db->sync(db, 0);
|
|
goto bad;
|
|
}
|
|
/* re-add entry, keyed only by ip */
|
|
memset(&dbk, 0, sizeof(dbk));
|
|
dbk.size = strlen(a);
|
|
dbk.data = a;
|
|
memset(&dbd, 0, sizeof(dbd));
|
|
gd.expire = now + whiteexp;
|
|
dbd.size = sizeof(gd);
|
|
dbd.data = &gd;
|
|
if (db->put(db, &dbk, &dbd, 0)) {
|
|
db->sync(db, 0);
|
|
goto bad;
|
|
}
|
|
db->sync(db, 0);
|
|
syslog_r(LOG_DEBUG, &sdata,
|
|
"whitelisting %s in %s", a, dbname);
|
|
|
|
}
|
|
if (debug)
|
|
fprintf(stderr, "whitelisted %s\n", a);
|
|
}
|
|
}
|
|
configure_pf(whitelist, whitecount);
|
|
db->close(db);
|
|
db = NULL;
|
|
freewhiteaddr();
|
|
return(0);
|
|
bad:
|
|
db->close(db);
|
|
db = NULL;
|
|
freewhiteaddr();
|
|
return(-1);
|
|
}
|
|
|
|
int
|
|
greyupdate(char *dbname, char *ip, char *from, char *to)
|
|
{
|
|
BTREEINFO btreeinfo;
|
|
DBT dbk, dbd;
|
|
DB *db;
|
|
char *key = NULL;
|
|
struct gdata gd;
|
|
time_t now;
|
|
int r;
|
|
|
|
now = time(NULL);
|
|
|
|
/* open with lock, find record, update, close, unlock */
|
|
memset(&btreeinfo, 0, sizeof(btreeinfo));
|
|
db = dbopen(dbname, O_EXLOCK|O_RDWR, 0600, DB_BTREE, &btreeinfo);
|
|
if (db == NULL)
|
|
return(-1);
|
|
if (asprintf(&key, "%s\n%s\n%s", ip, from, to) == -1)
|
|
goto bad;
|
|
memset(&dbk, 0, sizeof(dbk));
|
|
dbk.size = strlen(key);
|
|
dbk.data = key;
|
|
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.bcount = 1;
|
|
gd.pass = now + greyexp;
|
|
gd.expire = now + greyexp;
|
|
memset(&dbk, 0, sizeof(dbk));
|
|
dbk.size = strlen(key);
|
|
dbk.data = key;
|
|
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\n", key);
|
|
} else {
|
|
/* existing entry */
|
|
if (dbd.size != sizeof(gd)) {
|
|
/* whatever this is, it doesn't belong */
|
|
db->del(db, &dbk, 0);
|
|
db->sync(db, 0);
|
|
goto bad;
|
|
}
|
|
memcpy(&gd, dbd.data, sizeof(gd));
|
|
gd.bcount++;
|
|
if (gd.first + passtime < now)
|
|
gd.pass = now;
|
|
memset(&dbk, 0, sizeof(dbk));
|
|
dbk.size = strlen(key);
|
|
dbk.data = key;
|
|
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", key);
|
|
}
|
|
free(key);
|
|
key = NULL;
|
|
db->close(db);
|
|
db = NULL;
|
|
return(0);
|
|
bad:
|
|
free(key);
|
|
key = NULL;
|
|
db->close(db);
|
|
db = NULL;
|
|
return(-1);
|
|
}
|
|
|
|
int
|
|
greyreader(void)
|
|
{
|
|
char ip[32], from[MAX_MAIL], to[MAX_MAIL], *buf;
|
|
size_t len;
|
|
int state;
|
|
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;
|
|
if (grey == NULL) {
|
|
syslog_r(LOG_ERR, &sdata, "No greylist pipe stream!\n");
|
|
exit(1);
|
|
}
|
|
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;
|
|
|
|
switch (state) {
|
|
case 0:
|
|
if (strncmp(buf, "IP:", 3) != 0)
|
|
break;
|
|
strlcpy(ip, buf+3, sizeof(ip));
|
|
if (getaddrinfo(ip, NULL, &hints, &res) == 0) {
|
|
freeaddrinfo(res);
|
|
state = 1;
|
|
} else
|
|
state = 0;
|
|
break;
|
|
case 1:
|
|
if (strncmp(buf, "FR:", 3) != 0) {
|
|
state = 0;
|
|
break;
|
|
}
|
|
strlcpy(from, buf+3, sizeof(from));
|
|
state = 2;
|
|
break;
|
|
case 2:
|
|
if (strncmp(buf, "TO:", 3) != 0) {
|
|
state = 0;
|
|
break;
|
|
}
|
|
strlcpy(to, buf+3, sizeof(to));
|
|
if (debug)
|
|
fprintf(stderr,
|
|
"Got Grey IP %s from %s to %s\n",
|
|
ip, from, to);
|
|
greyupdate(PATH_SPAMD_DB, ip, from, to);
|
|
state = 0;
|
|
break;
|
|
}
|
|
}
|
|
return (0);
|
|
}
|
|
|
|
void
|
|
greyscanner(void)
|
|
{
|
|
int i;
|
|
|
|
for (;;) {
|
|
sleep(DB_SCAN_INTERVAL);
|
|
i = greyscan(PATH_SPAMD_DB);
|
|
if (i == -1)
|
|
syslog_r(LOG_NOTICE, &sdata, "scan of %s failed",
|
|
PATH_SPAMD_DB);
|
|
}
|
|
/* NOTREACHED */
|
|
}
|
|
|
|
int
|
|
greywatcher(void)
|
|
{
|
|
int i;
|
|
|
|
pfdev = open("/dev/pf", O_RDWR);
|
|
if (pfdev == -1) {
|
|
syslog_r(LOG_ERR, &sdata, "open of /dev/pf failed (%m)");
|
|
exit(1);
|
|
}
|
|
|
|
/* check to see if /var/db/spamd exists, if not, create it */
|
|
if ((i = open(PATH_SPAMD_DB, O_RDWR, 0)) == -1 && errno == 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);
|
|
}
|
|
}
|
|
if (i != -1)
|
|
close(i);
|
|
|
|
/*
|
|
* lose root, continue as non-root user
|
|
* XXX Should not be _spamd - as it currently is.
|
|
*/
|
|
if (pw) {
|
|
setgroups(1, &pw->pw_gid);
|
|
setegid(pw->pw_gid);
|
|
setgid(pw->pw_gid);
|
|
seteuid(pw->pw_uid);
|
|
setuid(pw->pw_uid);
|
|
}
|
|
|
|
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);
|
|
greyreader();
|
|
/* NOTREACHED */
|
|
_exit(1);
|
|
}
|
|
/*
|
|
* parent, scans db periodically for changes and updates
|
|
* pf whitelist table accordingly.
|
|
*/
|
|
fclose(grey);
|
|
signal(SIGTERM, sig_term_chld);
|
|
signal(SIGHUP, sig_term_chld);
|
|
signal(SIGCHLD, sig_term_chld);
|
|
signal(SIGINT, sig_term_chld);
|
|
|
|
setproctitle("(pf <spamd-white> update)");
|
|
greyscanner();
|
|
/* NOTREACHED */
|
|
exit(1);
|
|
}
|