How we make Exim cut off bounce loops

March 6, 2013

Under certain circumstances it's possible to get bounce loops; a message bounces, the bounce for the message bounces, the bounce of the bounce then bounces, and you repeat endlessly (possibly until something explodes). When this happened to us, we decided to fix our Exim configuration so that it would detect and suppress these bounce loops (or at least as many of them as possible).

How we do it is a close relative of how we make Exim discard bounces of spam. First, we created a custom bounce message by setting bounce_message_file to an appropriately formatted file. This lets us add headers to bounce messages and those headers can refer to headers in the message bouncing. So we added a very special header:

X-CS-Bounce-Count: I$h_x-cs-bounce-count:

I have bolded the little bit of magic. What this does is count how many bounce-of-bounces we've seen by giving every such bounce one more 'I' than the previous bounce had (if there's no previous bounce the header is empty and we start out with one I).

We could handle detecting and discarding messages with too high a bounce count entirely in an Exim router, but it turns out that it is much easier and more convenient to do most of the work in an Exim filter. So what we have is a simple router that runs all messages through an Exim filter (although now that I look at it, we should make the router conditional on the presence of the header because there's no point otherwise). Thus we get the router:

    debug_print = "R: eat_looping_bounces for $local_part@$domain"
    driver = redirect
    file = <dir>/exim-loop-filter
    user = Debian-exim

(This router should be among your very first routers to insure that it runs before any other router that could conceivably generate a bounce.)

The Exim filter itself (with comments removed) is simply:

logfile /var/log/exim4/discardlog
if ${strlen:$h_x-cs-bounce-count:} is above 4
    logwrite "$tod_log junked looping-bounce $message_id from <$sender_address> to <$local_part@$domain> subject: $h_subject:"
    seen finish

(We allow more than one bounce just in case, as a safety measure. When I put all of this together I didn't want to sit down and go through the work to carefully make sure that we could never wind up with a bounce counter greater than one in a legitimate situation.)

Unlike our discarding of bounces of spam we don't carefully guard this router (and this filter) with conditions to make sure that we're really truly dealing with local bounces. The thing about bounce loops is that they can easily involve outside machines as well, so we want to squelch them whenever they pass through our mail machine.

Sidebar: how bounce loops happen

Some of my readers may now be wondering how on earth you get a bounce loop, since bounces are supposed to be sent using a null sender address (often written as '<>') and all messages to the null sender (bounces included) are just discarded (to stop exactly this sort of loop). The sad answer is that not all programs that resend messages are careful to preserve the null sender address on email they send out. In particular this includes the mail forwarding done by our version of procmail when it's run by a user from their .forward; instead it winds up changing the sender address to the user's email address (because it actually resubmits the email, exactly as if the user had sent it in the first place). This creates an immediate loop if the destination of the forwarding ever refuses some piece of email; the bounce of the refusal goes to the user, their .forward and procmail re-forwards it to the destination, the destination refuses it again, the system generates a new bounce to the user, repeat endlessly.

(The incident that saw us discover this issue managed to somehow multiply the email as it looped the bounces around. The result was quite dramatic.)

Written on 06 March 2013.
« Turning off delays on failed password authentications
Debian shows how to do Apache configuration right (and Fedora fumbles it) »

Page tools: View Source, Add Comment.
Login: Password:
Atom Syndication: Recent Comments.

Last modified: Wed Mar 6 22:55:47 2013
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.