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