/* $Id: asmtpd.C,v 1.45 2006/03/24 03:26:29 dm Exp $ */
/*
*
* Copyright (C) 2004 David Mazieres (dm@uun.org)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2, or (at
* your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
* USA
*
*/
#include "asmtpd.h"
#include "rawnet.h"
#include "getopt_long.h"
bool opt_d;
int opt_verbose;
bool terminated;
str path_avenger;
str path_bindir;
str path_pfos;
synfp_collect *synfpc;
struct listener {
const sockaddr_in sin;
int lfd;
list_entry<listener> link;
ihash_entry<listener> hlink;
listener (const sockaddr_in &a);
~listener ();
bool init ();
void doactive (bool on);
void doaccept ();
static void active (bool on);
static bool config (options *opt);
};
static list<listener, &listener::link> listen_list;
static ihash<const sockaddr_in, listener,
&listener::sin, &listener::hlink> listen_tab;
listener::listener (const sockaddr_in &a)
: sin (a), lfd (-1)
{
listen_list.insert_head (this);
listen_tab.insert (this);
}
listener::~listener ()
{
if (lfd >= 0) {
fdcb (lfd, selread, NULL);
close (lfd);
}
listen_list.remove (this);
listen_tab.remove (this);
}
bool
listener::init ()
{
if (lfd >= 0)
return true;
lfd = inetsocket (SOCK_STREAM, ntohs (sin.sin_port),
ntohl (sin.sin_addr.s_addr));
if (lfd < 0) {
warn ("TCP port %s:%d: %m\n", inet_ntoa (sin.sin_addr),
htons (sin.sin_port));
delete this;
return false;
}
close_on_exec (lfd);
make_async (lfd);
if (listen (lfd, 5) < 0) {
close (lfd);
lfd = -1;
return false;
}
return true;
}
void
listener::doactive (bool on)
{
if (on)
fdcb (lfd, selread, wrap (this, &listener::doaccept));
else
fdcb (lfd, selread, NULL);
}
void
listener::active (bool on)
{
for (listener *lp = listen_list.first; lp;
lp = listen_list.next (lp))
lp->doactive (on);
}
bool
listener::config (options *opt)
{
inet_bindaddr.s_addr = htonl (INADDR_ANY);
bool any = false;
for (sockaddr_in *sinp = opt->bindaddrv.base ();
sinp < opt->bindaddrv.lim (); sinp++) {
if (sinp->sin_addr.s_addr == htonl (INADDR_ANY))
any = true;
if (!listen_tab[*sinp])
vNew listener (*sinp);
}
for (listener *lp = listen_list.first, *nlp; lp; lp = nlp) {
nlp = listen_list.next (lp);
if (!opt->bindaddrh[lp->sin])
delete lp;
else
lp->init ();
}
// XXX
if (!any)
for (sockaddr_in *sinp = opt->bindaddrv.base ();
sinp < opt->bindaddrv.lim (); sinp++)
if (sinp->sin_addr.s_addr != htonl (INADDR_LOOPBACK)
&& listen_tab[*sinp]) {
inet_bindaddr = sinp->sin_addr;
break;
}
toggle_listen (true);
return listen_list.first;
}
newcon::newcon (int f, const sockaddr_in &sin)
: sin (sin), fd (f), ii (NULL), t (TRUST_NONE),
cbpending (0), failed (false)
{
}
void
newcon::init ()
{
cbpending++;
make_async (fd);
close_on_exec (fd);
if (sin.sin_addr.s_addr == htonl (INADDR_LOOPBACK))
t = TRUST_LOCAL;
else {
for (const ipmask *mp = opt->trustednets.base ();
t < TRUST_RCPT && mp < opt->trustednets.lim (); mp++)
if ((sin.sin_addr.s_addr & mp->mask) == mp->net)
t = TRUST_RCPT;
if (t < TRUST_RCPT) {
ii = ipinfo::lookup (sin.sin_addr, true);
if (str err = ii->addcon ()) {
write (fd, err, err.len ());
close (fd);
delete this;
return;
}
}
}
smtpd::num_smtpd++;
toggle_listen ();
if (!name) {
cbpending++;
identptr (fd, wrap (this, &newcon::ident_cb));
}
else if (!h) {
cbpending++;
dns_hostbyaddr (sin.sin_addr, wrap (this, &newcon::ptr_cb));
}
else {
cbpending++;
ptr_cb (h, 0);
}
#if USE_SYNFP
if (synfpc && t < TRUST_LOCAL) {
cbpending++;
synfpc->lookup (sin, wrap (this, &newcon::synfp_cb));
}
#endif /* USE_SYNFP */
maybe_start ();
}
void
newcon::ident_cb (str nn, ptr<hostent> hh, int err)
{
assert (nn);
name = nn;
ptr_cb (hh, err);
}
void
newcon::ptr_cb (ptr<hostent> hh, int err)
{
if (hh)
h = hh;
if (dns_tmperr (err) && sin.sin_addr.s_addr != htonl (INADDR_LOOPBACK)) {
warn << name << ": " << dns_strerror (err) << "\n";
if (opt->allow_dnsfail)
dns_error = strbuf () << name << ": " << dns_strerror (err);
else {
str msg (strbuf ("421 %s\r\n", dns_strerror (err)));
write (fd, msg.cstr (), msg.len ()); // Don't care if truncated
failed = true;
}
}
if (failed || opt->rbls.empty ())
maybe_start ();
else {
rs = New refcounted<rbl_status>;
rbl_check_con (rs, opt->rbls, sin.sin_addr,
h ? h->h_name : (char *) NULL,
wrap (this, &newcon::rbl_cb));
}
}
void
newcon::rbl_cb ()
{
maybe_start ();
}
void
newcon::synfp_cb (str fp)
{
#if SYNFP_DEBUG
if (fp)
warn ("synfp-cb %s:%d %s\n", inet_ntoa (sin.sin_addr),
ntohs (sin.sin_port), fp.cstr ());
else
warn ("synfp-cb %s:%d NULL\n", inet_ntoa (sin.sin_addr),
ntohs (sin.sin_port));
#endif
synfp = fp;
maybe_start ();
}
void
newcon::maybe_start ()
{
if (--cbpending)
return;
smtpd::num_smtpd--;
if (failed) {
if (ii) {
ii->error ();
ii->delcon ();
}
close (fd);
delete this;
toggle_listen ();
return;
}
if (h) {
str hname = h->h_name;
for (const str *tdp = opt->trusteddomains.base ();
t < TRUST_RCPT && tdp < opt->trusteddomains.lim (); tdp++) {
if (tdp->len () > hname.len ()
|| strcasecmp (tdp->cstr (),
hname.cstr () + hname.len () - tdp->len ()))
continue;
if (tdp->len () == hname.len ()) {
t = TRUST_RCPT;
}
else
switch (hname[hname.len () - tdp->len () - 1]) {
case '.':
case '@':
t = TRUST_RCPT;
}
}
}
if (t >= TRUST_RCPT && ii) {
ii->delcon ();
ii = NULL;
}
vNew smtpd (ii, fd, sin, name, synfp, t, rs, h, dns_error);
delete this;
}
void
listener::doaccept ()
{
sockaddr_in sin;
socklen_t sinlen = sizeof (sin);
bzero (&sin, sizeof (sin));
int fd = accept (lfd, (sockaddr *) &sin, &sinlen);
if (fd < 0) {
if (errno != EAGAIN)
warn ("accept: %m\n");
return;
}
if (opt->debug_smtpd)
warn ("accepted connection from %s:%d (fd %d)\n",
inet_ntoa (sin.sin_addr), ntohs (sin.sin_port), fd);
newcon *nc = New newcon (fd, sin);
nc->init ();
}
static void
doexit (int val)
{
warn << progname << " pid " << getpid () << " exiting\n";
exit (val);
}
void
toggle_listen (bool force)
{
static bool state;
if (terminated && !smtpd::num_smtpd) {
delaycb (0, 2000000, wrap (doexit, 0));
return;
}
bool ostate = state;
state = !terminated && smtpd::num_smtpd < opt->max_clients;
if (force || state != ostate)
listener::active (state);
}
static void
termsig (int sig)
{
if (terminated) {
static int nsig;
if (!smtpd::num_indata) {
warn ("hard shutdown on second signal\n");
doexit (1);
}
if (++nsig > 2) {
warn ("aborting clients in data and exiting immediately\n");
doexit (1);
}
warn ("waiting for clients still in data\n");
return;
}
warn ("shutting down on signal %d\n", sig);
terminated = true;
clear_filters ();
while (listen_list.first)
delete listen_list.first;
for (smtpd *s = smtplist.first, *ns; s; s = ns) {
ns = smtplist.next (s);
s->maybe_shutdown ();
}
toggle_listen ();
}
static void
reconfig ()
{
if (terminated)
return;
warn ("re-reading configuration file\n");
options *nopt = New options;
if (!parseconfig (nopt, config_file)) {
delete nopt;
warn ("errors found in config file, keeping old configuration\n");
return;
}
if (opt->logpriority != nopt->logpriority) {
syslog_priority = nopt->logpriority;
warn << "switching log priority to " << syslog_priority << "\n";
start_logger ();
}
if (!listener::config (nopt))
fatal ("No requested TCP ports available\n");
netpath_reset ();
delete opt;
opt = nopt;
#if USE_SYNFP
delete synfpc;
synfpc = NULL;
if (opt->synfp) {
synfpc = New synfp_collect (opt->synfp_wait, opt->synfp_buf);
if (!synfpc->init (opt->bindaddrv)) {
delete synfpc;
synfpc = NULL;
}
}
#endif /* USE_SYNFP */
ssl_init ();
}
static void
cleargroups ()
{
/* For efficiency, we want to be able to drop privileges to the
* avenger user without calling initgroups. So we'd better get rid
* of any supplemental privileged groups root might belong to. */
if (!getuid ()) {
GETGROUPS_T gid = getgid ();
#ifdef HAVE_EGID_IN_GROUPLIST
setgroups (1, &gid);
#else /* !HAVE_EGID_IN_GROUPLIST */
if (setgroups (0, NULL))
setgroups (1, &gid);
#endif /* !HAVE_EGID_IN_GROUPLIST */
}
}
static void
dumpstats ()
{
quota_dump (warnx);
}
static void
smtpstart ()
{
cleargroups ();
ssl_init ();
if (!listener::config (opt))
fatal ("No requested TCP ports available\n");
if (opt->smtp_filter)
run_cmd (opt->smtp_filter, "clear");
sigcb (SIGINT, wrap (&termsig, SIGINT));
sigcb (SIGTERM, wrap (&termsig, SIGTERM));
sigcb (SIGHUP, wrap (reconfig));
sigcb (SIGUSR1, wrap (dumpstats));
if (!opt_d) {
daemonize ();
if (!chdir ("/"))
xputenv ("PWD=/");
}
warn << progname << " (Mail Avenger) version " << VERSION << ", pid "
<< getpid () << "\n";
warn ("%s is %s\n", AVENGER, path_avenger.cstr ());
//warn ("pf.os is %s\n", path_pfos.cstr ());
warn ("bindir is %s\n", path_bindir.cstr ());
#if USE_SYNFP
if (opt->synfp) {
synfpc = New synfp_collect (opt->synfp_wait, opt->synfp_buf);
if (!synfpc->init (opt->bindaddrv)) {
delete synfpc;
synfpc = NULL;
}
}
#endif /* USE_SYNFP */
}
static bool tst_eof;
static int tst_n;
static void
spftst_2 (str line, spf_result res, str expl, str mech)
{
aout << ">>>" << spf_print (res) << ": " << line << "\n";
if (mech)
aout << " (" << mech << ")\n";
if (!--tst_n && tst_eof)
exit (0);
}
static void
spftst (str line, int err)
{
if (!line) {
tst_eof = true;
if (!tst_n)
exit (0);
return;
}
static rxx parse ("^(\\d+(\\.\\d+){3})\\s+(\\S+)(\\s+(\\S+))?$");
if (!parse.match (line))
warnx << "?syntax error\n";
else {
tst_n++;
in_addr a;
a.s_addr = inet_addr (parse[1]);
spf_check (a, parse[3], wrap (spftst_2, line), parse[5]);
}
ain->readline (wrap (spftst));
}
static void
rbltst_3 (str name, ref<rbl_status> stat)
{
aout << ">>> " << name << "\n";
aout << "score " << stat->score;
if (stat->trusted)
aout << ", whitelisted";
aout << "\n";
for (rbl_status::result *rp = stat->results.base ();
rp < stat->results.lim (); rp++)
aout << rp->tostr (false) << "\n";
aout << "----------------\n";
if (!--tst_n && tst_eof)
exit (0);
}
static void
rbltst_2 (ref<rbl_status> rs, in_addr addr, ptr<hostent> h, int err)
{
if (err && dns_tmperr (err))
warn << inet_ntoa (addr) << ": " << dns_strerror (err) << "\n";
if (h)
rbl_check_con (rs, opt->rbls, addr, h->h_name,
wrap (rbltst_3, inet_ntoa (addr), rs));
else
rbl_check_con (rs, opt->rbls, addr, NULL,
wrap (rbltst_3, inet_ntoa (addr), rs));
}
static void
rbltst (str line, int err)
{
if (!line) {
tst_eof = true;
if (!tst_n)
exit (0);
return;
}
ref<rbl_status> rs (New refcounted<rbl_status>);
in_addr a;
if (str r = extract_domain (line)) {
tst_n++;
rbl_check_env (rs, opt->rbls, r, wrap (rbltst_3, line, rs));
}
else if (inet_aton (line, &a) == 1) {
tst_n++;
dns_hostbyaddr (a, wrap (rbltst_2, rs, a));
}
else
aout << "?syntax error\n";
ain->readline (wrap (rbltst));
}
static void avenge_usage () __attribute__ ((noreturn));
static void
avenge_usage ()
{
warnx << "usage: " << progname << " --avenge recipient [sender [ip-addr]]\n";
exit (1);
}
static void
avenge_c (aios_t in, strbuf sb, ref<vec<str> > cmdv, str line, int err)
{
static rxx resprx ("^(\\d\\d\\d)(-| )(.*)$");
if (!line)
fatal ("test SMTP server: %s\n", strerror (err));
if (!resprx.match (line))
fatal ("bad line from test SMTP server:\n%s\n", line.cstr ());
sb << line << "\n";
if (resprx[2] == " ") {
str r (sb);
//warnx << r;
if (r[0] != '2') {
warnx << "\nrejected by SMTP server:\n" << r;
exit (1);
}
sb.tosuio ()->clear ();
if (cmdv->empty ()) {
warnx << "\naccepted by SMTP server:\n" << r;
exit (0);
}
//warnx << "\nSMTP test <<< " << cmdv->front () << "\n";
in << cmdv->pop_front () << "\r\n";
}
in->readline (wrap (avenge_c, in, sb, cmdv));
}
static void
avenge_s (int s, sockaddr_in sin, ptr<hostent> h, int err)
{
str msg;
if (!h && dns_tmperr (err))
msg = strbuf ("%s: %s", inet_ntoa (sin.sin_addr), dns_strerror (err));
vNew smtpd (NULL, s, sin, "SMTP-test", NULL, TRUST_NONE, NULL, h, msg);
}
static void
avenge (int argc, char **argv)
{
if (argc < 1 || argc > 3)
avenge_usage ();
int ls = inetsocket (SOCK_STREAM, 0,
htonl (opt->bindaddrv[0].sin_addr.s_addr));
if (ls < 0)
fatal ("socket: %m\n");
if (listen (ls, 1) < 0)
fatal ("listen: %m\n");
sockaddr_in sin;
socklen_t sinlen = sizeof (sin);
bzero (&sin, sizeof (sin));
getsockname (ls, (sockaddr *) &sin, &sinlen);
if (sin.sin_addr.s_addr == htonl (INADDR_ANY)) {
sin.sin_addr.s_addr = htonl (INADDR_LOOPBACK);
vec<in_addr> myips;
for (myipaddrs (&myips); !myips.empty (); myips.pop_front ())
if (myips[0].s_addr != ntohl (INADDR_LOOPBACK)) {
sin.sin_addr = myips[0];
break;
}
}
int c = inetsocket (SOCK_STREAM);
if (c < 0)
fatal ("socket: %m\n");
make_async (c);
if (connect (c, (sockaddr *) &sin, sizeof (sin)) < 0
&& errno != EINPROGRESS)
fatal ("TCP connect: %m\n");
sinlen = sizeof (sin);
int s = accept (ls, (sockaddr *) &sin, &sinlen);
if (s < 0)
fatal ("accept: %m\n");
close (ls);
make_async (s);
opt->debug_avenger = true;
opt->vrfy_delay = 0;
if (argc >= 3 && inet_aton (argv[2], &sin.sin_addr) != 1)
avenge_usage ();
dns_hostbyaddr (sin.sin_addr, wrap (avenge_s, s, sin));
ref<vec<str> > cmdv = New refcounted<vec<str> >;
cmdv->push_back (strbuf () << "ehlo " << opt->hostname);
if (argc >= 2)
cmdv->push_back (strbuf () << "mail from:<" << argv[1] <<">");
else
cmdv->push_back (strbuf () << "mail from:<postmaster@"
<< opt->hostname << ">");
cmdv->push_back (strbuf () << "rcpt to:<" << argv[0] <<">");
aios_t in (aios::alloc (c));
in->readline (wrap (avenge_c, in, strbuf (), cmdv));
}
static void
path_init ()
{
static rxx colonplus (":+");
str path = getenv ("PATH");
strbuf sb;
sb << "PATH=" << path_bindir;
vec<str> comp;
split (&comp, colonplus, path);
while (!comp.empty ())
if (comp.front () == path_bindir)
comp.pop_front ();
else
sb << ":" << comp.pop_front ();
path = sb;
xputenv (path);
//warn << path << "\n";
}
inline bool
execok (const char *path)
{
struct stat sb;
return !stat (path, &sb) && S_ISREG (sb.st_mode) && (sb.st_mode & 0111);
}
inline str
striplast (const char *in)
{
const char *p = in + strlen (in);
while (p > in && p[-1] == '/')
p--;
while (p > in && p[-1] != '/')
p--;
while (p > in && p[-1] == '/')
p--;
if (p > in)
return str (in, p - in);
return NULL;
}
inline str
stripfirst (const char *in)
{
while (in && *in && *in != '/')
in++;
while (in && *in && *in == '/')
in++;
if (*in)
return in;
return NULL;
}
static str
mycwd ()
{
struct stat sb1, sb2;
if (stat (".", &sb1))
return NULL;
if (char *pwd = getenv ("PWD"))
if (!stat (pwd, &sb2) && sb1.st_dev == sb2.st_dev
&& sb1.st_ino == sb2.st_ino)
return pwd;
char buf[MAXPATHLEN + 1];
return getcwd (buf, sizeof (buf));
}
static str
normalize_path (str path)
{
str dir;
if (path[0] != '/') {
while (path && path[0] == '.' && path[1] == '/')
path = stripfirst (path);
if (path && (dir = mycwd ())) {
if (!strncmp (path, "../", 3))
if (str ndir = striplast (dir)) {
path = stripfirst (path);
dir = ndir;
}
path = dir << "/" << path;
}
else if (!path)
path = mycwd ();
}
return path;
}
static void
find_avenger ()
{
str dir = progdir;
if (!dir) {
dir = find_program (progname);
if (dir)
dir = striplast (dir);
}
while (dir && dir.len () && dir[dir.len () - 1] == '/')
dir = substr (dir, 0, dir.len () - 1);
str path, bindir;
if (dir) {
if ((path = striplast (dir))) {
bindir = path << "/bin";
path_pfos = path << "/share/pf.os";
path = path << "/" << "libexec/" AVENGER;
if (!execok (path))
path = NULL;
}
if (!path && (path = striplast (dir))) {
bindir = path << "/util";
path = path << "/" AVENGER;
path_pfos = path << "/pf.os";
if (!execok (path))
path = NULL;
}
else if (!path && dir == ".") {
bindir = "../util";
path = "../" AVENGER;
path_pfos = "../pf.os";
if (!execok (path))
path = NULL;
}
else if (!path) {
bindir = "util";
path = AVENGER;
path_pfos = "pf.os";
if (!execok (path))
path = NULL;
}
}
if (!path) {
bindir = BINDIR;
path_pfos = DATADIR "/pf.os";
if (!execok (path = LIBEXEC "/" AVENGER))
path = NULL;
}
if (path) {
path = normalize_path (path);
bindir = normalize_path (bindir);
path_pfos = normalize_path (path_pfos);
}
if (!path)
fatal ("cannot find %s program\n", AVENGER);
path_avenger = path;
path_bindir = bindir;
if (access (path_pfos, 0) < 0)
path_pfos = DATADIR "/pf.os";
if (access (path_pfos, 0) && !access ("/etc/pf.os", 0))
path_pfos = "/etc/pf.os";
path_init ();
}
static str
compile_options ()
{
bool set = false;
strbuf sb;
#define setopt(opt) \
do { \
sb << (set ? " " : " (") << #opt; \
set = true; \
} while (0)
#if !USE_SYNFP
setopt (no-synfp);
#endif /* !USE_SYNFP */
#ifdef SASL
setopt (SASL);
#endif /* SASL */
#ifndef STARTTLS
setopt (no-starttls);
#endif /* !STARTTLS */
#undef setopt
if (set)
sb << ")";
return sb;
}
static void usage () __attribute__ ((noreturn));
static void
usage ()
{
warnx << "usage: " << progname << " [-d] [-f <config-file]\n"
#if USE_SYNFP
<< " [--spf | --rbl | --avenge | --synfp | --netpath] ...\n";
#else /* !USE_SYNFP */
<< " [--spf | --rbl | --avenge | --netpath] ...\n";
#endif /* !USE_SYNFP */
exit (1);
}
int
main (int argc, char **argv)
{
setprogname (argv[0]);
int mode = 0;
option o[] = {
{ "version", no_argument, &mode, 1 },
{ "help", no_argument, &mode, 2 },
{ "spf", no_argument, &mode, 3 },
{ "rbl", no_argument, &mode, 4 },
#if USE_SYNFP
{ "synfp", no_argument, &mode, 5 },
#endif /* USE_SYNFP */
{ "netpath", no_argument, &mode, 6 },
{ "avenge", no_argument, &mode, 7 },
{ "verbose", no_argument, &opt_verbose, 1 },
{ NULL, 0, NULL, 0 }
};
int c;
while ((c = getopt_long (argc, argv, "+dDf:", o, NULL)) != -1)
switch (c) {
case 0:
break;
case 'd':
opt_d = true;
break;
case 'D':
opt_d = false;
break;
case 'f':
config_file = optarg;
break;
default:
usage ();
break;
}
switch (mode) {
case 1:
warnx << progname << " (Mail Avenger) " << VERSION
<< compile_options () << "\n"
<< "Copyright (C) 2004-2005 David Mazieres\n"
<< "This program comes with NO WARRANTY,"
<< " to the extent permitted by law.\n"
<< "You may redistribute it under the terms of"
<< " the GNU General Public License;\n"
<< "see the file named COPYING for details.\n";
return 0;
case 2:
usage ();
#if USE_SYNFP
case 5:
find_avenger ();
synfp_test (argc - optind, argv + optind);
amain ();
#endif /* USE_SYNFP */
case 6:
netpath_test (argc - optind, argv + optind);
amain ();
}
if (!parseconfig (opt, config_file))
fatal ("error parsing asmtpd.conf file\n");
syslog_priority = opt->logpriority;
if (mode == 7) {
find_avenger ();
avenge (argc - optind, argv + optind);
amain ();
}
if (optind != argc)
usage ();
switch (mode) {
case 0:
find_avenger ();
smtpstart ();
break;
case 3:
aout << "SPF test mode; enter: <IP address> <from address>"
" [<helo host>]\n";
ain->readline (wrap (spftst));
amain ();
break;
case 4:
aout << "RBL test mode; enter {<IP address> | <from address>}\n";
ain->readline (wrap (rbltst));
amain ();
break;
default:
usage ();
return 1;
}
amain ();
}
syntax highlighted by Code2HTML, v. 0.9.1