From: D. Stussy Date: Sun, 10 Mar 2019 06:45:11 +0000 (+0000) Subject: xt_asn: new module X-Git-Tag: v3.22~4 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=cd778808cd0793013b116de10e10c1fa9523d37d;p=thirdparty%2Fxtables-addons.git xt_asn: new module Recevied by private mail. Date: Thu, 7 Mar 2019 00:49:16 +0000 (UTC) """ New feature: In thinking about various blocking of IP address groups, I came to the conclusion that blocking by ASN may be a good choice. Therefore, taking the lead of the geoip match module, attached is what I have for an ASN matching module. I assume that the support files generated will be the same format as those used for the geoip match. [...] I bet someone might want the ASNs on the same rule to be sorted in numerical order. However, geoip didn't do that with country names, so I didn't bother. Matching by ASN may be "better" than matching by an ipset of all one entities IP blocks (assuming that all of an entity's ASNs are known if multiples exist). Of course, I would like to see this module make it into your next release (3.3).  ;-) """ Date: Sun, 10 Mar 2019 06:45:11 +0000 (UTC) """ I think I got everything including the documentation and build script this time. [...] I noticed that some other people tried to write similar patches (saw one on github), but those have things that were missed. I'm running the module on my colocated server now, and it's working well. Already blocked ASN 4134 (a botnet-infected Chinese net) a few hundred times in the first hour. """ --- diff --git a/Makefile.am b/Makefile.am index e62be08..e8062a0 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,7 +1,7 @@ # -*- Makefile -*- ACLOCAL_AMFLAGS = -I m4 -SUBDIRS = extensions extensions/ACCOUNT extensions/pknock geoip +SUBDIRS = asn extensions extensions/ACCOUNT extensions/pknock geoip man_MANS := xtables-addons.8 diff --git a/asn/.gitignore b/asn/.gitignore new file mode 100644 index 0000000..da1d663 --- /dev/null +++ b/asn/.gitignore @@ -0,0 +1,6 @@ +/BE +/LE +/GeoIPCountryCSV.zip +/GeoIPCountryWhois.csv +/GeoIPv6.csv +/GeoIPv6.csv.gz diff --git a/asn/Makefile.am b/asn/Makefile.am new file mode 100644 index 0000000..6ad3300 --- /dev/null +++ b/asn/Makefile.am @@ -0,0 +1,5 @@ +# -*- Makefile -*- + +pkglibexec_SCRIPTS = xt_asn_build xt_asn_dl + +man1_MANS = xt_asn_build.1 xt_asn_dl.1 diff --git a/asn/xt_asn_build b/asn/xt_asn_build new file mode 100755 index 0000000..4c40679 --- /dev/null +++ b/asn/xt_asn_build @@ -0,0 +1,207 @@ +#!/usr/bin/perl +# +# Converter for MaxMind (GeoLite2) CSV database to binary, for xt_geoip +# Copyright Jan Engelhardt, 2008-2011 +# Copyright Philip Prindeville, 2018 +# D. Stussy, 2019 - Converted GeoIP module for ASN use +# D. Stussy, 2019 - Added -O output for ASN zone DNS records +# +use Getopt::Long; +use Net::CIDR::Lite; +use Socket qw(AF_INET AF_INET6 inet_pton); +use warnings; +use Text::CSV_XS; # or trade for Text::CSV +use strict; + +my $csv = Text::CSV_XS->new({ + allow_whitespace => 1, + binary => 1, + eol => $/, +}); # or Text::CSV +my $source_dir = "."; +my $target_dir = "."; +my $output_txt; + +&Getopt::Long::Configure(qw(bundling)); +&GetOptions( + "D=s" => \$target_dir, + "S=s" => \$source_dir, + "O=s" => \$output_txt, +); + +if (!-d $source_dir) { + print STDERR "Source directory \"$source_dir\" does not exist.\n"; + exit 1; +} +if (!-d $target_dir) { + print STDERR "Target directory \"$target_dir\" does not exist.\n"; + exit 1; +} + +&dump(&collect()); + +sub collect +{ + my ($file, $fh, $row, $outfile, %asns, %header, %pairs); + + sub net; sub asn; sub name; + + $file = "$source_dir/GeoLite2-ASN-Blocks-IPv4.csv"; + open($fh, '<', $file) || die "Can't open IPv4 database\n"; + + # first line is headers + $row = $csv->getline($fh); + + %header = map { ($row->[$_], $_); } (0..$#{$row}); + + # verify that the columns we need are present + map { die "Table has no %pairs{$_} column\n" unless (exists $header{$_}); } keys %pairs; + + my %remapping = ( + net => 'network', + asn => 'autonomous_system_number', + name => 'autonomous_system_organization', + ); + + # now create a function which returns the value of that column # + map { eval "sub $_ () { \$header{\$remapping{$_}}; }" ; } keys %remapping; + + if ($output_txt) { + open($outfile, '>', $output_txt); + } + + while ($row = $csv->getline($fh)) { + my ($asn, $cidr, $name); + + $asn = $row->[asn]; + $cidr = $row->[net]; + + if (!exists $asns{$asn}) { + $asns{$asn} = { + pool_v4 => Net::CIDR::Lite->new(), + pool_v6 => Net::CIDR::Lite->new(), + }; + } + + $asns{$asn}->{pool_v4}->add($cidr); + + if ($. % 4096 == 0) { + print STDERR "\r\e[2K$. entries"; + } + + if ($outfile) { + print $outfile "$asn\t\tIN\tAPL\t1:$cidr\n"; + print $outfile "$asn\t\tIN\tTXT\t\"$row->[name]\"\n"; + } + } + + print STDERR "\r\e[2K$. entries total\n"; + + close($fh); + + # clean up the namespace + undef &net; undef &asn; undef &name; + + $file = "$source_dir/GeoLite2-ASN-Blocks-IPv6.csv"; + open($fh, '<', $file) || die "Can't open IPv6 database\n"; + + # first line is headers + $row = $csv->getline($fh); + + %header = map { ($row->[$_], $_); } (0..$#{$row}); + + # verify that the columns we need are present + map { die "Table has no %pairs{$_} column\n" unless (exists $header{$_}); } keys %pairs; + + # unlikely the IPv6 table has different columns, but just to be sure + # create a function which returns the value of that column # + map { eval "sub $_ () { \$header{\$remapping{$_}}; }" ; } keys %remapping; + + while ($row = $csv->getline($fh)) { + my ($asn, $cidr, $name); + + $asn = $row->[asn]; + $cidr = $row->[net]; + + if (!exists $asns{$asn}) { + $asns{$asn} = { + pool_v4 => Net::CIDR::Lite->new(), + pool_v6 => Net::CIDR::Lite->new(), + }; + } + + $asns{$asn}->{pool_v6}->add($cidr); + + if ($. % 4096 == 0) { + print STDERR "\r\e[2K$. entries"; + } + + if ($outfile) { + print $outfile "$asn\t\tIN\tAPL\t2:$cidr\n"; + print $outfile "$asn\t\tIN\tTXT\t\"$row->[name]\"\n"; + } + } + + print STDERR "\r\e[2K$. entries total\n"; + + close($fh); + + # clean up the namespace + undef &net; undef &asn; undef &name; + + if ($outfile) { + close($outfile); + } + + return \%asns; +} + +sub dump +{ + my $asns = shift @_; + + foreach my $asn_number (sort {$a <=> $b} keys %{$asns}) { + &dump_one($asn_number, $asns->{$asn_number}); + } +} + +sub dump_one +{ + my($asn_number, $asns) = @_; + my @ranges; + + @ranges = $asns->{pool_v4}->list_range(); + + writeASN($asn_number, AF_INET, @ranges); + + @ranges = $asns->{pool_v6}->list_range(); + + writeASN($asn_number, AF_INET6, @ranges); +} + +sub writeASN +{ + my ($asn_number, $family, @ranges) = @_; + my $fh; + + printf "%5u IPv%s ranges for %s\n", + scalar(@ranges), + ($family == AF_INET ? '4' : '6'), + $asn_number; + + my $file = "$target_dir/".$asn_number.".iv".($family == AF_INET ? '4' : '6'); + if (!open($fh, '>', $file)) { + print STDERR "Error opening $file: $!\n"; + exit 1; + } + + binmode($fh); + + foreach my $range (@ranges) { + my ($start, $end) = split('-', $range); + $start = inet_pton($family, $start); + $end = inet_pton($family, $end); + print $fh $start, $end; + } + close $fh; +} diff --git a/asn/xt_asn_build.1 b/asn/xt_asn_build.1 new file mode 100644 index 0000000..13987ad --- /dev/null +++ b/asn/xt_asn_build.1 @@ -0,0 +1,43 @@ +.TH xt_asn_build 1 "2010-12-17" "xtables-addons" "xtables-addons" +.SH Name +.PP +xt_asn_build \(em convert ASN.csv to packed format for xt_asn +.SH Syntax +.PP +\fI/usr/libexec/xt_asn/\fP\fBxt_asn_build\fP [\fB\-D\fP \fItarget_dir\fP] +[\fB\-S\fP \fIsource_dir\fP] [\fB\-O\fP \fIoutput_file\fP] +.SH Description +.PP +xt_asn_build is used to build packed raw representations of the range +database that the xt_asn module relies on. Since kernel memory is precious, +much of the preprocessing is done in userspace by this very building tool. One +file is produced for each country, so that no more addresses than needed are +required to be loaded into memory. The ranges in the packed database files are +also ordered, as xt_asn relies on this property for its bisection approach to +work. +.PP +Since the script is usually installed to the libexec directory of the +xtables-addons package and this is outside $PATH (on purpose), invoking the +script requires it to be called with a path. +.PP Options +.TP +\fB\-D\fP \fItarget_dir\fP +Specifies the target directory into which the files are to be put. Defaults to ".". +.TP +\fB\-S\fP \fIsource_dir\fP +Specifies the source directory from which to read the two files by the name +of \fBGeoLite2\-ASN\-Blocks\-IPv?.csv\fP, +.TP +\fB\-O\fP \fIoutput_file\fP +Specifies an optioan target file to output DNS records for ASN to name +(TXT-RRs) and network (APL-RRs). Defaults to no output. The file should be +sorted postprocessing to remove duplicate TXT records and to combine APL-RRs +into a more compact record set. +.SH Application +.PP +Shell commands to build the databases and put them to where they are expected: +.PP +xt_asn_build \-D /usr/share/xt_asn +.SH See also +.PP +xt_asn_dl(1) diff --git a/asn/xt_asn_dl b/asn/xt_asn_dl new file mode 100755 index 0000000..4232036 --- /dev/null +++ b/asn/xt_asn_dl @@ -0,0 +1,5 @@ +#!/bin/sh +rm -rf GeoLite2-ASN-CSV_* +wget -q http://geolite.maxmind.com/download/geoip/database/GeoLite2-ASN-CSV.zip +unzip -q GeoLite2-ASN-CSV.zip +rm -f GeoLite2-ASN-CSV.zip diff --git a/asn/xt_asn_dl.1 b/asn/xt_asn_dl.1 new file mode 100644 index 0000000..e7e484e --- /dev/null +++ b/asn/xt_asn_dl.1 @@ -0,0 +1,21 @@ +.TH xt_asn_dl 1 "2010-12-17" "xtables-addons" "xtables-addons" +.SH Name +.PP +xt_asn_dl \(em download ASN database files +.SH Syntax +.PP +\fI/usr/libexec/xt_asn/\fP\fBxt_asn_dl\fP +.SH Description +.PP +Downloads and unpacks the MaxMind ASN Lite databases for IPv4 and +IPv6 and unpacks them to the current directory. +.PP +Since the script is usually installed to the libexec directory of the +xtables-addons package and this is outside $PATH (on purpose), invoking the +script requires it to be called with a path. +.SH Options +.PP +None. +.SH See also +.PP +xt_asn_build(1) diff --git a/asn/xt_asn_fetch b/asn/xt_asn_fetch new file mode 100755 index 0000000..b94cc56 --- /dev/null +++ b/asn/xt_asn_fetch @@ -0,0 +1,94 @@ +#!/usr/bin/perl +# +# Utility to query GeoIP database +# Copyright Philip Prindeville, 2018 +# Adapted for ASN database, D. Stussy, 2019 +# +use Getopt::Long; +use Socket qw(AF_INET AF_INET6 inet_ntop); +use warnings; +use strict; + +sub AF_INET_SIZE() { 4 } +sub AF_INET6_SIZE() { 16 } + +my $target_dir = "."; +my $ipv4 = 0; +my $ipv6 = 0; + +&Getopt::Long::Configure(qw(bundling)); +&GetOptions( + "D=s" => \$target_dir, + "4" => \$ipv4, + "6" => \$ipv6, +); + +if (!-d $target_dir) { + print STDERR "Target directory $target_dir does not exit.\n"; + exit 1; +} + +# if neither specified, assume both +if (! $ipv4 && ! $ipv6) { + $ipv4 = $ipv6 = 1; +} + +foreach my $asn (@ARGV) { + if ($asn !~ m/^([1-9][0-9]*)$/) { + print STDERR "Invalid ASN '$asn'\n"; + exit 1; + } + + my $file = $target_dir . '/' . uc($asn) . '.iv4'; + + if (! -f $file) { + printf STDERR "Can't find data for ASN '$asn'\n"; + exit 1; + } + + my ($contents, $buffer, $bytes, $fh); + + if ($ipv4) { + open($fh, '<', $file) || die "Couldn't open file '$file'\n"; + + binmode($fh); + + while (($bytes = read($fh, $buffer, AF_INET_SIZE * 2)) == AF_INET_SIZE * 2) { + my $start = inet_ntop(AF_INET, substr($buffer, 0, AF_INET_SIZE)); + my $end = inet_ntop(AF_INET, substr($buffer, AF_INET_SIZE)); + print $start, '-', $end, "\n"; + } + close($fh); + if (! defined $bytes) { + printf STDERR "Error reading file for ASN '$asn'\n"; + exit 1; + } elsif ($bytes != 0) { + printf STDERR "Short read on file for ASN '$asn'\n"; + exit 1; + } + } + + substr($file, -1) = '6'; + + if ($ipv6) { + open($fh, '<', $file) || die "Couldn't open file '$file'\n"; + + binmode($fh); + + while (($bytes = read($fh, $buffer, AF_INET6_SIZE * 2)) == AF_INET6_SIZE * 2) { + my $start = inet_ntop(AF_INET6, substr($buffer, 0, AF_INET6_SIZE)); + my $end = inet_ntop(AF_INET6, substr($buffer, AF_INET6_SIZE)); + print $start, '-', $end, "\n"; + } + close($fh); + if (! defined $bytes) { + printf STDERR "Error reading file for ASN '$asn'\n"; + exit 1; + } elsif ($bytes != 0) { + printf STDERR "Short read on file for ASN '$asn'\n"; + exit 1; + } + } +} + +exit 0; diff --git a/extensions/Kbuild b/extensions/Kbuild index 5031937..02ca25f 100644 --- a/extensions/Kbuild +++ b/extensions/Kbuild @@ -19,6 +19,7 @@ obj-${build_TARPIT} += xt_TARPIT.o obj-${build_condition} += xt_condition.o obj-${build_fuzzy} += xt_fuzzy.o obj-${build_geoip} += xt_geoip.o +obj-${build_asn} += xt_asn.o obj-${build_iface} += xt_iface.o obj-${build_ipp2p} += xt_ipp2p.o obj-${build_ipv4options} += xt_ipv4options.o diff --git a/extensions/Mbuild b/extensions/Mbuild index 462633e..22243ba 100644 --- a/extensions/Mbuild +++ b/extensions/Mbuild @@ -14,6 +14,7 @@ obj-${build_TARPIT} += libxt_TARPIT.so obj-${build_condition} += libxt_condition.so obj-${build_fuzzy} += libxt_fuzzy.so obj-${build_geoip} += libxt_geoip.so +obj-${build_asn} += libxt_asn.so obj-${build_iface} += libxt_iface.so obj-${build_ipp2p} += libxt_ipp2p.so obj-${build_ipv4options} += libxt_ipv4options.so diff --git a/extensions/libxt_asn.c b/extensions/libxt_asn.c new file mode 100644 index 0000000..e9c59b4 --- /dev/null +++ b/extensions/libxt_asn.c @@ -0,0 +1,348 @@ +/* + * "asn" match extension for iptables + * Copyright © Samuel Jean , 2004 - 2008 + * Copyright © Nicolas Bouliane , 2004 - 2008 + * Jan Engelhardt, 2008-2011 + * D. Stussy, 2019 - Converted libxt_geoip.c to ASN use + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License; either + * version 2 of the License, or any later version, as published by the + * Free Software Foundation. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "xt_asn.h" +#include "compat_user.h" +#define ASN_DB_DIR "/usr/share/xt_asn" + +static void asn_help(void) +{ + printf ( + "asn match options:\n" + "[!] --src-asn, --source-number number[,number...]\n" + " Match packet coming from (one of) the specified ASN(s)\n" + "[!] --dst-asn, --destination-number number[,number...]\n" + " Match packet going to (one of) the specified ASN(s)\n" + "\n" + "NOTE: The number is inputed by its ISO3166 code.\n" + "\n" + ); +} + +static struct option asn_opts[] = { + {.name = "dst-asn", .has_arg = true, .val = '2'}, + {.name = "destination-number", .has_arg = true, .val = '2'}, + {.name = "src-asn", .has_arg = true, .val = '1'}, + {.name = "source-number", .has_arg = true, .val = '1'}, + {NULL}, +}; + +#if __BYTE_ORDER == __LITTLE_ENDIAN +static void asn_swap_le16(uint16_t *buf) +{ + unsigned char *p = (void *)buf; + uint16_t n= p[0] + (p[1] << 8); + p[0] = (n >> 8) & 0xff; + p[1] = n & 0xff; +} + +static void asn_swap_in6(struct in6_addr *in6) +{ + asn_swap_le16(&in6->s6_addr16[0]); + asn_swap_le16(&in6->s6_addr16[1]); + asn_swap_le16(&in6->s6_addr16[2]); + asn_swap_le16(&in6->s6_addr16[3]); + asn_swap_le16(&in6->s6_addr16[4]); + asn_swap_le16(&in6->s6_addr16[5]); + asn_swap_le16(&in6->s6_addr16[6]); + asn_swap_le16(&in6->s6_addr16[7]); +} + +static void asn_swap_le32(uint32_t *buf) +{ + unsigned char *p = (void *)buf; + uint32_t n = p[0] + (p[1] << 8) + (p[2] << 16) + (p[3] << 24); + p[0] = (n >> 24) & 0xff; + p[1] = (n >> 16) & 0xff; + p[2] = (n >> 8) & 0xff; + p[3] = n & 0xff; +} +#endif + +static void * +asn_get_subnets(const char *code, uint32_t *count, uint8_t nfproto) +{ + void *subnets; + struct stat sb; + char buf[256]; + int fd; +#if __BYTE_ORDER == __LITTLE_ENDIAN + unsigned int n; +#endif + + /* Use simple integer vector files */ + if (nfproto == NFPROTO_IPV6) + snprintf(buf, sizeof(buf), ASN_DB_DIR "/%s.iv6", code); + else + snprintf(buf, sizeof(buf), ASN_DB_DIR "/%s.iv4", code); + + if ((fd = open(buf, O_RDONLY)) < 0) { + fprintf(stderr, "Could not open %s: %s\n", buf, strerror(errno)); + xtables_error(OTHER_PROBLEM, "Could not read asn database"); + } + + fstat(fd, &sb); + *count = sb.st_size; + switch (nfproto) { + case NFPROTO_IPV6: + if (sb.st_size % sizeof(struct asn_subnet6) != 0) + xtables_error(OTHER_PROBLEM, + "Database file %s seems to be corrupted", buf); + *count /= sizeof(struct asn_subnet6); + break; + case NFPROTO_IPV4: + if (sb.st_size % sizeof(struct asn_subnet4) != 0) + xtables_error(OTHER_PROBLEM, + "Database file %s seems to be corrupted", buf); + *count /= sizeof(struct asn_subnet4); + break; + } + subnets = malloc(sb.st_size); + if (subnets == NULL) + xtables_error(OTHER_PROBLEM, "asn: insufficient memory"); + read(fd, subnets, sb.st_size); + close(fd); + +#if __BYTE_ORDER == __LITTLE_ENDIAN + for (n = 0; n < *count; ++n) { + switch (nfproto) { + case NFPROTO_IPV6: { + struct asn_subnet6 *gs6 = &(((struct asn_subnet6 *)subnets)[n]); + asn_swap_in6(&gs6->begin); + asn_swap_in6(&gs6->end); + break; + } + case NFPROTO_IPV4: { + struct asn_subnet4 *gs4 = &(((struct asn_subnet4 *)subnets)[n]); + asn_swap_le32(&gs4->begin); + asn_swap_le32(&gs4->end); + break; + } + } + } +#endif + return subnets; +} + +static struct asn_number_user *asn_load_asn(const char *code, + unsigned long asn, uint8_t nfproto) +{ + struct asn_number_user *ginfo; + ginfo = malloc(sizeof(struct asn_number_user)); + + if (!ginfo) + return NULL; + + ginfo->subnets = (unsigned long)asn_get_subnets(code, + &ginfo->count, nfproto); + ginfo->asn = asn; + + return ginfo; +} + +static u_int32_t +check_asn_value(char *asn, u_int32_t asn_used[], u_int8_t count) +{ + u_int8_t i; + u_int32_t tmp_asn = 0; + + for (i = 0; i < strlen(asn); i++) + if (!isdigit(asn[i])) + xtables_error(PARAMETER_PROBLEM, + "asn: invalid number code '%s'", asn); + + if (i < 1) /* Empty string */ + xtables_error(PARAMETER_PROBLEM, "asn: missing number code"); + + tmp_asn = strtoul(asn, NULL, 10); + + // Check for presence of value in asn_used + for (i = 0; i < count; i++) + if (tmp_asn == asn_used[i]) + return 0; // Present, skip it! + + return tmp_asn; +} + +static unsigned int parse_asn_value(const char *asnstr, uint32_t *asn, + union asn_number_group *mem, uint8_t nfproto) +{ + char *buffer, *cp, *next; + u_int8_t i, count = 0; + u_int32_t asntmp; + + buffer = strdup(asnstr); + if (!buffer) + xtables_error(OTHER_PROBLEM, + "asn: insufficient memory available"); + + for (cp = buffer, i = 0; cp && i < XT_ASN_MAX; cp = next, i++) + { + next = strchr(cp, ','); + if (next) *next++ = '\0'; + + if ((asntmp = check_asn_value(cp, asn, count)) != 0) { + if ((mem[count++].user = + (unsigned long)asn_load_asn(cp, asntmp, nfproto)) == 0) + xtables_error(OTHER_PROBLEM, + "asn: insufficient memory available"); + asn[count-1] = asntmp; + } /* ASN 0 is reserved and ignored */ + } + + if (cp) + xtables_error(PARAMETER_PROBLEM, + "asn: too many ASNs specified"); + free(buffer); + + if (count == 0) + xtables_error(PARAMETER_PROBLEM, + "asn: don't know what happened"); + + return count; +} + +static int asn_parse(int c, bool invert, unsigned int *flags, + const char *arg, struct xt_asn_match_info *info, uint8_t nfproto) +{ + switch (c) { + case '1': + if (*flags & (XT_ASN_SRC | XT_ASN_DST)) + xtables_error(PARAMETER_PROBLEM, + "asn: Only exactly one of --src-asn " + "or --dst-asn must be specified!"); + + *flags |= XT_ASN_SRC; + if (invert) + *flags |= XT_ASN_INV; + + info->count = parse_asn_value(arg, info->asn, info->mem, + nfproto); + info->flags = *flags; + return true; + + case '2': + if (*flags & (XT_ASN_SRC | XT_ASN_DST)) + xtables_error(PARAMETER_PROBLEM, + "asn: Only exactly one of --src-asn " + "or --dst-asn must be specified!"); + + *flags |= XT_ASN_DST; + if (invert) + *flags |= XT_ASN_INV; + + info->count = parse_asn_value(arg, info->asn, info->mem, + nfproto); + info->flags = *flags; + return true; + } + + return false; +} + +static int asn_parse6(int c, char **argv, int invert, unsigned int *flags, + const void *entry, struct xt_entry_match **match) +{ + return asn_parse(c, invert, flags, optarg, + (void *)(*match)->data, NFPROTO_IPV6); +} + +static int asn_parse4(int c, char **argv, int invert, unsigned int *flags, + const void *entry, struct xt_entry_match **match) +{ + return asn_parse(c, invert, flags, optarg, + (void *)(*match)->data, NFPROTO_IPV4); +} + +static void +asn_final_check(unsigned int flags) +{ + if (!flags) + xtables_error(PARAMETER_PROBLEM, + "asn: missing arguments"); +} + +static void +asn_save(const void *ip, const struct xt_entry_match *match) +{ + const struct xt_asn_match_info *info = (void *)match->data; + u_int8_t i; + + if (info->flags & XT_ASN_INV) + printf(" !"); + + if (info->flags & XT_ASN_SRC) + printf(" --src-asn "); + else + printf(" --dst-asn "); + + for (i = 0; i < info->count; i++) + printf("%s%u", i ? "," : "", info->asn[i]); +} + +static void +asn_print(const void *ip, const struct xt_entry_match *match, int numeric) +{ + printf(" -m asn"); + asn_save(ip, match); +} + +static struct xtables_match asn_match[] = { + { + .family = NFPROTO_IPV6, + .name = "asn", + .revision = 1, + .version = XTABLES_VERSION, + .size = XT_ALIGN(sizeof(struct xt_asn_match_info)), + .userspacesize = offsetof(struct xt_asn_match_info, mem), + .help = asn_help, + .parse = asn_parse6, + .final_check = asn_final_check, + .print = asn_print, + .save = asn_save, + .extra_opts = asn_opts, + }, + { + .family = NFPROTO_IPV4, + .name = "asn", + .revision = 1, + .version = XTABLES_VERSION, + .size = XT_ALIGN(sizeof(struct xt_asn_match_info)), + .userspacesize = offsetof(struct xt_asn_match_info, mem), + .help = asn_help, + .parse = asn_parse4, + .final_check = asn_final_check, + .print = asn_print, + .save = asn_save, + .extra_opts = asn_opts, + }, +}; + +static __attribute__((constructor)) void asn_mt_ldr(void) +{ + xtables_register_matches(asn_match, + sizeof(asn_match) / sizeof(*asn_match)); +} diff --git a/extensions/libxt_asn.man b/extensions/libxt_asn.man new file mode 100644 index 0000000..7032cd7 --- /dev/null +++ b/extensions/libxt_asn.man @@ -0,0 +1,21 @@ +.PP +Match a packet by its source or destination autonomous system number (ASN). +.TP +[\fB!\fP] \fB\-\-src\-asn\fP, \fB\-\-source\-number\fP \fInumber\fP[\fB,\fP\fInumber\fP\fB...\fP] +Match packet coming from (one of) the specified ASN(s) +.TP +[\fB!\fP] \fB\-\-dst\-asn\fP, \fB\-\-destination\-number\fP \fIcountry\fP[\fB,\fP\fIcountry\fP\fB...\fP] +Match packet going to (one of) the specified ASN(s) +.TP +.PP +The extra files you will need are the binary database files. They are generated +from a ASN-subnet database with the asn_build_db.pl tool that is shipped +with the source package, and which should be available in compiled packages in +/usr/lib(exec)/xtables-addons/. The first command retrieves CSV files from +MaxMind, while the other two build packed bisectable range files: +.PP +mkdir \-p /usr/share/xt_asn; cd /tmp; $path/to/xt_asn_dl; +.PP +$path/to/xt_asn_build \-D /usr/share/xt_asn +.PP +The shared library is hardcoded to look in these paths, so use them. diff --git a/extensions/xt_asn.c b/extensions/xt_asn.c new file mode 100644 index 0000000..a7ed8ef --- /dev/null +++ b/extensions/xt_asn.c @@ -0,0 +1,374 @@ +/* iptables kernel module for the asn match + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Copyright (c) 2004, 2005, 2006, 2007, 2008 + * Samuel Jean & Nicolas Bouliane + * + * D. Stussy - 2019 - Repurposed xt_geoip.c for ASN match. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "xt_asn.h" +#include "compat_xtables.h" + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Nicolas Bouliane"); +MODULE_AUTHOR("Samuel Jean"); +MODULE_DESCRIPTION("xtables module for asn match"); +MODULE_ALIAS("ip6t_asn"); +MODULE_ALIAS("ipt_asn"); + +enum asn_proto { + ASNROTO_IPV6, + ASNROTO_IPV4, + __ASNROTO_MAX, +}; + +/** + * @list: anchor point for asn_head + * @subnets: packed ordered list of ranges (either v6 or v4) + * @count: number of ranges + * @asn: number code + */ +struct asn_number_kernel { + struct list_head list; + void *subnets; + atomic_t ref; + unsigned int count; + unsigned long int asn; +}; + +static struct list_head asn_head[__ASNROTO_MAX]; +static DEFINE_SPINLOCK(asn_lock); + +static const enum asn_proto nfp2geo[] = { + [NFPROTO_IPV6] = ASNROTO_IPV6, + [NFPROTO_IPV4] = ASNROTO_IPV4, +}; +static const size_t asnproto_size[] = { + [ASNROTO_IPV6] = sizeof(struct asn_subnet6), + [ASNROTO_IPV4] = sizeof(struct asn_subnet4), +}; + +static struct asn_number_kernel * +asn_add_node(const struct asn_number_user __user *umem_ptr, + enum asn_proto proto) +{ + struct asn_number_user umem; + struct asn_number_kernel *p; + size_t size; + void *subnet; + int ret; + + if (copy_from_user(&umem, umem_ptr, sizeof(umem)) != 0) + return ERR_PTR(-EFAULT); + if (umem.count > SIZE_MAX / asnproto_size[proto]) + return ERR_PTR(-E2BIG); + p = kmalloc(sizeof(struct asn_number_kernel), GFP_KERNEL); + if (p == NULL) + return ERR_PTR(-ENOMEM); + + p->count = umem.count; + p->asn = umem.asn; + size = p->count * asnproto_size[proto]; + if (size == 0) { + /* + * Believe it or not, vmalloc prints a warning to dmesg for + * zero-sized allocations :-/ + */ + subnet = NULL; + } else { + subnet = vmalloc(size); + if (subnet == NULL) { + ret = -ENOMEM; + goto free_p; + } + } + if (copy_from_user(subnet, + (const void __user *)(unsigned long)umem.subnets, size) != 0) { + ret = -EFAULT; + goto free_s; + } + + p->subnets = subnet; + atomic_set(&p->ref, 1); + INIT_LIST_HEAD(&p->list); + + spin_lock(&asn_lock); + list_add_tail_rcu(&p->list, &asn_head[proto]); + spin_unlock(&asn_lock); + + return p; + + free_s: + vfree(subnet); + free_p: + kfree(p); + return ERR_PTR(ret); +} + +static void asn_try_remove_node(struct asn_number_kernel *p) +{ + spin_lock(&asn_lock); + if (!atomic_dec_and_test(&p->ref)) { + spin_unlock(&asn_lock); + return; + } + + /* So now am unlinked or the only one alive, right ? + * What are you waiting ? Free up some memory! + */ + list_del_rcu(&p->list); + spin_unlock(&asn_lock); + + synchronize_rcu(); + vfree(p->subnets); + kfree(p); +} + +static struct asn_number_kernel *find_node(unsigned long asn, + enum asn_proto proto) +{ + struct asn_number_kernel *p; + spin_lock(&asn_lock); + + list_for_each_entry_rcu(p, &asn_head[proto], list) + if (p->asn == asn) { + atomic_inc(&p->ref); + spin_unlock(&asn_lock); + return p; + } + + spin_unlock(&asn_lock); + return NULL; +} + +static inline int +ipv6_cmp(const struct in6_addr *p, const struct in6_addr *q) +{ + unsigned int i; + + for (i = 0; i < 4; ++i) { + if (p->s6_addr32[i] < q->s6_addr32[i]) + return -1; + else if (p->s6_addr32[i] > q->s6_addr32[i]) + return 1; + } + + return 0; +} + +static bool asn_bsearch6(const struct asn_subnet6 *range, + const struct in6_addr *addr, int lo, int hi) +{ + int mid; + + while (true) { + if (hi <= lo) + return false; + mid = (lo + hi) / 2; + if (ipv6_cmp(&range[mid].begin, addr) <= 0 && + ipv6_cmp(addr, &range[mid].end) <= 0) + return true; + if (ipv6_cmp(&range[mid].begin, addr) > 0) + hi = mid; + else if (ipv6_cmp(&range[mid].end, addr) < 0) + lo = mid + 1; + else + break; + } + + WARN_ON(true); + return false; +} + +static bool +xt_asn_mt6(const struct sk_buff *skb, struct xt_action_param *par) +{ + const struct xt_asn_match_info *info = par->matchinfo; + const struct asn_number_kernel *node; + const struct ipv6hdr *iph = ipv6_hdr(skb); + unsigned int i; + struct in6_addr ip; + + memcpy(&ip, (info->flags & XT_ASN_SRC) ? &iph->saddr : &iph->daddr, + sizeof(ip)); + for (i = 0; i < 4; ++i) + ip.s6_addr32[i] = ntohl(ip.s6_addr32[i]); + + rcu_read_lock(); + for (i = 0; i < info->count; i++) { + if ((node = info->mem[i].kernel) == NULL) { + printk(KERN_ERR "xt_asn: what the hell ?? '%u' isn't loaded into memory... skip it!\n", + info->asn[i]); + continue; + } + if (asn_bsearch6(node->subnets, &ip, 0, node->count)) { + rcu_read_unlock(); + return !(info->flags & XT_ASN_INV); + } + } + + rcu_read_unlock(); + return info->flags & XT_ASN_INV; +} + +static bool asn_bsearch4(const struct asn_subnet4 *range, + uint32_t addr, int lo, int hi) +{ + int mid; + + while (true) { + if (hi <= lo) + return false; + mid = (lo + hi) / 2; + if (range[mid].begin <= addr && addr <= range[mid].end) + return true; + if (range[mid].begin > addr) + hi = mid; + else if (range[mid].end < addr) + lo = mid + 1; + else + break; + } + + WARN_ON(true); + return false; +} + +static bool +xt_asn_mt4(const struct sk_buff *skb, struct xt_action_param *par) +{ + const struct xt_asn_match_info *info = par->matchinfo; + const struct asn_number_kernel *node; + const struct iphdr *iph = ip_hdr(skb); + unsigned int i; + uint32_t ip; + + ip = ntohl((info->flags & XT_ASN_SRC) ? iph->saddr : iph->daddr); + rcu_read_lock(); + for (i = 0; i < info->count; i++) { + if ((node = info->mem[i].kernel) == NULL) { + printk(KERN_ERR "xt_asn: what the hell ?? '%u' isn't loaded into memory... skip it!\n", + info->asn[i]); + continue; + } + if (asn_bsearch4(node->subnets, ip, 0, node->count)) { + rcu_read_unlock(); + return !(info->flags & XT_ASN_INV); + } + } + + rcu_read_unlock(); + return info->flags & XT_ASN_INV; +} + +static int xt_asn_mt_checkentry(const struct xt_mtchk_param *par) +{ + struct xt_asn_match_info *info = par->matchinfo; + struct asn_number_kernel *node; + unsigned int i; + + for (i = 0; i < info->count; i++) { + node = find_node(info->asn[i], nfp2geo[par->family]); + if (node == NULL) { + node = asn_add_node((const void __user *)(unsigned long)info->mem[i].user, + nfp2geo[par->family]); + if (IS_ERR(node)) { + printk(KERN_ERR + "xt_asn: unable to load '%u' into memory: %ld\n", + info->asn[i], PTR_ERR(node)); + return PTR_ERR(node); + } + } + + /* Overwrite the now-useless pointer info->mem[i] with + * a pointer to the node's kernelspace structure. + * This avoids searching for a node in the match() and + * destroy() functions. + */ + info->mem[i].kernel = node; + } + + return 0; +} + +static void xt_asn_mt_destroy(const struct xt_mtdtor_param *par) +{ + struct xt_asn_match_info *info = par->matchinfo; + struct asn_number_kernel *node; + unsigned int i; + + /* This entry has been removed from the table so + * decrease the refcount of all countries it is + * using. + */ + + for (i = 0; i < info->count; i++) + if ((node = info->mem[i].kernel) != NULL) { + /* Free up some memory if that node isn't used + * anymore. */ + asn_try_remove_node(node); + } + else + /* Something strange happened. There's no memory allocated for this + * number. Please send this bug to the mailing list. */ + printk(KERN_ERR + "xt_asn: What happened peejix ? What happened acidfu ?\n" + "xt_asn: please report this bug to the maintainers\n"); +} + +static struct xt_match xt_asn_match[] __read_mostly = { + { + .name = "asn", + .revision = 1, + .family = NFPROTO_IPV6, + .match = xt_asn_mt6, + .checkentry = xt_asn_mt_checkentry, + .destroy = xt_asn_mt_destroy, + .matchsize = sizeof(struct xt_asn_match_info), + .me = THIS_MODULE, + }, + { + .name = "asn", + .revision = 1, + .family = NFPROTO_IPV4, + .match = xt_asn_mt4, + .checkentry = xt_asn_mt_checkentry, + .destroy = xt_asn_mt_destroy, + .matchsize = sizeof(struct xt_asn_match_info), + .me = THIS_MODULE, + }, +}; + +static int __init xt_asn_mt_init(void) +{ + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(asn_head); ++i) + INIT_LIST_HEAD(&asn_head[i]); + return xt_register_matches(xt_asn_match, ARRAY_SIZE(xt_asn_match)); +} + +static void __exit xt_asn_mt_fini(void) +{ + xt_unregister_matches(xt_asn_match, ARRAY_SIZE(xt_asn_match)); +} + +module_init(xt_asn_mt_init); +module_exit(xt_asn_mt_fini); diff --git a/extensions/xt_asn.h b/extensions/xt_asn.h new file mode 100644 index 0000000..f3b405d --- /dev/null +++ b/extensions/xt_asn.h @@ -0,0 +1,58 @@ +/* ipt_asn.h header file for libipt_asn.c and ipt_asn.c + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Copyright (c) 2004, 2005, 2006, 2007, 2008 + * + * Samuel Jean + * Nicolas Bouliane + * + * D. Stussy - 2019 - Repurposed xt_geoip.h for ASN use. + */ +#ifndef _LINUX_NETFILTER_XT_ASN_H +#define _LINUX_NETFILTER_XT_ASN_H 1 + +enum { + XT_ASN_SRC = 1 << 0, /* Perform check on Source IP */ + XT_ASN_DST = 1 << 1, /* Perform check on Destination IP */ + XT_ASN_INV = 1 << 2, /* Negate the condition */ + + XT_ASN_MAX = 15, /* Maximum of countries */ +}; + +/* Yup, an address range will be passed in with host-order */ +struct asn_subnet4 { + __u32 begin; + __u32 end; +}; + +struct asn_subnet6 { + struct in6_addr begin, end; +}; + +struct asn_number_user { + aligned_u64 subnets; + __u32 count; + __u32 asn; +}; + +struct asn_number_kernel; + +union asn_number_group { + aligned_u64 user; /* struct asn_number_user * */ + struct asn_number_kernel *kernel; +}; + +struct xt_asn_match_info { + __u32 asn[XT_ASN_MAX]; + __u8 flags; + __u8 count; + + /* Used internally by the kernel */ + union asn_number_group mem[XT_ASN_MAX]; +}; + +#endif /* _LINUX_NETFILTER_XT_ASN_H */ diff --git a/mconfig b/mconfig index 2434ac9..8862c0e 100644 --- a/mconfig +++ b/mconfig @@ -11,6 +11,7 @@ build_LOGMARK=m build_PROTO=m build_SYSRQ=m build_TARPIT=m +build_asn=m build_condition=m build_fuzzy=m build_geoip=m