Adding SMTP transaction delays

Spam Filtering for Mail Exchangers:
Prev | Appendix A. Exim Implementation | Next

# A.5. Adding SMTP transaction delays

## A.5.1. The simple way

The simplest way to add SMTP transaction delays is to append a `delay` control to the final `accept` statement in each of the ACLs we have declared, as follows:

      accept
        delay = 20s

In addition, you may want to add progressive delays in the `deny` statement pertaining to invalid recipients ("unknown user") within acl_rcpt_to. This is to slow down dictionary attacks. For instance:

      deny
        message     = unknown user
        !verify     = recipient/callout=20s,defer_ok,use_sender
        delay       = ${eval:$rcpt_fail_count*10 + 20}s

It should be noted that there is no point in imposing a delay in acl_data, after the message data has been received. Ratware commonly disconnect at this point, before even receiving a response from your server. In any case, whether or not the client disconnects at this point has no bearing on whether Exim will proceed with the delivery of the message.

## A.5.2. Selective Delays

If you are like me, you want to be a little bit more selective about which hosts you subject to SMTP transaction delays. For instance, as described earlier in this document, you may decide that a match from a DNS blacklist or a non-verifiable EHLO/HELO greeting are not conditions that by themselves warrant a rejection - but they may well be sufficient triggers for transaction delays.

In order perform selective delays, we want move some of the checks that we previously did in acl_rcpt_to to earlier points in the SMTP transaction. This is so that we can start imposing the delays as soon as we see any sign of trouble, and thereby increase the chance of causing synchronization errors and other trouble for ratware.

Specifically, we want to:

- Move the DNS checks to acl_connect.

- Move the Hello checks to acl_helo. One exception: We cannot yet check for a missing Hello greeting at this point, because this ACL is processed in response to an EHLO or HELO command. We will do this check in the acl_mail_from ACL.

- Move the Sender Address Checks checks to acl_mail_from.

However, for reasons described above, we do not want to actually reject the mail until after the RCPT TO: command. Instead, in the earlier ACLs, we will convert the various `deny` statements into `warn` statements, and use Exim's general purpose ACL variables to store any error messages or warnings until after the RCPT TO: command. We do that as follows:

- If we decide to reject the delivery, we store an error message to be used in the forthcoming 550 response in `$acl_c0` or `$acl_m0`:

  - If we identify the condition before a mail delivery has started (i.e. in acl_connect or acl_helo), we use the connection-persistent variable `$acl_c0`

  - Once a mail transaction has started (i.e. after the MAIL FROM: command), we copy any contents from `$acl_c0` into the message-specific variable `$acl_m0`, and use the latter from this point forward. This way, any conditions identified in this particular message will not affect any subsequent messages received in the same connection.

  Also, we store a corresponding log message in `$acl_c1` or `$acl_m1`, in a similar manner.

- If we come across a condition that does not warrant an outright rejection, we only store a warning message in `$acl_c1` or `$acl_m1`. Once a mail transaction has started (i.e. in acl_mail_from), we add any content in this variable to the message header as well.

- If we decide to accept a message without regard to the results of any subsequent checks (such as a SpamAssassin scan), we set a flag in `$acl_c0` or `$acl_m0`, but `$acl_c1` and `$acl_m1` empty.

- At the beginning of every ACL to and including acl_mail_from, we record the current timestamp in `$acl_m2`. At the end of the ACL, we use the presence of `$acl_c1` or `$acl_m1` to trigger a SMTP transaction delay until a total of 20 seconds has elapsed.

The following table summarizes our use of these variables:

Table A-1. Use of ACL connection/message variables

Variables: | $acl_[cm]0 unset | $acl_[cm]0 set
--- | --- | ---
$acl_[cm]1 unset | (No decision yet) | Accept the mail
$acl_[cm]1 set | Add warning in header | Reject the mail

As an example of this approach, let us consider two checks that we do in response to the Hello greeting; one that will reject mails if the peer greets with an IP address, and one that will warn about an unverifiable name in the greeting. Previously, we did both of these checks in acl_rcpt_to - now we move them to the acl_helo ACL.

    acl_helo:
      # Record the current timestamp, in order to calculate elapsed time
      # for subsequent delays
      warn
        set acl_m2  = $tod_epoch

      # Accept mail received over local SMTP (i.e. not over TCP/IP).
      # We do this by testing for an empty sending host field.
      # Also accept mails received from hosts for which we relay mail.
      #
      accept
        hosts       = : +relay_from_hosts

      # If the remote host greets with an IP address, then prepare a reject
      # message in $acl_c0, and a log message in $acl_c1.  We will later use
      # these in a "deny" statement.  In the mean time, their presence indicate
      # that we should keep stalling the sender.
      #
      warn
        condition   = ${if isip {$sender_helo_name}{true}{false}}
        set acl_c0  = Message was delivered by ratware
        set acl_c1  = remote host used IP address in HELO/EHLO greeting

      # If HELO verification fails, we prepare a warning message in acl_c1.
      # We will later add this message to the mail header.  In the mean time,
      # its presence indicates that we should keep stalling the sender.
      #
      warn
        condition   = ${if !def:acl_c1 {true}{false}}
        !verify     = helo
        set acl_c1  = X-HELO-Warning: Remote host $sender_host_address \
                      ${if def:sender_host_name {($sender_host_name) }}\
                      incorrectly presented itself as $sender_helo_name
        log_message = remote host presented unverifiable HELO/EHLO greeting.

      #
      # ... additional checks omitted for this example ...
      #

      # Accept the connection, but if we previously generated a message in
      # $acl_c1, stall the sender until 20 seconds has elapsed.
      accept
        set acl_m2  = ${if def:acl_c1 {${eval:20 + $acl_m2 - $tod_epoch}}{0}}
        delay       = ${if >{$acl_m2}{0}{$acl_m2}{0}}s

Then, in acl_mail_from we transfer the messages from `$acl_c{0,1}` to `$acl_m{0,1}`. We also add the contents of `$acl_c1` to the message header.

    acl_mail_from:
      # Record the current timestamp, in order to calculate elapsed time
      # for subsequent delays
      warn
        set acl_m2  = $tod_epoch

      # Accept mail received over local SMTP (i.e. not over TCP/IP).
      # We do this by testing for an empty sending host field.
      # Also accept mails received from hosts for which we relay mail.
      #
      accept
        hosts     = : +relay_from_hosts

      # If present, the ACL variables $acl_c0 and $acl_c1 contain rejection
      # and/or warning messages to be applied to every delivery attempt in
      # in this SMTP transaction.  Assign these to the corresponding
      # $acl_m{0,1} message-specific variables, and add any warning message
      # from $acl_m1 to the message header.  (In the case of a rejection,
      # $acl_m1 actually contains a log message instead, but this does not
      # matter, as we will discard the header along with the message).
      #
      warn
        set acl_m0  = $acl_c0
        set acl_m1  = $acl_c1
        message     = $acl_c1

      #
      # ... additional checks omitted for this example ...
      #

      # Accept the sender, but if we previously generated a message in
      # $acl_c1, stall the sender until 20 seconds has elapsed.
      accept
        set acl_m2  = ${if def:acl_c1 {${eval:20 + $acl_m2 - $tod_epoch}}{0}}
        delay       = ${if >{$acl_m2}{0}{$acl_m2}{0}}s

All the pertinent changes are incorporated in the Final ACLs, to follow.

Prev | Home | Next
Building the ACLs - First Pass | Up | Adding Greylisting Support