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