rspamd/utils/asn.pl
2022-03-17 18:01:23 +01:00

332 righe
8.6 KiB
Perl

#!/usr/bin/env perl
#
use warnings;
use strict;
use autodie;
use File::Basename;
use File::Fetch;
use Getopt::Long;
use Pod::Usage;
use FindBin;
use lib "$FindBin::Bin/extlib/lib/perl5";
use URI;
my %config = (
asn_sources => [
'ftp://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest',
'ftp://ftp.ripe.net/ripe/stats/delegated-ripencc-latest',
'http://ftp.afrinic.net/pub/stats/afrinic/delegated-afrinic-latest',
'ftp://ftp.apnic.net/pub/stats/apnic/delegated-apnic-latest',
'ftp://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-latest'
],
bgp_sources => ['http://data.ris.ripe.net/rrc00/latest-bview.gz']
);
my $download_asn = 0;
my $download_bgp = 0;
my $download_target = "./";
my $help = 0;
my $man = 0;
my $v4 = 1;
my $v6 = 1;
my $parse = 1;
my $v4_zone = "asn.rspamd.com";
my $v6_zone = "asn6.rspamd.com";
my $v4_file = "asn.zone";
my $v6_file = "asn6.zone";
my $ns_servers = [ "asn-ns.rspamd.com", "asn-ns2.rspamd.com" ];
my $unknown_placeholder = "--";
GetOptions(
"download-asn" => \$download_asn,
"download-bgp" => \$download_bgp,
"4!" => \$v4,
"6!" => \$v6,
"parse!" => \$parse,
"target=s" => \$download_target,
"zone-v4=s" => \$v4_zone,
"zone-v6=s" => \$v6_zone,
"file-v4=s" => \$v4_file,
"file-v6=s" => \$v6_file,
"ns-server=s@" => \$ns_servers,
"help|?" => \$help,
"man" => \$man,
"unknown-placeholder" => \$unknown_placeholder,
) or
pod2usage(2);
pod2usage(1) if $help;
pod2usage(-exitval => 0, -verbose => 2) if $man;
if ($download_asn) {
foreach my $u (@{ $config{'asn_sources'} }) {
download_file($u);
}
}
if ($download_bgp) {
foreach my $u (@{ $config{'bgp_sources'} }) {
download_file($u);
}
}
if (!$parse) {
exit 0;
}
# Prefix to ASN map
my $networks = { 4 => {}, 6 => {} };
foreach my $u (@{ $config{'bgp_sources'} }) {
my $parsed = URI->new($u);
my $fname = $download_target . '/' . basename($parsed->path);
use constant {
F_MARKER => 0,
F_TIMESTAMP => 1,
F_PEER_IP => 3,
F_PEER_AS => 4,
F_PREFIX => 5,
F_AS_PATH => 6,
F_ORIGIN => 7,
};
open(my $bgpd, '-|', "bgpdump -v -M $fname") or die "can't start bgpdump: $!";
while (<$bgpd>) {
chomp;
my @e = split /\|/;
if ($e[F_MARKER] ne 'TABLE_DUMP2') {
warn "bad line: $_\n";
next;
}
my $origin_as;
my $prefix = $e[F_PREFIX];
my $ip_ver = 6;
if ($prefix =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}$/) {
$ip_ver = 4;
}
if ($e[F_AS_PATH]) {
# not empty AS_PATH
my @as_path = split /\s/, $e[F_AS_PATH];
$origin_as = pop @as_path;
if (substr($origin_as, 0, 1) eq '{') {
# route is aggregated
if ($origin_as =~ /^{(\d+)}$/) {
# single AS aggregated, just remove { } around
$origin_as = $1;
} else {
# use previous AS from AS_PATH
$origin_as = pop @as_path;
}
}
# strip bogus AS
while (is_bougus_asn($origin_as)) {
$origin_as = pop @as_path;
last if scalar @as_path == 0;
}
}
# empty AS_PATH or all AS_PATH elements was stripped as bogus - use
# PEER_AS as origin AS
$origin_as //= $e[F_PEER_AS];
$networks->{$ip_ver}{$prefix} = int($origin_as);
}
}
# Remove default routes
delete $networks->{4}{'0.0.0.0/0'};
delete $networks->{6}{'::/0'};
# Now roughly detect countries
my $as_info = {};
# RIR statistics exchange format
# https://www.apnic.net/publications/media-library/documents/resource-guidelines/rir-statistics-exchange-format
# https://www.arin.net/knowledge/statistics/nro_extended_stats_format.pdf
# first 7 fields for this two formats are same
use constant {
F_REGISTRY => 0, # {afrinic,apnic,arin,iana,lacnic,ripencc}
F_CC => 1, # ISO 3166 2-letter country code
F_TYPE => 2, # {asn,ipv4,ipv6}
F_START => 3,
F_VALUE => 4,
F_DATE => 5,
F_STATUS => 6,
};
foreach my $u (@{ $config{'asn_sources'} }) {
my $parsed = URI->new($u);
my $fname = $download_target . '/' . basename($parsed->path);
open(my $fh, "<", $fname) or die "Cannot open $fname: $!";
while (<$fh>) {
next if /^\#/;
chomp;
my @elts = split /\|/;
if ($elts[F_TYPE] eq 'asn' && $elts[F_START] ne '*') {
my $as_start = int($elts[F_START]);
my $as_end = $as_start + int($elts[F_VALUE]) - 1;
for my $as ($as_start .. $as_end) {
$as_info->{$as}{'country'} = $elts[F_CC];
$as_info->{$as}{'rir'} = $elts[F_REGISTRY];
}
}
}
}
# Write zone files
my $ns_list = join ' ', @{$ns_servers};
my $zone_header = << "EOH";
\$SOA 43200 $ns_servers->[0] support.rspamd.com 0 600 300 86400 300
\$NS 43200 $ns_list
EOH
if ($v4) {
# create temp file in the same dir so we can be sure that mv is atomic
my $out_dir = dirname($v4_file);
my $out_file = basename($v4_file);
my $temp_file = "$out_dir/.$out_file.tmp";
open my $v4_fh, '>', $temp_file;
print $v4_fh $zone_header;
while (my ($net, $asn) = each %{ $networks->{4} }) {
my $country = $as_info->{$asn}{'country'} || $unknown_placeholder;
my $rir = $as_info->{$asn}{'rir'} || $unknown_placeholder;
# "8.8.8.0/24 15169|8.8.8.0/24|US|arin|" for 8.8.8.8
printf $v4_fh "%s %s|%s|%s|%s|\n", $net, $asn, $net, $country, $rir;
}
close $v4_fh;
rename $temp_file, $v4_file;
}
if ($v6) {
my $out_dir = dirname($v6_file);
my $out_file = basename($v6_file);
my $temp_file = "$out_dir/.$out_file.tmp";
open my $v6_fh, '>', $temp_file;
print $v6_fh $zone_header;
while (my ($net, $asn) = each %{ $networks->{6} }) {
my $country = $as_info->{$asn}{'country'} || $unknown_placeholder;
my $rir = $as_info->{$asn}{'rir'} || $unknown_placeholder;
# "2606:4700:4700::/48 13335|2606:4700:4700::/48|US|arin|" for 2606:4700:4700::1111
printf $v6_fh "%s %s|%s|%s|%s|\n", $net, $asn, $net, $country, $rir;
}
close $v6_fh;
rename $temp_file, $v6_file;
}
exit 0;
########################################################################
sub download_file {
my ($url) = @_;
local $File::Fetch::WARN = 0;
local $File::Fetch::TIMEOUT = 180; # connectivity to ftp.lacnic.net is bad
my $ff = File::Fetch->new(uri => $url);
my $where = $ff->fetch(to => $download_target) or
die "$url: ", $ff->error;
return $where;
}
# Returns true if AS number is bogus
# e. g. a private AS.
# List of allocated and reserved AS:
# https://www.iana.org/assignments/as-numbers/as-numbers.txt
sub is_bougus_asn {
my $as = shift;
# 64496-64511 Reserved for use in documentation and sample code
# 64512-65534 Designated for private use
# 65535 Reserved
# 65536-65551 Reserved for use in documentation and sample code
# 65552-131071 Reserved
return 1 if $as >= 64496 && $as <= 131071;
# Reserved (RFC6996, RFC7300, RFC7607)
return 1 if $as == 0 || $as >= 4200000000;
return 0;
}
__END__
=head1 NAME
asn.pl - download and parse ASN data for Rspamd
=head1 SYNOPSIS
asn.pl [options]
Options:
--download-asn Download ASN data from RIRs
--download-bgp Download BGP full view dump from RIPE RIS
--target Where to download files (default: current dir)
--zone-v4 IPv4 zone (default: asn.rspamd.com)
--zone-v6 IPv6 zone (default: asn6.rspamd.com)
--file-v4 IPv4 zone file (default: ./asn.zone)
--file-v6 IPv6 zone (default: ./asn6.zone)
--unknown-placeholder Placeholder for unknown elements (default: --)
--help Brief help message
--man Full documentation
=head1 OPTIONS
=over 8
=item B<--download-asn>
Download ASN data from RIR.
=item B<--download-bgp>
Download GeoIP data from Ripe
=item B<--target>
Specifies where to download files.
=item B<--help>
Print a brief help message and exits.
=item B<--man>
Prints the manual page and exits.
=back
=head1 DESCRIPTION
B<asn.pl> is intended to download ASN data and GeoIP data and create a rbldnsd zone.
=cut
# vim: et:ts=4:sw=4