How we make Exim cut off bounce loops
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:
eat_looping_bounces: debug_print = "R: eat_looping_bounces for $local_part@$domain" driver = redirect file = <dir>/exim-loop-filter allow_filter allow_freeze user = Debian-exim no_verify no_expn
(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 then logwrite "$tod_log junked looping-bounce $message_id from <$sender_address> to <$local_part@$domain> subject: $h_subject:" seen finish endif
(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.)
|
|