source: trunk/dnsbl/DNSBL.pm@ 29

Last change on this file since 29 was 29, checked in by Kris Deugau, 14 years ago

/trunk/dnsbl

Add islisted()
Add waslisted table in SQL def
Indicate block status on adding an IP to an existing block
Fix for scripts-not-at-webroot
Tweak for scrolling "blocks in this registrar block" list
Load more config from DB on export

File size: 14.0 KB
Line 
1# DNSBL
2# Functions for interacting with the DNSBL database
3
4package DNSBL;
5
6use strict;
7use warnings;
8use Exporter;
9use DBI;
10use NetAddr::IP;
11
12use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
13
14$VERSION = 2.0;
15@ISA = qw(Exporter);
16@EXPORT_OK = qw(
17 );
18
19@EXPORT = (); # Export nothing by default.
20%EXPORT_TAGS = ( ALL => [qw(
21 )]
22 );
23
24## "constants"
25
26# 8 bits available
27# 128 is per-IP shitlist
28# 2 is IP hitlist
29# 1 not available so we don't $self->shoot(foot)
30our %bitfields = (
31 # block levels
32 0 => 16,
33 1 => 8,
34 2 => 4,
35 # ip
36 ip => 2,
37 # OOB
38 org => 32,
39 block => 64,
40 # "I'm a total spamming moron!" - per-IP only!
41 slist => 128
42);
43
44# probably needs some tuning; even 7 hits in a /24 is a pretty small percentage
45# number of IPs in a block of the given masklength needed to have that block automatically listed
46# defaults: (overridden by entries in db:autolist)
47our %autolist = (
48 31 => 1,
49 30 => 1,
50 29 => 2,
51 28 => 3,
52 27 => 4,
53 26 => 5,
54 25 => 6,
55 24 => 7,
56 23 => 8,
57 22 => 10,
58 21 => 13,
59 20 => 16,
60 19 => 19,
61 18 => 22,
62 17 => 26,
63 16 => 30,
64 15 => 34,
65 14 => 38,
66 13 => 42,
67 12 => 46,
68 11 => 50,
69 10 => 54,
70 9 => 58,
71 8 => 62,
72 7 => 2**31,
73 6 => 2**31,
74 5 => 2**31,
75 4 => 2**31,
76 3 => 2**31,
77 2 => 2**31,
78 1 => 2**31,
79 0 => 2**31
80);
81
82# le sigh. constants for masklength iterationing
83our @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);
84
85# variables
86our $dbh;
87
88our $err;
89our $errstr;
90
91# basic object subs
92sub new {
93# iff we want to start taking arguments, or doing other things on instantiation
94# my $self = {};
95# bless $self, "DNSBL";
96# return $self;
97 bless {};
98}
99
100sub DESTROY {
101 my $self = shift;
102 $self->dbclose() if $dbh;
103}
104
105# JIC someone wants to close the db but not finish the script
106sub dbclose {
107 $dbh->rollback;
108 $dbh->disconnect;
109}
110
111## specific object subs:
112
113sub connect {
114 my $self = shift;
115 my $dbhost = shift;
116 my $dbname = shift;
117 my $dbuser = shift;
118 my $dbpass = shift;
119 ## want to NOT autocommit everything, it's unlikely we'll step on our own toes but...
120 $dbh = DBI->connect("DBI:Pg:host=$dbhost;dbname=$dbname", $dbuser, $dbpass, {
121 AutoCommit => 0,
122 PrintError => 1
123 })
124 or die "database inaccessible: ".$DBI::errstr;
125 my $sth = $dbh->prepare("SELECT masklen,ipcount FROM autolist");
126 $sth->execute;
127 while (my ($masklen,$ipcount) = $sth->fetchrow_array) {
128 $autolist{$masklen} = $ipcount;
129 }
130 return $dbh;
131}
132
133
134## DNSBLDB::initexport()
135# Prepare a couple of statement handles for later processing in export(). Assists in ~3x speed increase.
136my $parsth;
137my $sthmoron;
138sub initexport {
139 $parsth = $dbh->prepare("SELECT count(i.ip),b.block,b.level,b.listme AS oobblock,o.listme AS ooborg ".
140 "FROM iplist i INNER JOIN blocks b ON i.ip << b.block INNER JOIN orgs o ON b.orgid = o.orgid ".
141 "WHERE b.block >>= ? ".
142 "GROUP BY b.block,b.level,b.listme,o.listme ORDER BY b.block");
143 $sthmoron = $dbh->prepare("SELECT ip,s4list FROM iplist WHERE ip << ? ORDER BY ip");
144}
145
146
147## DNSBL::ipexists()
148# return report count if the IP has been reported, otherwise return undef
149sub ipexists {
150 my $self = shift;
151 my $ip = shift;
152 my $sth = $dbh->prepare("SELECT count FROM iplist WHERE ip=?");
153 $sth->execute($ip);
154 my ($ret) = $sth->fetchrow_array();
155 return $ret;
156} # end ipexists()
157
158
159# report an IP or URI to the db
160# increments a hit counter iff the reported IP or URI exists, otherwise it adds it
161sub report {
162 my $self = shift;
163 my $rep = shift;
164 my $sth;
165 my $rows = 0;
166 if ($rep =~ /^[\d.]+$/) {
167 # weesa gonna ASS-U-ME IP addresses are sanely formatted.
168 $sth = $dbh->prepare("SELECT count FROM iplist WHERE ip=?");
169 $sth->execute($rep) or die "eep? ".$dbh->errstr."\n";
170 $rows = $sth->rows;
171 if ($rows == 0) {
172 $sth = $dbh->prepare("INSERT INTO iplist (ip) VALUES (?)");
173 } elsif ($rows == 1) {
174 $sth = $dbh->prepare("UPDATE iplist SET count=count+1 WHERE ip=?");
175 } else {
176 die "db corrupt: found $rows matches on $rep\n";
177 }
178 $sth->execute($rep) or die "couldn't update listing for $rep: ".$dbh->errstr."\n";
179 } else {
180 return;
181 }
182 $dbh->commit;
183 return $rows;
184} # end report()
185
186
187# add a new org
188# return the orgid
189# if the org exists, return the orgid anyway
190sub addorg {
191 my $self = shift;
192 my $orgname = shift;
193 my $listme = shift || 'n';
194 my $ret = $self->orgexists($orgname);
195 return $ret if $ret;
196 my $sth = $dbh->prepare("INSERT INTO orgs (orgname,listme) VALUES (?,?)");
197 $sth->execute($orgname,$listme) or die "couldn't add org $orgname: ".$dbh->errstr."\n";
198 $dbh->commit;
199 $sth = $dbh->prepare("SELECT orgid FROM orgs WHERE orgname=?");
200 $sth->execute($orgname);
201 my ($orgid) = $sth->fetchrow_array();
202 return $orgid;
203} # end addorg
204
205
206# checks for existence - nb, exact match! No way to really handle anything else. :/
207sub orgexists {
208 my $self = shift;
209 my $org = shift;
210 my $sth = $dbh->prepare("SELECT orgid FROM orgs WHERE orgname=?");
211 $sth->execute($org);
212 my ($ret) = $sth->fetchrow_array();
213 return $ret;
214} # end orgexists();
215
216
217# add a block. requires the orgid
218##fixme needs error handling
219sub addblock {
220 my $self = shift;
221 my $blockin = shift;
222 my $orgid = shift;
223 my $level = shift;
224 $blockin =~ s/^\s+//;
225 $blockin =~ s/\s+$//;
226 my $block = new NetAddr::IP "$blockin"; # need this to clean up messes like ranges. sigh.
227
228 return "$blockin not a single CIDR range" if !$block;
229
230 local $dbh->{AutoCommit} = 1; # force autocommit
231
232 my $sth = $dbh->prepare("INSERT INTO blocks (block,orgid,level) VALUES (?,?,?)");
233 $sth->execute("$block",$orgid,$level);
234 return $sth->errstr if $sth->err;
235 # nb: no need to return anything, since the CIDR block is the key
236}
237
238
239sub blockexists {
240 my $self = shift;
241 my $block = shift;
242 my $sth = $dbh->prepare("SELECT count(*) FROM blocks WHERE block=?");
243 $sth->execute($block);
244 my ($ret) = $sth->fetchrow_array();
245 return $ret;
246}
247
248
249# returns list (block,orgname) for the block that contains the passed IP.
250# accepts a level argument if you don't want the top-level registrar allocation block
251sub getcontainer {
252 my $self = shift;
253 my $ip = shift;
254 my $level = shift || 0;
255 my $sth = $dbh->prepare("SELECT b.block,o.orgname FROM blocks b INNER JOIN orgs o ".
256 "ON b.orgid=o.orgid WHERE b.block >> ? AND b.level = ?");
257 $sth->execute($ip,$level);
258 return $sth->fetchrow_array();
259} # end getcontainer()
260
261
262# Get info about whether a block, IP or org is listed
263# Returns ?
264sub islisted {
265 my $self = shift;
266 my $entity = shift;
267
268 my $sth;
269
270 if ($entity =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) {
271 # looking for IP
272
273 $sth = $dbh->prepare("SELECT ip,s4list FROM iplist WHERE ip=?");
274 $sth->execute($entity);
275 my @ret = $sth->fetchrow_array;
276 return @ret if @ret;
277
278 } elsif ($entity =~ m|^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/(\d+)$|) {
279 # block
280
281 my $masklen = $1;
282
283 $sth = $dbh->prepare("SELECT block,listme FROM blocks WHERE block=?");
284 $sth->execute($entity);
285 my ($block,$listme) = $sth->fetchrow_array;
286
287 return if !$block;
288
289 $sth = $dbh->prepare("SELECT count(*) FROM iplist WHERE ip << ?");
290 $sth->execute($entity);
291 my ($bcount) = $sth->fetchrow_array;
292 my @ret = ( ($bcount >= $autolist{$masklen}), $listme);
293 return @ret;
294
295 } else {
296 # org
297
298 $sth = $dbh->prepare("SELECT orgid,listme FROM orgs WHERE orgname=?");
299 $sth->execute($entity);
300 my ($orgid,$listme) = $sth->fetchrow_array;
301 return $listme if $orgid;
302
303 }
304
305 return undef;
306
307} # end islisted()
308
309
310# whee! Recursion is Fun!
311# Call ourself to dig down through the layers of blocks from registar-allocation
312# (level 0) to final block (level n, not to exceed 2)
313# Take a reference to a hash, and stuff it full of blacklisting goodness.
314# Optionally accept a level, block-container, and OOB block and org arguments for
315# the container to check and return
316# Returns no value directly
317# Calls itself to walk down the tree of containers
318sub export {
319 my $self = shift;
320 my $listhosts = shift;
321
322# Export data as CIDR netblocks or classful (A/B/C) blocks
323# Assume classful as it's more compatible with different DNS servers
324 my $mode = shift || 'class';
325
326# Assume we're checking the whole enchilada if we don't get told where to look.
327 my $level = shift || 0;
328 my $container = shift || '0.0.0.0/0';
329 my $bitmask = shift || 0;
330
331 if ($level > 3) {
332 warn "getting too deep, breaking off! ($container, $level)\n";
333 return;
334 }
335
336# fiddle $container into a sane state.
337 if ($container =~ m|^\d+\.\d+\.\d+/\d+$|) {
338 $container =~ s|/(\d+)$|.0/$1|;
339 } elsif ($container =~ m|^\d+\.\d+/\d+$|) {
340 $container =~ s|/(\d+)$|.0.0/$1|;
341 } elsif ($container =~ m|^\d+/(\d+)$|) {
342 $container =~ s|/(\d+)$|.0.0.0/$1|;
343 }
344
345 my $sth = $dbh->prepare("SELECT count(*) FROM blocks WHERE block << ?");
346 $sth->execute($container);
347 my ($nblocks) = $sth->fetchrow_array();
348
349 # need this for a bunch of things, may as well do it here
350 my ($masklen) = ($container =~ m|/(\d+)$|);
351
352# Update the bitmask variable with the current block info as needed.
353# Much faster than retrieving this data later (~3x faster!).
354 my $listme;
355 my $listorg;
356 my $bcount;
357 if ($container ne '0.0.0.0/0') {
358 $sth = $dbh->prepare("SELECT count(*) FROM iplist WHERE ip << ?");
359 $sth->execute($container);
360 ($bcount) = $sth->fetchrow_array();
361
362 $sth = $dbh->prepare("SELECT b.listme,o.listme ".
363 "FROM blocks b INNER JOIN orgs o ON b.orgid=o.orgid ".
364 "WHERE b.block = ?");
365 $sth->execute($container);
366 ($listme,$listorg) = $sth->fetchrow_array();
367
368 $bitmask |= $bitfields{$level-1} if $bcount >= $autolist{$masklen};
369 $bitmask |= $bitfields{block} if $listme;
370 $bitmask |= $bitfields{org} if $listorg;
371 }
372
373# hm. can't seem to move this prepare elsewhere. :(
374 if ($nblocks > 0) {
375 my $sthsubblocks = $dbh->prepare("SELECT block FROM blocks ".
376 "WHERE level=? and block << ? ORDER BY block, masklen(block) DESC");
377 $sthsubblocks->execute($level, $container);
378 while (my ($cidr) = $sthsubblocks->fetchrow_array()) {
379 $self->export($listhosts,$mode,$level+1,$cidr,$bitmask);
380 }
381 } # avoid checking content of subs if we don't have any
382
383 # don't check all 4.2 billion IPs individually if we're looking at all teh Intarwebs
384 return if $container eq '0.0.0.0/0';
385
386##fixme: need a way to dig out orphan IPs at all levels - IPs not found in a
387# subblock of the current container when the current container *has* subblocks
388# NB: this may be better handled as an out-of-band data-integrity-checker
389
390 # decrement level here so the right bitfield setting gets picked. this segment
391 # is inherently off-by-one from the block-recursion loop, and I can't see a
392 # better way to work around that. >:(
393 $level--;
394
395 if ($mode eq 'cidr') {
396 $listhosts->{$container} |= $bitmask if $bitmask && ($listme || $listorg || ($bcount >= $autolist{$masklen}));
397 } else {
398 # if $cidr->masklen is <= 24, iterate on /24 boundaries for bulk sublisting
399 # if $cidr->masklen is <= 16, iterate on /16 boundaries for bulk sublisting
400 # if $cidr->masklen is <= 8, iterate on /8 boundaries for bulk sublisting
401
402 if ($bitmask) {
403 my @blocksubs;
404 if ($masklen <= 30 && $masklen > 24) {
405 my ($net,$octet) = ($container =~ m|^(\d+\.\d+\.\d+\.)(\d+)/|);
406 for (my $entry = $octet; $entry < ($octet + $howmany[$masklen]); $entry++) {
407 my $host = "$net$entry";
408 $listhosts->{$host} = 0 if !defined($listhosts->{$host});
409 $listhosts->{$host} |= $bitmask;
410 }
411 } elsif ($masklen <= 24 && $masklen > 16) {
412 my ($net,$octet) = ($container =~ m|^(\d+\.\d+\.)(\d+)\.\d+/|);
413 for (my $entry = $octet; $entry < ($octet + $howmany[$masklen]); $entry++) {
414 my $twofour = "$net$entry.*";
415 $listhosts->{$twofour} |= $bitmask;
416 }
417 } elsif ($masklen <= 16 && $masklen > 8) {
418 my ($net,$octet) = ($container =~ m|^(\d+\.)(\d+)\.\d+\.\d+/|);
419 for (my $entry = $octet; $entry < ($octet + $howmany[$masklen]); $entry++) {
420 my $sixteen = "$net$entry.*";
421 $listhosts->{$sixteen} |= $bitmask;
422 }
423 } elsif ($masklen <= 8) {
424 my ($octet) = ($container =~ m|^(\d+)\.\d+\.\d+\.\d+/|);
425 for (my $entry = $octet; $entry < ($octet + $howmany[$masklen]); $entry++) {
426 my $eight = "$entry.*";
427 $listhosts->{$eight} |= $bitmask;
428 }
429 }
430
431 } # generate autolist entries for ips/octets not (yet) seen in reports
432
433 } # cidr vs classful mode
434
435 $sthmoron->execute($container);
436 while (my ($ip,$moron) = $sthmoron->fetchrow_array()) {
437 $listhosts->{$ip} |= $bitmask;
438 if ($moron) {
439 $listhosts->{$ip} = $bitfields{slist};
440 } else {
441 $listhosts->{$ip} |= $bitfields{ip};
442 }
443 }
444
445# get IPs which for reasons unknown are apparently allocated directly from the
446# parent registry (and so do not have containing netblocks in this system) O_o
447# select * from iplist where not (select count(*) from blocks where ip << block) > 0;
448
449 return;
450} # end export()
451
452
453sub export_alt {
454 my $self = shift;
455 my $listhosts = shift;
456 my $level = shift || 0;
457 my $container = shift || '0.0.0.0/0';
458 my $oobblock = shift || 0;
459 my $ooborg = shift || 0;
460
461#print "\nDEBUG: called with $level, $container, $oobblock, $ooborg\n";
462# if $level > 2 or $container =~ /^64\.76\./;
463# my %listhosts;
464
465# $level = 0 if !$level;
466 if ($level > 3) {
467 warn "getting too deep, breaking off!\n";
468 return;
469 }
470
471 my $sth = $dbh->prepare("select ip,s4list from iplist order by ip");
472 my $bsth = $dbh->prepare("select b.block,b.listme,b.level,o.listme ".
473 "from blocks b inner join orgs o on b.orgid=o.orgid ".
474 "where b.block >> ?");
475 while (my ($ip,$s4list) = $sth->fetchrow_array) {
476 $bsth->execute($ip);
477 while (my ($block,$blisted,$blevel,$olisted) = $bsth->fetchrow_array) {
478 $listhosts->{$ip} |= 0;
479 }
480 }
481
482} # end export_alt()
483
484
485## DNSBL::autolist_block()
486# check if a block should be autolisted
487sub autolist_block {
488 my $self = shift;
489 my $block = shift;
490
491 my $cidr = new NetAddr::IP "$block";
492 my $sth = $dbh->prepare("select count(*) from iplist where ip << ?");
493 $sth->execute("$cidr");
494 my ($count) = $sth->fetchrow_array;
495
496 return 1 if $count >= $autolist{$cidr->masklen};
497 return 0;
498} # end autolist_block()
499
500
501# make Perl happy
5021;
Note: See TracBrowser for help on using the repository browser.