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