Dual-instance sendmail with amavisd-new
---------------------------------------
  Mark Martinec,  2003-05-06
  (based on initial research by Ricardo Stella)
  updated on: 2005-09-22 (added a reference to 'milter-ahead');
  updated on: 2005-09-29 (added custom rules to reject unknown users outright,
                          provided by Matej Vela, thanks to Simone Marx)
  updated on: 2006-09-15 (placement of DKIM/DK milters, mention the feature
                          FEATURE(`nocanonify',`canonify_hosts'), the absence
                          of which can make header procesing by sm *very* long)

The most recent version of this document can be found at:
  http://www.ijs.si/software/amavisd/README.sendmail-dual

==========================================================================

The setup is very similar to the one described in README.sendmail
(by Rainer Link) in section 'Scanning incoming/outgoing and relayed mail',
except that it uses SMTP protocol over inet socket (instead of pipes
to commands) to transfer files between MTA and amavisd-new and back,
and that it uses a permanently running second sendmail instance
in 'queue only' delivery mode, instead of bringing it up every time
a new checked mail comes from amavisd.

==========================================================================

Comparing the setup described in this document with the sendmail milter
setup, as described in README.milter:

milter - reasons in favour:
- can REJECT on the original SMTP session, instead of generating a bounce
  (sending a non-delivery notification _after_ the mail has been enqueued);

- only one sendmail daemon need be running, only one config file needed,
  no additional queue area needed  (although starting with sendmail 8.12
  more than one queue area is already a norm: clientmqueue, queue groups;
  and MSP already uses a different .cf file);


dual-MTA - reasons in favour:
- Full amavisd-new functionality is available, including adding spam and
  virus information header fields, adding address extensions and removing
  certain recipients from delivery while delivering the same message to
  the rest (*_lovers). Also a message can be split if different recipients
  need different header edits. All this is not available when using
  amavis-milter helper program.

- Content scanning need not be performed at the time of mail reception.
  This allows better control on CPU-intensive content filtering: mail
  checking can be streamlined and performed at optimum throughput setting
  (number of content checker processes) so as not to overwhelm host resources,
  instead of leaving it at the mercy of the current number of incoming
  SMTP sessions where available crude controls are mostly based on system
  load. Typically the number of incoming SMTP sessions (tiny processes)
  is desired to be many times above the number of content filtering
  processes (heavy resource consumers).

- No helper programs needed, MTA communicates with amavisd-new directly
  via SMTP, saves on creating one directory and one file for each message,
  and deleting it (at the cost of one additional transfer);

- Receiving sendmail daemon (MTA-RX) need not run as root (using option
  RunAsUser) since it does not need to run any local delivery agents (LDA)
  or to access user .forward files. This avoids external SMTP clients
  talking directly to a process running as root.

==========================================================================


The following setup is described in this document:

      ............................   ............................
      : sendmail instance MTA-RX :   : sendmail instance MTA-TX :
      :                          :   :                          :
 25 -----> \ (mqueue-rx)         :   : (mqueue)          / -------> forward
587 ----->  > -queue-            :   : -queue-   ->-----+       :
 ^    :    /     | MAIL_HUB,     :   :   |               \ -------> local
 |    :          v SMART_HOST    :   :   ^                      :  delivery
msp   ...........|................   ....|.......................
                 |                       ^ loopback interface
                 v                       | port 10025
 loopback interf.| port 10024            |
            .....|.......................|.............
            : $inet_socket_port=10024    |            :
            :                            |            :
            : $forward_method='smtp:127.0.0.1:10025'  :
            : $notify_method ='smtp:127.0.0.1:10025'  :
            :                                         :
            :    amavisd-new                          :
            ...........................................


The setup is based on the recent sendmail (8.12.9 or later) with its set of
m4 configuration macros. Because of several security problems with earlier
versions of sendmail it is advised to stick to the most recent version,
although the functionality needed for this setup has long been available.
If a particular macro or feature is not available with some older version,
it is usually possible to achieve the same or similar by manually writing
a new 'mailer' specification and/or tweaking the .cf file.


We'll prepare two sendmail daemon instances (processes), let's call them
MTA-RX (receiving, accepting) and MTA-TX (transmitting, delivering).

For convenience we keep the name of the configuration file and the
queue (spool) area at default names for one mailer instance, and choose
non-default names for the other. Let's choose the MTA-TX to keep default
names, and supply non-default names to MTA-RX explicitly. This will make
admin utilities like mailq, newaliases, hoststat and purgestat operate
on the outgoing mailer instance unless explicitly told otherwise.
It could just as well be the other way around.

MTA-RX (receiving mailer) will be responsible for accepting mail from
the Internet or from internal hosts on port 25, optionally accepting local
message submissions on tcp port 587 (rfc2476), and for message submissions
via sendmail program. It will forward all mail (both for local and for
nonlocal recipients) via SMTP protocol (or LMTP) to 127.0.0.1 (a loopback
interface) on tcp port 10024, where amavisd daemon will be listening.
- its queue:       /var/spool/mqueue-rx
- its config file: /etc/mail/sendmail-rx.cf, /etc/mail/submit.cf
- the source (.mc) of the configuration file: thishost-rx.mc
  (where 'thishost' is often by convention the name of the system (uname))

MTA-TX (transmitting mailer) will be responsible for accepting checked
mail and notifications from amavisd-new via SMTP on the loopback interface
(127.0.0.1) at tcp port 10025, and will forward all mail to its final
destinations, either for local delivery, or delivering outgoing mail
to the Internet or to other internal mailers.
- its queue:         /var/spool/mqueue
- its config file:   /etc/mail/sendmail.cf
- the source (.mc) of the configuration file: thishost-tx.mc

In-between the two MTAs an amavisd daemon will accept mail via SMTP (or LMTP)
protocol on tcp port 10024, check it, and forward checked mail and
notifications via SMTP to MTA-TX.

If you already have an existing sendmail installation, you already
have a queue directory /var/spool/mqueue and the configuration file(s)
(.mc source and the compiled .cf file). Most of the existing settings
in your .mc file can be reused, and are to be moved to the new files
thishost-rx.mc or thishost-tx.mc, or (some of them) to both.
The settings pertaining to receiving mail, including recource limits,
should go to thishost-rx.mc; settings pertaining to delivering mail
(locally or to other mailers) should go to thishost-tx.mc, and general
settings should go to both.

The MTA-TX should have none or hardly any resource limits, or at least
have them larger than MTA-RX. Large messages, common errors in mail, and
mail rush-ins should be stopped or limited at their entry to the system.
Accepting them first, but choking later can lead to trouble or at least
to wasted resources.

The file names thishost-rx.mc and thishost-tx.mc are arbitrary, they only
serve as source (to the m4 macro processor) for producing .cf files,
which control sendmail's behaviour. Sendmail never uses .mc files directly.


MTA-TX already got its queue directory during sendmail installation.

For MTA-RX a new queue directory needs to be created where incoming
mail can be collected. Use the same ownership and protection as used
for /var/spool/mqueue, e.g:
  # mkdir /var/spool/mqueue-rx
  # chown root:wheel /var/spool/mqueue-rx
  # chmod 700 /var/spool/mqueue-rx

SECURITY NOTE:
  starting with sendmail 8.12 it is possible to start sendmail daemon
  as root and let it drop privileges (become user specified by RunAsUser)
  after binding to port 25. This is normally used by MSP, and it can
  just as well be used by MTA-RX, since it has no need to access user
  mailboxes and .forward files. To use this feature, specify user and
  group in the macro confRUN_AS_USER (file thishost-rx.mc), and set the
  ownership of mqueue-rx to this user and group:
  # chown smmsp:smmsp /var/spool/mqueue-rx
  # chmod 770 /var/spool/mqueue-rx


More complex queue setup is possible if needed, like separating
sendmail work area and core dump area from actual queues.
For details about queue groups see sendmail documentation.


Create file thishost-rx.mc:
---cut-here------------------------------
dnl To be used for MTA-RX, the first MTA instance (receiving mail)

dnl Insert here the usual .mc preamble, including OSTYPE and DOMAIN calls.

dnl Specify here also access controls, relayable domains, anti-spam measures
dnl including milter settings if needed, mail submission settings, client
dnl authentication, resource controls, maximum mail size and header size,
dnl confMIN_FREE_BLOCKS, and other settings needed for receiving mail.
dnl
dnl NOTE:
dnl   confMIN_FREE_BLOCKS at MTA-RX should be kept higher than the same
dnl   setting at MTA-TX to quench down clients when disk space is low,
dnl   and not to stop processing the already received mail.
dnl
dnl In particular, here are some settings to be considered:
dnl   ( see also http://www.sendmail.org/m4/anti_spam.html )
dnl
dnl FEATURE(`access_db')
dnl VIRTUSER_DOMAIN(`sub1.example.com')dnl  list valid users here
dnl VIRTUSER_DOMAIN(`sub2.example.com')dnl  list valid users here
dnl FEATURE(`virtusertable')
dnl define(`confUSERDB_SPEC', `/etc/mail/userdb.db')
dnl FEATURE(`blacklist_recipients')
dnl FEATURE(`use_cw_file')
dnl FEATURE(`use_ct_file')
dnl FEATURE(`nocanonify', `canonify_hosts')dnl
dnl INPUT_MAIL_FILTER(...)
dnl define(`confPRIVACY_FLAGS', `noexpn,novrfy,authwarnings')  nobodyreturn ?
dnl define(`confDONT_PROBE_INTERFACES')
dnl MASQUERADE_AS(...) FEATURE(`allmasquerade') FEATURE(`masquerade_envelope')
dnl define(`confTO_IDENT', `0')dnl  Disable IDENT
dnl define(`confMAX_MESSAGE_SIZE',`10485760')
dnl define(`confMAX_MIME_HEADER_LENGTH', `256/128')
dnl define(`confNO_RCPT_ACTION', `add-to-undisclosed')
dnl define(`confBIND_OPTS', ...)
dnl define(`confTO_RESOLVER_*... )
dnl define(`confDELAY_LA,    8)
dnl define(`confREFUSE_LA', 12)
dnl define(`confMAX_DAEMON_CHILDREN',20)
dnl define(`confMIN_FREE_BLOCKS', `10000')
dnl define(`confDEF_USER_ID', ...)

define(`confRUN_AS_USER',`smmsp:smmsp')dnl  Drop privileges (see SECURITY NOTE)

define(`confPID_FILE', `/var/run/sendmail-rx.pid')dnl  Non-default pid file
define(`STATUS_FILE', `/etc/mail/stat-rx')dnl    Non-default stat file
define(`QUEUE_DIR', `/var/spool/mqueue-rx')dnl   Non-default queue area
define(`confQUEUE_SORT_ORDER',`Modification')dnl Modif or Random are reasonable

dnl Match the number of queue runners (R=) to the number of amavisd-new child
dnl processes ($max_servers). 2 to 7 OK, 10 is plenty, 20 is too many
QUEUE_GROUP(`mqueue', `P=/var/spool/mqueue-rx, R=2, F=f')dnl

dnl Direct all mail to be forwarded to amavisd-new at 127.0.0.1:10024
FEATURE(stickyhost)dnl  Keep envelope addr "u@local.host" when fwd to MAIL_HUB
define(`MAIL_HUB',   `esmtp:[127.0.0.1]')dnl  Forward all local mail to amavisd
define(`SMART_HOST', `esmtp:[127.0.0.1]')dnl  Forward all other mail to amavisd
define(`LOCAL_RELAY',`esmtp:[127.0.0.1]')dnl

define(`confDELIVERY_MODE',`q')dnl     Delivery mode: queue only (a must,
dnl  ... otherwise the advantage of this setup of being able to specify
dnl  ... the number of queue runners is lost)
define(`ESMTP_MAILER_ARGS',`TCP $h 10024')dnl  To tcp port 10024 instead of 25
MODIFY_MAILER_FLAGS(`ESMTP', `+z')dnl  Speak LMTP (this is optional)
define(`SMTP_MAILER_MAXMSGS',`10')dnl  Max no. of msgs in a single connection
define(`confTO_DATAFINAL',`20m')dnl    20 minute timeout for content checking
DAEMON_OPTIONS(`Name=MTA-RX')dnl       Daemon name used in logged messages

dnl Disable local delivery, as all local mail will go to MAIL_HUB
undefine(`ALIAS_FILE')dnl     No aliases file, all local mail goes to MAIL_HUB
define(`confFORWARD_PATH')dnl Empty search path for .forward files
undefine(`UUCP_RELAY')dnl
undefine(`BITNET_RELAY')dnl
undefine(`DECNET_RELAY')dnl

MAILER(smtp)

dnl  The following solution to reject unknown recipients outright
dnl  is provided by Matej Vela <m...@irb.hr>, see:
dnl  http://groups.google.com/group/comp.mail.sendmail/
dnl    browse_thread/thread/88cc72d7c4d3a6e/ee2a9474b3a4558d
dnl  The FEATURE(stickyhost) short-circuits FEATURE(luser_relay) so that a:
dnl    define(`LUSER_RELAY',`error:5.1.1:"550 User unknown"') can't be used.
dnl  A simple solution is to disable FEATURE(stickyhost). If this is not
dnl  possible, the alternative is to replace FEATURE(luser_relay) with custom
dnl  rules below. The latter has the advantage of properly handling special
dnl  aliases like ("|program", "/mailbox", and ":include:/list").  If choosing
dnl  this route, one should NOT use `undefine(`ALIAS_FILE')dnl', and use the
dnl  following custom rules:
dnl

LOCAL_CONFIG
Kaliasp hash -m /etc/mail/aliases
Kuserp user -m

LOCAL_RULESETS
SLocal_check_rcpt
R$*		$: <?> $&{rcpt_addr}
R<?> $+ @ $=w	$: <@> $1				mark local address
R<?> $* @ $*	$@ OK					ignore remote address
R<?> $+		$: <@> $1				mark unqualified user
R<@> $+ + $*	$: < $(aliasp $1+$2 $: @ $) > $1 + *	plussed alias?
R<@> $+ + $*	$: < $(aliasp $1+$2 $: @ $) > $1	+* alias?
R<@> $+		$: < $(aliasp $1 $: @ $) > $1		normal alias?
R<@> $+		$: < $(userp $1 $: @ $) > $1		system user?
R<@> $+		$#error $@ 5.1.1 $: "550 User unknown"	nope, go away 

---end-----------------------------------



Create file thishost-tx.mc:
---cut-here------------------------------
dnl To be used for MTA-TX, the second MTA instance
dnl (delivering outgoing and local mail)

dnl Insert here the usual .mc preamble, including OSTYPE and DOMAIN calls.

dnl Specify here also the required outgoing mail processing and
dnl local delivery settings such as mailertables, needed mailers, aliases,
dnl local delivery mailer settings, smrsh, delivery mode, queue groups, ...
dnl Don't use milters here - for all common filtering purposes they belong
dnl to MTA-RX; an exception to this rule would be DKIM or DomainKeys mail
dnl signing milters (signature _verification_ milters still belong to MTA-RX).

define(`confREFUSE_LA',999)dnl  Disable the feature, limiting belongs to MTA-RX
define(`confMAX_DAEMON_CHILDREN',0)dnl  Disable, limiting belongs to MTA-RX
FEATURE(`no_default_msa')dnl  No need for another MSA, MTA-RX already has one
FEATURE(`nocanonify')dnl      Host/domain names are considered canonical
DAEMON_OPTIONS(`Addr=127.0.0.1, Port=10025, Name=MTA-TX')dnl Listen on lo:10025
define(`confSMTP_LOGIN_MSG', `$w.tx.$m Sendmail $v/$Z; $b')dnl
define(`confTO_IDENT', `0')dnl  Disable IDENT

MAILER(smtp)
MAILER(local)
---end-----------------------------------


Now macro-expand .mc files into .cf files:
(adjust the path if needed to where your cf/m4/cf.m4 file resides)

# m4 /usr/share/sendmail/cf/m4/cf.m4 thishost-rx.mc >/etc/mail/sendmail-rx.cf
# m4 /usr/share/sendmail/cf/m4/cf.m4 thishost-tx.mc >/etc/mail/sendmail.cf

Start MTA-RX and MTA-TX daemons:
# /usr/sbin/sendmail -C/etc/mail/sendmail-rx.cf -L sm-mta-rx -bd -qp
# /usr/sbin/sendmail                            -L sm-mta-tx -bd -q15m

Start queue runner for the MSP client queue as usual, if using it:
# /usr/sbin/sendmail -Ac -L sm-msp-queue -q10m


Start amavisd-new:
# amavisd

Test if MTA-RX is listening:
# telnet localhost 25
QUIT

Test if MTA-RX is listening on MSA port 587 (a newer sendmail invention)
# telnet localhost 587
QUIT

Test if MTA-TX is listening:
# telnet localhost 10025
QUIT

Test if amavisd is listening:
# telnet localhost 10024
QUIT



For convenience some shell aliases may be defined:

alias mailq-rx='mailq -C/etc/mail/sendmail-rx.cf'
alias mailq-tx='mailq'
alias sendmail-rx='/usr/sbin/sendmail -C/etc/mail/sendmail-rx.cf'
alias sendmail-tx='/usr/sbin/sendmail'


All done!


NOTES

- In amavisd.conf file follow the 'POSTFIX or EXIM V4 or dual MTA setup',
  which is also the default.

- The $final_*_destiny should not specify D_REJECT.
  The D_BOUNCE (or D_PASS or D_DISCARD) is preferred.

- To make MTA-RX content-check only some mail but not all, one may use
  mailertables instead of MAIL_HUB and SMART_HOST. For example setting
  some recipient domains to be passed to MTA-TX at 127.0.0.1:10025
  directly (e.g. via mailer 'esmtp'), while sending all the rest to
  amavisd at 127.0.0.1:10024. To be able to specify the port number,
  a new 'mailer' needs to be defined, let's call it 'amavis', with similar
  settings as the already defined 'esmtp', except with port number 10024.

- depending on how local addresses are translated by MTA-RX, the
  %local_domains (or @local_domains_maps) in amavisd.conf needs to be
  adjusted accordingly to be able to recognize local domains. Check the
  amavisd-new log what recipient addresses it sees for local recipients.
  The '[127.0.0.1]' may need to be added to the @local_domains.

- To make MTA-RX reject mail for nonexistent local users by itself
  (instead of generating a bounce later on), one may use the 'virtusertable'
  in thishost-rx.mc, listing all known recipients, and rejecting the rest,
  e.g.:

  VIRTUSER_DOMAIN(`example.com')dnl
  FEATURE(`virtusertable', `hash /etc/mail/virtusertable')dnl

    jim@example.com         %1%3
    joe@example.com         %1%3
    postmaster@example.com  %1
    @example.com            error:5.7.0:550 No such user here

  You may use the righthand side of the map to specify local user
  (e.g. %1%3, or just jim, without domain name) in which case MAIL_HUB will
  be used for forwarding, or specify an explicit domain name that is not
  in the {w} class, in which case the SMART_HOST will get consulted.

  Perhaps what Stephane Lentz writes is even better:

    Dictionary attacks and messages to retired accounts can be bounced with
    sendmail: just replicate your aliases or write some SLocal_check_rcpt
    rule-set that checks addresses of your domain against a map of valid users
    (valid_addresses.db).  I hope some standard FEATURE will be provided
    with sendmail - something like FEATURE(checkdomainaddresses) and
    CHECKDOMAINADDRESSES(mydomain.com).

  An alternative solution is to use a milter to do address verification
  against the second MTA in chain. See the milter-ahead project:

    http://www.milter.info/sendmail/milter-ahead/



PERFORMANCE NOTES

- Mail handling is I/O-intensive. For better performance one may place
  the two mail queue areas (/var/spool/mqueue and /var/spool/mqueue-rx),
  and the /var/amavis work directory ($TEMPBASE) on three separate disks.
  The /var/amavis/tmp may be a tmpfs or a RAMdisk or delayed-sync fs.

- One of the important arguments for choosing the dual-MTA setup is to be
  able to keep the number of content filtering processes under control,
  and not at the mercy of current mail inflow. Don't blow this advantage
  by setting the number of amavisd-new processes and MTA-RX queue runners
  too high!

  Throughput optimum is somewhere between 3 and 10 with fast daemonized
  av-scanner (or no av scanner) (with or without SpamAssassin),
  and between 2 and 3 with many command line scanners (regardless of SA).
  If the host is low on memory and when spam checking (SpamAssassin)
  is used, even 2 may be a lot for an elderly host.

  Start conservatively, e.g. at 2 or 3, and if everything works normally
  and higher throughput is needed, try a bit more. Anything above the point
  where throughput function levels off is just a waste of memory and
  gains nothing!

  The optimum may be higher if high-latency external SpamAssassin tests
  are enabled (e.g. Razor, RBL), Still, never go beyond available memory.
  For example with SpamAssassin enabled, the 20..25 processes on a 256 MB
  machine is where throughput begins to drop rapidly on a way to a swapping
  tar pit.
