spf-discuss
[Top] [All Lists]

Practical implementation of SRS for Exim 4.

2004-02-24 06:11:04
Two points to note, having implemented this in the wild.

Firstly, a lot of people assume local-parts are case-insensitive, and
may even _lose_ case information when transferring the reverse-path of
your original message into the destination of their bounce.

It's useful, therefore, to make the hash case-insensitive. You should do
your best to _preserve_ case, so you generate addresses of the form

 SRS0+HHH+TT+domain(_dot_)com+StudlyCapsUser(_at_)yourdomain(_dot_)com

but it's probably useful if you also accept in return this form:
 srs0+HHH+TT+domain(_dot_)com+studlycapsuser(_at_)yourdomain(_dot_)com
... rewriting it to studlycapsuser(_at_)domain(_dot_)com as appropriate. Someone
_else_ lost the case information, but that shouldn't actually matter in
the majority of cases.

The security implications of this appear to be negligible.

The second point to note is that some recipients (like pobox.com)
perform sender verification callouts using <postmaster(_at_)$domain> as the
sender address in the 'test' message. So rejecting all non-DSN mail to
SRS0+ addresses is suboptimal. My implementation now accepts messages
from postmaster(_at_)* to the SRS0+ addresses too.

My Exim 4 implementation now conforms to Shevek's specification for
shorter local-parts, although obviously not implementing the shortcuts
which I believe would violate the AUP of my network providers. It looks
like this...


--------- In the config somewhere ---------

# Define this to handle SRS-bounces
SRS_SECRET=somesecret
# And this if you want to (probably temporarily) accept two keys.
#SRS_OLD_SECRET=
domainlist rpr_domains = *.srs.mydomain.com
SRS_HASH_LENGTH=20
SRS_DSN_TIMEOUT=7
SRS_URL=http://spf.pobox.com/srs.html
# Define this to enable SRS on forwarding.
SRS_DOMAIN=srs.mydomain.com

--------- In the routers immediately before 'lookuphost' ---------

.ifdef SRS_SECRET
  # Urgh. Isn't there a better way to detect that we're in sender verification?
rpr_mark_sender_verify:
  verify_only
  verify_recipient = false
  driver = redirect
  data = ${quote_local_part:$local_part}(_at_)$domain
  address_data = verifying sender
  redirect_router = rpr_bounce

  # Verify, and extract return address from, an SRS-address.
  # Don't allow non-bounces, except from postmaster(_at_)* since some people use
  # that for sender-verification callbacks.
rpr_bounce:
  caseful_local_part
  driver = redirect
  domains = +rpr_domains
  allow_fail
  data = ${if !match 
{$local_part}{\N^[sS][rR][sS]0\+([^+]+)\+([0-9]+)\+([^+]+)\+(.*)\N} \
                {:fail: Invalid SRS bounce \
.ifdef SRS_DEBUG
                        (malformed)\
.endif
                } \
        {${if and {{!eq 
{$1}{${length_SRS_HASH_LENGTH:${hmac{md5}{SRS_SECRET}{${lc:$2+$3+$4(_at_)$domain}}}}}}
 \
.ifdef SRS_OLD_SECRET
                  {!eq 
{$1}{${length_SRS_HASH_LENGTH:${hmac{md5}{SRS_OLD_SECRET}{${lc:$2+$3+$4(_at_)$domain}}}}}}
 \
.endif
                  } \
                {:fail: Invalid SRS bounce \
.ifdef SRS_DEBUG
                        (HMAC should be 
${length_SRS_HASH_LENGTH:${hmac{md5}{SRS_SECRET}{${lc:$2+$3+$4(_at_)$domain}}}} 
not $1)\
.endif
                } \
        {${if <{$2}{${eval:$tod_epoch/86400-12288-SRS_DSN_TIMEOUT}} \
                {:fail: Invalid SRS bounce \
.ifdef SRS_DEBUG
                        (expired 
${eval:$tod_epoch/86400-12288-SRS_DSN_TIMEOUT-$2} days ago)\
.endif
                } \
        {${if >{$2}{${eval:$tod_epoch/86400-12288}} \
                {:fail: Invalid SRS bounce \
.ifdef SRS_DEBUG
                        (timestamp in future)\
.endif
                } \
        {${if and { {!eq {$sender_address}{}} \
                    {!eqi {$sender_address_local_part}{postmaster}}\
                    {!eq {$address_data}{verifying sender}}\
                  } \
                {:fail: Invalid SRS bounce \
.ifdef SRS_DEBUG
                        (Not DSN: $sender_address instead)\
.endif
                }\
# Wheee. At last the actual rewrite part...
        {${quote_local_part:$4}(_at_)$3}\
        }}}}}}}}}
  headers_add = X-SRS-Return: DSN routed via $primary_hostname. See SRS_URL

