We want to detect zombie PCs. When PCs connect using
an internal private IP, and a bogus domain, they are
most likely a zombie. Currently, the milter
application uses the "internal_domains" config item to
restrict internal connections to the listed domains.
It would be nice to have the full flexibility of SPF
for this job, but the public SPF records can't be used
because the connections are on private IPs. I would
like a way to extend the public SPF records with local
additions for internal connections. The list of
extended domains would be relatively small (a single
enterprise), so could be supplied via a python dict.
The config might look like this in the milter
application (SPF module would just get the dict):
[spf]
mycompany.com = ip4:192.168.0.0/24
myalias.net = ip4:192.168.0.0/24 ip4:192.168.1.0/24
Logged In: YES
user_id=1300068
I think we also want to add a layer of separation between
SPF results and SMTP results. I.e. SPF = Pass, Fail, None,
Unknown, Softfail, PermError, TempError - SMTP = 250, 450, 550.
Logged In: YES
user_id=142072
It just occured to me that the SPF module should not even
have to deal with a dict for local policy. There should be
a local parameter when creating a query. The caller can get
it from a dict or database as needed.
For instance,
q =
spf.query(self.connectip,self.canonfrom,self.hello_name,receiver=receiver,
local="ip4:1.2.3.4")
Logged In: YES
user_id=142072
The SMTP result returned along with the SPF result in a
tuple is a suggestion only. You can remove it if you wish,
since we are already breaking the API by changing the result
set. It is mostly ignored.
It might be cleaner, since you are changing the API, to
return a simple string for the result, and have an attribute
for the explanation. For example,
q =
spf.query(self.connectip,'@'.join(t),self.hello_name,receiver=receiver)
res = q.check() # was res,code,txt = q.check()
txt = q.explanation
But this is good too:
res,txt = q.check()
Logged In: YES
user_id=1300068
# Recommended SMTP codes for certain SPF results. For
results not in
# this table the recommendation is to accept the message.
# The softfail result requires special processing.
SMTP_CODES = {
'fail': (550,'5.7.1'),
'temperror': (451,'4.4.3'),
'permerror': (550,'5.5.2'),
'softfail':(451,'4.3.0')
Logged In: YES
user_id=1300068
Should this also return the SPF-received header field for
the Milter to write into the message header?
Logged In: YES
user_id=1300068
I've changed my mind on this. pySPF 1.6 (the Terrence
version) is the preferred SPF library for one Postfix policy
daemon and has also recently been added to Debian.
So, if we want to get other users to move up to the more
modern/complete version here, we have to maintain a
compatible result set. I think that's res, code, txt, but
I'm going to check to make sure.
Rather than pass only the SPF result out with and
explanation, I'm going to pass the local policy in so that
the returned unified result can be returned with an
appropriate SMTP code.
Logged In: YES
user_id=1300068
It turns out that the current local implementation is done
the same way the Mail::SPF::Query and Libspf2 do it. Had a
conversation with Wayne about it on #SPF today (below).
Bottom line is that I think that pySPF as it ought to
function the way it does now (local after SPF) as a default,
but that the local policy module ought to give this as a
policy option.
Full transcript is attached. Here are the highlights:
Message from grumpy at 02:41:52 pm
You might want to make the "local policy" stuff work
like M:S:Q and libspf2. (I copied M;S:Q semantics.) They are
well thought out, not obvious, and somewhat standardized.
Message from grumpy at 02:42:25 pm
In particular, local policies need to be ignore for
records like "v=spf1 -all"
Message to #spf at 02:42:33 pm
OK. Is there documentation I can look at...
Message to #spf at 02:44:21 pm
I certainly have no desire to be gratuitously incompatible.
* grumpy returns with lunch
Message from grumpy at 02:49:57 pm
I'm afraid the best documentation that I know if is
somewhat burried in the libspf2 source...
Message from grumpy at 02:50:05 pm
let me get you a URL or something.
Message to #spf at 02:50:45 pm
Thanks. I'd rather avoid having to learn Perl to write
the Python. Of course if the option is to learn C, maybe
Perl isn't so bad...
Message from grumpy at 02:52:30 pm
google for "spf local policy" returns as the first hit:
http://www.libspf2.org/docs/api.html
Message from grumpy at 02:52:41 pm
amazingly, that has the description.
Message from grumpy at 02:52:46 pm
thanks Shevek!
Message from grumpy at 02:54:06 pm
the description is about half way down the web page
under the SPF_compile_local_policy() function.
Logged In: YES
user_id=1300068
Upon further investigation it appears to me that the current
local policy implementation doesn't actually work.
Logged In: YES
user_id=142072
I said as much. It is rarely useful to apply after public
policy. Libspf2 has some neat ideas (especially compling
the SPF records), but their local_policy idea is too
complex. Maybe it is something different from what libspf2
calls "local policy", but what I need in pymilter is to put
some mechanisms in *front* of the record for selected domains.
For instance, blockbuster.com sends event email with a MAIL
FROM: <blockbuster@custhelp.com>. But custhelp.com has an
SPF records that flags this mail as a forgery:
2005Aug02 15:24:03 [153181] connect from
smtp1.online.blockbuster.com at ('129.33.92.74', 40308) EXTERNAL
2005Aug02 15:24:03 [153181] hello from
smtp1.online.blockbuster.com
2005Aug02 15:24:04 [153181] mail from
<blockbuster@custhelp.com> ('SIZE=1773',)
2005Aug02 15:24:04 [153181] REJECT: SPF fail 550 SPF fail:
see
http://spf.pobox.com/why.html?sender=blockbuster@custhelp.com&ip=129.33.92.74
Both companies have ignored months worth of emails to
postmaster and complaints on their website. Their solution
is for me to "whitelist" blockbuster@custhelp.com. Of
course, I also get *truly* forged email from
blockbuster@custhelp.com, so I can't simply skip SPF for
that MAIL FROM. Instead, I want to insert the following in
front of custhelp.com SPF record:
[local_policy]
custhelp.com = ptr:blockbuster.com
Whenever I fetch the public custhelp.com SPF record, currently:
v=spf1 ip4:63.240.89.0/24 ip4:63.240.103.0/26
ip4:216.136.162.64/26 ip4:216.136.229.0/24
ip4:216.136.168.64/27 ip4:206.17.168.0/25 ip4:64.79.35.0/24 -all
I stick the above in front to get:
v=spf1 ptr:blockbuster.com ip4:63.240.89.0/24
ip4:63.240.103.0/26 ip4:216.136.162.64/26
ip4:216.136.229.0/24 ip4:216.136.168.64/27
ip4:206.17.168.0/25 ip4:64.79.35.0/24 -all
This lets me whitelist just the blockbuster braindamage
without letting in all the real forgeries.
Similarly, I can use the above for local domains. If my
local domain is example.com:
[local_policy]
example.com = ip4:192.168.45.0/24 ip4:127.0.0.0/8
This says to accept the example.com MAIL FROM domain from
those internal networks, even though they are not mentioned
in the public record.
Logged In: YES
user_id=1300068
The problem is that the current code puts the local policy
AFTER the all and so unless the local policy is a modifier,
it never gets acted on.
My intent is to make pySPF local work exactly like
Mail::SPF::Query and libspf2 since they are the reference
implementations. I'll also have a switch to make it work
the way you want.
You are right. Their way is complex (you have to parse out
the record, find the all or redirect= if any and then add
the local policy right before it.... I do want to make sure
we are as compatible as possible, but then give options.
The complexity that their way requires will be there, but
I'll write it so that if the switch is thrown, the complex
code will never get excercised.
Logged In: YES
user_id=1300068
Local will now be applied after the last non-fail mechanism
in the record, so it at least functions now. Still to do is
to add something to the explanation to indicate it's a
result of local policy and to update the received SPF
header. Once that's done, it'll work like M:S:Q and libspf2.
Then I'll add a switch to make it work like you want.
Logged In: YES
user_id=1300068
SPF library is not the place to do this.