1774 lines
38 KiB
C
1774 lines
38 KiB
C
/* $OpenBSD: spamd.c,v 1.163 2024/05/09 08:35:03 florian Exp $ */
|
|
|
|
/*
|
|
* Copyright (c) 2015 Henning Brauer <henning@openbsd.org>
|
|
* Copyright (c) 2002-2007 Bob Beck. All rights reserved.
|
|
* Copyright (c) 2002 Theo de Raadt. 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/sysctl.h>
|
|
#include <sys/resource.h>
|
|
#include <sys/signal.h>
|
|
#include <sys/stat.h>
|
|
|
|
#include <netinet/in.h>
|
|
#include <arpa/inet.h>
|
|
|
|
#include <err.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <limits.h>
|
|
#include <poll.h>
|
|
#include <pwd.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <syslog.h>
|
|
#include <unistd.h>
|
|
#include <tls.h>
|
|
|
|
#include <netdb.h>
|
|
|
|
#include "sdl.h"
|
|
#include "grey.h"
|
|
#include "sync.h"
|
|
|
|
struct con {
|
|
struct pollfd *pfd;
|
|
int state;
|
|
int laststate;
|
|
int af;
|
|
int il;
|
|
struct sockaddr_storage ss;
|
|
void *ia;
|
|
char addr[32];
|
|
char caddr[32];
|
|
char helo[MAX_MAIL], mail[MAX_MAIL], rcpt[MAX_MAIL];
|
|
struct sdlist **blacklists;
|
|
struct tls *cctx;
|
|
|
|
/*
|
|
* we will do stuttering by changing these to time_t's of
|
|
* now + n, and only advancing when the time is in the past/now
|
|
*/
|
|
time_t r;
|
|
time_t w;
|
|
time_t s;
|
|
|
|
char ibuf[8192];
|
|
char *ip;
|
|
char rend[5]; /* any chars in here causes input termination */
|
|
|
|
char *obuf;
|
|
char *lists;
|
|
size_t osize;
|
|
char *op;
|
|
int ol;
|
|
int data_lines;
|
|
int data_body;
|
|
int stutter;
|
|
int badcmd;
|
|
int sr;
|
|
int tlsaction;
|
|
} *con;
|
|
|
|
#define SPAMD_TLS_ACT_NONE 0
|
|
#define SPAMD_TLS_ACT_READ_POLLIN 1
|
|
#define SPAMD_TLS_ACT_READ_POLLOUT 2
|
|
#define SPAMD_TLS_ACT_WRITE_POLLIN 3
|
|
#define SPAMD_TLS_ACT_WRITE_POLLOUT 4
|
|
|
|
#define SPAMD_USER "_spamd"
|
|
|
|
void usage(void);
|
|
char *grow_obuf(struct con *, int);
|
|
int parse_configline(char *);
|
|
void parse_configs(void);
|
|
void do_config(void);
|
|
int append_error_string (struct con *, size_t, char *, int, void *);
|
|
void doreply(struct con *);
|
|
void setlog(char *, size_t, char *);
|
|
void initcon(struct con *, int, struct sockaddr *);
|
|
void closecon(struct con *);
|
|
int match(const char *, const char *);
|
|
void nextstate(struct con *);
|
|
void handler(struct con *);
|
|
void handlew(struct con *, int one);
|
|
char *loglists(struct con *);
|
|
void getcaddr(struct con *);
|
|
void gethelo(char *, size_t, char *);
|
|
int read_configline(FILE *);
|
|
void spamd_tls_init(void);
|
|
void check_spamd_db(void);
|
|
void blackcheck(int);
|
|
|
|
char hostname[HOST_NAME_MAX+1];
|
|
struct syslog_data sdata = SYSLOG_DATA_INIT;
|
|
char *nreply = "450";
|
|
char *spamd = "spamd IP-based SPAM blocker";
|
|
int greyback[2];
|
|
int greypipe[2];
|
|
int trappipe[2];
|
|
FILE *grey;
|
|
FILE *trapcfg;
|
|
time_t passtime = PASSTIME;
|
|
time_t greyexp = GREYEXP;
|
|
time_t whiteexp = WHITEEXP;
|
|
time_t trapexp = TRAPEXP;
|
|
struct passwd *pw;
|
|
pid_t jail_pid = -1;
|
|
u_short cfg_port;
|
|
u_short sync_port;
|
|
struct tls_config *tlscfg;
|
|
struct tls *tlsctx;
|
|
char *tlskeyfile = NULL;
|
|
char *tlscertfile = NULL;
|
|
|
|
extern struct sdlist *blacklists;
|
|
extern int pfdev;
|
|
extern char *low_prio_mx_ip;
|
|
|
|
time_t slowdowntill;
|
|
|
|
int conffd = -1;
|
|
int trapfd = -1;
|
|
char *cb;
|
|
size_t cbs, cbu;
|
|
|
|
time_t t;
|
|
|
|
#define MAXCON 800
|
|
int maxfiles;
|
|
int maxcon = MAXCON;
|
|
int maxblack = MAXCON;
|
|
int blackcount;
|
|
int clients;
|
|
int debug;
|
|
int greylist = 1;
|
|
int grey_stutter = 10;
|
|
int verbose;
|
|
int stutter = 1;
|
|
int window;
|
|
int syncrecv;
|
|
int syncsend;
|
|
#define MAXTIME 400
|
|
|
|
#define MAXIMUM(a,b) (((a)>(b))?(a):(b))
|
|
|
|
void
|
|
usage(void)
|
|
{
|
|
extern char *__progname;
|
|
|
|
fprintf(stderr,
|
|
"usage: %s [-45bdv] [-B maxblack] [-C file] [-c maxcon] "
|
|
"[-G passtime:greyexp:whiteexp]\n"
|
|
"\t[-h hostname] [-K file] [-l address] [-M address] [-n name]\n"
|
|
"\t[-p port] [-S secs] [-s secs] "
|
|
"[-w window] [-Y synctarget] [-y synclisten]\n",
|
|
__progname);
|
|
|
|
exit(1);
|
|
}
|
|
|
|
char *
|
|
grow_obuf(struct con *cp, int off)
|
|
{
|
|
char *tmp;
|
|
|
|
tmp = realloc(cp->obuf, cp->osize + 8192);
|
|
if (tmp == NULL) {
|
|
free(cp->obuf);
|
|
cp->obuf = NULL;
|
|
cp->osize = 0;
|
|
return (NULL);
|
|
} else {
|
|
cp->osize += 8192;
|
|
cp->obuf = tmp;
|
|
return (cp->obuf + off);
|
|
}
|
|
}
|
|
|
|
int
|
|
parse_configline(char *line)
|
|
{
|
|
char *cp, prev, *name, *msg, *tmp;
|
|
char **v4 = NULL, **v6 = NULL;
|
|
const char *errstr;
|
|
u_int nv4 = 0, nv6 = 0;
|
|
int mdone = 0;
|
|
sa_family_t af;
|
|
|
|
name = line;
|
|
|
|
for (cp = name; *cp && *cp != ';'; cp++)
|
|
;
|
|
if (*cp != ';')
|
|
goto parse_error;
|
|
*cp++ = '\0';
|
|
if (!*cp) {
|
|
sdl_del(name);
|
|
return (0);
|
|
}
|
|
msg = cp;
|
|
if (*cp++ != '"')
|
|
goto parse_error;
|
|
prev = '\0';
|
|
for (; !mdone; cp++) {
|
|
switch (*cp) {
|
|
case '\\':
|
|
if (!prev)
|
|
prev = *cp;
|
|
else
|
|
prev = '\0';
|
|
break;
|
|
case '"':
|
|
if (prev != '\\') {
|
|
cp++;
|
|
if (*cp == ';') {
|
|
mdone = 1;
|
|
*cp = '\0';
|
|
} else {
|
|
if (debug > 0)
|
|
printf("bad message: %s\n", msg);
|
|
goto parse_error;
|
|
}
|
|
}
|
|
break;
|
|
case '\0':
|
|
if (debug > 0)
|
|
printf("bad message: %s\n", msg);
|
|
goto parse_error;
|
|
default:
|
|
prev = '\0';
|
|
break;
|
|
}
|
|
}
|
|
|
|
while ((tmp = strsep(&cp, ";")) != NULL) {
|
|
char **av;
|
|
u_int au, ac;
|
|
|
|
if (*tmp == '\0')
|
|
continue;
|
|
|
|
if (strncmp(tmp, "inet", 4) != 0)
|
|
goto parse_error;
|
|
switch (tmp[4]) {
|
|
case '\0':
|
|
af = AF_INET;
|
|
break;
|
|
case '6':
|
|
if (tmp[5] == '\0') {
|
|
af = AF_INET6;
|
|
break;
|
|
}
|
|
/* FALLTHROUGH */
|
|
default:
|
|
if (debug > 0)
|
|
printf("unsupported address family: %s\n", tmp);
|
|
goto parse_error;
|
|
}
|
|
|
|
tmp = strsep(&cp, ";");
|
|
if (tmp == NULL) {
|
|
if (debug > 0)
|
|
printf("missing address count\n");
|
|
goto parse_error;
|
|
}
|
|
ac = strtonum(tmp, 0, UINT_MAX, &errstr);
|
|
if (errstr != NULL) {
|
|
if (debug > 0)
|
|
printf("count \"%s\" is %s\n", tmp, errstr);
|
|
goto parse_error;
|
|
}
|
|
|
|
av = reallocarray(NULL, ac, sizeof(char *));
|
|
for (au = 0; au < ac; au++) {
|
|
tmp = strsep(&cp, ";");
|
|
if (tmp == NULL) {
|
|
if (debug > 0)
|
|
printf("expected %u addrs, got %u\n",
|
|
ac, au + 1);
|
|
free(av);
|
|
goto parse_error;
|
|
}
|
|
if (*tmp == '\0')
|
|
continue;
|
|
av[au] = tmp;
|
|
}
|
|
if (af == AF_INET) {
|
|
if (v4 != NULL) {
|
|
if (debug > 0)
|
|
printf("duplicate inet\n");
|
|
goto parse_error;
|
|
}
|
|
v4 = av;
|
|
nv4 = ac;
|
|
} else {
|
|
if (v6 != NULL) {
|
|
if (debug > 0)
|
|
printf("duplicate inet6\n");
|
|
goto parse_error;
|
|
}
|
|
v6 = av;
|
|
nv6 = ac;
|
|
}
|
|
}
|
|
if (nv4 == 0 && nv6 == 0) {
|
|
if (debug > 0)
|
|
printf("no addresses\n");
|
|
goto parse_error;
|
|
}
|
|
sdl_add(name, msg, v4, nv4, v6, nv6);
|
|
free(v4);
|
|
free(v6);
|
|
return (0);
|
|
|
|
parse_error:
|
|
if (debug > 0)
|
|
printf("bogus config line - need 'tag;message;af;count;a/m;a/m;a/m...'\n");
|
|
free(v4);
|
|
free(v6);
|
|
return (-1);
|
|
}
|
|
|
|
void
|
|
parse_configs(void)
|
|
{
|
|
char *start, *end;
|
|
size_t i;
|
|
|
|
/* We always leave an extra byte for the NUL. */
|
|
cb[cbu++] = '\0';
|
|
|
|
start = cb;
|
|
end = start;
|
|
for (i = 0; i < cbu; i++) {
|
|
if (*end == '\n') {
|
|
*end = '\0';
|
|
if (end > start + 1)
|
|
parse_configline(start);
|
|
start = ++end;
|
|
} else
|
|
++end;
|
|
}
|
|
if (end > start + 1)
|
|
parse_configline(start);
|
|
}
|
|
|
|
void
|
|
do_config(void)
|
|
{
|
|
int n;
|
|
|
|
if (debug > 0)
|
|
printf("got configuration connection\n");
|
|
|
|
/* Leave an extra byte for the terminating NUL. */
|
|
if (cbu + 1 >= cbs) {
|
|
char *tmp;
|
|
|
|
tmp = realloc(cb, cbs + (1024 * 1024));
|
|
if (tmp == NULL) {
|
|
if (debug > 0)
|
|
warn("realloc");
|
|
goto configdone;
|
|
}
|
|
cbs += 1024 * 1024;
|
|
cb = tmp;
|
|
}
|
|
|
|
n = read(conffd, cb + cbu, cbs - cbu);
|
|
if (debug > 0)
|
|
printf("read %d config bytes\n", n);
|
|
if (n == 0) {
|
|
if (cbu != 0)
|
|
parse_configs();
|
|
goto configdone;
|
|
} else if (n == -1) {
|
|
if (debug > 0)
|
|
warn("read");
|
|
goto configdone;
|
|
} else
|
|
cbu += n;
|
|
return;
|
|
|
|
configdone:
|
|
free(cb);
|
|
cb = NULL;
|
|
cbs = 0;
|
|
cbu = 0;
|
|
close(conffd);
|
|
conffd = -1;
|
|
slowdowntill = 0;
|
|
}
|
|
|
|
int
|
|
read_configline(FILE *config)
|
|
{
|
|
char *buf;
|
|
size_t len;
|
|
|
|
if ((buf = fgetln(config, &len))) {
|
|
if (buf[len - 1] == '\n')
|
|
buf[len - 1] = '\0';
|
|
else
|
|
return (-1); /* all valid lines end in \n */
|
|
parse_configline(buf);
|
|
} else {
|
|
syslog_r(LOG_DEBUG, &sdata, "read_configline: fgetln (%m)");
|
|
return (-1);
|
|
}
|
|
return (0);
|
|
}
|
|
|
|
void
|
|
spamd_tls_init(void)
|
|
{
|
|
if (tlskeyfile == NULL && tlscertfile == NULL)
|
|
return;
|
|
if (tlskeyfile == NULL || tlscertfile == NULL)
|
|
errx(1, "need key and certificate for TLS");
|
|
|
|
if ((tlscfg = tls_config_new()) == NULL)
|
|
errx(1, "failed to get tls config");
|
|
if ((tlsctx = tls_server()) == NULL)
|
|
errx(1, "failed to get tls server");
|
|
|
|
if (tls_config_set_protocols(tlscfg, TLS_PROTOCOLS_ALL) != 0)
|
|
errx(1, "failed to set tls protocols");
|
|
|
|
/* might need user-specified ciphers, tls_config_set_ciphers */
|
|
if (tls_config_set_ciphers(tlscfg, "all") != 0)
|
|
errx(1, "failed to set tls ciphers");
|
|
|
|
if (tls_config_set_cert_file(tlscfg, tlscertfile) == -1)
|
|
errx(1, "unable to set TLS certificate file %s", tlscertfile);
|
|
if (tls_config_set_key_file(tlscfg, tlskeyfile) == -1)
|
|
errx(1, "unable to set TLS key file %s", tlskeyfile);
|
|
if (tls_configure(tlsctx, tlscfg) != 0)
|
|
errx(1, "failed to configure TLS - %s", tls_error(tlsctx));
|
|
|
|
/* set hostname to cert's CN unless explicitly given? */
|
|
}
|
|
|
|
int
|
|
append_error_string(struct con *cp, size_t off, char *fmt, int af, void *ia)
|
|
{
|
|
char sav = '\0';
|
|
static int lastcont = 0;
|
|
char *c = cp->obuf + off;
|
|
char *s = fmt;
|
|
size_t len = cp->osize - off;
|
|
int i = 0;
|
|
|
|
if (off == 0)
|
|
lastcont = 0;
|
|
|
|
if (lastcont != 0)
|
|
cp->obuf[lastcont] = '-';
|
|
snprintf(c, len, "%s ", nreply);
|
|
i += strlen(c);
|
|
lastcont = off + i - 1;
|
|
if (*s == '"')
|
|
s++;
|
|
while (*s) {
|
|
/*
|
|
* Make sure we at minimum, have room to add a
|
|
* format code (4 bytes), and a v6 address(39 bytes)
|
|
* and a byte saved in sav.
|
|
*/
|
|
if (i >= len - 46) {
|
|
c = grow_obuf(cp, off);
|
|
if (c == NULL)
|
|
return (-1);
|
|
len = cp->osize - (off + i);
|
|
}
|
|
|
|
if (c[i-1] == '\n') {
|
|
if (lastcont != 0)
|
|
cp->obuf[lastcont] = '-';
|
|
snprintf(c + i, len, "%s ", nreply);
|
|
i += strlen(c);
|
|
lastcont = off + i - 1;
|
|
}
|
|
|
|
switch (*s) {
|
|
case '\\':
|
|
case '%':
|
|
if (!sav)
|
|
sav = *s;
|
|
else {
|
|
c[i++] = sav;
|
|
sav = '\0';
|
|
c[i] = '\0';
|
|
}
|
|
break;
|
|
case '"':
|
|
case 'A':
|
|
case 'n':
|
|
if (*(s+1) == '\0') {
|
|
break;
|
|
}
|
|
if (sav == '\\' && *s == 'n') {
|
|
c[i++] = '\n';
|
|
sav = '\0';
|
|
c[i] = '\0';
|
|
break;
|
|
} else if (sav == '\\' && *s == '"') {
|
|
c[i++] = '"';
|
|
sav = '\0';
|
|
c[i] = '\0';
|
|
break;
|
|
} else if (sav == '%' && *s == 'A') {
|
|
inet_ntop(af, ia, c + i, (len - i));
|
|
i += strlen(c + i);
|
|
sav = '\0';
|
|
break;
|
|
}
|
|
/* FALLTHROUGH */
|
|
default:
|
|
if (sav)
|
|
c[i++] = sav;
|
|
c[i++] = *s;
|
|
sav = '\0';
|
|
c[i] = '\0';
|
|
break;
|
|
}
|
|
s++;
|
|
}
|
|
return (i);
|
|
}
|
|
|
|
char *
|
|
loglists(struct con *cp)
|
|
{
|
|
static char matchlists[80];
|
|
struct sdlist **matches;
|
|
int s = sizeof(matchlists) - 4;
|
|
|
|
matchlists[0] = '\0';
|
|
matches = cp->blacklists;
|
|
if (matches == NULL)
|
|
return (NULL);
|
|
for (; *matches; matches++) {
|
|
|
|
/* don't report an insane amount of lists in the logs.
|
|
* just truncate and indicate with ...
|
|
*/
|
|
if (strlen(matchlists) + strlen(matches[0]->tag) + 1 >= s)
|
|
strlcat(matchlists, " ...", sizeof(matchlists));
|
|
else {
|
|
strlcat(matchlists, " ", s);
|
|
strlcat(matchlists, matches[0]->tag, s);
|
|
}
|
|
}
|
|
return matchlists;
|
|
}
|
|
|
|
void
|
|
doreply(struct con *cp)
|
|
{
|
|
struct sdlist **matches;
|
|
int off = 0;
|
|
|
|
matches = cp->blacklists;
|
|
if (matches == NULL)
|
|
goto nomatch;
|
|
for (; *matches; matches++) {
|
|
int used = 0;
|
|
int left = cp->osize - off;
|
|
|
|
used = append_error_string(cp, off, matches[0]->string,
|
|
cp->af, cp->ia);
|
|
if (used == -1)
|
|
goto bad;
|
|
off += used;
|
|
left -= used;
|
|
if (cp->obuf[off - 1] != '\n') {
|
|
if (left < 1) {
|
|
if (grow_obuf(cp, off) == NULL)
|
|
goto bad;
|
|
}
|
|
cp->obuf[off++] = '\n';
|
|
cp->obuf[off] = '\0';
|
|
}
|
|
}
|
|
return;
|
|
nomatch:
|
|
/* No match. give generic reply */
|
|
free(cp->obuf);
|
|
if (cp->blacklists != NULL)
|
|
cp->osize = asprintf(&cp->obuf,
|
|
"%s-Sorry %s\n"
|
|
"%s-You are trying to send mail from an address "
|
|
"listed by one\n"
|
|
"%s or more IP-based registries as being a SPAM source.\n",
|
|
nreply, cp->addr, nreply, nreply);
|
|
else
|
|
cp->osize = asprintf(&cp->obuf,
|
|
"451 Temporary failure, please try again later.\r\n");
|
|
if (cp->osize == -1)
|
|
cp->obuf = NULL;
|
|
cp->osize++; /* size includes the NUL (also changes -1 to 0) */
|
|
return;
|
|
bad:
|
|
if (cp->obuf != NULL) {
|
|
free(cp->obuf);
|
|
cp->obuf = NULL;
|
|
cp->osize = 0;
|
|
}
|
|
}
|
|
|
|
void
|
|
setlog(char *p, size_t len, char *f)
|
|
{
|
|
char *s;
|
|
|
|
s = strsep(&f, ":");
|
|
if (!f)
|
|
return;
|
|
while (*f == ' ' || *f == '\t')
|
|
f++;
|
|
s = strsep(&f, " \t");
|
|
if (s == NULL)
|
|
return;
|
|
strlcpy(p, s, len);
|
|
s = strsep(&p, " \t\n\r");
|
|
if (s == NULL)
|
|
return;
|
|
s = strsep(&p, " \t\n\r");
|
|
if (s)
|
|
*s = '\0';
|
|
}
|
|
|
|
/*
|
|
* Get address client connected to, by doing a getsockname call.
|
|
* Must not be used with a NAT'ed connection (use divert-to instead of rdr-to).
|
|
*/
|
|
void
|
|
getcaddr(struct con *cp)
|
|
{
|
|
struct sockaddr_storage original_destination;
|
|
struct sockaddr *odp = (struct sockaddr *) &original_destination;
|
|
socklen_t len = sizeof(struct sockaddr_storage);
|
|
int error;
|
|
|
|
cp->caddr[0] = '\0';
|
|
if (getsockname(cp->pfd->fd, odp, &len) == -1)
|
|
return;
|
|
error = getnameinfo(odp, odp->sa_len, cp->caddr, sizeof(cp->caddr),
|
|
NULL, 0, NI_NUMERICHOST);
|
|
if (error)
|
|
cp->caddr[0] = '\0';
|
|
}
|
|
|
|
void
|
|
gethelo(char *p, size_t len, char *f)
|
|
{
|
|
char *s;
|
|
|
|
/* skip HELO/EHLO */
|
|
f+=4;
|
|
/* skip whitespace */
|
|
while (*f == ' ' || *f == '\t')
|
|
f++;
|
|
s = strsep(&f, " \t");
|
|
if (s == NULL)
|
|
return;
|
|
strlcpy(p, s, len);
|
|
s = strsep(&p, " \t\n\r");
|
|
if (s == NULL)
|
|
return;
|
|
s = strsep(&p, " \t\n\r");
|
|
if (s)
|
|
*s = '\0';
|
|
}
|
|
|
|
void
|
|
initcon(struct con *cp, int fd, struct sockaddr *sa)
|
|
{
|
|
struct pollfd *pfd = cp->pfd;
|
|
char ctimebuf[26];
|
|
time_t tt;
|
|
int error;
|
|
|
|
if (sa->sa_family != AF_INET)
|
|
errx(1, "not supported yet");
|
|
|
|
time(&tt);
|
|
free(cp->obuf);
|
|
free(cp->blacklists);
|
|
free(cp->lists);
|
|
memset(cp, 0, sizeof(*cp));
|
|
if (grow_obuf(cp, 0) == NULL)
|
|
err(1, "malloc");
|
|
cp->pfd = pfd;
|
|
cp->pfd->fd = fd;
|
|
memcpy(&cp->ss, sa, sa->sa_len);
|
|
cp->af = sa->sa_family;
|
|
cp->ia = &((struct sockaddr_in *)&cp->ss)->sin_addr;
|
|
cp->blacklists = sdl_lookup(blacklists, cp->af, cp->ia);
|
|
cp->stutter = (greylist && !grey_stutter && cp->blacklists == NULL) ?
|
|
0 : stutter;
|
|
error = getnameinfo(sa, sa->sa_len, cp->addr, sizeof(cp->addr), NULL, 0,
|
|
NI_NUMERICHOST);
|
|
if (error)
|
|
strlcpy(cp->addr, "<unknown>", sizeof(cp->addr));
|
|
memset(ctimebuf, 0, sizeof(ctimebuf));
|
|
ctime_r(&t, ctimebuf);
|
|
ctimebuf[sizeof(ctimebuf) - 2] = '\0'; /* nuke newline */
|
|
snprintf(cp->obuf, cp->osize, "220 %s ESMTP %s; %s\r\n",
|
|
hostname, spamd, ctimebuf);
|
|
cp->op = cp->obuf;
|
|
cp->ol = strlen(cp->op);
|
|
cp->w = tt + cp->stutter;
|
|
cp->s = tt;
|
|
strlcpy(cp->rend, "\n", sizeof cp->rend);
|
|
clients++;
|
|
if (cp->blacklists != NULL) {
|
|
blackcount++;
|
|
if (greylist && blackcount > maxblack)
|
|
cp->stutter = 0;
|
|
cp->lists = strdup(loglists(cp));
|
|
if (cp->lists == NULL)
|
|
err(1, "malloc");
|
|
}
|
|
else
|
|
cp->lists = NULL;
|
|
}
|
|
|
|
void
|
|
closecon(struct con *cp)
|
|
{
|
|
time_t tt;
|
|
|
|
if (cp->cctx) {
|
|
tls_close(cp->cctx);
|
|
tls_free(cp->cctx);
|
|
}
|
|
close(cp->pfd->fd);
|
|
cp->pfd->fd = -1;
|
|
|
|
slowdowntill = 0;
|
|
|
|
time(&tt);
|
|
syslog_r(LOG_INFO, &sdata, "%s: disconnected after %lld seconds.%s%s",
|
|
cp->addr, (long long)(tt - cp->s),
|
|
((cp->lists == NULL) ? "" : " lists:"),
|
|
((cp->lists == NULL) ? "": cp->lists));
|
|
if (debug > 0)
|
|
printf("%s connected for %lld seconds.\n", cp->addr,
|
|
(long long)(tt - cp->s));
|
|
free(cp->lists);
|
|
cp->lists = NULL;
|
|
if (cp->blacklists != NULL) {
|
|
blackcount--;
|
|
free(cp->blacklists);
|
|
cp->blacklists = NULL;
|
|
}
|
|
if (cp->obuf != NULL) {
|
|
free(cp->obuf);
|
|
cp->obuf = NULL;
|
|
cp->osize = 0;
|
|
}
|
|
clients--;
|
|
}
|
|
|
|
int
|
|
match(const char *s1, const char *s2)
|
|
{
|
|
return (strncasecmp(s1, s2, strlen(s2)) == 0);
|
|
}
|
|
|
|
void
|
|
nextstate(struct con *cp)
|
|
{
|
|
if (match(cp->ibuf, "QUIT") && cp->state < 99) {
|
|
snprintf(cp->obuf, cp->osize, "221 %s\r\n", hostname);
|
|
cp->op = cp->obuf;
|
|
cp->ol = strlen(cp->op);
|
|
cp->w = t + cp->stutter;
|
|
cp->laststate = cp->state;
|
|
cp->state = 99;
|
|
return;
|
|
}
|
|
|
|
if (match(cp->ibuf, "RSET") && cp->state > 2 && cp->state < 50) {
|
|
snprintf(cp->obuf, cp->osize,
|
|
"250 Ok to start over.\r\n");
|
|
cp->op = cp->obuf;
|
|
cp->ol = strlen(cp->op);
|
|
cp->w = t + cp->stutter;
|
|
cp->laststate = cp->state;
|
|
cp->state = 2;
|
|
return;
|
|
}
|
|
switch (cp->state) {
|
|
case 0:
|
|
tlsinitdone:
|
|
/* banner sent; wait for input */
|
|
cp->ip = cp->ibuf;
|
|
cp->il = sizeof(cp->ibuf) - 1;
|
|
cp->laststate = cp->state;
|
|
cp->state = 1;
|
|
cp->r = t;
|
|
break;
|
|
case 1:
|
|
/* received input: parse, and select next state */
|
|
if (match(cp->ibuf, "HELO") ||
|
|
match(cp->ibuf, "EHLO")) {
|
|
int nextstate = 2;
|
|
cp->helo[0] = '\0';
|
|
gethelo(cp->helo, sizeof cp->helo, cp->ibuf);
|
|
if (cp->helo[0] == '\0') {
|
|
nextstate = 0;
|
|
snprintf(cp->obuf, cp->osize,
|
|
"501 helo requires domain name.\r\n");
|
|
} else {
|
|
if (cp->cctx == NULL && tlsctx != NULL &&
|
|
cp->blacklists == NULL &&
|
|
match(cp->ibuf, "EHLO")) {
|
|
snprintf(cp->obuf, cp->osize,
|
|
"250-%s\r\n"
|
|
"250-8BITMIME\r\n"
|
|
"250-SMTPUTF8\r\n"
|
|
"250 STARTTLS\r\n",
|
|
hostname);
|
|
nextstate = 7;
|
|
} else {
|
|
snprintf(cp->obuf, cp->osize,
|
|
"250 Hello, spam sender. Pleased "
|
|
"to be wasting your time.\r\n");
|
|
}
|
|
}
|
|
cp->op = cp->obuf;
|
|
cp->ol = strlen(cp->op);
|
|
cp->laststate = cp->state;
|
|
cp->state = nextstate;
|
|
cp->w = t + cp->stutter;
|
|
break;
|
|
}
|
|
goto mail;
|
|
case 2:
|
|
/* sent 250 Hello, wait for input */
|
|
cp->ip = cp->ibuf;
|
|
cp->il = sizeof(cp->ibuf) - 1;
|
|
cp->laststate = cp->state;
|
|
cp->state = 3;
|
|
cp->r = t;
|
|
break;
|
|
case 3:
|
|
mail:
|
|
if (match(cp->ibuf, "MAIL")) {
|
|
setlog(cp->mail, sizeof cp->mail, cp->ibuf);
|
|
snprintf(cp->obuf, cp->osize,
|
|
"250 You are about to try to deliver spam. "
|
|
"Your time will be spent, for nothing.\r\n");
|
|
cp->op = cp->obuf;
|
|
cp->ol = strlen(cp->op);
|
|
cp->laststate = cp->state;
|
|
cp->state = 4;
|
|
cp->w = t + cp->stutter;
|
|
break;
|
|
}
|
|
goto rcpt;
|
|
case 4:
|
|
/* sent 250 Sender ok */
|
|
cp->ip = cp->ibuf;
|
|
cp->il = sizeof(cp->ibuf) - 1;
|
|
cp->laststate = cp->state;
|
|
cp->state = 5;
|
|
cp->r = t;
|
|
break;
|
|
case 5:
|
|
rcpt:
|
|
if (match(cp->ibuf, "RCPT")) {
|
|
setlog(cp->rcpt, sizeof(cp->rcpt), cp->ibuf);
|
|
snprintf(cp->obuf, cp->osize,
|
|
"250 This is hurting you more than it is "
|
|
"hurting me.\r\n");
|
|
cp->op = cp->obuf;
|
|
cp->ol = strlen(cp->op);
|
|
cp->laststate = cp->state;
|
|
cp->state = 6;
|
|
cp->w = t + cp->stutter;
|
|
|
|
if (cp->mail[0] && cp->rcpt[0]) {
|
|
if (verbose)
|
|
syslog_r(LOG_INFO, &sdata,
|
|
"(%s) %s: %s -> %s",
|
|
cp->blacklists ? "BLACK" : "GREY",
|
|
cp->addr, cp->mail,
|
|
cp->rcpt);
|
|
if (debug)
|
|
fprintf(stderr, "(%s) %s: %s -> %s\n",
|
|
cp->blacklists ? "BLACK" : "GREY",
|
|
cp->addr, cp->mail, cp->rcpt);
|
|
if (greylist && cp->blacklists == NULL) {
|
|
/* send this info to the greylister */
|
|
getcaddr(cp);
|
|
fprintf(grey,
|
|
"CO:%s\nHE:%s\nIP:%s\nFR:%s\nTO:%s\n",
|
|
cp->caddr, cp->helo, cp->addr,
|
|
cp->mail, cp->rcpt);
|
|
fflush(grey);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
goto spam;
|
|
case 6:
|
|
/* sent 250 blah */
|
|
cp->ip = cp->ibuf;
|
|
cp->il = sizeof(cp->ibuf) - 1;
|
|
cp->laststate = cp->state;
|
|
cp->state = 5;
|
|
cp->r = t;
|
|
break;
|
|
case 7:
|
|
/* sent 250 STARTTLS, wait for input */
|
|
cp->ip = cp->ibuf;
|
|
cp->il = sizeof(cp->ibuf) - 1;
|
|
cp->laststate = cp->state;
|
|
cp->state = 8;
|
|
cp->r = t;
|
|
break;
|
|
case 8:
|
|
if (tlsctx != NULL && cp->blacklists == NULL &&
|
|
cp->cctx == NULL && match(cp->ibuf, "STARTTLS")) {
|
|
snprintf(cp->obuf, cp->osize,
|
|
"220 glad you want to burn more CPU cycles on "
|
|
"your spam\r\n");
|
|
cp->op = cp->obuf;
|
|
cp->ol = strlen(cp->op);
|
|
cp->laststate = cp->state;
|
|
cp->state = 9;
|
|
cp->w = t + cp->stutter;
|
|
break;
|
|
}
|
|
goto mail;
|
|
case 9:
|
|
if (tls_accept_socket(tlsctx, &cp->cctx, cp->pfd->fd) == -1) {
|
|
snprintf(cp->obuf, cp->osize,
|
|
"500 STARTTLS failed\r\n");
|
|
cp->op = cp->obuf;
|
|
cp->ol = strlen(cp->op);
|
|
cp->laststate = cp->state;
|
|
cp->state = 98;
|
|
goto done;
|
|
}
|
|
goto tlsinitdone;
|
|
|
|
case 50:
|
|
spam:
|
|
if (match(cp->ibuf, "DATA")) {
|
|
snprintf(cp->obuf, cp->osize,
|
|
"354 Enter spam, end with \".\" on a line by "
|
|
"itself\r\n");
|
|
cp->state = 60;
|
|
if (window && setsockopt(cp->pfd->fd, SOL_SOCKET,
|
|
SO_RCVBUF, &window, sizeof(window)) == -1) {
|
|
syslog_r(LOG_DEBUG, &sdata,"setsockopt: %m");
|
|
/* don't fail if this doesn't work. */
|
|
}
|
|
cp->ip = cp->ibuf;
|
|
cp->il = sizeof(cp->ibuf) - 1;
|
|
cp->op = cp->obuf;
|
|
cp->ol = strlen(cp->op);
|
|
cp->w = t + cp->stutter;
|
|
if (greylist && cp->blacklists == NULL) {
|
|
cp->laststate = cp->state;
|
|
cp->state = 98;
|
|
goto done;
|
|
}
|
|
} else {
|
|
if (match(cp->ibuf, "NOOP"))
|
|
snprintf(cp->obuf, cp->osize,
|
|
"250 2.0.0 OK I did nothing\r\n");
|
|
else {
|
|
snprintf(cp->obuf, cp->osize,
|
|
"500 5.5.1 Command unrecognized\r\n");
|
|
cp->badcmd++;
|
|
if (cp->badcmd > 20) {
|
|
cp->laststate = cp->state;
|
|
cp->state = 98;
|
|
goto done;
|
|
}
|
|
}
|
|
cp->state = cp->laststate;
|
|
cp->ip = cp->ibuf;
|
|
cp->il = sizeof(cp->ibuf) - 1;
|
|
cp->op = cp->obuf;
|
|
cp->ol = strlen(cp->op);
|
|
cp->w = t + cp->stutter;
|
|
}
|
|
break;
|
|
case 60:
|
|
/* sent 354 blah */
|
|
cp->ip = cp->ibuf;
|
|
cp->il = sizeof(cp->ibuf) - 1;
|
|
cp->laststate = cp->state;
|
|
cp->state = 70;
|
|
cp->r = t;
|
|
break;
|
|
case 70: {
|
|
char *p, *q;
|
|
|
|
for (p = q = cp->ibuf; q <= cp->ip; ++q)
|
|
if (*q == '\n' || q == cp->ip) {
|
|
*q = 0;
|
|
if (q > p && q[-1] == '\r')
|
|
q[-1] = 0;
|
|
if (!strcmp(p, ".") ||
|
|
(cp->data_body && ++cp->data_lines >= 10)) {
|
|
cp->laststate = cp->state;
|
|
cp->state = 98;
|
|
goto done;
|
|
}
|
|
if (!cp->data_body && !*p)
|
|
cp->data_body = 1;
|
|
if (verbose && cp->data_body && *p)
|
|
syslog_r(LOG_DEBUG, &sdata, "%s: "
|
|
"Body: %s", cp->addr, p);
|
|
else if (verbose && (match(p, "FROM:") ||
|
|
match(p, "TO:") || match(p, "SUBJECT:")))
|
|
syslog_r(LOG_INFO, &sdata, "%s: %s",
|
|
cp->addr, p);
|
|
p = ++q;
|
|
}
|
|
cp->ip = cp->ibuf;
|
|
cp->il = sizeof(cp->ibuf) - 1;
|
|
cp->r = t;
|
|
break;
|
|
}
|
|
case 98:
|
|
done:
|
|
doreply(cp);
|
|
cp->op = cp->obuf;
|
|
cp->ol = strlen(cp->op);
|
|
cp->w = t + cp->stutter;
|
|
cp->laststate = cp->state;
|
|
cp->state = 99;
|
|
break;
|
|
case 99:
|
|
closecon(cp);
|
|
break;
|
|
default:
|
|
errx(1, "illegal state %d", cp->state);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void
|
|
handler(struct con *cp)
|
|
{
|
|
int end = 0;
|
|
ssize_t n;
|
|
|
|
if (cp->r || cp->tlsaction != SPAMD_TLS_ACT_NONE) {
|
|
if (cp->cctx) {
|
|
cp->tlsaction = SPAMD_TLS_ACT_NONE;
|
|
n = tls_read(cp->cctx, cp->ip, cp->il);
|
|
if (n == TLS_WANT_POLLIN)
|
|
cp->tlsaction = SPAMD_TLS_ACT_READ_POLLIN;
|
|
if (n == TLS_WANT_POLLOUT)
|
|
cp->tlsaction = SPAMD_TLS_ACT_READ_POLLOUT;
|
|
if (cp->tlsaction != SPAMD_TLS_ACT_NONE)
|
|
return;
|
|
} else
|
|
n = read(cp->pfd->fd, cp->ip, cp->il);
|
|
|
|
if (n == 0)
|
|
closecon(cp);
|
|
else if (n == -1) {
|
|
if (errno == EAGAIN)
|
|
return;
|
|
if (debug > 0)
|
|
warn("read");
|
|
closecon(cp);
|
|
} else {
|
|
cp->ip[n] = '\0';
|
|
if (cp->rend[0])
|
|
if (strpbrk(cp->ip, cp->rend))
|
|
end = 1;
|
|
cp->ip += n;
|
|
cp->il -= n;
|
|
}
|
|
}
|
|
if (end || cp->il == 0) {
|
|
while (cp->ip > cp->ibuf &&
|
|
(cp->ip[-1] == '\r' || cp->ip[-1] == '\n'))
|
|
cp->ip--;
|
|
*cp->ip = '\0';
|
|
cp->r = 0;
|
|
nextstate(cp);
|
|
}
|
|
}
|
|
|
|
void
|
|
handlew(struct con *cp, int one)
|
|
{
|
|
ssize_t n;
|
|
|
|
/* kill stutter on greylisted connections after initial delay */
|
|
if (cp->stutter && greylist && cp->blacklists == NULL &&
|
|
(t - cp->s) > grey_stutter)
|
|
cp->stutter=0;
|
|
|
|
if (cp->w || cp->tlsaction != SPAMD_TLS_ACT_NONE) {
|
|
if (*cp->op == '\n' && !cp->sr) {
|
|
/* insert \r before \n */
|
|
if (cp->cctx) {
|
|
cp->tlsaction = SPAMD_TLS_ACT_NONE;
|
|
n = tls_write(cp->cctx, "\r", 1);
|
|
if (n == TLS_WANT_POLLIN)
|
|
cp->tlsaction =
|
|
SPAMD_TLS_ACT_WRITE_POLLIN;
|
|
if (n == TLS_WANT_POLLOUT)
|
|
cp->tlsaction =
|
|
SPAMD_TLS_ACT_WRITE_POLLOUT;
|
|
if (cp->tlsaction != SPAMD_TLS_ACT_NONE)
|
|
return;
|
|
} else
|
|
n = write(cp->pfd->fd, "\r", 1);
|
|
|
|
if (n == 0) {
|
|
closecon(cp);
|
|
goto handled;
|
|
} else if (n == -1) {
|
|
if (errno == EAGAIN)
|
|
return;
|
|
if (debug > 0 && errno != EPIPE)
|
|
warn("write");
|
|
closecon(cp);
|
|
goto handled;
|
|
}
|
|
}
|
|
if (*cp->op == '\r')
|
|
cp->sr = 1;
|
|
else
|
|
cp->sr = 0;
|
|
if (cp->cctx) {
|
|
cp->tlsaction = SPAMD_TLS_ACT_NONE;
|
|
n = tls_write(cp->cctx, cp->op, cp->ol);
|
|
if (n == TLS_WANT_POLLIN)
|
|
cp->tlsaction = SPAMD_TLS_ACT_WRITE_POLLIN;
|
|
if (n == TLS_WANT_POLLOUT)
|
|
cp->tlsaction = SPAMD_TLS_ACT_WRITE_POLLOUT;
|
|
if (cp->tlsaction != SPAMD_TLS_ACT_NONE)
|
|
return;
|
|
} else
|
|
n = write(cp->pfd->fd, cp->op,
|
|
(one && cp->stutter) ? 1 : cp->ol);
|
|
|
|
if (n == 0)
|
|
closecon(cp);
|
|
else if (n == -1) {
|
|
if (errno == EAGAIN)
|
|
return;
|
|
if (debug > 0 && errno != EPIPE)
|
|
warn("write");
|
|
closecon(cp);
|
|
} else {
|
|
cp->op += n;
|
|
cp->ol -= n;
|
|
}
|
|
}
|
|
handled:
|
|
cp->w = t + cp->stutter;
|
|
if (cp->ol == 0) {
|
|
cp->w = 0;
|
|
nextstate(cp);
|
|
}
|
|
}
|
|
|
|
static int
|
|
get_maxfiles(void)
|
|
{
|
|
int mib[2], maxfiles;
|
|
size_t len;
|
|
|
|
mib[0] = CTL_KERN;
|
|
mib[1] = KERN_MAXFILES;
|
|
len = sizeof(maxfiles);
|
|
if (sysctl(mib, 2, &maxfiles, &len, NULL, 0) == -1)
|
|
return(MAXCON);
|
|
if ((maxfiles - 200) < 10)
|
|
errx(1, "kern.maxfiles is only %d, can not continue\n",
|
|
maxfiles);
|
|
else
|
|
return(maxfiles - 200);
|
|
}
|
|
|
|
/* Symbolic indexes for pfd[] below */
|
|
#define PFD_SMTPLISTEN 0
|
|
#define PFD_CONFLISTEN 1
|
|
#define PFD_SYNCFD 2
|
|
#define PFD_CONFFD 3
|
|
#define PFD_TRAPFD 4
|
|
#define PFD_GREYBACK 5
|
|
#define PFD_FIRSTCON 6
|
|
|
|
int
|
|
main(int argc, char *argv[])
|
|
{
|
|
struct pollfd *pfd;
|
|
struct sockaddr_in sin;
|
|
struct sockaddr_in lin;
|
|
int ch, smtplisten, conflisten, syncfd = -1, i, one = 1;
|
|
u_short port;
|
|
long long passt, greyt, whitet;
|
|
struct servent *ent;
|
|
struct rlimit rlp;
|
|
char *bind_address = NULL;
|
|
const char *errstr;
|
|
char *sync_iface = NULL;
|
|
char *sync_baddr = NULL;
|
|
struct addrinfo hints, *res;
|
|
char *addr;
|
|
char portstr[6];
|
|
int error;
|
|
|
|
tzset();
|
|
openlog_r("spamd", LOG_PID | LOG_NDELAY, LOG_DAEMON, &sdata);
|
|
|
|
if ((ent = getservbyname("spamd", "tcp")) == NULL)
|
|
errx(1, "Can't find service \"spamd\" in /etc/services");
|
|
port = ntohs(ent->s_port);
|
|
if ((ent = getservbyname("spamd-cfg", "tcp")) == NULL)
|
|
errx(1, "Can't find service \"spamd-cfg\" in /etc/services");
|
|
cfg_port = ntohs(ent->s_port);
|
|
if ((ent = getservbyname("spamd-sync", "udp")) == NULL)
|
|
errx(1, "Can't find service \"spamd-sync\" in /etc/services");
|
|
sync_port = ntohs(ent->s_port);
|
|
|
|
if (gethostname(hostname, sizeof hostname) == -1)
|
|
err(1, "gethostname");
|
|
maxfiles = get_maxfiles();
|
|
if (maxcon > maxfiles)
|
|
maxcon = maxfiles;
|
|
if (maxblack > maxfiles)
|
|
maxblack = maxfiles;
|
|
while ((ch =
|
|
getopt(argc, argv, "45l:c:B:p:bdG:h:s:S:M:n:vw:y:Y:C:K:")) != -1) {
|
|
switch (ch) {
|
|
case '4':
|
|
nreply = "450";
|
|
break;
|
|
case '5':
|
|
nreply = "550";
|
|
break;
|
|
case 'l':
|
|
bind_address = optarg;
|
|
break;
|
|
case 'B':
|
|
maxblack = strtonum(optarg, 0, INT_MAX, &errstr);
|
|
if (errstr)
|
|
errx(1, "-B %s: %s", optarg, errstr);
|
|
break;
|
|
case 'c':
|
|
maxcon = strtonum(optarg, 1, maxfiles, &errstr);
|
|
if (errstr) {
|
|
fprintf(stderr, "-c %s: %s\n", optarg, errstr);
|
|
usage();
|
|
}
|
|
break;
|
|
case 'p':
|
|
port = strtonum(optarg, 1, USHRT_MAX, &errstr);
|
|
if (errstr)
|
|
errx(1, "-p %s: %s", optarg, errstr);
|
|
break;
|
|
case 'd':
|
|
debug = 1;
|
|
break;
|
|
case 'b':
|
|
greylist = 0;
|
|
break;
|
|
case 'G':
|
|
if (sscanf(optarg, "%lld:%lld:%lld", &passt, &greyt,
|
|
&whitet) != 3)
|
|
usage();
|
|
passtime = passt;
|
|
greyexp = greyt;
|
|
whiteexp = whitet;
|
|
/* convert to seconds from minutes */
|
|
passtime *= 60;
|
|
/* convert to seconds from hours */
|
|
whiteexp *= (60 * 60);
|
|
/* convert to seconds from hours */
|
|
greyexp *= (60 * 60);
|
|
break;
|
|
case 'h':
|
|
memset(hostname, 0, sizeof(hostname));
|
|
if (strlcpy(hostname, optarg, sizeof(hostname)) >=
|
|
sizeof(hostname))
|
|
errx(1, "-h arg too long");
|
|
break;
|
|
case 's':
|
|
stutter = strtonum(optarg, 0, 10, &errstr);
|
|
if (errstr)
|
|
usage();
|
|
break;
|
|
case 'S':
|
|
grey_stutter = strtonum(optarg, 0, 90, &errstr);
|
|
if (errstr)
|
|
usage();
|
|
break;
|
|
case 'M':
|
|
low_prio_mx_ip = optarg;
|
|
break;
|
|
case 'n':
|
|
spamd = optarg;
|
|
break;
|
|
case 'v':
|
|
verbose = 1;
|
|
break;
|
|
case 'w':
|
|
window = strtonum(optarg, 1, INT_MAX, &errstr);
|
|
if (errstr)
|
|
errx(1, "-w %s: %s", optarg, errstr);
|
|
break;
|
|
case 'Y':
|
|
if (sync_addhost(optarg, sync_port) != 0)
|
|
sync_iface = optarg;
|
|
syncsend++;
|
|
break;
|
|
case 'y':
|
|
sync_baddr = optarg;
|
|
syncrecv++;
|
|
break;
|
|
case 'C':
|
|
tlscertfile = optarg;
|
|
break;
|
|
case 'K':
|
|
tlskeyfile = optarg;
|
|
break;
|
|
default:
|
|
usage();
|
|
break;
|
|
}
|
|
}
|
|
|
|
setproctitle("[priv]%s%s",
|
|
greylist ? " (greylist)" : "",
|
|
(syncrecv || syncsend) ? " (sync)" : "");
|
|
|
|
if (syncsend || syncrecv) {
|
|
syncfd = sync_init(sync_iface, sync_baddr, sync_port);
|
|
if (syncfd == -1)
|
|
err(1, "sync init");
|
|
}
|
|
|
|
if (geteuid())
|
|
errx(1, "need root privileges");
|
|
|
|
if ((pw = getpwnam(SPAMD_USER)) == NULL)
|
|
errx(1, "no such user %s", SPAMD_USER);
|
|
|
|
if (!greylist) {
|
|
maxblack = maxcon;
|
|
} else if (maxblack > maxcon)
|
|
usage();
|
|
|
|
spamd_tls_init();
|
|
|
|
rlp.rlim_cur = rlp.rlim_max = maxcon + 15;
|
|
if (setrlimit(RLIMIT_NOFILE, &rlp) == -1)
|
|
err(1, "setrlimit");
|
|
|
|
pfd = reallocarray(NULL, PFD_FIRSTCON + maxcon, sizeof(*pfd));
|
|
if (pfd == NULL)
|
|
err(1, "reallocarray");
|
|
|
|
con = calloc(maxcon, sizeof(*con));
|
|
if (con == NULL)
|
|
err(1, "calloc");
|
|
|
|
con->obuf = malloc(8192);
|
|
|
|
if (con->obuf == NULL)
|
|
err(1, "malloc");
|
|
con->osize = 8192;
|
|
|
|
for (i = 0; i < maxcon; i++) {
|
|
con[i].pfd = &pfd[PFD_FIRSTCON + i];
|
|
con[i].pfd->fd = -1;
|
|
}
|
|
|
|
signal(SIGPIPE, SIG_IGN);
|
|
|
|
smtplisten = socket(AF_INET, SOCK_STREAM, 0);
|
|
if (smtplisten == -1)
|
|
err(1, "socket");
|
|
|
|
if (setsockopt(smtplisten, SOL_SOCKET, SO_REUSEADDR, &one,
|
|
sizeof(one)) == -1)
|
|
return (-1);
|
|
|
|
conflisten = socket(AF_INET, SOCK_STREAM, 0);
|
|
if (conflisten == -1)
|
|
err(1, "socket");
|
|
|
|
if (setsockopt(conflisten, SOL_SOCKET, SO_REUSEADDR, &one,
|
|
sizeof(one)) == -1)
|
|
return (-1);
|
|
|
|
memset(&hints, 0, sizeof(hints));
|
|
hints.ai_family = AF_INET;
|
|
addr = bind_address;
|
|
snprintf(portstr, sizeof(portstr), "%hu", port);
|
|
|
|
if ((error = getaddrinfo(addr, portstr, &hints, &res)) != 0) {
|
|
errx(1, "getaddrinfo: %s", gai_strerror(error));
|
|
}
|
|
|
|
if (bind(smtplisten, res->ai_addr, res->ai_addrlen) == -1) {
|
|
freeaddrinfo(res);
|
|
err(1, "bind");
|
|
}
|
|
freeaddrinfo(res);
|
|
|
|
memset(&lin, 0, sizeof sin);
|
|
lin.sin_len = sizeof(sin);
|
|
lin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
lin.sin_family = AF_INET;
|
|
lin.sin_port = htons(cfg_port);
|
|
|
|
if (bind(conflisten, (struct sockaddr *)&lin, sizeof lin) == -1)
|
|
err(1, "bind local");
|
|
|
|
if (debug == 0) {
|
|
if (daemon(1, 1) == -1)
|
|
err(1, "daemon");
|
|
}
|
|
|
|
if (greylist) {
|
|
pfdev = open("/dev/pf", O_RDWR);
|
|
if (pfdev == -1) {
|
|
syslog_r(LOG_ERR, &sdata, "open /dev/pf: %m");
|
|
exit(1);
|
|
}
|
|
|
|
check_spamd_db();
|
|
|
|
maxblack = (maxblack >= maxcon) ? maxcon - 100 : maxblack;
|
|
if (maxblack < 0)
|
|
maxblack = 0;
|
|
|
|
/* open pipe to talk to greylister */
|
|
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, greyback) == -1) {
|
|
syslog(LOG_ERR, "socketpair (%m)");
|
|
exit(1);
|
|
}
|
|
if (pipe(greypipe) == -1) {
|
|
syslog(LOG_ERR, "pipe (%m)");
|
|
exit(1);
|
|
}
|
|
/* open pipe to receive spamtrap configs */
|
|
if (pipe(trappipe) == -1) {
|
|
syslog(LOG_ERR, "pipe (%m)");
|
|
exit(1);
|
|
}
|
|
jail_pid = fork();
|
|
switch (jail_pid) {
|
|
case -1:
|
|
syslog(LOG_ERR, "fork (%m)");
|
|
exit(1);
|
|
case 0:
|
|
/* child - continue */
|
|
signal(SIGPIPE, SIG_IGN);
|
|
grey = fdopen(greypipe[1], "w");
|
|
if (grey == NULL) {
|
|
syslog(LOG_ERR, "fdopen (%m)");
|
|
_exit(1);
|
|
}
|
|
close(greyback[0]);
|
|
close(greypipe[0]);
|
|
trapfd = trappipe[0];
|
|
trapcfg = fdopen(trappipe[0], "r");
|
|
if (trapcfg == NULL) {
|
|
syslog(LOG_ERR, "fdopen (%m)");
|
|
_exit(1);
|
|
}
|
|
close(trappipe[1]);
|
|
|
|
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))
|
|
err(1, "failed to drop privs");
|
|
|
|
goto jail;
|
|
}
|
|
/* parent - run greylister */
|
|
close(greyback[1]);
|
|
grey = fdopen(greypipe[0], "r");
|
|
if (grey == NULL) {
|
|
syslog(LOG_ERR, "fdopen (%m)");
|
|
exit(1);
|
|
}
|
|
close(greypipe[1]);
|
|
trapcfg = fdopen(trappipe[1], "w");
|
|
if (trapcfg == NULL) {
|
|
syslog(LOG_ERR, "fdopen (%m)");
|
|
exit(1);
|
|
}
|
|
close(trappipe[0]);
|
|
return (greywatcher());
|
|
}
|
|
|
|
jail:
|
|
if (pledge("stdio inet", NULL) == -1)
|
|
err(1, "pledge");
|
|
|
|
if (listen(smtplisten, 10) == -1)
|
|
err(1, "listen");
|
|
|
|
if (listen(conflisten, 10) == -1)
|
|
err(1, "listen");
|
|
|
|
if (debug != 0)
|
|
printf("listening for incoming connections.\n");
|
|
syslog_r(LOG_WARNING, &sdata, "listening for incoming connections.");
|
|
|
|
/* We always check for trap and sync events if configured. */
|
|
if (trapfd != -1) {
|
|
pfd[PFD_TRAPFD].fd = trapfd;
|
|
pfd[PFD_TRAPFD].events = POLLIN;
|
|
} else {
|
|
pfd[PFD_TRAPFD].fd = -1;
|
|
pfd[PFD_TRAPFD].events = 0;
|
|
}
|
|
if (syncrecv) {
|
|
pfd[PFD_SYNCFD].fd = syncfd;
|
|
pfd[PFD_SYNCFD].events = POLLIN;
|
|
} else {
|
|
pfd[PFD_SYNCFD].fd = -1;
|
|
pfd[PFD_SYNCFD].events = 0;
|
|
}
|
|
if (greylist) {
|
|
pfd[PFD_GREYBACK].fd = greyback[1];
|
|
pfd[PFD_GREYBACK].events = POLLIN;
|
|
} else {
|
|
pfd[PFD_GREYBACK].fd = -1;
|
|
pfd[PFD_GREYBACK].events = 0;
|
|
}
|
|
|
|
/* events and pfd entries for con[] are filled in below. */
|
|
pfd[PFD_SMTPLISTEN].fd = smtplisten;
|
|
pfd[PFD_CONFLISTEN].fd = conflisten;
|
|
|
|
while (1) {
|
|
int numcon = 0, n, timeout, writers;
|
|
|
|
time(&t);
|
|
|
|
writers = 0;
|
|
for (i = 0; i < maxcon; i++) {
|
|
if (con[i].pfd->fd == -1)
|
|
continue;
|
|
con[i].pfd->events = 0;
|
|
if (con[i].r) {
|
|
if (con[i].r + MAXTIME <= t) {
|
|
closecon(&con[i]);
|
|
continue;
|
|
}
|
|
con[i].pfd->events |= POLLIN;
|
|
}
|
|
if (con[i].w) {
|
|
if (con[i].w + MAXTIME <= t) {
|
|
closecon(&con[i]);
|
|
continue;
|
|
}
|
|
if (con[i].w <= t)
|
|
con[i].pfd->events |= POLLOUT;
|
|
writers = 1;
|
|
}
|
|
if (con[i].tlsaction == SPAMD_TLS_ACT_READ_POLLIN ||
|
|
con[i].tlsaction == SPAMD_TLS_ACT_WRITE_POLLIN)
|
|
con[i].pfd->events = POLLIN;
|
|
if (con[i].tlsaction == SPAMD_TLS_ACT_READ_POLLOUT ||
|
|
con[i].tlsaction == SPAMD_TLS_ACT_WRITE_POLLOUT)
|
|
con[i].pfd->events = POLLOUT;
|
|
if (i + 1 > numcon)
|
|
numcon = i + 1;
|
|
}
|
|
pfd[PFD_SMTPLISTEN].events = 0;
|
|
pfd[PFD_CONFLISTEN].events = 0;
|
|
pfd[PFD_CONFFD].events = 0;
|
|
pfd[PFD_CONFFD].fd = conffd;
|
|
if (slowdowntill == 0) {
|
|
pfd[PFD_SMTPLISTEN].events = POLLIN;
|
|
|
|
/* only one active config conn at a time */
|
|
if (conffd == -1)
|
|
pfd[PFD_CONFLISTEN].events = POLLIN;
|
|
else
|
|
pfd[PFD_CONFFD].events = POLLIN;
|
|
}
|
|
|
|
/* If we are not listening, wake up at least once a second */
|
|
if (writers == 0 && slowdowntill == 0)
|
|
timeout = INFTIM;
|
|
else
|
|
timeout = 1000;
|
|
|
|
n = poll(pfd, PFD_FIRSTCON + numcon, timeout);
|
|
if (n == -1) {
|
|
if (errno != EINTR)
|
|
err(1, "poll");
|
|
continue;
|
|
}
|
|
|
|
/* Check if we can speed up accept() calls */
|
|
if (slowdowntill && slowdowntill > t)
|
|
slowdowntill = 0;
|
|
|
|
for (i = 0; i < maxcon; i++) {
|
|
if (con[i].pfd->fd == -1)
|
|
continue;
|
|
if (pfd[PFD_FIRSTCON + i].revents & POLLHUP) {
|
|
closecon(&con[i]);
|
|
continue;
|
|
}
|
|
if (pfd[PFD_FIRSTCON + i].revents & POLLIN) {
|
|
if (con[i].tlsaction ==
|
|
SPAMD_TLS_ACT_WRITE_POLLIN)
|
|
handlew(&con[i], clients + 5 < maxcon);
|
|
else
|
|
handler(&con[i]);
|
|
}
|
|
if (pfd[PFD_FIRSTCON + i].revents & POLLOUT) {
|
|
if (con[i].tlsaction ==
|
|
SPAMD_TLS_ACT_READ_POLLOUT)
|
|
handler(&con[i]);
|
|
else
|
|
handlew(&con[i], clients + 5 < maxcon);
|
|
}
|
|
}
|
|
if (pfd[PFD_SMTPLISTEN].revents & (POLLIN|POLLHUP)) {
|
|
socklen_t sinlen;
|
|
int s2;
|
|
|
|
sinlen = sizeof(sin);
|
|
s2 = accept4(smtplisten, (struct sockaddr *)&sin, &sinlen,
|
|
SOCK_NONBLOCK);
|
|
if (s2 == -1) {
|
|
switch (errno) {
|
|
case EINTR:
|
|
case ECONNABORTED:
|
|
break;
|
|
case EMFILE:
|
|
case ENFILE:
|
|
slowdowntill = time(NULL) + 1;
|
|
break;
|
|
default:
|
|
errx(1, "accept");
|
|
}
|
|
} else {
|
|
/* Check if we hit the chosen fd limit */
|
|
for (i = 0; i < maxcon; i++)
|
|
if (con[i].pfd->fd == -1)
|
|
break;
|
|
if (i == maxcon) {
|
|
close(s2);
|
|
slowdowntill = 0;
|
|
} else {
|
|
initcon(&con[i], s2,
|
|
(struct sockaddr *)&sin);
|
|
syslog_r(LOG_INFO, &sdata,
|
|
"%s: connected (%d/%d)%s%s",
|
|
con[i].addr, clients, blackcount,
|
|
((con[i].lists == NULL) ? "" :
|
|
", lists:"),
|
|
((con[i].lists == NULL) ? "":
|
|
con[i].lists));
|
|
}
|
|
}
|
|
}
|
|
if (pfd[PFD_CONFLISTEN].revents & (POLLIN|POLLHUP)) {
|
|
socklen_t sinlen;
|
|
|
|
sinlen = sizeof(lin);
|
|
conffd = accept(conflisten, (struct sockaddr *)&lin,
|
|
&sinlen);
|
|
if (conffd == -1) {
|
|
switch (errno) {
|
|
case EINTR:
|
|
case ECONNABORTED:
|
|
break;
|
|
case EMFILE:
|
|
case ENFILE:
|
|
slowdowntill = time(NULL) + 1;
|
|
break;
|
|
default:
|
|
errx(1, "accept");
|
|
}
|
|
} else if (ntohs(lin.sin_port) >= IPPORT_RESERVED) {
|
|
close(conffd);
|
|
conffd = -1;
|
|
slowdowntill = 0;
|
|
}
|
|
} else if (pfd[PFD_CONFFD].revents & (POLLIN|POLLHUP))
|
|
do_config();
|
|
if (pfd[PFD_TRAPFD].revents & (POLLIN|POLLHUP))
|
|
read_configline(trapcfg);
|
|
if (pfd[PFD_SYNCFD].revents & (POLLIN|POLLHUP))
|
|
sync_recv();
|
|
if (pfd[PFD_GREYBACK].revents & (POLLIN|POLLHUP))
|
|
blackcheck(greyback[1]);
|
|
}
|
|
exit(1);
|
|
}
|
|
|
|
void
|
|
blackcheck(int fd)
|
|
{
|
|
struct sockaddr_storage ss;
|
|
ssize_t nread;
|
|
void *ia;
|
|
char ch;
|
|
|
|
/* Read sockaddr from greylister and look it up in the blacklists. */
|
|
nread = recv(fd, &ss, sizeof(ss), 0);
|
|
if (nread == -1) {
|
|
syslog(LOG_ERR, "%s: recv: %m", __func__);
|
|
return;
|
|
}
|
|
if (nread != sizeof(struct sockaddr_in) &&
|
|
nread != sizeof(struct sockaddr_in6)) {
|
|
syslog(LOG_ERR, "%s: invalid size %zd", __func__, nread);
|
|
return;
|
|
}
|
|
if (ss.ss_family == AF_INET) {
|
|
ia = &((struct sockaddr_in *)&ss)->sin_addr;
|
|
} else if (ss.ss_family == AF_INET6) {
|
|
ia = &((struct sockaddr_in6 *)&ss)->sin6_addr;
|
|
} else {
|
|
syslog(LOG_ERR, "%s: bad family %d", __func__, ss.ss_family);
|
|
return;
|
|
}
|
|
ch = sdl_check(blacklists, ss.ss_family, ia) ? '1' : '0';
|
|
|
|
/* Send '1' for match or '0' for no match. */
|
|
if (send(fd, &ch, sizeof(ch), 0) == -1) {
|
|
syslog(LOG_ERR, "%s: send: %m", __func__);
|
|
return;
|
|
}
|
|
}
|