# SpamAssassin - ASN Lookup Plugin # # <@LICENSE> # Copyright 2006 dnswl.org, Matthias Leisi # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # ########################################################################### =head1 NAME B Plugin - SpamAssassin plugin to look up the Autonomous System Number (ASN) of the connecting IP address. This plugin uses DNS lookups to the services of http://www.routeviews.org/ to do the actual work. Please make sure that your use of the plugin does not overload their infrastructure - this generally means that B or that you should use a local mirror of the zone (see ftp://ftp.routeviews.org/dnszones/). =head1 SYNOPSIS loadplugin Mail::SpamAssassin::Plugin::ASN header ASN_LOOKUP eval:asn_lookup('asn.routeviews.org') =head1 TEMPLATE TAGS This plugin adds two tags, C<_ASN_> and C<_ASNCIDR_>, which can be used in places where such tags can usually be used. add_header all ASN _ASN_ _ASNCIDR_ may add something like X-Spam-ASN: AS24940 213.239.192.0/18 where "AS24940" is the ASN and "213.239.192.0/18" is the route announced by that ASN where the connecting IP address came from. =head1 CONFIGURATION This plugin has no user-serviceable parts or configurations. The standard loading sequence is as follows: loadplugin Mail::SpamAssassin::Plugin::ASN ifplugin Mail::SpamAssassin::Plugin::ASN header ASN_LOOKUP eval:asn_lookup('asn.routeviews.org') add_header all ASN _ASN_ _ASNCIDR_ endif B that you should not score on this rule - it is merely informational. Bayes learning will probably trigger on the _ASNCIDR_ tag, but probably not very well on the _ASN_ tag alone. B that the zone to lookup the ASN in must be given as a parameter to the asn_lookup eval function. This is especially important if you use a locally mirrored zone. =cut package Mail::SpamAssassin::Plugin::ASN; use strict; use Mail::SpamAssassin; use Mail::SpamAssassin::Plugin; use Mail::SpamAssassin::Logger; use Mail::SpamAssassin::Dns; our @ISA = qw(Mail::SpamAssassin::Plugin); sub new { my ($class, $mailsa) = @_; $class = ref($class) || $class; my $self = $class->SUPER::new($mailsa); bless ($self, $class); $self->register_eval_rule("asn_lookup"); return $self; } sub asn_lookup { my ($self, $scanner, $zone) = @_; if (!$scanner->is_dns_available()) { $self->{dns_not_available} = 1; return; } # Default to empty strings; otherwise, the tags will be left as _ASN_ # and _ASNCIDR_ which may confuse bayes learning, I suppose. $scanner->{tag_data}->{ASN} = ''; $scanner->{tag_data}->{ASNCIDR} = ''; # We need to grab this here since the check_tick event does not # get *our* name, but the name of whatever rule is currently # being worked on. $scanner->{myname} = $scanner->get_current_eval_rule_name(); my $ip = $scanner->{relays_external}->[0]->{ip} || ''; if ($ip eq '') { db("ASN: No IP address from relays_external"); return 1; } else { dbg("ASN: external IP address $ip"); } my $lookup = ''; if ($ip =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) { $lookup = "$4.$3.$2.$1.$zone"; } if ($lookup eq '') { dbg("ASN: $ip does not look like an IP address"); return 1; } else { dbg("ASN: will look up $lookup"); } # DNS magic - start the lookup and have the Net::DNS package # store the result in our own structure $scanner->{main}->{resolver}->bgsend($lookup, 'TXT', undef, sub { my $pkt = shift; my $id = shift; $scanner->{asnlookup} = $pkt; }); return; } sub check_tick { my ($self, $opts) = @_; return if ($self->{dns_not_available}); my $pms = $opts->{permsgstatus}; # This will be defined if Net::DNS had something to deliver (see # ->bgsend() in sub asn_lookup() above) if ($pms->{asnlookup}) { # The regular Net::DNS dance around RRs; make sure to delete # the asnlookup structure, otherwise we would re-do on each # call of check_tick my $packet = delete $pms->{asnlookup}; my @answer = $packet->answer; foreach my $rr (@answer) { dbg("ASN: lookup result packet: " . $rr->string); if ($rr->type eq 'TXT') { my @items = split(/ /, $rr->txtdata); $pms->{tag_data}->{ASN} = sprintf('AS%s', $items[0]); $pms->{tag_data}->{ASNCIDR} = sprintf('%s/%s', $items[1], $items[2]); # We are calling the internal _handle_hit because we want the # score to be zero, but still show it up in the report $pms->_handle_hit($pms->{myname}, 0, sprintf('AS%s %s/%s', @items)); } } } return; } 1; =head1 SEE ALSO http://www.routeviews.org/ - all data regarding routing, ASNs etc; http://matthias.leisi.net/ - my blog with eventual updates; http://issues.apache.org/SpamAssassin/show_bug.cgi?id=4770 - SpamAssassin Issue #4770 =head1 AUTHOR Matthias Leisi =head1 VERSION Experimental - Dec. 17, 2006