| [2] | 1 | # DNSBL
|
|---|
| 2 | # Functions for interacting with the DNSBL database
|
|---|
| [40] | 3 | ##
|
|---|
| 4 | # $Id: DNSBL.pm 104 2025-09-24 17:20:10Z kdeugau $
|
|---|
| [73] | 5 | # Copyright 2009-2012,2014,2018,2025 Kris Deugau <kdeugau@deepnet.cx>
|
|---|
| [40] | 6 | #
|
|---|
| 7 | # This program is free software: you can redistribute it and/or modify
|
|---|
| 8 | # it under the terms of the GNU General Public License as published by
|
|---|
| 9 | # the Free Software Foundation, either version 3 of the License, or
|
|---|
| 10 | # (at your option) any later version.
|
|---|
| 11 | #
|
|---|
| 12 | # This program is distributed in the hope that it will be useful,
|
|---|
| 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|---|
| 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|---|
| 15 | # GNU General Public License for more details.
|
|---|
| 16 | #
|
|---|
| 17 | # You should have received a copy of the GNU General Public License
|
|---|
| 18 | # along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|---|
| 19 | ##
|
|---|
| [2] | 20 |
|
|---|
| 21 | package DNSBL;
|
|---|
| 22 |
|
|---|
| 23 | use strict;
|
|---|
| 24 | use warnings;
|
|---|
| 25 | use Exporter;
|
|---|
| [73] | 26 |
|
|---|
| [2] | 27 | use DBI;
|
|---|
| 28 | use NetAddr::IP;
|
|---|
| 29 |
|
|---|
| 30 | use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
|
|---|
| 31 |
|
|---|
| [73] | 32 | $VERSION = 3.0;
|
|---|
| [2] | 33 | @ISA = qw(Exporter);
|
|---|
| [73] | 34 | @EXPORT_OK = qw( $dbh );
|
|---|
| [2] | 35 |
|
|---|
| [73] | 36 | @EXPORT = qw( $dbh );
|
|---|
| [2] | 37 | %EXPORT_TAGS = ( ALL => [qw(
|
|---|
| 38 | )]
|
|---|
| 39 | );
|
|---|
| 40 |
|
|---|
| 41 | ## "constants"
|
|---|
| 42 |
|
|---|
| [36] | 43 | # w00t! somewhere along the line, by accident or intent, SA's
|
|---|
| 44 | # check_dnsbl_sub can now check up to 24 bits of an DNSBL return value!
|
|---|
| [2] | 45 | # 1 not available so we don't $self->shoot(foot)
|
|---|
| 46 | our %bitfields = (
|
|---|
| [50] | 47 | # ip
|
|---|
| 48 | ip => 2,
|
|---|
| 49 | # "I'm a total spamming moron!" - per-IP only!
|
|---|
| 50 | slist => 128,
|
|---|
| 51 |
|
|---|
| 52 | # Block listings. Ordering for levels 0, 1, 2 not ideal due to evolution of code.
|
|---|
| 53 | # Levels 3 and higher are more coherently ordered
|
|---|
| 54 |
|
|---|
| 55 | # Automatically listed blocks based on IP counts
|
|---|
| [2] | 56 | 0 => 16,
|
|---|
| 57 | 1 => 8,
|
|---|
| 58 | 2 => 4,
|
|---|
| [50] | 59 | 3 => 4096,
|
|---|
| 60 | 4 => 32768,
|
|---|
| 61 | 5 => 262144,
|
|---|
| 62 | 6 => 2097152,
|
|---|
| 63 |
|
|---|
| 64 | # Out-of-band
|
|---|
| 65 | org0 => 32,
|
|---|
| 66 | block0 => 64,
|
|---|
| 67 | org1 => 256,
|
|---|
| 68 | org2 => 512,
|
|---|
| 69 | block1 => 1024,
|
|---|
| 70 | block2 => 2048,
|
|---|
| 71 | org3 => 8192,
|
|---|
| 72 | block3 => 16384,
|
|---|
| 73 | org4 => 65536,
|
|---|
| 74 | block4 => 131072,
|
|---|
| 75 | org5 => 524288,
|
|---|
| 76 | block5 => 1048576,
|
|---|
| 77 | org6 => 4194304,
|
|---|
| 78 | block6 => 8388608,
|
|---|
| 79 |
|
|---|
| [2] | 80 | );
|
|---|
| 81 |
|
|---|
| 82 | # probably needs some tuning; even 7 hits in a /24 is a pretty small percentage
|
|---|
| 83 | # number of IPs in a block of the given masklength needed to have that block automatically listed
|
|---|
| [24] | 84 | # defaults: (overridden by entries in db:autolist)
|
|---|
| [2] | 85 | our %autolist = (
|
|---|
| [10] | 86 | 31 => 1,
|
|---|
| [2] | 87 | 30 => 1,
|
|---|
| 88 | 29 => 2,
|
|---|
| 89 | 28 => 3,
|
|---|
| 90 | 27 => 4,
|
|---|
| 91 | 26 => 5,
|
|---|
| 92 | 25 => 6,
|
|---|
| 93 | 24 => 7,
|
|---|
| 94 | 23 => 8,
|
|---|
| 95 | 22 => 10,
|
|---|
| [15] | 96 | 21 => 13,
|
|---|
| 97 | 20 => 16,
|
|---|
| 98 | 19 => 19,
|
|---|
| 99 | 18 => 22,
|
|---|
| 100 | 17 => 26,
|
|---|
| 101 | 16 => 30,
|
|---|
| 102 | 15 => 34,
|
|---|
| 103 | 14 => 38,
|
|---|
| 104 | 13 => 42,
|
|---|
| 105 | 12 => 46,
|
|---|
| 106 | 11 => 50,
|
|---|
| 107 | 10 => 54,
|
|---|
| 108 | 9 => 58,
|
|---|
| 109 | 8 => 62,
|
|---|
| [2] | 110 | 7 => 2**31,
|
|---|
| 111 | 6 => 2**31,
|
|---|
| 112 | 5 => 2**31,
|
|---|
| 113 | 4 => 2**31,
|
|---|
| 114 | 3 => 2**31,
|
|---|
| 115 | 2 => 2**31,
|
|---|
| 116 | 1 => 2**31,
|
|---|
| 117 | 0 => 2**31
|
|---|
| 118 | );
|
|---|
| 119 |
|
|---|
| 120 | # le sigh. constants for masklength iterationing
|
|---|
| 121 | our @howmany = (1,128,64,32,16,8,4,2,1,128,64,32,16,8,4,2,1,128,64,32,16,8,4,2,1,128,64,32,16,8,4,2);
|
|---|
| 122 |
|
|---|
| [48] | 123 | # hard max depth. There are not enough bits in a 32-bit IP in 127/8 for more than 7 sets of 3 block-level
|
|---|
| 124 | # flags, plus one for the IP, plus one for an "alternate" IP flag, plus reserving the least significant bit
|
|---|
| 125 | # as a "don't use this because Reasons"
|
|---|
| 126 | our $maxlvl = 6;
|
|---|
| 127 |
|
|---|
| [2] | 128 | # variables
|
|---|
| 129 | our $dbh;
|
|---|
| 130 |
|
|---|
| 131 | our $err;
|
|---|
| [66] | 132 | our $errstr = '';
|
|---|
| [2] | 133 |
|
|---|
| 134 | # basic object subs
|
|---|
| 135 | sub new {
|
|---|
| [73] | 136 | my $this = shift;
|
|---|
| 137 | my $class = ref($this) || $this;
|
|---|
| 138 | my %args = @_;
|
|---|
| [2] | 139 |
|
|---|
| [73] | 140 | # Prepopulate a basic config. Note some of these *will* cause errors if left unset.
|
|---|
| 141 | my %defconfig = (
|
|---|
| 142 | dbhost => "localhost",
|
|---|
| 143 | dbname => "dnsbl",
|
|---|
| 144 | dbuser => "dnsbl",
|
|---|
| 145 | dbpass => "spambgone",
|
|---|
| [104] | 146 | misc => {
|
|---|
| 147 | blzone => "spamhosts.example.com",
|
|---|
| 148 | altblzone => "company.dnsbl",
|
|---|
| 149 | bladmin => "systems.example.com",
|
|---|
| 150 | ttl => 600,
|
|---|
| 151 | soa => "600 600 600 600",
|
|---|
| 152 | iplisted => '$ relayed a reported spam',
|
|---|
| 153 | blocklisted => 'Netblock listed on one or more criteria',
|
|---|
| 154 | },
|
|---|
| [73] | 155 | );
|
|---|
| 156 |
|
|---|
| 157 | my %siteconfig;
|
|---|
| 158 | my $dbhost;
|
|---|
| 159 | my $dbname;
|
|---|
| 160 | my $dbuser;
|
|---|
| 161 | my $dbpass;
|
|---|
| 162 | if (defined($args{configfile})) {
|
|---|
| 163 | if (-e $args{configfile} && -f $args{configfile}) {
|
|---|
| 164 | my $ret = eval `cat $args{configfile}`;
|
|---|
| 165 | unless ($ret) {
|
|---|
| 166 | if ($@) { $errstr = "couldn't parse $args{configfile}: $@\n"; return; }
|
|---|
| 167 | if (!defined($ret)) { $errstr = "couldn't load $args{configfile}: $!\n"; return; }
|
|---|
| 168 | if (!$ret) { $errstr = "couldn't load $args{configfile}\n"; return; }
|
|---|
| 169 | }
|
|---|
| 170 | # crossload legacy variables, but prefer new %siteconfig values
|
|---|
| 171 | $siteconfig{dbhost} = $dbhost if !$siteconfig{dbhost} && $dbhost;
|
|---|
| 172 | $siteconfig{dbname} = $dbname if !$siteconfig{dbname} && $dbname;
|
|---|
| 173 | $siteconfig{dbuser} = $dbuser if !$siteconfig{dbuser} && $dbuser;
|
|---|
| 174 | $siteconfig{dbpass} = $dbpass if !$siteconfig{dbpass} && $dbpass;
|
|---|
| 175 | }
|
|---|
| 176 | }
|
|---|
| 177 |
|
|---|
| 178 | # Assemble the object. Apply configuration hashes in order of precedence.
|
|---|
| 179 | my $self = {
|
|---|
| 180 | # Hardcoded defaults
|
|---|
| 181 | %defconfig,
|
|---|
| 182 | # Default config file OR caller-specified one, loaded above
|
|---|
| 183 | %siteconfig,
|
|---|
| 184 | # Caller-specified arguments
|
|---|
| 185 | %args
|
|---|
| 186 | };
|
|---|
| 187 | bless $self, $class;
|
|---|
| 188 |
|
|---|
| 189 | return $self;
|
|---|
| 190 | } # new()
|
|---|
| 191 |
|
|---|
| [2] | 192 | sub DESTROY {
|
|---|
| 193 | my $self = shift;
|
|---|
| [25] | 194 | $self->dbclose() if $dbh;
|
|---|
| [2] | 195 | }
|
|---|
| 196 |
|
|---|
| 197 | # JIC someone wants to close the db but not finish the script
|
|---|
| 198 | sub dbclose {
|
|---|
| 199 | $dbh->disconnect;
|
|---|
| 200 | }
|
|---|
| 201 |
|
|---|
| 202 | ## specific object subs:
|
|---|
| 203 |
|
|---|
| 204 | sub connect {
|
|---|
| [25] | 205 | my $self = shift;
|
|---|
| [103] | 206 | # after jumping a HUUUGE number of PG versions, AutoCommit => 0 produced some bizarre bugs
|
|---|
| [73] | 207 | $dbh = DBI->connect("DBI:Pg:host=$self->{dbhost};dbname=$self->{dbname}", $self->{dbuser}, $self->{dbpass}, {
|
|---|
| [103] | 208 | AutoCommit => 1,
|
|---|
| 209 | PrintError => 0
|
|---|
| [2] | 210 | })
|
|---|
| 211 | or die "database inaccessible: ".$DBI::errstr;
|
|---|
| [24] | 212 | my $sth = $dbh->prepare("SELECT masklen,ipcount FROM autolist");
|
|---|
| 213 | $sth->execute;
|
|---|
| 214 | while (my ($masklen,$ipcount) = $sth->fetchrow_array) {
|
|---|
| 215 | $autolist{$masklen} = $ipcount;
|
|---|
| 216 | }
|
|---|
| [95] | 217 | $sth = $dbh->prepare("SELECT key,value FROM misc");
|
|---|
| [92] | 218 | $sth->execute;
|
|---|
| 219 | while (my ($key,$value) = $sth->fetchrow_array) {
|
|---|
| 220 | $self->{misc}{$key} = $value;
|
|---|
| 221 | }
|
|---|
| [2] | 222 | return $dbh;
|
|---|
| 223 | }
|
|---|
| 224 |
|
|---|
| [5] | 225 |
|
|---|
| [23] | 226 | ## DNSBLDB::initexport()
|
|---|
| 227 | # Prepare a couple of statement handles for later processing in export(). Assists in ~3x speed increase.
|
|---|
| 228 | my $parsth;
|
|---|
| 229 | my $sthmoron;
|
|---|
| 230 | sub initexport {
|
|---|
| 231 | $parsth = $dbh->prepare("SELECT count(i.ip),b.block,b.level,b.listme AS oobblock,o.listme AS ooborg ".
|
|---|
| [32] | 232 | "FROM iplist i INNER JOIN blocks b ON i.parent = b.block INNER JOIN orgs o ON b.orgid = o.orgid ".
|
|---|
| [23] | 233 | "WHERE b.block >>= ? ".
|
|---|
| 234 | "GROUP BY b.block,b.level,b.listme,o.listme ORDER BY b.block");
|
|---|
| [69] | 235 | $sthmoron = $dbh->prepare("SELECT ip,s4list,exclude FROM iplist WHERE parent = ?");
|
|---|
| [23] | 236 | }
|
|---|
| 237 |
|
|---|
| 238 |
|
|---|
| [5] | 239 | ## DNSBL::ipexists()
|
|---|
| 240 | # return report count if the IP has been reported, otherwise return undef
|
|---|
| [2] | 241 | sub ipexists {
|
|---|
| 242 | my $self = shift;
|
|---|
| 243 | my $ip = shift;
|
|---|
| [66] | 244 | my $sth = $dbh->prepare("SELECT count, exclude FROM iplist WHERE ip=?");
|
|---|
| [2] | 245 | $sth->execute($ip);
|
|---|
| [66] | 246 | my $ret = $sth->fetchrow_arrayref();
|
|---|
| [2] | 247 | return $ret;
|
|---|
| [5] | 248 | } # end ipexists()
|
|---|
| [2] | 249 |
|
|---|
| 250 |
|
|---|
| 251 | # report an IP or URI to the db
|
|---|
| 252 | # increments a hit counter iff the reported IP or URI exists, otherwise it adds it
|
|---|
| 253 | sub report {
|
|---|
| 254 | my $self = shift;
|
|---|
| 255 | my $rep = shift;
|
|---|
| [70] | 256 | my $exclude = shift || 'n';
|
|---|
| [77] | 257 | $exclude = 'y' if $exclude eq 'on';
|
|---|
| [2] | 258 | my $sth;
|
|---|
| 259 | my $rows = 0;
|
|---|
| [103] | 260 |
|
|---|
| 261 | local $dbh->{AutoCommit} = 0;
|
|---|
| 262 | local $dbh->{RaiseError} = 1;
|
|---|
| 263 |
|
|---|
| [2] | 264 | if ($rep =~ /^[\d.]+$/) {
|
|---|
| 265 | # weesa gonna ASS-U-ME IP addresses are sanely formatted.
|
|---|
| [32] | 266 | eval {
|
|---|
| 267 | $sth = $dbh->prepare("SELECT count FROM iplist WHERE ip=?");
|
|---|
| 268 | $sth->execute($rep) or die "eep? ".$dbh->errstr."\n";
|
|---|
| 269 | $rows = $sth->rows;
|
|---|
| 270 | if ($rows == 0) {
|
|---|
| [66] | 271 | $sth = $dbh->prepare("INSERT INTO iplist (ip,parent,exclude) VALUES ".
|
|---|
| 272 | "(?,(SELECT block FROM blocks WHERE block >> ? ORDER BY level DESC LIMIT 1),?)");
|
|---|
| [77] | 273 | $sth->execute($rep, $rep, $exclude) or die "couldn't add entry for $rep: ".$dbh->errstr."\n";
|
|---|
| [32] | 274 | } elsif ($rows == 1) {
|
|---|
| [66] | 275 | $sth = $dbh->prepare("UPDATE iplist SET count=count+1,".
|
|---|
| [77] | 276 | " exclude = ? WHERE ip = ?");
|
|---|
| [71] | 277 | $sth->execute($exclude, $rep) or die "couldn't update listing for $rep: ".$dbh->errstr."\n";
|
|---|
| [32] | 278 | } else {
|
|---|
| 279 | die "db corrupt: found $rows matches on $rep\n";
|
|---|
| 280 | }
|
|---|
| 281 | $sth = $dbh->prepare("SELECT block FROM blocks WHERE block >> ?");
|
|---|
| 282 | $sth->execute($rep);
|
|---|
| [70] | 283 | my $updsth = $dbh->prepare("UPDATE blocks SET ipcount=(".
|
|---|
| 284 | "SELECT count(*) FROM iplist i JOIN blocks b ON b.block=i.parent WHERE i.ip << ? AND i.exclude='n' AND b.exclude='n'".
|
|---|
| [77] | 285 | ") WHERE block = ?");
|
|---|
| [32] | 286 | while (my ($block) = $sth->fetchrow_array) {
|
|---|
| 287 | $updsth->execute($block,$block);
|
|---|
| 288 | }
|
|---|
| 289 | $dbh->commit;
|
|---|
| 290 | };
|
|---|
| 291 | if ($@) {
|
|---|
| 292 | my $msg = $@;
|
|---|
| 293 | return "failed adding $rep: $msg";
|
|---|
| [2] | 294 | }
|
|---|
| 295 | } else {
|
|---|
| 296 | return;
|
|---|
| 297 | }
|
|---|
| 298 | return $rows;
|
|---|
| 299 | } # end report()
|
|---|
| 300 |
|
|---|
| 301 |
|
|---|
| 302 | # add a new org
|
|---|
| 303 | # return the orgid
|
|---|
| 304 | # if the org exists, return the orgid anyway
|
|---|
| 305 | sub addorg {
|
|---|
| 306 | my $self = shift;
|
|---|
| 307 | my $orgname = shift;
|
|---|
| 308 | my $listme = shift || 'n';
|
|---|
| 309 | my $ret = $self->orgexists($orgname);
|
|---|
| 310 | return $ret if $ret;
|
|---|
| 311 | my $sth = $dbh->prepare("INSERT INTO orgs (orgname,listme) VALUES (?,?)");
|
|---|
| 312 | $sth->execute($orgname,$listme) or die "couldn't add org $orgname: ".$dbh->errstr."\n";
|
|---|
| 313 | $dbh->commit;
|
|---|
| 314 | $sth = $dbh->prepare("SELECT orgid FROM orgs WHERE orgname=?");
|
|---|
| 315 | $sth->execute($orgname);
|
|---|
| 316 | my ($orgid) = $sth->fetchrow_array();
|
|---|
| 317 | return $orgid;
|
|---|
| 318 | } # end addorg
|
|---|
| 319 |
|
|---|
| 320 |
|
|---|
| 321 | # checks for existence - nb, exact match! No way to really handle anything else. :/
|
|---|
| 322 | sub orgexists {
|
|---|
| 323 | my $self = shift;
|
|---|
| 324 | my $org = shift;
|
|---|
| 325 | my $sth = $dbh->prepare("SELECT orgid FROM orgs WHERE orgname=?");
|
|---|
| 326 | $sth->execute($org);
|
|---|
| 327 | my ($ret) = $sth->fetchrow_array();
|
|---|
| 328 | return $ret;
|
|---|
| 329 | } # end orgexists();
|
|---|
| 330 |
|
|---|
| 331 |
|
|---|
| [54] | 332 | # take an arbitrary IP range and an IP, and return the CIDR block (if any) the IP is in.
|
|---|
| 333 | sub range2cidr {
|
|---|
| 334 | my $self = shift;
|
|---|
| 335 | my $rstart = shift;
|
|---|
| 336 | my $rend = shift;
|
|---|
| 337 | my $ip = shift;
|
|---|
| 338 |
|
|---|
| 339 | $rstart = new NetAddr::IP $rstart;
|
|---|
| 340 | $rend = new NetAddr::IP $rend;
|
|---|
| 341 | # Basic algoithm: Set the mask on the IP, and see if both $rstart and $rend
|
|---|
| 342 | # are within the range defined by that IP/mask. Continue making the mask
|
|---|
| 343 | # larger until success.
|
|---|
| 344 |
|
|---|
| 345 | my $mask;
|
|---|
| 346 | for ($mask = 32; $mask > 0; $mask--) {
|
|---|
| 347 | my $ip = NetAddr::IP->new("$ip/$mask");
|
|---|
| 348 | if (NetAddr::IP->new($ip->network->addr) >= $rstart &&
|
|---|
| 349 | NetAddr::IP->new($ip->broadcast->addr) <= $rend) {
|
|---|
| 350 | next;
|
|---|
| 351 | } else {
|
|---|
| 352 | $mask++;
|
|---|
| 353 | last;
|
|---|
| 354 | }
|
|---|
| 355 | }
|
|---|
| 356 | my $realnet = NetAddr::IP->new("$ip/$mask")->network;
|
|---|
| 357 |
|
|---|
| 358 | return "$realnet";
|
|---|
| 359 | } # end range2cidr()
|
|---|
| 360 |
|
|---|
| 361 |
|
|---|
| [2] | 362 | # add a block. requires the orgid
|
|---|
| 363 | ##fixme needs error handling
|
|---|
| 364 | sub addblock {
|
|---|
| 365 | my $self = shift;
|
|---|
| 366 | my $blockin = shift;
|
|---|
| 367 | my $orgid = shift;
|
|---|
| 368 | my $level = shift;
|
|---|
| [72] | 369 | my $exclude = shift || 'n';
|
|---|
| [66] | 370 | my $comment = shift;
|
|---|
| [2] | 371 | $blockin =~ s/^\s+//;
|
|---|
| 372 | $blockin =~ s/\s+$//;
|
|---|
| 373 | my $block = new NetAddr::IP "$blockin"; # need this to clean up messes like ranges. sigh.
|
|---|
| 374 |
|
|---|
| [11] | 375 | return "$blockin not a single CIDR range" if !$block;
|
|---|
| 376 |
|
|---|
| [103] | 377 | local $dbh->{AutoCommit} = 0;
|
|---|
| 378 | local $dbh->{RaiseError} = 1;
|
|---|
| [2] | 379 |
|
|---|
| [32] | 380 | my $sth;
|
|---|
| 381 | eval {
|
|---|
| 382 | my $parent = '0/0';
|
|---|
| 383 | if ($level > 0) {
|
|---|
| 384 | $sth = $dbh->prepare("SELECT block FROM blocks WHERE block >> ? ORDER BY level DESC LIMIT 1");
|
|---|
| 385 | $sth->execute("$block");
|
|---|
| 386 | ($parent) = $sth->fetchrow_array;
|
|---|
| 387 | }
|
|---|
| [103] | 388 | $dbh->do("INSERT INTO blocks (block,orgid,level,parent,exclude,comments,ipcount) VALUES (?,?,?,?,?,?,".
|
|---|
| 389 | "(SELECT count(*) FROM iplist i JOIN blocks b ON b.block=i.parent WHERE i.ip << ? AND i.exclude='n' AND b.exclude='n'))",
|
|---|
| 390 | undef, "$block",$orgid,$level,$parent,$exclude,$comment,"$block");
|
|---|
| 391 | $dbh->do("UPDATE iplist SET parent=? WHERE parent=? AND ip << ?", undef, "$block", $parent, "$block");
|
|---|
| [32] | 392 | $dbh->commit;
|
|---|
| 393 | };
|
|---|
| 394 | if ($@) {
|
|---|
| 395 | my $msg = $@;
|
|---|
| 396 | eval { dbh->rollback; };
|
|---|
| 397 | return "failed to add $block: $msg";
|
|---|
| 398 | }
|
|---|
| [2] | 399 | # nb: no need to return anything, since the CIDR block is the key
|
|---|
| 400 | }
|
|---|
| 401 |
|
|---|
| 402 |
|
|---|
| [66] | 403 | # Update a netblock entry. Supports (un)setting the exclude flag and the comment.
|
|---|
| 404 | # Does NOT do any magic around leftover IPs within the block
|
|---|
| 405 | sub updateblock {
|
|---|
| 406 | my $self = shift;
|
|---|
| 407 | my $blockin = shift;
|
|---|
| 408 | my $orgid = shift;
|
|---|
| 409 | my $level = shift;
|
|---|
| [72] | 410 | my $exclude = shift || 'n';
|
|---|
| [66] | 411 | my $comment = shift;
|
|---|
| 412 | $blockin =~ s/^\s+//;
|
|---|
| 413 | $blockin =~ s/\s+$//;
|
|---|
| 414 | my $block = new NetAddr::IP "$blockin"; # need this to clean up messes like ranges. sigh.
|
|---|
| 415 |
|
|---|
| 416 | return "$blockin not a single CIDR range" if !$block;
|
|---|
| 417 |
|
|---|
| 418 | local $dbh->{AutoCommit} = 0;
|
|---|
| 419 | local $dbh->{RaiseError} = 1;
|
|---|
| 420 |
|
|---|
| 421 | my $sth;
|
|---|
| 422 | eval {
|
|---|
| 423 | my $parent = '0/0';
|
|---|
| 424 | if ($level > 0) {
|
|---|
| 425 | $sth = $dbh->prepare("SELECT block FROM blocks WHERE block >> ? ORDER BY level DESC LIMIT 1");
|
|---|
| 426 | $sth->execute("$block");
|
|---|
| 427 | ($parent) = $sth->fetchrow_array;
|
|---|
| 428 | }
|
|---|
| 429 | $sth = $dbh->prepare("UPDATE blocks SET exclude = ?, comments = ?, ipcount = ".
|
|---|
| [77] | 430 | "(SELECT count(*) FROM iplist i JOIN blocks b ON b.block=i.parent WHERE i.ip << ? AND i.exclude='n' AND b.exclude='n')".
|
|---|
| [66] | 431 | " WHERE block = ?");
|
|---|
| 432 | $sth->execute($exclude, $comment, "$block", "$block");
|
|---|
| 433 | $sth = $dbh->prepare("UPDATE iplist SET parent=? WHERE parent=? AND ip << ?");
|
|---|
| 434 | $sth->execute("$block", $parent, "$block");
|
|---|
| 435 | $dbh->commit;
|
|---|
| 436 | };
|
|---|
| 437 | if ($@) {
|
|---|
| 438 | my $msg = $@;
|
|---|
| 439 | eval { dbh->rollback; };
|
|---|
| 440 | return "failed to update $block: $msg";
|
|---|
| 441 | }
|
|---|
| 442 | # nb: no need to return anything, since the CIDR block is the key
|
|---|
| [72] | 443 | } # updateblock()
|
|---|
| [66] | 444 |
|
|---|
| 445 |
|
|---|
| [2] | 446 | sub blockexists {
|
|---|
| 447 | my $self = shift;
|
|---|
| 448 | my $block = shift;
|
|---|
| 449 | my $sth = $dbh->prepare("SELECT count(*) FROM blocks WHERE block=?");
|
|---|
| 450 | $sth->execute($block);
|
|---|
| 451 | my ($ret) = $sth->fetchrow_array();
|
|---|
| 452 | return $ret;
|
|---|
| 453 | }
|
|---|
| 454 |
|
|---|
| 455 |
|
|---|
| [66] | 456 | # returns list (block,blockcomment,orgname) for the block that contains the passed IP.
|
|---|
| [2] | 457 | # accepts a level argument if you don't want the top-level registrar allocation block
|
|---|
| 458 | sub getcontainer {
|
|---|
| 459 | my $self = shift;
|
|---|
| 460 | my $ip = shift;
|
|---|
| 461 | my $level = shift || 0;
|
|---|
| [66] | 462 | my $sth = $dbh->prepare("SELECT b.block,b.comments,o.orgname FROM blocks b INNER JOIN orgs o ".
|
|---|
| [2] | 463 | "ON b.orgid=o.orgid WHERE b.block >> ? AND b.level = ?");
|
|---|
| 464 | $sth->execute($ip,$level);
|
|---|
| 465 | return $sth->fetchrow_array();
|
|---|
| 466 | } # end getcontainer()
|
|---|
| 467 |
|
|---|
| 468 |
|
|---|
| [29] | 469 | # Get info about whether a block, IP or org is listed
|
|---|
| 470 | # Returns ?
|
|---|
| 471 | sub islisted {
|
|---|
| 472 | my $self = shift;
|
|---|
| 473 | my $entity = shift;
|
|---|
| 474 |
|
|---|
| 475 | my $sth;
|
|---|
| 476 |
|
|---|
| 477 | if ($entity =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) {
|
|---|
| 478 | # looking for IP
|
|---|
| 479 |
|
|---|
| [66] | 480 | $sth = $dbh->prepare("SELECT ip,s4list,exclude FROM iplist WHERE ip=?");
|
|---|
| [29] | 481 | $sth->execute($entity);
|
|---|
| 482 | my @ret = $sth->fetchrow_array;
|
|---|
| 483 | return @ret if @ret;
|
|---|
| 484 |
|
|---|
| 485 | } elsif ($entity =~ m|^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/(\d+)$|) {
|
|---|
| 486 | # block
|
|---|
| 487 |
|
|---|
| 488 | my $masklen = $1;
|
|---|
| 489 |
|
|---|
| [66] | 490 | $sth = $dbh->prepare("SELECT block,listme,exclude,ipcount FROM blocks WHERE block = ?");
|
|---|
| [29] | 491 | $sth->execute($entity);
|
|---|
| [66] | 492 | my ($block, $listme, $exclude, $bcount) = $sth->fetchrow_array;
|
|---|
| [29] | 493 |
|
|---|
| 494 | return if !$block;
|
|---|
| 495 |
|
|---|
| [66] | 496 | my @ret = ( ($bcount >= $autolist{$masklen}), $listme, $exclude);
|
|---|
| [29] | 497 | return @ret;
|
|---|
| 498 |
|
|---|
| 499 | } else {
|
|---|
| 500 | # org
|
|---|
| 501 |
|
|---|
| 502 | $sth = $dbh->prepare("SELECT orgid,listme FROM orgs WHERE orgname=?");
|
|---|
| 503 | $sth->execute($entity);
|
|---|
| 504 | my ($orgid,$listme) = $sth->fetchrow_array;
|
|---|
| 505 | return $listme if $orgid;
|
|---|
| 506 |
|
|---|
| 507 | }
|
|---|
| 508 |
|
|---|
| 509 | return undef;
|
|---|
| 510 |
|
|---|
| 511 | } # end islisted()
|
|---|
| 512 |
|
|---|
| 513 |
|
|---|
| [2] | 514 | # whee! Recursion is Fun!
|
|---|
| 515 | # Call ourself to dig down through the layers of blocks from registar-allocation
|
|---|
| [48] | 516 | # (level 0) to final block (level n, not to exceed $maxlvl)
|
|---|
| [2] | 517 | # Take a reference to a hash, and stuff it full of blacklisting goodness.
|
|---|
| 518 | # Optionally accept a level, block-container, and OOB block and org arguments for
|
|---|
| 519 | # the container to check and return
|
|---|
| 520 | # Returns no value directly
|
|---|
| 521 | # Calls itself to walk down the tree of containers
|
|---|
| 522 | sub export {
|
|---|
| 523 | my $self = shift;
|
|---|
| 524 | my $listhosts = shift;
|
|---|
| 525 |
|
|---|
| 526 | # Export data as CIDR netblocks or classful (A/B/C) blocks
|
|---|
| 527 | # Assume classful as it's more compatible with different DNS servers
|
|---|
| 528 | my $mode = shift || 'class';
|
|---|
| 529 |
|
|---|
| 530 | # Assume we're checking the whole enchilada if we don't get told where to look.
|
|---|
| 531 | my $level = shift || 0;
|
|---|
| 532 | my $container = shift || '0.0.0.0/0';
|
|---|
| [23] | 533 | my $bitmask = shift || 0;
|
|---|
| [2] | 534 |
|
|---|
| [66] | 535 | if ($level == 0) {
|
|---|
| 536 | $errstr = '';
|
|---|
| 537 | }
|
|---|
| 538 |
|
|---|
| 539 | return if ($errstr =~ /no connection to the server/);
|
|---|
| [48] | 540 | if ($level > $maxlvl) {
|
|---|
| [2] | 541 | warn "getting too deep, breaking off! ($container, $level)\n";
|
|---|
| 542 | return;
|
|---|
| 543 | }
|
|---|
| 544 |
|
|---|
| 545 | # fiddle $container into a sane state.
|
|---|
| 546 | if ($container =~ m|^\d+\.\d+\.\d+/\d+$|) {
|
|---|
| 547 | $container =~ s|/(\d+)$|.0/$1|;
|
|---|
| 548 | } elsif ($container =~ m|^\d+\.\d+/\d+$|) {
|
|---|
| 549 | $container =~ s|/(\d+)$|.0.0/$1|;
|
|---|
| 550 | } elsif ($container =~ m|^\d+/(\d+)$|) {
|
|---|
| 551 | $container =~ s|/(\d+)$|.0.0.0/$1|;
|
|---|
| 552 | }
|
|---|
| 553 |
|
|---|
| [66] | 554 |
|
|---|
| 555 | # catch database-went-away errors
|
|---|
| 556 | local $dbh->{RaiseError} = 1;
|
|---|
| 557 | eval {
|
|---|
| 558 |
|
|---|
| 559 |
|
|---|
| [32] | 560 | my $sth = $dbh->prepare("SELECT count(*) FROM blocks WHERE parent = ?");
|
|---|
| [2] | 561 | $sth->execute($container);
|
|---|
| 562 | my ($nblocks) = $sth->fetchrow_array();
|
|---|
| 563 |
|
|---|
| [23] | 564 | # need this for a bunch of things, may as well do it here
|
|---|
| 565 | my ($masklen) = ($container =~ m|/(\d+)$|);
|
|---|
| 566 |
|
|---|
| 567 | # Update the bitmask variable with the current block info as needed.
|
|---|
| 568 | # Much faster than retrieving this data later (~3x faster!).
|
|---|
| 569 | my $listme;
|
|---|
| 570 | my $listorg;
|
|---|
| 571 | my $bcount;
|
|---|
| [66] | 572 | my $bexclude;
|
|---|
| [23] | 573 | if ($container ne '0.0.0.0/0') {
|
|---|
| [66] | 574 | $sth = $dbh->prepare("SELECT b.ipcount,b.listme,b.exclude,o.listme ".
|
|---|
| [23] | 575 | "FROM blocks b INNER JOIN orgs o ON b.orgid=o.orgid ".
|
|---|
| 576 | "WHERE b.block = ?");
|
|---|
| 577 | $sth->execute($container);
|
|---|
| [66] | 578 | ($bcount,$listme,$bexclude,$listorg) = $sth->fetchrow_array();
|
|---|
| [77] | 579 | $bcount = 0 if !$bcount;
|
|---|
| [23] | 580 | $bitmask |= $bitfields{$level-1} if $bcount >= $autolist{$masklen};
|
|---|
| [36] | 581 | $bitmask |= $bitfields{"block".($level-1)} if $listme;
|
|---|
| 582 | $bitmask |= $bitfields{"org".($level-1)} if $listorg;
|
|---|
| [23] | 583 | }
|
|---|
| 584 |
|
|---|
| 585 | # hm. can't seem to move this prepare elsewhere. :(
|
|---|
| [2] | 586 | if ($nblocks > 0) {
|
|---|
| [66] | 587 | my $sthsubblocks = $dbh->prepare("SELECT block,exclude FROM blocks ".
|
|---|
| [32] | 588 | "WHERE level = ? AND parent = ?");
|
|---|
| [23] | 589 | $sthsubblocks->execute($level, $container);
|
|---|
| [66] | 590 | while (my ($cidr, $exclude) = $sthsubblocks->fetchrow_array()) {
|
|---|
| 591 | if ($exclude) {
|
|---|
| 592 | $listhosts->{$cidr} = -1;
|
|---|
| 593 | } else { # don't check subtrees of an excluded block; rbldnsd doesn't support deep flip-flopping like that
|
|---|
| 594 | $self->export($listhosts,$mode,$level+1,$cidr,$bitmask)
|
|---|
| 595 | or die $errstr;
|
|---|
| 596 | }
|
|---|
| [2] | 597 | }
|
|---|
| 598 | } # avoid checking content of subs if we don't have any
|
|---|
| 599 |
|
|---|
| 600 | # don't check all 4.2 billion IPs individually if we're looking at all teh Intarwebs
|
|---|
| 601 | return if $container eq '0.0.0.0/0';
|
|---|
| 602 |
|
|---|
| 603 | ##fixme: need a way to dig out orphan IPs at all levels - IPs not found in a
|
|---|
| 604 | # subblock of the current container when the current container *has* subblocks
|
|---|
| 605 | # NB: this may be better handled as an out-of-band data-integrity-checker
|
|---|
| 606 |
|
|---|
| 607 | # decrement level here so the right bitfield setting gets picked. this segment
|
|---|
| 608 | # is inherently off-by-one from the block-recursion loop, and I can't see a
|
|---|
| 609 | # better way to work around that. >:(
|
|---|
| 610 | $level--;
|
|---|
| 611 |
|
|---|
| [4] | 612 | if ($mode eq 'cidr') {
|
|---|
| [23] | 613 | $listhosts->{$container} |= $bitmask if $bitmask && ($listme || $listorg || ($bcount >= $autolist{$masklen}));
|
|---|
| [2] | 614 | } else {
|
|---|
| 615 | # if $cidr->masklen is <= 24, iterate on /24 boundaries for bulk sublisting
|
|---|
| 616 | # if $cidr->masklen is <= 16, iterate on /16 boundaries for bulk sublisting
|
|---|
| 617 | # if $cidr->masklen is <= 8, iterate on /8 boundaries for bulk sublisting
|
|---|
| 618 |
|
|---|
| [23] | 619 | if ($bitmask) {
|
|---|
| [2] | 620 | my @blocksubs;
|
|---|
| 621 | if ($masklen <= 30 && $masklen > 24) {
|
|---|
| 622 | my ($net,$octet) = ($container =~ m|^(\d+\.\d+\.\d+\.)(\d+)/|);
|
|---|
| 623 | for (my $entry = $octet; $entry < ($octet + $howmany[$masklen]); $entry++) {
|
|---|
| 624 | my $host = "$net$entry";
|
|---|
| 625 | $listhosts->{$host} = 0 if !defined($listhosts->{$host});
|
|---|
| [23] | 626 | $listhosts->{$host} |= $bitmask;
|
|---|
| [2] | 627 | }
|
|---|
| 628 | } elsif ($masklen <= 24 && $masklen > 16) {
|
|---|
| 629 | my ($net,$octet) = ($container =~ m|^(\d+\.\d+\.)(\d+)\.\d+/|);
|
|---|
| 630 | for (my $entry = $octet; $entry < ($octet + $howmany[$masklen]); $entry++) {
|
|---|
| 631 | my $twofour = "$net$entry.*";
|
|---|
| [23] | 632 | $listhosts->{$twofour} |= $bitmask;
|
|---|
| [2] | 633 | }
|
|---|
| 634 | } elsif ($masklen <= 16 && $masklen > 8) {
|
|---|
| 635 | my ($net,$octet) = ($container =~ m|^(\d+\.)(\d+)\.\d+\.\d+/|);
|
|---|
| 636 | for (my $entry = $octet; $entry < ($octet + $howmany[$masklen]); $entry++) {
|
|---|
| 637 | my $sixteen = "$net$entry.*";
|
|---|
| [23] | 638 | $listhosts->{$sixteen} |= $bitmask;
|
|---|
| [2] | 639 | }
|
|---|
| 640 | } elsif ($masklen <= 8) {
|
|---|
| 641 | my ($octet) = ($container =~ m|^(\d+)\.\d+\.\d+\.\d+/|);
|
|---|
| 642 | for (my $entry = $octet; $entry < ($octet + $howmany[$masklen]); $entry++) {
|
|---|
| 643 | my $eight = "$entry.*";
|
|---|
| [23] | 644 | $listhosts->{$eight} |= $bitmask;
|
|---|
| [2] | 645 | }
|
|---|
| 646 | }
|
|---|
| 647 |
|
|---|
| 648 | } # generate autolist entries for ips/octets not (yet) seen in reports
|
|---|
| 649 |
|
|---|
| 650 | } # cidr vs classful mode
|
|---|
| 651 |
|
|---|
| [23] | 652 | $sthmoron->execute($container);
|
|---|
| [66] | 653 | while (my ($ip,$moron,$exclude) = $sthmoron->fetchrow_array()) {
|
|---|
| [2] | 654 | if ($moron) {
|
|---|
| 655 | $listhosts->{$ip} = $bitfields{slist};
|
|---|
| [66] | 656 | } elsif ($exclude) {
|
|---|
| 657 | $listhosts->{$ip} = -1;
|
|---|
| [2] | 658 | } else {
|
|---|
| [66] | 659 | $listhosts->{$ip} |= $bitmask;
|
|---|
| [2] | 660 | $listhosts->{$ip} |= $bitfields{ip};
|
|---|
| 661 | }
|
|---|
| 662 | }
|
|---|
| 663 |
|
|---|
| [66] | 664 |
|
|---|
| 665 | }; # db-went-away-catching eval
|
|---|
| 666 | if ($@) {
|
|---|
| 667 | $errstr = $@;
|
|---|
| 668 | warn "export truncated: $errstr\n";
|
|---|
| 669 | return;
|
|---|
| 670 | }
|
|---|
| 671 |
|
|---|
| 672 |
|
|---|
| [2] | 673 | # get IPs which for reasons unknown are apparently allocated directly from the
|
|---|
| 674 | # parent registry (and so do not have containing netblocks in this system) O_o
|
|---|
| 675 | # select * from iplist where not (select count(*) from blocks where ip << block) > 0;
|
|---|
| 676 |
|
|---|
| [66] | 677 | return 1;
|
|---|
| [2] | 678 | } # end export()
|
|---|
| 679 |
|
|---|
| 680 |
|
|---|
| 681 | ## DNSBL::autolist_block()
|
|---|
| 682 | # check if a block should be autolisted
|
|---|
| 683 | sub autolist_block {
|
|---|
| 684 | my $self = shift;
|
|---|
| 685 | my $block = shift;
|
|---|
| 686 |
|
|---|
| 687 | my $cidr = new NetAddr::IP "$block";
|
|---|
| [32] | 688 | my $sth = $dbh->prepare("SELECT ipcount FROM blocks WHERE block = ?");
|
|---|
| [2] | 689 | $sth->execute("$cidr");
|
|---|
| 690 | my ($count) = $sth->fetchrow_array;
|
|---|
| 691 |
|
|---|
| 692 | return 1 if $count >= $autolist{$cidr->masklen};
|
|---|
| 693 | return 0;
|
|---|
| 694 | } # end autolist_block()
|
|---|
| 695 |
|
|---|
| 696 |
|
|---|
| 697 | # make Perl happy
|
|---|
| 698 | 1;
|
|---|