/* $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 link; ihash_entry 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 listen_list; static ihash 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 hh, int err) { assert (nn); name = nn; ptr_cb (hh, err); } void newcon::ptr_cb (ptr 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_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 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 rs, in_addr addr, ptr 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 rs (New refcounted); 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 > 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 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 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 > cmdv = New refcounted >; 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: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 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 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: " " []\n"; ain->readline (wrap (spftst)); amain (); break; case 4: aout << "RBL test mode; enter { | }\n"; ain->readline (wrap (rbltst)); amain (); break; default: usage (); return 1; } amain (); }