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:

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:

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 setAdd warning in headerReject 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.