# Rewrite reverse-path so that forwarding to known SPF-afflicted
# servers doesn't break. We generate a limited-lifetime hash cookie,
# from which we can later recreate the original sender address. We
# include the hostname and more precise timestamp in the domain of the
# generated address, so that we can track down the offending message
# in the log if it _does_ offend us.
        
.ifdef SRS_DOMAIN
rpr_outgoing_goto:
  driver = redirect
  # Don't rewrite unless the recipient is in a domain we _know_ to be broken
  # but for local reasons have decided we need to work around.
  # The text file listing broken recipient domains should look something like:
  #    gmx.net: all
  #    gmx.de: all
  #    aol.com: spf
  # This lookup will leave the result of the lookup in $domain_data.
  domains = lsearch;CONFDIR/spf-afflicted-domains
  # Don't rewrite if it's a bounce, or from one of our own addresses.
  senders = ! : ! *(_at_)+local_domains : ! *(_at_)+virtual_domains
  # We expect either 'all' or 'spf' in $domain_data from the textfile lookup
  # If the reason for the breakage is listed as 'SPF', then don't rewrite
  # unless the sender's domain actually advertises SPF records.
  condition = ${if or { {!eq{$domain_data}{spf}} \
                        {match {${lookup 
dnsdb{txt=$sender_address_domain}{$value}fail}}{v=spf1}} \
                        } {1}}
  # We want to rewrite. We just jump to the rpr_rewrite router which is itself 
unconditional.
  data = ${quote_local_part:$local_part}(_at_)$domain
  redirect_router = rpr_rewrite

  # Some addresses are joe-job protected by _always_ using SRS, and never 
actually
  # sending mail from that address. That way, we can always reject bounces to 
these
  # addresses, and prevent joe-jobs from being received by anyone who actually 
bothers
  # to do sender verification callouts.
rpr_always_else:
  driver = redirect
  senders = !@@lsearch;CONFDIR/always-srs-senders
  data = ${quote_local_part:$local_part}(_at_)$domain
  redirect_router = lookuphost


  # This is now unconditional. Either rpr_outgoing_goto jumped here
  # because it's a mail we're forwarding to a known broken server, or
  # rpr_always_else _didn't_ jump over us because it's from a sender
  # listed in always-srs-senders. 
rpr_rewrite:
  headers_add = "X-SRS-Rewrite: SMTP reverse-path rewritten from 
<$sender_address> by $primary_hostname\n\tSee SRS_URL"
  # Encode sender address, hash and timestamp according to 
http://www.anarres.org/projects/srs/
  # We try to keep the generated localpart small. We add our own tracking info 
to the domain part.
  address_data = ${eval:($tod_epoch/86400)-12288}+\
                ${sender_address_domain}+$sender_address_local_part\
                @${sg {$primary_hostname}{^([^.]*)\..*}{\$1}}-\
                ${sg {$tod_log}{^.* ([0-9]+):([0-9]+):([0-9+])}{\$1\$2\$3}}.\
                SRS_DOMAIN
  errors_to = 
${quote_local_part:SRS0+${length_SRS_HASH_LENGTH:${hmac{md5}{SRS_SECRET}{${lc:$address_data}}}}+\
                ${sg{$address_data}{(^.*)@[^(_at_)]*}{\$1}}}@\
                ${sg{$address_data}{^.*@([^(_at_)]*)}{\$1}}
  driver = redirect
  data = ${quote_local_part:$local_part}(_at_)$domain
# Straight to output; don't start routing again from the beginning.
  redirect_router = lookuphost
  no_verify
.endif // SRS_DOMAIN
.endif // SRS_SECRET




-- 
dwmw2


<Prev in Thread] Current Thread [Next in Thread>