Allow including additional ignore files.
[people/stevee/guardian.git] / modules / Events.pm
1 package Guardian::Events;
2 use strict;
3 use warnings;
4
5 use Exporter qw(import);
6
7 our @EXPORT_OK = qw(Init CheckAction GenerateIgnoreList Update);
8
9 # Hash which stores all supported commands from the queue.
10 my %commands = (
11         'count' => \&Counter,
12         'block' => \&CallBlock,
13         'unblock' => \&CallUnblock,
14         'flush' => \&CallFlush,
15         'reload' => \&main::Reload,
16 );
17
18 # Hash to store addresses and their current count.
19 my %counthash = ();
20
21 # Hash to store all currentyl blocked addresses and a timestamp
22 # when the block for this address can be released.
23 my %blockhash = ();
24
25 # Hash to store user-defined IP addresses and/or subnets which should be
26 # ignored in case any events should be repored for them. 
27 my %ignorehash = ();
28
29 # Array to store localhost related IP addresses.
30 # They are always white-listed to prevent guardian from blocking
31 # any local traffic.
32 my @localhost_addresses = ("127.0.0.1", "::1");
33
34 # This object will contain the reference to the logger object after calling Init.
35 my $logger;
36
37 #
38 ## The "Init" (Block) function.
39 #
40 ## This function is responsible to initialize Block as a class based object.
41 ## It has to be called once before any blocking event can be processed.
42 #
43 ## The following arguments must be passed, during initialization:
44 ## "BlockCount" and "BlockTime" which both have to be natural numbers.
45 #
46 sub Init (%) {
47         my ( $class, %args ) = @_;
48         my $self = \%args;
49
50         # Fail, if some critical arguments are missing.
51         unless ((exists($self->{BlockCount})) && (exists($self->{BlockTime}))) {
52                 die "Could not initialize Block: Too less arguments are given.\n";
53         }
54
55         # Use bless to make "$self" to an object of class "$class".
56         bless($self, $class);
57
58         # Assign logger object.
59         $logger = $self->{Logger};
60
61         # Log used firewall engine.
62         $logger->Log("debug", "Using firewall engine: $self->{FirewallEngine}");
63
64         # Try to load the specified firewall engine or die.
65         my $module_name = "Guardian::" . $self->{FirewallEngine};
66         eval "use $module_name; 1" or die "Could not load a module for firewall engine: $self->{FirewallEngine}!";
67
68         # Check if an IgnoreFile has been configured.
69         if (exists($self->{IgnoreFile})) {
70                 # Call function to handle the ignore mechanism.
71                 &GenerateIgnoreList($self->{IgnoreFile});
72         } else {
73                 # Whitelist local addresses.
74                 %ignorehash = &_whitelist_localhost();
75         }
76
77         # Return the class object.
78         return $self;
79 }
80
81 #
82 ## The main "CheckAction" function.
83 #
84 ## This function is used to handle each recived event from the main event queue of guardian.
85 #
86 ## It will check if the given command is valid and will pass it to the responsible function.
87 #
88 sub CheckAction ($$) {
89         my $self = shift;
90         my @event = split(/ /, $_[0], 4);
91         my ($command, $address, $module, $message) = @event;
92
93         # Check if we got an invalid command.
94         unless(exists($commands{$command})) {
95                 $logger->Log("err", "The CheckAction function has been called with an unsupported command ($command)!");
96                 return;
97         }
98
99         # Check if the given event contains an address.
100         if ($address) {
101                 # Convert and validate the given address.
102                 my $bin_address = &Guardian::Base::IPOrNet2Int($address);
103
104                 # Abort if the given address could not be converted because it is not valid.
105                 unless ($bin_address) {
106                         $logger->Log("err", "Invalid IP address: $address");
107                         return;
108                 }
109
110                 # Check if address should be ignored.
111                 if(&_IsOnIgnoreList($bin_address)) {
112                         # Log message.
113                         $logger->Log("info", "Ignoring event for $address, because it is part of the ignore list.");
114                         return;
115                 }
116         }
117
118         # Call required handler.
119         my $error = $commands{$command}->($self, $address, $module, $message);
120
121         # If we got any return code, something went wrong and should be logged.
122         if ($error) {
123                 $logger->Log("err", "Got error: $error");
124                 return;
125         }
126 }
127
128 #
129 ## The main "Counter" function.
130 #
131 ## This function is used to handle each count message + address, which has been sent by the main event
132 ## loop of guardian.
133 #
134 ## It stores the address and the current count into the counthash and increase the count each time when
135 ## the same address should be counted again. When the current count reaches the configured BlockCount,
136 ## the responsible function will be called to block the address.
137 #
138 sub Counter ($@) {
139         my $self = shift;
140         my ($address, $module, $message) = @_;
141
142         # Log event.
143         $logger->Log("debug", "$module reported $message for address: $address");
144
145         # Increase existing count or start counting for new source addresses.
146         if (exists($counthash{$address})) {
147                 # Skip already blocked addresses.
148                 if (exists($blockhash{$address})) {
149                         return undef;
150                 }
151
152                 # Increase count of the existing entry.
153                 $counthash{$address} = $counthash{$address} + 1;
154
155                 # Log altered count of the address.
156                 $logger->Log("debug", "Source $address now has count $counthash{$address}/$self->{BlockCount}...");
157         } else {
158                 # Log that counting for the address has been started.
159                 $logger->Log("debug", "Start counting for $address...");
160
161                 # Set count to "1".
162                 $counthash{$address} = 1;
163         }
164
165         # Check if the address has reached the configured count limit.
166         if ($counthash{$address} >= $self->{BlockCount}) {
167                 # Write out log message.
168                 $logger->Log("info", "Blocking $address for $self->{BlockTime} seconds...");
169
170                 # Call block subroutine to block the address.
171                 my $error = &CallBlock($self, $address, $module, $message);
172
173                 # Return the message if an error occurs.
174                 return $error;
175         }
176
177         # Everything worked well, return nothing.
178         return undef;
179 }
180
181 #
182 ## The RemoveBlocks function.
183 #
184 ## This function periodly will be called and is responsible for releasing the block if the Blocktime
185 ## on an address has expired.
186 #
187 ## To do this, the code will loop through all entries of the blockhash and check
188 ## if the estimiated BlockTime of each address has reached and so the block can be released.
189 #
190 sub RemoveBlocks () {
191         my $self = shift;
192
193         # Get the current time.
194         my $time = time();
195
196         # Loop through the blockhash.
197         foreach my $address (keys %blockhash) {
198                 # Check if the blocktime for the address has expired.
199                 if ($blockhash{$address} <= $time) {
200                         # Call unblock subroutine.
201                         my $error = &CallUnblock($self, $address, "BlockTime", "has expired for $address");
202
203                         # Log error messages if returned.
204                         if ($error) {
205                                 $logger->Log("err", "$error");
206                         }
207                 }
208         }
209
210         # Everything okay, return nothing.
211         return undef;
212 }
213
214 #
215 ## The CallBlock function.
216 #
217 ## This function is called, if the BlockCount for an address is reached or a direct
218 ## request for blocking an address has been recieved.
219 #
220 sub CallBlock ($@) {
221         my $self = shift;
222         my ($address, $module, $message) = @_;
223
224         # Log the call for blocking an address.
225         $logger->Log("info", "$module - $message");
226
227         # Check if an entry for this address exists
228         # in the blockhash. If not, the address has
229         # not been blocked yet, call the responisible
230         # function to do this now.
231         unless (exists($blockhash{$address})) {
232                 # Obtain the configured FirewallAction.
233                 my $action = $self->{FirewallAction};
234
235                 # Block the given address.
236                 my $error = &DoBlock($address, $action);
237
238                 # If we got back an error message something went wrong.
239                 if ($error) {
240                         # Exit function and return the used FirewallEngine and the error message.
241                         return "$self->{FirewallEngine} - $error";
242                 } else {
243                         # Address has been successfully blocked, print a log message.
244                         $logger->Log("debug", "Address $address successfully has been blocked...");
245                 }
246         }
247
248         # Generate time when the block will expire.
249         my $expire = time() + $self->{BlockTime};
250
251         # Store the blocked address and the expire time
252         # in the blockhash.
253         $blockhash{$address} = $expire;
254
255         # Return nothing "undef" if everything is okay.
256         return undef;
257 }
258
259 #
260 ## CallUnblock function.
261 #
262 ## This function is responsible for unblocking and deleting a given
263 ## address from the blockhash.
264 #
265 sub CallUnblock ($) {
266         my $self = shift;
267         my ($address, $module, $message) = @_;
268
269         # Log the call for unblocking an address.
270         $logger->Log("info", "$module - $message");
271
272         # Return an error if no entry for the given address
273         # is present in the blockhash.
274         unless (exists($blockhash{$address})) {
275                 return "Address $address was not blocked!";
276         }
277
278         # Unblock the address.
279         my $error = &DoUnblock($address);
280
281         # If an error message is returned, something went wrong.
282         if ($error) {
283                 # Exit function and return the error message.
284                 return $error;
285         } else {
286                 # Address successfully has been unblocked.
287                 $logger->Log("debug", "Address $address successfully has been unblocked...");
288         }
289
290         # Drop address from blockhash.
291         delete ($blockhash{$address});
292
293         # Everything worked well, return nothing.
294         return undef;
295 }
296
297 #
298 ## GenerateIgnoreList function.
299 #
300 ## This function is responsible for generating/updating the
301 ## IgnoreHash which contains all ignored IP addresses and
302 ## networks.
303 #
304 sub GenerateIgnoreList($) {
305         my $file = shift;
306         my @include_files;
307
308         # Reset current ignore hash and add
309         # localhost related IP addresses.
310         %ignorehash = &_whitelist_localhost();
311
312         # Check if the given IgnoreFile could be opened.
313         unless(-e $file) {
314                 $logger->Log("err", "The configured IgnoreFile \($file\) could not be opened. Skipped!");
315                 return;
316         }
317
318         # Open the given IgnoreFile.
319         open (IGNORE, $file);
320
321         # Read-in the file line by line.
322         while (<IGNORE>) {
323                 # Skip comments.
324                 next if (/\#/);
325
326                 # Skip blank lines.
327                 next if (/^\s*$/);
328
329                 # Remove any newlines.
330                 chomp;
331
332                 # Check for an include instruction.
333                 if ($_ =~ /^Include_File = (.*)/) {
334                         my $include_file = $1;
335
336                         # Check if the parsed include file exists and is read-able.
337                         if (-e $include_file) {
338                                 # Add file to the array of files wich will be included.
339                                 push(@include_files, $include_file);
340
341                                 # Write out log message.
342                                 $logger->Log("debug", "Addresses from $include_file will be included...");
343                         } else {
344                                 # Log missing file.
345                                 $logger->Log("err", "$include_file will not be included. File does not exist!");
346                         }
347                 } else {
348                         # Check if the line contains a valid single address or network and
349                         # convert it into binary format. Store the result/start and
350                         # end values in a temporary array.
351                         my @values = &Guardian::Base::IPOrNet2Int($_);
352
353                         # If the function returned any values, the line contained a valid
354                         # single address or network which successfully has been converted into
355                         # binary format.
356                         if (@values) {
357                                 # Assign the array as value to the ignorehash.
358                                 $ignorehash{$_} = [@values];
359                         } else {
360                                 # Log invalid entry.
361                                 $logger->Log("err", "IgnoreFile contains an invalid address/network: $_");
362
363                                 # Skip line.
364                                 next;
365                         }
366                 }
367         }
368
369         # Close filehandle for the IgnoreFile.
370         close (IGNORE);
371
372         # Check if any files should be included.
373         if (@include_files) {
374                 # Loop through the array of files which should be included.
375                 foreach my $file (@include_files) {
376                         # Open the file.
377                         open(INCLUDE, $file);
378
379                         # Read-in file line by line.
380                         while(<INCLUDE>) {
381                                 # Skip any comments.
382                                 next if (/\#/);
383
384                                 # Skip any blank lines.
385                                 next if (/^\s*$/);
386
387                                 # Chomp any newlines.
388                                 chomp;
389
390                                 # Check if the line contains a valid single address or network and
391                                 # convert it into binary format. Store the result/start and
392                                 # end values in a temporary array.
393                                 my @values = &Guardian::Base::IPOrNet2Int($_);
394
395                                 # If the function returned any values, the line contained a valid
396                                 # single address or network which successfully has been converted into
397                                 # binary format.
398                                 if (@values) {
399                                         # Assign the array as value to the ignorehash.
400                                         $ignorehash{$_} = [@values];
401                                 } else {
402                                         # Log invalid entry.
403                                         $logger->Log("err", "$file contains an invalid address/network: $_");
404
405                                         # Skip line.
406                                         next;
407                                 }
408                         }
409
410                         # Close filehandle.
411                         close(INCLUDE);
412                 }
413         }
414
415         # Get amount of current elements in hash.
416         my $amount = scalar(keys(%ignorehash));
417
418         # Write out log message.
419         $logger->Log("debug", "Ignore list currently contains $amount entries.");
420 }
421
422 #
423 ## Private function to check if an address is part of the Ignore Hash.
424 #
425 ## This function checks if a passed IP address in binary format (!),
426 ## directly or as part of an ignored network is stored in the ignore hash.
427 #
428 sub _IsOnIgnoreList ($) {
429         my $bin_address = shift;
430
431         # Loop through the ignore hash and grab the stored values.
432         foreach my $key ( keys %ignorehash ) {
433                 # Dereference values array.
434                 my @values = @{$ignorehash{$key}};
435
436                 # Obtain amount of items for the current value array.
437                 my $items = scalar @values;
438
439                 # Whether the amount equals one, the given binary address just
440                 # needs to be compared against a single address.
441                 if ($items eq "1") {
442                         my ($ignored_address) = @values;
443
444                         # Simple check if the stored and the given binary address
445                         # are the same.
446                         if ($bin_address eq $ignored_address) {
447                                 # The given address is part of the ignore list.
448                                 $logger->Log("debug", "Address $key found on the ignore list.");
449
450                                 # Return "1" (True).
451                                 return 1;
452                         }
453                 }
454
455                 # If the amount equals two, for passed binary address needs to
456                 # be checked if it is part of the ignored network range.
457                 elsif ($items eq "2") {
458                         my ($first_address, $last_address) = @values;
459
460                         # Check if the passed binary address is bigger than
461                         # the first address and smaler than the last address
462                         # (between) the stored network range.
463                         if (($bin_address >= $first_address) && ($bin_address <= $last_address)) {
464                                 # The address is part of an ignored network.
465                                 $logger->Log("debug", "Address is found inside the ignored network $key.");
466
467                                 # Return "1" (True).
468                                 return 1;
469                         }
470
471                 # If the amount is not eighter one or two, the current entry of the ignorehash seems
472                 # to be corrupted. Skip and log it.
473                 } else {
474                         # Write log message about this corruped item in the ignore hash.
475                         $logger->Log("err", "Invalid item in the Ignore Hash: $key - @values");
476
477                         # Skip this element of the ignore hash.
478                         next;
479                 }
480         }
481
482         # If we got here, the given address is not part of the ignore hash.
483         # Return nothing (False).
484         return;
485 }
486
487 #
488 ## The _whitelist_localhost function.
489 #
490 ## This tiny private function simple generates and returns a hash which contains
491 ## the clear and binary converted addresses for all array-stored
492 ## (@localhost_addresses) in an ignorelist compatible format.
493 #
494 sub _whitelist_localhost () {
495         my %temphash;
496
497         # Loop through the array of localhost related addresses.
498         foreach my $address (@localhost_addresses) {
499                 # Validate and convert the addresss into binary format.
500                 my @values = &Guardian::Base::IPOrNet2Int($address);
501
502                 # Check if any values are returned.
503                 if (@values) {
504                         # Store the converted binary values in the temporary hash.
505                         $temphash{$address} = [@values];
506                 }
507         }
508
509         # Return the temporary hash.
510         return %temphash;
511 }
512
513 1;