mirror of
https://git.proxmox.com/git/proxmox-spamassassin
synced 2025-04-28 12:19:37 +00:00
630 lines
20 KiB
Perl
630 lines
20 KiB
Perl
# SpamAssassin - ASN Lookup Plugin
|
|
#
|
|
# <@LICENSE>
|
|
# Licensed to the Apache Software Foundation (ASF) under one or more
|
|
# contributor license agreements. See the NOTICE file distributed with
|
|
# this work for additional information regarding copyright ownership.
|
|
# The ASF licenses this file to you 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.
|
|
# </@LICENSE>
|
|
#
|
|
###########################################################################
|
|
#
|
|
# Originated by Matthias Leisi, 2006-12-15 (SpamAssassin enhancement #4770).
|
|
# Modifications by D. Stussy, 2010-12-15 (SpamAssassin enhancement #6484):
|
|
#
|
|
# Since SA 3.4.0 a fixed text prefix (such as AS) to each ASN is configurable
|
|
# through an asn_prefix directive. Its value is 'AS' by default for backward
|
|
# compatibility with SA 3.3.*, but is rather redundant and can be set to an
|
|
# empty string for clarity if desired.
|
|
#
|
|
# Enhanced TXT-RR decoding for alternative formats from other DNS zones.
|
|
# Some of the supported formats of TXT RR are (quoted strings here represent
|
|
# individual string fields in a TXT RR):
|
|
# "1103" "192.88.99.0" "24"
|
|
# "559 1103 1239 1257 1299 | 192.88.99.0/24 | US | iana | 2001-06-01"
|
|
# "192.88.99.0/24 | AS1103 | SURFnet, The Netherlands | 2002-10-15 | EU"
|
|
# "15169 | 2a00:1450::/32 | IE | ripencc | 2009-10-05"
|
|
# "as1103"
|
|
# Multiple routes are sometimes provided by returning multiple TXT records
|
|
# (e.g. from cymru.com). This form of a response is handled as well.
|
|
#
|
|
# Some zones also support IPv6 lookups, for example:
|
|
# asn_lookup_ipv6 origin6.asn.cymru.com [_ASN_ _ASNCIDR_]
|
|
|
|
=head1 NAME
|
|
|
|
Mail::SpamAssassin::Plugin::ASN - SpamAssassin plugin to look up the
|
|
Autonomous System Number (ASN) of the connecting IP address.
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
loadplugin Mail::SpamAssassin::Plugin::ASN
|
|
|
|
# Default / recommended settings
|
|
asn_use_geodb 1
|
|
asn_use_dns 1
|
|
asn_prefer_geodb 1
|
|
|
|
# Do lookups and add tags / X-Spam-ASN header
|
|
asn_lookup asn.routeviews.org _ASN_ _ASNCIDR_
|
|
asn_lookup_ipv6 origin6.asn.cymru.com _ASN_ _ASNCIDR_
|
|
add_header all ASN _ASN_ _ASNCIDR_
|
|
|
|
# Rules to test ASN or Organization
|
|
# NOTE: Do not use rules that check metadata X-ASN header,
|
|
# only check_asn() eval function works correctly.
|
|
# Rule argument is full regexp to match.
|
|
|
|
# ASN Number: GeoIP ASN or DNS
|
|
# Matched string includes asn_prefix if defined, and normally
|
|
# looks like "AS1234" (DNS) or "AS1234 Google LLC" (GeoIP)
|
|
header AS_1234 eval:check_asn('/^AS1234\b/')
|
|
|
|
# ASN Organisation: GeoIP ASN has, DNS lists might not have
|
|
# Note the second parameter which checks MYASN tag (default is ASN)
|
|
asn_lookup myview.example.com _MYASN_ _MYASNCIDR_
|
|
header AS_GOOGLE eval:check_asn('/\bGoogle\b/i', 'MYASN')
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
This plugin uses DNS lookups to the services of an external DNS zone such
|
|
as at C<https://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<you should not use this plugin in a
|
|
high-volume environment> or that you should use a local mirror of the
|
|
zone (see C<ftp://ftp.routeviews.org/dnszones/>). Other similar zones
|
|
may also be used.
|
|
|
|
GeoDB (GeoIP ASN) database lookups are supported since SpamAssassin 4.0 and
|
|
it's recommended to use them instead of DNS queries, unless C<_ASNCIDR_>
|
|
is needed.
|
|
|
|
=head1 TEMPLATE TAGS
|
|
|
|
This plugin allows you to create template tags containing the connecting
|
|
IP's AS number and route info for that AS number.
|
|
|
|
If you use add_header as documented in the example before, a header field is
|
|
added that looks like this:
|
|
|
|
X-Spam-ASN: AS24940 213.239.192.0/18
|
|
|
|
where "24940" is the ASN and "213.239.192.0/18" is the route
|
|
announced by that ASN where the connecting IP address came from.
|
|
If the AS announces multiple networks (more/less specific), they will
|
|
all be added to the C<_ASNCIDR_> tag, separated by spaces, eg:
|
|
|
|
X-Spam-ASN: AS1680 89.138.0.0/15 89.139.0.0/16
|
|
|
|
Note that the literal "AS" before the ASN in the _ASN_ tag is configurable
|
|
through the I<asn_prefix> directive and may be set to an empty string.
|
|
|
|
C<_ASNCIDR_> is not available with local GeoDB ASN lookups.
|
|
|
|
=head1 USER SETTINGS
|
|
|
|
=over 4
|
|
|
|
=item clear_asn_lookups
|
|
|
|
Removes all previously declared I<asn_lookup> or I<asn_lookup_ipv6> entries
|
|
from the list of queries.
|
|
|
|
=item asn_prefix 'prefix_string' (default: 'AS')
|
|
|
|
The string specified in the argument is prepended to each ASN when storing
|
|
it as a tag. This prefix is rather redundant, but its default value 'AS'
|
|
is kept for backward compatibility with versions of SpamAssassin earlier
|
|
than 3.4.0. A sensible setting is an empty string. The argument may be (but
|
|
need not be) enclosed in single or double quotes for clarity.
|
|
|
|
=back
|
|
|
|
=head1 RULE DEFINITIONS AND PRIVILEGED SETTINGS
|
|
|
|
=over 4
|
|
|
|
=item asn_lookup asn-zone.example.com [ _ASNTAG_ _ASNCIDRTAG_ ]
|
|
|
|
Use this to lookup the ASN info in the specified zone for the first external
|
|
IPv4 address and add the AS number to the first specified tag and routing info
|
|
to the second specified tag.
|
|
|
|
If no tags are specified the AS number will be added to the _ASN_ tag and the
|
|
routing info will be added to the _ASNCIDR_ tag. You must specify either none
|
|
or both of the tag names. Tag names must start and end with an underscore.
|
|
|
|
If two or more I<asn_lookup>s use the same set of template tags, the results of
|
|
their lookups will be appended to each other in the template tag values in no
|
|
particular order. Duplicate results will be omitted when combining results.
|
|
In a similar fashion, you can also use the same template tag for both the AS
|
|
number tag and the routing info tag.
|
|
|
|
Examples:
|
|
|
|
asn_lookup asn.routeviews.org
|
|
|
|
asn_lookup asn.routeviews.org _ASN_ _ASNCIDR_
|
|
asn_lookup myview.example.com _MYASN_ _MYASNCIDR_
|
|
|
|
asn_lookup asn.routeviews.org _COMBINEDASN_ _COMBINEDASNCIDR_
|
|
asn_lookup myview.example.com _COMBINEDASN_ _COMBINEDASNCIDR_
|
|
|
|
asn_lookup in1tag.example.net _ASNDATA_ _ASNDATA_
|
|
|
|
=item asn_lookup_ipv6 asn-zone6.example.com [_ASN_ _ASNCIDR_]
|
|
|
|
Use specified zone for lookups of IPv6 addresses. If zone supports both
|
|
IPv4 and IPv6 queries, use both asn_lookup and asn_lookup_ipv6 for the same
|
|
zone.
|
|
|
|
=back
|
|
|
|
=head1 ADMINISTRATOR SETTINGS
|
|
|
|
=over 4
|
|
|
|
=item asn_use_geodb ( 0 / 1 ) (default: 1)
|
|
|
|
Use Mail::SpamAssassin::GeoDB module to lookup ASN numbers. You need
|
|
suitable supported module like GeoIP2 or GeoIP with ISP or ASN database
|
|
installed (for example, add EditionIDs GeoLite2-ASN in GeoIP.conf for
|
|
geoipupdate program).
|
|
|
|
GeoDB can only set _ASN_ tag, it has no data for _ASNCIDR_. If you need
|
|
both, then set asn_prefer_geodb 0 so DNS rules are tried.
|
|
|
|
=item asn_prefer_geodb ( 0 / 1 ) (default: 1)
|
|
|
|
If set, DNS lookups (asn_lookup rules) will not be run if GeoDB successfully
|
|
finds ASN. Set this to 0 to get _ASNCIDR_ even if GeoDB finds _ASN_.
|
|
|
|
=item asn_use_dns ( 0 / 1 ) (default: 1)
|
|
|
|
Set to 0 to never allow DNS queries.
|
|
|
|
=back
|
|
|
|
=head1 BAYES
|
|
|
|
The bayes tokenizer will use ASN data for bayes calculations, and thus
|
|
affect which BAYES_* rule will trigger for a particular message. No
|
|
in-depth analysis of the usefulness of bayes tokenization of ASN data has
|
|
been performed.
|
|
|
|
=head1 SEE ALSO
|
|
|
|
https://www.routeviews.org/ - all data regarding routing, ASNs, etc....
|
|
|
|
=cut
|
|
|
|
package Mail::SpamAssassin::Plugin::ASN;
|
|
|
|
use strict;
|
|
use warnings;
|
|
use re 'taint';
|
|
|
|
use Mail::SpamAssassin::Plugin;
|
|
use Mail::SpamAssassin::Logger;
|
|
use Mail::SpamAssassin::Util qw(reverse_ip_address compile_regexp);
|
|
use Mail::SpamAssassin::Constants qw(:ip);
|
|
|
|
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("check_asn", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
|
|
|
|
$self->set_config($mailsa->{conf});
|
|
|
|
# we need GeoDB ASN
|
|
$self->{main}->{geodb_wanted}->{asn} = 1;
|
|
|
|
return $self;
|
|
}
|
|
|
|
###########################################################################
|
|
|
|
sub set_config {
|
|
my ($self, $conf) = @_;
|
|
my @cmds;
|
|
|
|
push (@cmds, {
|
|
setting => 'asn_lookup',
|
|
is_priv => 1,
|
|
code => sub {
|
|
my ($conf, $key, $value, $line) = @_;
|
|
unless (defined $value && $value !~ /^$/) {
|
|
return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
|
|
}
|
|
local($1,$2,$3);
|
|
unless ($value =~ /^(\S+?)\.?(?:\s+_(\S+)_\s+_(\S+)_)?$/) {
|
|
return $Mail::SpamAssassin::Conf::INVALID_VALUE;
|
|
}
|
|
my ($zone, $asn_tag, $route_tag) = ($1, $2, $3);
|
|
$asn_tag = 'ASN' if !defined $asn_tag;
|
|
$route_tag = 'ASNCIDR' if !defined $route_tag;
|
|
push @{$conf->{asnlookups}},
|
|
{ zone=>$zone, asn_tag=>$asn_tag, route_tag=>$route_tag };
|
|
}
|
|
});
|
|
|
|
push (@cmds, {
|
|
setting => 'asn_lookup_ipv6',
|
|
is_priv => 1,
|
|
code => sub {
|
|
my ($conf, $key, $value, $line) = @_;
|
|
unless (defined $value && $value !~ /^$/) {
|
|
return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
|
|
}
|
|
local($1,$2,$3);
|
|
unless ($value =~ /^(\S+?)\.?(?:\s+_(\S+)_\s+_(\S+)_)?$/) {
|
|
return $Mail::SpamAssassin::Conf::INVALID_VALUE;
|
|
}
|
|
my ($zone, $asn_tag, $route_tag) = ($1, $2, $3);
|
|
$asn_tag = 'ASN' if !defined $asn_tag;
|
|
$route_tag = 'ASNCIDR' if !defined $route_tag;
|
|
push @{$conf->{asnlookups_ipv6}},
|
|
{ zone=>$zone, asn_tag=>$asn_tag, route_tag=>$route_tag };
|
|
}
|
|
});
|
|
|
|
push (@cmds, {
|
|
setting => 'clear_asn_lookups',
|
|
type => $Mail::SpamAssassin::Conf::CONF_TYPE_NOARGS,
|
|
code => sub {
|
|
my ($conf, $key, $value, $line) = @_;
|
|
if (defined $value && $value ne '') {
|
|
return $Mail::SpamAssassin::Conf::INVALID_VALUE;
|
|
}
|
|
delete $conf->{asnlookups};
|
|
delete $conf->{asnlookups_ipv6};
|
|
}
|
|
});
|
|
|
|
push (@cmds, {
|
|
setting => 'asn_prefix',
|
|
type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
|
|
default => 'AS',
|
|
code => sub {
|
|
my ($conf, $key, $value, $line) = @_;
|
|
$value = '' if !defined $value;
|
|
local($1,$2);
|
|
$value = $2 if $value =~ /^(['"])(.*)\1\z/; # strip quotes if any
|
|
$conf->{$key} = $value; # keep tainted
|
|
}
|
|
});
|
|
|
|
push (@cmds, {
|
|
setting => 'asn_use_geodb',
|
|
default => 1,
|
|
is_admin => 1,
|
|
type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
|
|
});
|
|
|
|
push (@cmds, {
|
|
setting => 'asn_prefer_geodb',
|
|
default => 1,
|
|
is_admin => 1,
|
|
type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
|
|
});
|
|
|
|
push (@cmds, {
|
|
setting => 'asn_use_dns',
|
|
default => 1,
|
|
is_admin => 1,
|
|
type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
|
|
});
|
|
|
|
$conf->{parser}->register_commands(\@cmds);
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
sub extract_metadata {
|
|
my ($self, $opts) = @_;
|
|
|
|
my $pms = $opts->{permsgstatus};
|
|
my $conf = $pms->{conf};
|
|
|
|
my $geodb = $self->{main}->{geodb};
|
|
my $has_geodb = $conf->{asn_use_geodb} && $geodb && $geodb->can('asn');
|
|
if ($has_geodb) {
|
|
dbg("asn: using GeoDB ASN for lookups");
|
|
} else {
|
|
dbg("asn: GeoDB ASN not available");
|
|
if (!$conf->{asn_use_dns} || !$pms->is_dns_available()) {
|
|
dbg("asn: DNS is not available, skipping ASN check");
|
|
return;
|
|
}
|
|
if ($self->{main}->{learning}) {
|
|
dbg("asn: learning message, skipping DNS-based ASN check");
|
|
return;
|
|
}
|
|
}
|
|
|
|
# initialize the tag data so that if no result is returned from the DNS
|
|
# query we won't end up with a missing tag. Don't use $pms->set_tag()
|
|
# here to avoid triggering any tag-dependent action unnecessarily
|
|
if ($conf->{asnlookups}) {
|
|
foreach my $entry (@{$conf->{asnlookups}}) {
|
|
$pms->{tag_data}->{$entry->{asn_tag}} ||= '';
|
|
$pms->{tag_data}->{$entry->{route_tag}} ||= '';
|
|
}
|
|
}
|
|
if ($conf->{asnlookups_ipv6}) {
|
|
foreach my $entry (@{$conf->{asnlookups_ipv6}}) {
|
|
$pms->{tag_data}->{$entry->{asn_tag}} ||= '';
|
|
$pms->{tag_data}->{$entry->{route_tag}} ||= '';
|
|
}
|
|
}
|
|
|
|
# Initialize status
|
|
$pms->{asn_results} = ();
|
|
|
|
# get IP address of last external relay to lookup
|
|
my $relay = $opts->{msg}->{metadata}->{relays_external}->[0];
|
|
if (!defined $relay) {
|
|
dbg("asn: no first external relay IP available, skipping ASN check");
|
|
return;
|
|
} elsif ($relay->{ip_private}) {
|
|
dbg("asn: first external relay is a private IP, skipping ASN check");
|
|
return;
|
|
}
|
|
my $ip = $relay->{ip};
|
|
dbg("asn: using first external relay IP for lookups: %s", $ip);
|
|
|
|
# GeoDB lookup
|
|
my $asn_found;
|
|
if ($has_geodb) {
|
|
my $asn = $geodb->get_asn($ip);
|
|
my $org = $geodb->get_asn_org($ip);
|
|
if (!defined $asn) {
|
|
dbg("asn: GeoDB ASN lookup failed");
|
|
} else {
|
|
$asn_found = 1;
|
|
dbg("asn: GeoDB found ASN $asn");
|
|
# Prevent double prefix
|
|
my $asn_value =
|
|
length($conf->{asn_prefix}) && index($asn, $conf->{asn_prefix}) != 0 ?
|
|
$conf->{asn_prefix}.$asn : $asn;
|
|
$asn_value .= ' '.$org if defined $org && length($org);
|
|
$pms->set_tag('ASN', $asn_value);
|
|
# For Bayes
|
|
$pms->{msg}->put_metadata('X-ASN', $asn);
|
|
}
|
|
}
|
|
|
|
# Skip DNS if GeoDB was successful and preferred
|
|
if ($asn_found && $conf->{asn_prefer_geodb}) {
|
|
dbg("asn: GeoDB lookup successful, skipping DNS lookups");
|
|
return;
|
|
}
|
|
|
|
# No point continuing without DNS from now on
|
|
if (!$conf->{asn_use_dns} || !$pms->is_dns_available()) {
|
|
dbg("asn: skipping disabled DNS lookups");
|
|
return;
|
|
}
|
|
|
|
dbg("asn: using DNS for lookups");
|
|
my $lookup_zone;
|
|
if ($ip =~ IS_IPV4_ADDRESS) {
|
|
if (!defined $conf->{asnlookups}) {
|
|
dbg("asn: asn_lookup for IPv4 not defined, skipping");
|
|
return;
|
|
}
|
|
$lookup_zone = "asnlookups";
|
|
} else {
|
|
if (!defined $conf->{asnlookups_ipv6}) {
|
|
dbg("asn: asn_lookup_ipv6 for IPv6 not defined, skipping");
|
|
return;
|
|
}
|
|
$lookup_zone = "asnlookups_ipv6";
|
|
}
|
|
|
|
my $reversed_ip = reverse_ip_address($ip);
|
|
if (!defined $reversed_ip) {
|
|
dbg("asn: could not parse IP: %s, skipping", $ip);
|
|
return;
|
|
}
|
|
|
|
# we use arrays and array indices rather than hashes and hash keys
|
|
# in case someone wants the same zone added to multiple sets of tags
|
|
my $index = 0;
|
|
foreach my $entry (@{$conf->{$lookup_zone}}) {
|
|
# do the DNS query, have the callback process the result
|
|
my $zone_index = $index;
|
|
my $zone = $reversed_ip . '.' . $entry->{zone};
|
|
$pms->{async}->bgsend_and_start_lookup($zone, 'TXT', undef,
|
|
{ rulename => 'asn_lookup', type => 'ASN' },
|
|
sub { my($ent, $pkt) = @_;
|
|
$self->process_dns_result($pms, $pkt, $zone_index, $lookup_zone) },
|
|
master_deadline => $pms->{master_deadline}
|
|
);
|
|
$index++;
|
|
}
|
|
}
|
|
|
|
#
|
|
# TXT-RR format of response:
|
|
# 3 fields, each as one TXT RR <character-string> (RFC 1035): ASN IP MASK
|
|
# The latter two fields are combined to create a CIDR.
|
|
# or: At least 2 fields made of a single or multiple
|
|
# <character-string>s, fields are separated by a vertical bar.
|
|
# They will be the ASN and CIDR fields in any order.
|
|
# If only one field is returned, it is the ASN. There will
|
|
# be no CIDR field in that case.
|
|
#
|
|
sub process_dns_result {
|
|
my ($self, $pms, $pkt, $zone_index, $lookup_zone) = @_;
|
|
|
|
# NOTE: $pkt will be undef if the DNS query was aborted (e.g. timed out)
|
|
return if !$pkt;
|
|
|
|
my $conf = $self->{main}->{conf};
|
|
|
|
my $zone = $conf->{$lookup_zone}[$zone_index]->{zone};
|
|
my $asn_tag = $conf->{$lookup_zone}[$zone_index]->{asn_tag};
|
|
my $route_tag = $conf->{$lookup_zone}[$zone_index]->{route_tag};
|
|
|
|
my($any_asn_updates, $any_route_updates, $tag_value);
|
|
|
|
my(@asn_tag_data, %asn_tag_data_seen);
|
|
$tag_value = $pms->get_tag($asn_tag);
|
|
if (defined $tag_value) {
|
|
my $prefix = $pms->{conf}->{asn_prefix};
|
|
if (defined $prefix && $prefix ne '') {
|
|
# must strip prefix before splitting on whitespace
|
|
$tag_value =~ s/(^| )\Q$prefix\E(?=\d+)/$1/gs;
|
|
}
|
|
@asn_tag_data = split(/ /,$tag_value);
|
|
%asn_tag_data_seen = map(($_,1), @asn_tag_data);
|
|
}
|
|
|
|
my(@route_tag_data, %route_tag_data_seen);
|
|
$tag_value = $pms->get_tag($route_tag);
|
|
if (defined $tag_value) {
|
|
@route_tag_data = split(/ /,$tag_value);
|
|
%route_tag_data_seen = map(($_,1), @route_tag_data);
|
|
}
|
|
|
|
foreach my $rr ($pkt->answer) {
|
|
#dbg("asn: %s: lookup result packet: %s", $zone, $rr->string);
|
|
next if $rr->type ne 'TXT';
|
|
my @strings = $rr->txtdata;
|
|
next if !@strings;
|
|
for (@strings) { utf8::encode($_) if utf8::is_utf8($_) }
|
|
|
|
my @items;
|
|
if (@strings > 1 && join('',@strings) !~ m{\|}) {
|
|
# routeviews.org style, multiple string fields in a TXT RR
|
|
@items = @strings;
|
|
if (@items >= 3 && $items[1] !~ m{/} && $items[2] =~ /^\d+\z/) {
|
|
$items[1] .= '/' . $items[2]; # append the net mask length to route
|
|
}
|
|
} else {
|
|
# cymru.com and spameatingmonkey.net style, or just a single field
|
|
@items = split(/\s*\|\s*/, join(' ',@strings));
|
|
}
|
|
|
|
my(@route_value, @asn_value);
|
|
if (@items && $items[0] =~ /(?: (?:^|\s+) (?:AS)? \d+ )+ \z/xsi) {
|
|
# routeviews.org and cymru.com style, ASN is the first field,
|
|
# possibly a whitespace-separated list (e.g. cymru.com)
|
|
@asn_value = split(' ',$items[0]);
|
|
@route_value = split(' ',$items[1]) if @items >= 2;
|
|
} elsif (@items > 1 && $items[1] =~ /(?: (?:^|\s+) (?:AS)? \d+ )+ \z/xsi) {
|
|
# spameatingmonkey.net style, ASN is the second field
|
|
@asn_value = split(' ',$items[1]);
|
|
@route_value = split(' ',$items[0]);
|
|
} else {
|
|
dbg("asn: unparseable response: %s", join(' ', map("\"$_\"",@strings)));
|
|
}
|
|
|
|
foreach my $route (@route_value) {
|
|
if (!defined $route || $route eq '') {
|
|
# ignore, just in case
|
|
} elsif ($route =~ m{/0+\z}) {
|
|
# unassigned/unannounced address space
|
|
} elsif ($route_tag_data_seen{$route}) {
|
|
dbg("asn: %s duplicate route %s", $route_tag, $route);
|
|
} else {
|
|
dbg("asn: %s added route %s", $route_tag, $route);
|
|
push(@route_tag_data, $route);
|
|
$route_tag_data_seen{$route} = 1;
|
|
$any_route_updates = 1;
|
|
}
|
|
}
|
|
|
|
foreach my $asn (@asn_value) {
|
|
$asn =~ s/^AS(?=\d+)//si;
|
|
if (!$asn || $asn == 4294967295) {
|
|
# unassigned/unannounced address space
|
|
} elsif ($asn_tag_data_seen{$asn}) {
|
|
dbg("asn: %s duplicate asn %s", $asn_tag, $asn);
|
|
} else {
|
|
dbg("asn: %s added asn %s", $asn_tag, $asn);
|
|
push(@asn_tag_data, $asn);
|
|
$asn_tag_data_seen{$asn} = 1;
|
|
$any_asn_updates = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($any_asn_updates && @asn_tag_data) {
|
|
$pms->{msg}->put_metadata('X-ASN', join(' ',@asn_tag_data));
|
|
my $prefix = $pms->{conf}->{asn_prefix};
|
|
if (defined $prefix && $prefix ne '') { s/^/$prefix/ for @asn_tag_data }
|
|
$pms->set_tag($asn_tag,
|
|
@asn_tag_data == 1 ? $asn_tag_data[0] : \@asn_tag_data);
|
|
}
|
|
if ($any_route_updates && @route_tag_data) {
|
|
# Bayes already has X-ASN, Route is pointless duplicate, skip
|
|
#$pms->{msg}->put_metadata('X-ASN-Route', join(' ',@route_tag_data));
|
|
$pms->set_tag($route_tag,
|
|
@route_tag_data == 1 ? $route_tag_data[0] : \@route_tag_data);
|
|
}
|
|
}
|
|
|
|
sub check_asn {
|
|
my ($self, $pms, $re, $asn_tag) = @_;
|
|
|
|
my $rulename = $pms->get_current_eval_rule_name();
|
|
if (!defined $re) {
|
|
warn "asn: rule $rulename eval argument missing\n";
|
|
return 0;
|
|
}
|
|
|
|
my ($rec, $err) = compile_regexp($re, 2);
|
|
if (!$rec) {
|
|
warn "asn: invalid regexp for $rulename '$re': $err\n";
|
|
return 0;
|
|
}
|
|
|
|
$asn_tag = 'ASN' unless defined $asn_tag;
|
|
$pms->action_depends_on_tags($asn_tag,
|
|
sub { my($pms,@args) = @_;
|
|
$self->_check_asn($pms, $rulename, $rec, $asn_tag);
|
|
}
|
|
);
|
|
|
|
return; # return undef for async status
|
|
}
|
|
|
|
sub _check_asn {
|
|
my ($self, $pms, $rulename, $rec, $asn_tag) = @_;
|
|
|
|
$pms->rule_ready($rulename); # mark rule ready for metas
|
|
|
|
my $asn = $pms->get_tag($asn_tag);
|
|
return if !defined $asn;
|
|
|
|
if ($asn =~ $rec) {
|
|
$pms->test_log("$asn_tag: $asn", $rulename);
|
|
$pms->got_hit($rulename, "");
|
|
}
|
|
}
|
|
|
|
# Version features
|
|
sub has_asn_lookup_ipv6 { 1 }
|
|
sub has_asn_geodb { 1 }
|
|
sub has_check_asn { 1 }
|
|
sub has_check_asn_tag { 1 } # $asn_tag parameter for check_asn()
|
|
|
|
1;
|