File Coverage

lib/Net/ISP/Balance.pm
Criterion Covered Total %
statement 486 729 66.6
branch 114 260 43.8
condition 34 79 43.0
subroutine 73 104 70.1
pod 60 70 85.7
total 767 1242 61.7


line stmt bran cond sub pod time code
1             package Net::ISP::Balance;
2              
3 1     1   877 use strict;
  1         2  
  1         27  
4 1     1   4 use Fcntl ':flock';
  1         2  
  1         118  
5 1     1   7 use Carp 'croak','carp';
  1         2  
  1         43  
6 1     1   539 use Data::Dumper;
  1         6082  
  1         56  
7 1     1   6 no warnings;
  1         2  
  1         53  
8              
9 1     1   508 eval 'use Net::Netmask';
  1         16110  
  1         100  
10 1     1   768 eval 'use Net::ISP::Balance::ConfigData';
  1         7  
  1         27  
11              
12             our $VERSION = '1.28';
13              
14             =head1 NAME
15              
16             Net::ISP::Balance - Support load balancing across multiple internet service providers
17              
18             =head1 SYNOPSIS
19              
20             use Net::ISP::Balance;
21              
22             # initialize the module with its configuration file
23             my $bal = Net::ISP::Balance->new('/etc/network/balance.conf');
24              
25             $bal->verbose(1); # verbosely print routing and firewall
26             # commands to STDERR before running them.
27             $bal->echo_only(1); # echo commands to STDOUT; don't execute them.
28              
29             # mark the balanced services that are up
30             $bal->up('CABLE','DSL','SATELLITE');
31              
32             # write out routing and firewall commands
33             $bal->set_routes_and_firewall();
34              
35             # write out a forwarding rule
36             $bal->forward(80 => '192.168.10.35'); # forward web requests to this host
37              
38             # write out an arbitrary routing rule
39             $bal->ip_route('add 192.168.100.1 dev eth0 src 198.162.1.14');
40              
41             # write out an arbitrary iptables rule
42             $bal->iptables('-A INCOMING -p tcp --dport 6000 -j REJECT');
43              
44             # get information about all services
45             my @s = $bal->service_names;
46             for my $s (@s) {
47             print $bal->dev($s);
48             print $bal->ip($s);
49             print $bal->gw($s);
50             print $bal->net($s);
51             print $bal->fwmark($s);
52             print $bal->table($s);
53             print $bal->running($s);
54             print $bal->weight($s);
55             }
56              
57             =cut
58              
59 1     1   5 use Carp;
  1         1  
  1         8665  
60              
61             =head1 USAGE
62              
63             This library supports load_balance.pl, a script to load-balance a home
64             network across two or more Internet Service Providers (ISP). The
65             load_balance.pl script can be found in the bin subdirectory of this
66             distribution. Installation and configuration instructions can be found
67             at http://lstein.github.io/Net-ISP-Balance/.
68              
69             =head1 CONFIGURATION FILE
70              
71             This module reads a configuration file with the following format:
72              
73             #service device role ping-ip weight gateway
74             CABLE eth0 isp 173.194.43.95 1 173.193.43.1
75             DSL ppp0 isp 173.194.43.95 1
76             LAN1 eth1 lan
77             LAN2 eth2 lan
78             LAN3 eth3 lan
79              
80              
81             The first column is a service name that is used to bring up or down
82             the needed routes and firewall rules.
83              
84             The second column is the name of the network interface device that
85             connects to that service.
86              
87             The third column is either "isp" or "lan". There may be any number of
88             these. The script will firewall traffic passing through any of the
89             ISPs, and will load balance traffic among them. Traffic can flow
90             freely among any of the interfaces marked as belonging to a LAN.
91              
92             The fourth column (optional) is the IP address of a host that can be
93             periodically pinged to test the integrity of each ISP connection. If
94             too many pings failed, the service will be brought down and all
95             traffic routed through the remaining ISP(s). The service will continue
96             to be monitored and will be brought up when it is once again
97             working. Choose a host that is not likely to go offline for reasons
98             unrelated to your network connectivity, such as google.com, or the
99             ISP's web site. If this column is absent or marked "default", then the
100             host will default to www.google.ca.
101              
102             The fifth column (optional) is a weight to assign to the service, and
103             is only valid for ISP rows. If weights are equal, traffic will be
104             apportioned evenly between the two routes. Increase a weight to favor
105             one ISP over the others. For example, if "CABLE" has a weight of 2 and
106             "DSL" has a weight of 1, then twice as much traffic will flow through
107             the CABLE service. If this column is omitted or marked "default", then
108             equal weights are assumed.
109              
110             The sixth column (optional) is the gateway for this service using
111             dotted IP notation. If absent or named "default", the system will
112             attempt to determine the proper gateway automatically. Note the
113             algorithm relies on the fact that the gateway is almost always the
114             first address in the IP range for the subnetwork. If this is not the
115             case, then routing through the interface won't work properly. Add the
116             correct gateway IP address manually to correct this.
117              
118             If this package is running on a single Internet-connected host, not a
119             router, then do not include a "lan" line.
120              
121             In addition to the main table, there are several configuration options
122             that follow the format "configuration_name=value":
123              
124             =over 4
125              
126             =item forwarding_group=
127              
128             The forwarding_group configuration option defines a set of services
129             that the router is allowed to forward packets among. Provide a
130             space-delimited set of service names or one or more of the
131             abbreviations ":isp" and ":lan". ":isp" is an abbreviation for all
132             ISP services, while ":lan" is an abbreviation for all LAN services. So
133             for example, the two configuration lines below will allow forwarding
134             of packets between LAN1, LAN2, LAN3 and both ISPs. LAN4 will be
135             granted access to both ISPs but won't be able to exchange packets with
136             LANs 1 through 3:
137              
138             forwarding_group=LAN1 LAN2 LAN3 :isp
139             forwarding_group=LAN4 :isp
140              
141             If no forwarding_group options are defined, then the router will
142             forward packets among all LANs and ISP interfaces. It is equivalent to
143             this:
144              
145             forwarding_group=:lan :isp
146              
147             =item warn_email=
148              
149             Warn_email provides an email address to send notification messages to
150             if the status of a link changes (goes down, or comes back up). You
151             must have the "mail" program installed and configured for this to
152             work.
153              
154             =item interval_ms=
155              
156             Indicates how often to check the ping host for each ISP.
157              
158             =item min_packet_loss=
159              
160             =item max_packet_loss=
161              
162             These define the minimum and maximum packet losses required to declare
163             a link up or down.
164              
165             =item min_successive_pkts_rcvd=
166              
167             =item max_successive_pkts_recvd=
168              
169             These define the minimum and maximum numbers of
170             successively-transmitted pings that must be returned in order to
171             declare a link up or down.
172              
173             =item long_down_time=
174              
175             This is a value in seconds after a service that has gone down is
176             considered to have been down for a long time. You may optionally run a
177             series of shell scripts when this has occurred (see below).
178              
179             =back
180              
181             =head1 FREQUENTLY-USED METHODS
182              
183             Here are the class methods for this module that can be called on the
184             class name.
185              
186             =head2 $bal = Net::ISP::Balance->new('/path/to/config_file.conf');
187              
188             Creates a new balancer object.
189              
190             The first optional argument is the balancer configuration file, which
191             defaults to /etc/network/balance.conf on Ubuntu/Debian-derived
192             systems, and /etc/sysconfig/network-scripts/balance.conf on
193             RedHat/CentOS-derived systems. From hereon, we'll refer to the base of
194             the various configuration files as $ETC_NETWORK.
195              
196             =cut
197              
198             sub new {
199 2     2 1 1168 my $class = shift;
200 2         10 my ($conf,%options) = @_;
201 2   33     10 $conf ||= $class->default_conf_file;
202 2 50 33     59 $conf && -r $conf || croak 'Must provide a readable configuration file path';
203             my $self = bless {
204             verbose => 0,
205             echo_only => 0,
206             services => {},
207             rules_directory => $class->default_rules_directory,
208             lsm_conf_file => $class->default_lsm_conf_file,
209             lsm_scripts_dir => $class->default_lsm_scripts_dir,
210             bal_conf_file => $conf,
211             keep_custom_chains => 1,
212             dummy_data => $options{dummy_test_data},
213             dev_lookup_retries => $options{dev_lookup_retries},
214             dev_lookup_retry_delay => $options{dev_lookup_retry_delay},
215 2   33     13 },ref $class || $class;
216              
217 2         12 $self->_parse_configuration_file($conf);
218              
219             # Instead of potentially timing out on new(), we collect information on all
220             # interfaces that are currently up. We do this again with the timeout before
221             # actually changing the routing table, when it is critical that all interfaces
222             # be configured.
223             # $self->_collect_interfaces_retry(); # try to collect interfaces over 10 seconds
224 2         3 my %ifs;
225 2         11 $self->_collect_interfaces(\%ifs);
226 2         9 $self->{services} = \%ifs;
227              
228 2         10 return $self;
229             }
230              
231             =head2 $bal->set_routes_and_firewall
232              
233             Once the Balance objecty is created, call set_routes_and_firewall() to
234             configure the routing tables and firewall for load balancing. These
235             rules will either be executed on the system, or printed to standard
236             output as a series of shell script commands if echo_only() is set to
237             true.
238              
239             The routing tables and firewall rules are based on the configuration
240             described in $ETC_NETWORK/balance.conf. You may add custom routes and
241             rules by creating files in $ETC_NETWORK/balance/routes and
242             $ETC_NETWORK/balance/firewall. The former contains a series of files
243             or perl scripts that define additional routing rules. The latter
244             contains files or perl scripts that define additional firewall rules.
245              
246             Files located in $ETC_NETWORK/balance/pre-run will be executed AFTER
247             load_balance.pl has cleared the routing table and firewall, but before
248             it has emitted any any route/firewall commands. Files located in
249             in $ETC_NETWORK/balance/post-run will be run after load_balance.pl is
250             finished.
251              
252             Any files you put into these directories will be read in alphabetic
253             order and added to the routes and/or firewall rules emitted by the
254             load balancing script.Contained in this directory are subdirectories named "routes" and
255             "firewall". The former contains a series of files or perl scripts that
256             define additional routing rules. The latter contains files or perl
257             scripts that define additional firewall rules.
258              
259             Note that files ending in ~ or starting with # are treated as autosave files
260             and ignored.
261              
262             A typical routing rules file will look like the example shown
263             below.
264              
265             # file: /etc/network/balance/01.my_routes
266             ip route add 192.168.100.1 dev eth0 src 198.162.1.14
267             ip route add 192.168.1.0/24 dev eth2 src 10.0.0.4
268              
269             Each line will be sent to the shell, and it is intended (but not
270             required) that these be calls to the "ip" command. General shell
271             scripting constructs are not allowed here.
272              
273             A typical firewall rules file will look like the example shown here:
274              
275             # file: /etc/network/firewall/01.my_firewall_rules
276              
277             # accept incoming telnet connections to the router
278             iptable -A INPUT -p tcp --syn --dport telnet -j ACCEPT
279              
280             # masquerade connections to the DSL modem's control interface
281             iptables -t nat -A POSTROUTING -o eth2 -j MASQUERADE
282              
283             You may also insert routing and firewall rules via fragments of Perl
284             code, which is convenient because you don't have to hard-code any
285             network addresses and can make use of a variety of shortcuts. To do
286             this, simply end the file's name with .pl and make it executable.
287              
288             Here's an example that defines a series of port forwarding rules for
289             incoming connections:
290              
291             # file: /etc/network/firewall/02.forwardings.pl
292              
293             $B->forward(80 => '192.168.10.35'); # forward port 80 to internal web server
294             $B->forward(443=> '192.168.10.35'); # forward port 443 to
295             $B->forward(23 => '192.168.10.35:22'); # forward port 23 to ssh on web sever
296              
297             The main thing to know is that on entry to the script the global
298             variable $B will contain an initialized instance of a
299             Net::ISP::Balance object. You may then make method calls on this
300             object to emit firewall and routing rules.
301              
302             A typical routing rules file will look like the example shown
303             below.
304              
305             # file: /etc/network/balance/01.my_routes
306             ip route add 192.168.100.1 dev eth0 src 198.162.1.14
307             ip route add 192.168.1.0/24 dev eth2 src 10.0.0.4
308              
309             Each line will be sent to the shell, and it is intended (but not
310             required) that these be calls to the "ip" command. General shell
311             scripting constructs are not allowed here.
312              
313             A typical firewall rules file will look like the example shown here:
314              
315             # file: /etc/network/firewall/01.my_firewall_rules
316              
317             # accept incoming telnet connections to the router
318             iptable -A INPUT -p tcp --syn --dport telnet -j ACCEPT
319              
320             # masquerade connections to the DSL modem's control interface
321             iptables -t nat -A POSTROUTING -o eth2 -j MASQUERADE
322              
323             You may also insert routing and firewall rules via fragments of Perl
324             code, which is convenient because you don't have to hard-code any
325             network addresses and can make use of a variety of shortcuts. To do
326             this, simply end the file's name with .pl and make it executable.
327              
328             Here's an example that defines a series of port forwarding rules for
329             incoming connections:
330              
331             # file: /etc/network/firewall/02.forwardings.pl
332              
333             $B->forward(80 => '192.168.10.35'); # forward port 80 to internal web server
334             $B->forward(443=> '192.168.10.35'); # forward port 443 to
335             $B->forward(23 => '192.168.10.35:22'); # forward port 23 to ssh on web sever
336              
337             The main thing to know is that on entry to the script the global
338             variable $B will contain an initialized instance of a
339             Net::ISP::Balance object. You may then make method calls on this
340             object to emit firewall and routing rules.
341              
342             =cut
343              
344             sub set_routes_and_firewall {
345 0     0 1 0 my $self = shift;
346              
347 0         0 $self->save_routing_and_firewall();
348              
349             # first disable forwarding
350 0         0 $self->enable_forwarding(0);
351              
352 0         0 $self->_collect_interfaces_retry();
353 0 0       0 if ($self->isp_services) {
354 0         0 $self->pre_run_rules();
355 0         0 $self->set_routes();
356 0         0 $self->set_firewall();
357 0         0 $self->enable_forwarding(1);
358 0         0 $self->post_run_rules();
359             } else {
360 0         0 warn "No ISP services seem to be up. Restoring routing tables and firewall.\n";
361 0 0       0 $self->restore_routing_and_firewall() unless $self->echo_only;
362 0         0 return;
363             }
364             }
365              
366             sub save_routing_and_firewall {
367 0     0 0 0 my $self = shift;
368              
369 0         0 $self->{stored_routes} = [];
370 0         0 $self->{stored_rules} = '';
371 0         0 $self->{stored_firewall} = '';
372              
373 0 0       0 open my $f,"ip route show table all|" or die $!; # binary
374 0         0 while (<$f>) {
375 0         0 chomp;
376 0 0       0 next if /unreachable/;
377 0 0       0 next if /proto none/;
378 0         0 unshift @{$self->{stored_routes}},$_;
  0         0  
379             }
380 0         0 close $f;
381              
382 0 0       0 open $f,"ip rule show|" or die $!; # text
383 0         0 while (<$f>) {
384 0         0 $self->{stored_rules} .= $_;
385             }
386 0         0 close $f;
387              
388 0 0       0 open $f,"iptables-save|" or die $!; # text
389 0         0 while (<$f>) {
390 0         0 $self->{stored_firewall} .= $_;
391             }
392 0         0 close $f;
393             }
394              
395             sub restore_routing_and_firewall {
396 0     0 0 0 my $self = shift;
397              
398 0         0 $self->_initialize_routes();
399 0 0       0 if ($self->{stored_routes}) {
400 0         0 for (@{$self->{stored_routes}}) {
  0         0  
401 0         0 $self->ip_route("add $_");
402             }
403             }
404              
405 0 0       0 if ($self->{stored_rules}) {
406 0         0 my @rules = split "\n",$self->{stored_rules};
407 0         0 for my $r (@rules) {
408 0         0 my ($priority,$rule) = $r =~ /^(\d+):\s*(.+)/;
409 0 0       0 next if $priority == 32766; # these are created by _initialize!
410 0 0       0 next if $priority == 32767;
411 0         0 $self->ip_rule('add',$rule,"priority $priority");
412             }
413             }
414              
415 0 0       0 if ($self->{stored_firewall}) {
416 0 0       0 open my $f,"|iptables-restore" or die $!;
417 0         0 print $f $self->{stored_firewall};
418 0         0 close $f;
419             }
420             }
421              
422             =head2 $verbose = $bal->verbose([boolean]);
423              
424             sub bal_conf_file { my $self = shift; my $d = $self->{bal_conf_file};
425             $self->{bal_conf_file} = shift if @_; $d; } Get/set verbosity of
426             the module. If verbose is true, then firewall and routing rules
427             will be echoed to STDERR before being executed on the system.
428              
429             =cut
430              
431             sub verbose {
432 236     236 1 268 my $self = shift;
433 236         298 my $d = $self->{verbose};
434 236 50       374 $self->{verbose} = shift if @_;
435 236         382 $d;
436             }
437              
438             =head2 $echo = $bal->echo_only([boolean]);
439              
440             Get/set the echo_only flag. If this is true (default false), then
441             routing and firewall rules will be printed to STDOUT rathar than being
442             executed.
443              
444             =cut
445              
446             sub echo_only {
447 216     216 1 727 my $self = shift;
448 216         267 my $d = $self->{echo_only};
449 216 100       328 $self->{echo_only} = shift if @_;
450 216         336 $d;
451             }
452              
453             =head2 $mode = $bal->operating_mode([$mode])
454              
455             Set or interrogate the operating mode. Will return one of "balanced"
456             (currently the default) or "failover". This corresponds to the "mode"
457             option in the configuration file. If the option is neither "balanced"
458             nor "failover", then "balanced" is chosen (be warned!)
459              
460             =cut
461              
462             sub operating_mode {
463 10     10 1 314 my $self = shift;
464 10         16 my $d = $self->{operating_mode};
465 10 100       22 $self->{operating_mode} = shift if @_;
466 10 100 100     49 return 'failover' if $d && $d =~ /failover/i;
467 6         15 return 'balanced';
468             }
469              
470             =head2 $retries = $bal->dev_lookup_retries([$retries])
471              
472             Get/set the number of times the library will try to look up an interface
473             that is not up or does not have an IP address. Default is 10
474              
475             =cut
476              
477             sub dev_lookup_retries {
478 0     0 1 0 my $self = shift;
479 0   0     0 my $d = $self->{dev_lookup_retries} || 10;
480 0 0       0 $self->{dev_lookup_retries} = shift if @_;
481 0         0 $d;
482             }
483              
484             =head2 $seconds = $bal->dev_lookup_retry_delay([$seconds])
485              
486             Get/set the number of seconds between retries when an interface is not up
487             or is missing an IP address. Default is 1.
488              
489             =cut
490              
491             sub dev_lookup_retry_delay {
492 0     0 1 0 my $self = shift;
493 0   0     0 my $d = $self->{dev_lookup_retry_delay} || 1;
494 0 0       0 $self->{dev_lookup_retry_delay} = shift if @_;
495 0         0 $d;
496             }
497              
498             =head2 $boolean = $bal->keep_custom_chains([boolean]);
499              
500             Get/set the keep_custom_chains flag. If this is true (default), then
501             any custom iptables chains, such as those created by miniunpnpd or
502             fail2ban, will be restored after execution of the firewall rules. If
503             false, then these rules were be flushed.
504              
505             =cut
506              
507             sub keep_custom_chains {
508 0     0 1 0 my $self = shift;
509 0         0 my $d = $self->{keep_custom_chains};
510 0 0       0 $self->{keep_custom_chains} = shift if @_;
511 0         0 $d;
512             }
513              
514             =head2 $result_code = $bal->sh(@args)
515              
516             Pass @args to the shell for execution. If echo_only() is set to true,
517             the command will not be executed, but instead be printed to standard
518             output.
519              
520             Example:
521              
522             $bal->sh('ip rule flush');
523              
524             The result code is the same as CORE::system().
525              
526             =cut
527              
528             sub sh {
529 214     214 1 289 my $self = shift;
530 214         380 my @args = @_;
531 214         408 my $arg = join ' ',@args;
532 214         290 chomp($arg);
533 214 50       327 carp $arg if $self->verbose;
534 214 50       332 if ($self->echo_only) {
535 214         294 $arg .= "\n";
536 214         646 print $arg;
537             } else {
538 0         0 system $arg;
539             }
540             }
541              
542             =head2 $bal->iptables(@args)
543              
544             Invoke sh() to call "iptables @args".
545              
546             Example:
547              
548             $bal->iptables('-A OUTPUT -o eth0 -j DROP');
549              
550             You may pass an array reference to iptables(), in which case iptables
551             is called on each member of the array in turn.
552              
553             Example:
554              
555             $bal->iptables(['-P OUTPUT DROP',
556             '-P INPUT DROP',
557             '-P FORWARD DROP']);
558              
559             Note that the method keeps track of rules; if you try to enter the
560             same iptables rule more than once the redundant ones will be ignored.
561              
562             =cut
563              
564             my %seen_rule;
565              
566             sub iptables {
567 163     163 1 227 my $self = shift;
568 163 100       300 if (ref $_[0] eq 'ARRAY') {
569 4   50     5 $seen_rule{$_}++ || $self->sh('iptables',$_) foreach @{$_[0]};
  4         15  
570             } else {
571 159 100       644 $seen_rule{"@_"}++ || $self->sh('iptables',@_)
572             }
573             }
574              
575             sub _iptables_add_rule {
576 13     13   16 my $self = shift;
577 13         34 my ($operation,$chain,$table,@args) = @_;
578 13 50       23 croak "You must provide a chain name" unless $chain;
579 13 50       40 my $op = $operation eq 'append' ? '-A'
    50          
    100          
    100          
580             :$operation eq 'delete' ? '-D'
581             :$operation eq 'check ' ? '-C'
582             :$operation eq 'insert' ? '-I'
583             :'-A';
584            
585 13         14 my $command = '';
586 13 100       20 $command .= "-t $table " if $table;
587 13         22 $command .= "$op $chain ";
588 13         23 $command .= $self->_process_iptable_options(@args);
589 13         26 $self->iptables($command);
590             }
591              
592             sub iptables_append {
593 0     0 0 0 my $self = shift;
594 0         0 my ($table,$chain,@args) = @_;
595 0         0 $self->_iptables_add_rule('append',$table,$chain,@args);
596             }
597              
598             sub iptables_delete {
599 0     0 0 0 my $self = shift;
600 0         0 my ($table,$chain,@args) = @_;
601 0         0 $self->_iptables_add_rule('delete',$table,$chain,@args);
602             }
603              
604             sub iptables_check {
605 0     0 0 0 my $self = shift;
606 0         0 my ($table,$chain,@args) = @_;
607 0         0 $self->_iptables_add_rule('check',$table,$chain,@args);
608             }
609              
610             sub iptables_insert {
611 0     0 0 0 my $self = shift;
612 0         0 my ($table,$chain,@args) = @_;
613 0         0 $self->_iptables_add_rule('insert',$table,$chain,@args);
614             }
615              
616             =head2 $bal->firewall_rule($chain,$table,@args)
617              
618             Issue an iptables firewall rule.
619              
620             $chain -- The chain to apply the rule to, e.g. "INPUT".
621            
622             $table -- The table to apply the rule to, e.g. "nat". Undef defaults to
623             the standard "filter" table.
624              
625             @args -- The other arguments to pass to iptables.
626              
627             Here is a typical example of blocking incoming connections to port 25:
628              
629             $bal->firewall_rule(INPUT=>undef,-p=>'tcp',-dport=>25,-j=>'REJECT');
630              
631             This will issue the following command:
632              
633             iptables -A INPUT -p tcp --dport 25 -j REJECT
634              
635             The default operation is to append the rule to the chain using
636             -A. This can be changed by passing $bal->firewall_op() any of the
637             strings "append", "delete", "insert" or "check". Subsequent calls to
638             firewall_rule() will return commands for the indicated function:
639              
640             $bal->firewall_op('delete');
641             $bal->firewall_rule(INPUT=>undef,-p=>'tcp',-dport=>25,-j=>'REJECT');
642             # gives iptables -A INPUT -p tcp --dport 25 -j REJECT
643              
644             If you want to apply a series of deletes and then revert to the
645             original append behavior, then it is easiest to localize the hash key
646             "firewall_op":
647              
648             {
649             local $bal->{firewall_op} = 'delete';
650             $bal->firewall_rule(INPUT=>undef,-dport=>25,-j=>'ACCEPT');
651             $bal->firewall_rule(INPUT->undef,-dport=>80,-j=>'ACCEPT');
652             }
653            
654             $bal->firewall_rule(INPUT=>undef,-dport=>25,-j=>'DROP');
655             $bal->firewall_rule(INPUT=>undef,-dport=>80,-j=>'DROP');
656              
657             =cut
658              
659             sub firewall_rule {
660 13     13 1 20 my $self = shift;
661 13         32 my ($chain,$table,@args) = @_;
662 13         20 my $operation = $self->firewall_op();
663 13         27 $self->_iptables_add_rule($operation,$chain,$table,@args);
664             }
665              
666             sub firewall_op {
667 13     13 0 14 my $self = shift;
668 13 50       22 if (@_) {
669 0         0 $self->{firewall_op} = shift;
670 0         0 return;
671             }
672 13   100     26 my $d = $self->{firewall_op} || 'append';
673 13         21 return $d;
674             }
675              
676             =head2 $bal->force_route($service_or_device,@selectors)
677              
678             The force_route() method issues iptables commands that will force
679             certain traffic to travel over a particular ISP service or network
680             device. This is useful, for example, when one of your ISPs acts as
681             your e-mail relay and only accepts connections from the IP address
682             it assigns.
683              
684             $service_or_device is the symbolic name of an ISP service
685             (e.g. "CABLE") or a network device that a service is attached to
686             (e.g. "eth0").
687              
688             @selectors are a series of options that will be passed to
689             iptables to select the routing of packets. For example, to forward all
690             outgoing mail (destined to port 25) to the "CABLE" ISP, you would
691             write:
692              
693             $bal->force_route('CABLE','-p'=>'tcp','--syn','--dport'=>25);
694              
695             @selectors is a series of optional arguments that will be passed to
696             iptables on the command line. They will simply be space-separated, and
697             so the following is equivalent to the previous example:
698              
699             $bal->force_route('CABLE','-p tcp --syn --dport 25');
700              
701             Bare arguments that begin with a leading hyphen and are followed by
702             two or more alphanumeric characters are automatically converted into
703             double-hyphen arguments. This allows you to simplify commands
704             slightly. The following is equivalent to the previous examples:
705              
706             $bal->force_route('CABLE',-p=>'tcp',-syn,-dport=>25);
707              
708             You can delete force_route rules by setting firewall_op() to 'delete':
709              
710             $bal->firewall_op('delete');
711             $bal->force_route('CABLE',-p=>'tcp',-syn,-dport=>25);
712              
713             =cut
714              
715             sub force_route {
716 2     2 1 1103 my $self = shift;
717 2         10 my ($service_or_device,@selectors) = @_;
718            
719 2 50       9 my $service = $self->_service_or_device($service_or_device)
720             or croak "did not recognize $service_or_device as a service or a device";
721              
722 2         5 my $dest = $self->mark_table($service);
723 2         5 my $selectors = $self->_process_iptable_options(@selectors);
724 2         6 $self->firewall_rule(PREROUTING=>'mangle',$selectors,-j=>$dest);
725             }
726              
727             =head2 $bal->add_route($address => $device, [$masquerade])
728              
729             This method is used to create routing and firewall rules for a network
730             that isn't mentioned in balance.conf. This may be necessary to route
731             to VPNs and/or to the control interfaces of attached modems.
732              
733             The first argument is the network address in CIDR format,
734             e.g. '192.168.2.0/24'. The second is the network interface that the
735             network can be accessed via. The third, optional, argument is a
736             boolean. If true, then firewall rules will be set up to masquerade
737             from the LAN into the attached network.
738              
739             Note that this is pretty limited. If you want to do anything more
740             sophisticated you're better off setting the routes and firewall rules
741             manually.
742              
743             =cut
744              
745             sub add_route {
746 1     1 1 465 my $self = shift;
747 1         4 my ($network,$device,$masquerade) = @_;
748 1 50 33     7 $network && $device or croak "usage: add_network(\$network,\$device,[\$masquerade])";
749             # add the route to our main table
750 1         5 $self->ip_route("add $network dev $device");
751             # add the route to each outgoing table
752 1         3 $self->ip_route("add $network dev $device table $_") for map {$self->table($_)} $self->isp_services;
  3         5  
753            
754             # create appropriate firewall rules for the network
755             {
756 1         5 local $self->{firewall_op} = 'insert';
  1         4  
757 1         8 $self->firewall_rule(OUTPUT => undef,
758             -o => $device,
759             -d => $network,
760             -j => 'ACCEPT');
761 1         4 $self->firewall_rule(INPUT => undef,
762             -i => $device,
763             -s => $network,
764             -j => 'ACCEPT');
765             $self->firewall_rule(FORWARD => undef,
766             -i => $self->dev($_),
767             -s => $self->net($_),
768             -o => $device,
769             -d => $network,
770 1         2 -j => 'ACCEPT') for $self->lan_services;
771             $self->firewall_rule(FORWARD => undef,
772             -i => $device,
773             -s => $network,
774             -o => $self->dev($_),
775             -d => $self->net($_),
776 1         3 -j => 'ACCEPT') for $self->lan_services;
777             }
778 1 50       4 if ($masquerade) {
779 1         4 $self->firewall_rule(POSTROUTING=>'nat',
780             -d => $network,
781             -o => $device,
782             -j => 'MASQUERADE');
783             }
784             }
785              
786             sub _process_iptable_options {
787 15     15   17 my $self = shift;
788 15         34 my @opt = @_;
789 15         21 foreach (@opt) {
790 114 100       212 $_ = "-$_" if /^-\w{2,}/; # add an extra hyphen to -arguments
791 114         663 $_ =~ quotemeta($_);
792             }
793 15         50 return join ' ',@opt;
794             }
795              
796             sub _mark {
797 30     30   35 my $self = shift;
798 30         35 my $service = shift;
799 30         56 return "MARK-${service}";
800             }
801              
802             =head2 $table_name = $bal->mark_table($service)
803              
804             This returns the iptables table name for connections marked for output
805             on a particular ISP service. The name is simply the word "MARK-"
806             appended to the service name. For example, for a service named "DSL",
807             the corresponding firewall table will be named "MARK-DSL".
808              
809             =cut
810              
811 30     30 1 48 sub mark_table { shift->_mark(shift) }
812              
813             sub _service_or_device {
814 2     2   4 my $self = shift;
815 2         4 my $sod = shift;
816 2 50       5 return $sod if $self->dev($sod);
817             # otherwise try looking for devices
818 0         0 my %dev2s = map {$self->dev($_) => $_} $self->service_names;
  0         0  
819 0         0 return $dev2s{$sod};
820             }
821              
822             =head2 $bal->forward($incoming_port,$destination_host,@protocols)
823              
824             This method emits appropriate port/host forwarding rules using DNAT
825             address translation. The destination host can be specified using
826             either of these forms:
827              
828             192.168.100.1 # forward to same port as incoming
829             192.168.100.1:8080 # forward to a different port on host
830              
831             Protocols are one or more of 'tcp','udp'. If omitted defaults to tcp.
832              
833             Examples:
834            
835             $bal->forward(80 => '192.168.100.1');
836             $bal->forward(80 => '192.168.100.1:8080','tcp');
837              
838             =cut
839              
840             sub forward {
841 3     3 1 1267 my $self = shift;
842 3         52 my ($port,$host,@protocols) = @_;
843 3 100       17 @protocols = ('tcp') unless @protocols;
844              
845 3         13 my ($dhost,$dport) = split ':',$host;
846 3   33     11 $dhost ||= $host;
847 3   66     11 $dport ||= $port;
848              
849 3         8 my @dev = map {$self->dev($_)} $self->isp_services;
  9         19  
850              
851 3         9 for my $dev (@dev) {
852 9         16 for my $protocol (@protocols) {
853 12         50 $self->iptables("-t nat -A PREROUTING -i $dev -p $protocol --dport $port -j DNAT --to-destination $host");
854 12         30 for my $lan ($self->lan_services) {
855 48         102 my $landev = $self->dev($lan);
856 48         103 my $lannet = $self->net($lan);
857 48         99 my $lanip = $self->ip($lan);
858 48 100       106 my $syn = $protocol eq 'tcp' ? '--syn' : '';
859 48         153 $self->iptables("-A FORWARD -p $protocol -o $landev $syn -d $dhost --dport $dport -j ACCEPT");
860             }
861             }
862             }
863             }
864              
865             =head2 $bal->forward_with_snat($incoming_port,$destination_host,@protocols)
866              
867             This method is the same as forward(), except that it also does source
868             NATing from LAN-based requests to make the request appear to have come
869             from the router. This is used when you expose a server, such as a web
870             server, to the internet, but you also need to access the server from
871             machines on the LAN. Use this if you find that the service is visible
872             from outside the LAN but not inside the LAN.
873              
874             Examples:
875              
876             $bal->forward_with_snat(80 => '192.168.100.1');
877             $bal->forward_with_snat(80 => '192.168.100.1:8080','tcp');
878              
879              
880             =cut
881              
882             sub forward_with_snat {
883 0     0 1 0 my $self = shift;
884 0         0 my ($port,$host,@protocols) = @_;
885              
886 0 0       0 @protocols = ('tcp') unless @protocols;
887              
888 0         0 my ($dhost,$dport) = split ':',$host;
889 0   0     0 $dhost ||= $host;
890 0   0     0 $dport ||= $port;
891              
892 0         0 for my $protocol (@protocols) {
893 0         0 for my $svc ($self->isp_services) {
894 0         0 my $external_ip = $self->ip($svc);
895 0         0 $self->iptables("-t nat -A PREROUTING -d $external_ip -p $protocol --dport $port -j DNAT --to-destination $host");
896             }
897              
898 0         0 for my $lan ($self->lan_services) {
899 0         0 my $lannet = $self->net($lan);
900 0         0 $self->iptables("-t nat -A POSTROUTING -s $lannet -p $protocol --dport $port -d $host -j MASQUERADE");
901             }
902              
903 0         0 $self->iptables("-A FORWARD -p $protocol --dport $port -d $host -j ACCEPT");
904              
905             }
906              
907             }
908              
909             =head2 $bal->ip_route(@args)
910              
911             Shortcut for $bal->sh('ip route',@args);
912              
913             =cut
914              
915 61     61 1 94 sub ip_route {shift->sh('ip','route',@_)}
916              
917             =head2 $bal->ip_rule(@args)
918              
919             Shortcut for $bal->sh('ip rule',@args);
920              
921             =cut
922              
923 10     10 1 18 sub ip_rule {shift->sh('ip','rule',@_)}
924              
925             =head2 $verbose = $bal->iptables_verbose([boolean])
926              
927             Makes iptables send an incredible amount of debugging information to
928             syslog.
929              
930             =cut
931              
932             sub iptables_verbose {
933 0     0 1 0 my $self = shift;
934 0         0 my $d = $self->{iptables_verbose};
935 0 0       0 $self->{iptables_verbose} = shift if @_;
936 0         0 $d;
937             }
938              
939             =head1 QUERYING THE CONFIGURATION
940              
941             These methods allow you to get information about the Net::ISP::Balance
942             object's configuration, including settings and other characteristics
943             of the various network interfaces.
944              
945             =head2 @names = $bal->service_names
946              
947             Return the list of service names defined in balance.conf.
948              
949             =cut
950              
951             sub service_names {
952 48     48 1 64 my $self = shift;
953 48         90 my $s = $self->services;
954 48         290 return sort keys %$s;
955             }
956              
957             =head2 @names = $bal->isp_services
958              
959             Return list of service names that correspond to load-balanced ISPs.
960              
961             =cut
962              
963             sub isp_services {
964 20     20 1 32 my $self = shift;
965 20         52 my @n = $self->service_names;
966 20         50 return grep {$self->role($_) eq 'isp'} @n; # kill uninit warning
  156         254  
967             }
968              
969             =head2 @names = $bal->lan_services
970              
971             Return list of service names that correspond to lans.
972              
973              
974             =cut
975              
976             sub lan_services {
977 21     21 1 39 my $self = shift;
978 21         37 my @n = $self->service_names;
979 21         57 return grep {$self->role($_) eq 'lan'} @n; # kill uninit warning...
  168         286  
980             }
981              
982             =head2 $state = $bal->event($service => $new_state)
983              
984             Record a transition between "up" and "down" for a named service. The
985             first argument is the name of the ISP service that has changed,
986             e.g. "CABLE". The second argument is either "up" or "down".
987              
988             The method returns a hashref in which the keys are the ISP service names
989             and the values are one of 'up' or 'down'.
990              
991             The persistent state information is stored in /var/lib/lsm/ under a
992             series of files named .state.
993              
994             =cut
995              
996             sub event {
997 0     0 1 0 my $self = shift;
998              
999 0 0       0 if (@_) {
1000 0         0 my ($svc,$new_state) = @_;
1001 0 0       0 $new_state =~ /^(up|down)$/ or croak "state must be 'up' or down'";
1002 0 0       0 $self->dev($svc) or croak "service '$svc' is unknown";
1003 0         0 my $file = "/var/lib/lsm/${svc}.state";
1004 0 0       0 my $mode = -e $file ? '+<' : '>';
1005 0 0       0 open my $fh,$mode,$file or croak "Couldn't open $file mode $mode: $!";
1006 0         0 flock $fh,LOCK_EX;
1007 0         0 truncate $fh,0;
1008 0         0 seek($fh,0,0);
1009 0         0 print $fh $new_state;
1010 0         0 close $fh;
1011             }
1012              
1013 0         0 my %state;
1014 0         0 for my $svc ($self->isp_services) {
1015 0         0 my $file = "/var/lib/lsm/${svc}.state";
1016 0 0       0 if (open my $fh,'<',$file) {
1017 0         0 flock $fh,LOCK_SH;
1018 0         0 my $state = <$fh>;
1019 0         0 close $fh;
1020 0         0 $state{$svc}=$state;
1021             } else {
1022 0         0 $state{$svc}='unknown';
1023             }
1024             }
1025 0         0 my @up = grep {$state{$_} eq 'up'} keys %state;
  0         0  
1026 0         0 $self->up(@up);
1027 0         0 return \%state;
1028             }
1029              
1030             =head2 $bal->run_eventd(@args)
1031              
1032             Runs scripts in response to lsm events. The scripts are stored in
1033             directories named after the events, e.g.:
1034              
1035             /etc/network/lsm/up.d/*
1036             /etc/network/lsm/down.d/*
1037             /etc/network/lsm/long_down.d/*
1038              
1039             Scripts are called with the following arguments:
1040              
1041             0. STATE
1042             1. SERVICE NAME
1043             2. CHECKIP
1044             3. DEVICE
1045             4. WARN_EMAIL
1046             5. REPLIED
1047             6. WAITING
1048             7. TIMEOUT
1049             8. REPLY_LATE
1050             9. CONS_RCVD
1051             10. CONS_WAIT
1052             11. CONS_MISS
1053             12. AVG_RTT
1054             13. SRCIP
1055             14. PREVSTATE
1056             15. TIMESTAMP
1057              
1058             =cut
1059              
1060             sub run_eventd {
1061 0     0 1 0 my $self = shift;
1062 0         0 my @args = @_;
1063 0         0 my $state = $args[0];
1064 0         0 my $dir = $self->lsm_scripts_dir();
1065 0         0 my $dird = "$dir/${state}.d";
1066 0         0 my @files = sort glob("$dird/*");
1067 0         0 for my $script (sort @files) {
1068 0 0       0 next if $script =~ /^#/;
1069 0 0       0 next if $script =~ /~$/;
1070 0 0 0     0 next unless -f $script && -x _;
1071 0         0 system $script,@args;
1072             }
1073             }
1074              
1075             =head2 @up = $bal->up(@up_services)
1076              
1077             Get or set the list of ISP interfaces that are currently active and
1078             should be used for balancing.
1079              
1080             =cut
1081              
1082             sub up {
1083 6     6 1 572 my $self = shift;
1084 6 100       18 $self->{up} = \@_ if @_;
1085 6 100       13 unless ($self->{up}) { # initialize with running services
1086 2         6 my @svc = grep {$self->running($_)} $self->isp_services;
  5         11  
1087 2         6 $self->{up} = \@svc;
1088             }
1089 6         8 my @up = @{$self->{up}};
  6         14  
1090 6         19 return @up;
1091             }
1092              
1093             =head2 $services = $bal->services
1094              
1095             Return a hash containing the configuration information for each
1096             service. The keys are the service names. Here's an example:
1097              
1098             {
1099             0 HASH(0x91201e8)
1100             'CABLE' => HASH(0x9170500)
1101             'dev' => 'eth0'
1102             'fwmark' => 2
1103             'gw' => '191.3.88.1'
1104             'ip' => '191.3.88.152'
1105             'net' => '191.3.88.128/27'
1106             'ping' => 'www.google.ca'
1107             'role' => 'isp'
1108             'running' => 1
1109             'table' => 2
1110             'DSL' => HASH(0x9113e00)
1111             'dev' => 'ppp0'
1112             'fwmark' => 1
1113             'gw' => '112.211.154.198'
1114             'ip' => '11.120.199.108'
1115             'net' => '112.211.154.198/32'
1116             'ping' => 'www.google.ca'
1117             'role' => 'isp'
1118             'running' => 1
1119             'table' => 1
1120             'LAN' => HASH(0x913ce58)
1121             'dev' => 'eth1'
1122             'fwmark' => undef
1123             'gw' => '192.168.10.1'
1124             'ip' => '192.168.10.1'
1125             'net' => '192.168.10.0/24'
1126             'ping' => ''
1127             'role' => 'lan'
1128             'running' => 1
1129             }
1130              
1131             =cut
1132              
1133 49     49 1 494 sub services { return shift->{services} }
1134              
1135             =head2 $service = $bal->service('CABLE')
1136              
1137             Return the subhash describing the single named service (see services()
1138             above).
1139              
1140             =cut
1141              
1142             sub service {
1143 0     0 1 0 shift->{services}{shift()};
1144             }
1145              
1146             =head2 $dev = $bal->dev('CABLE')
1147              
1148             =head2 $ip = $bal->ip('CABLE')
1149              
1150             =head2 $gateway = $bal->gw('CABLE')
1151              
1152             =head2 $network = $bal->net('CABLE')
1153              
1154             =head2 $role = $bal->role('CABLE')
1155              
1156             =head2 $running = $bal->running('CABLE')
1157              
1158             =head2 $mark_number = $bal->fwmark('CABLE')
1159              
1160             =head2 $routing_table_number = $bal->table('CABLE')
1161              
1162             =head2 $ping_dest = $bal->ping('CABLE')
1163              
1164             These methods pull out the named information from the configuration
1165             data. fwmark() returns a small integer that will be used for marking
1166             connections for routing through one of the ISP connections when an
1167             outgoing connection originates on the LAN and is routed through the
1168             router. table() returns a small integer corresponding to a routing
1169             table used to route connections originating on the router itself.
1170              
1171             =cut
1172              
1173 194     194 1 9327 sub dev { shift->_service_field(shift,'dev') }
1174 106     106 1 168 sub ip { shift->_service_field(shift,'ip') }
1175 9     9 1 18 sub gw { shift->_service_field(shift,'gw') }
1176 156     156 1 261 sub net { shift->_service_field(shift,'net') }
1177 5     5 1 7 sub running { shift->_service_field(shift,'running') }
1178 326     326 1 504 sub role { shift->_service_field(shift,'role') }
1179 11     11 1 17 sub fwmark { shift->_service_field(shift,'fwmark') }
1180 54     54 1 74 sub table { shift->_service_field(shift,'table') }
1181 3     3 1 9 sub ping { shift->_service_field(shift,'ping') }
1182 17     17 0 26 sub weight { shift->_service_field(shift,'weight') }
1183              
1184             sub _service_field {
1185 881     881   1042 my $self = shift;
1186 881         1289 my ($service,$field) = @_;
1187 881 50       1642 my $s = $self->{services}{$service} or return;
1188 881         2102 $s->{$field};
1189             }
1190              
1191             sub _save_custom_chains {
1192 0     0   0 my $self = shift;
1193 0         0 for my $table ('filter','nat','mangle') {
1194 0         0 my @rules = split("\n",`sudo iptables -t $table -S`);
1195             # find custom chains
1196 0         0 my $mine = 'MARK-|REJECTPERM|DROPGEN|DROPINVAL|DROPPERM|DROPSPOOF|DROPFLOOD|DEBUG';
1197 0 0       0 my @chains = grep {!/^-N ($mine)/} grep {/^-N (\S+)/} @rules or next;
  0         0  
  0         0  
1198 0         0 s/^-N // foreach @chains;
1199 0         0 my $chains = join '|',map {quotemeta($_)} @chains;
  0         0  
1200 0         0 my @targets = grep {/-(?:j|A|I) (?:$chains)/} @rules;
  0         0  
1201 0         0 $self->{_custom_chains}{$table} = [(map {"-N $_"} @chains),@targets];
  0         0  
1202             }
1203             }
1204              
1205             sub _restore_custom_chains {
1206 0     0   0 my $self = shift;
1207 0 0       0 my $custom_chains = $self->{_custom_chains} or return;
1208 0         0 for my $table (keys %{$custom_chains}) {
  0         0  
1209 0 0       0 my @rules = @{$custom_chains->{$table}} or next;
  0         0  
1210 0         0 $self->iptables([map {"-t $table $_"} @rules]);
  0         0  
1211             }
1212             }
1213              
1214             =head1 FILES AND PATHS
1215              
1216             These are methods that determine where Net::ISP::Balance finds its
1217             configuration files.
1218              
1219             =head2 $path = Net::ISP::Balance->install_etc
1220              
1221             Returns the path to where the network configuration files reside on
1222             this system, e.g. /etc/network. Note that this only knows about
1223             Ubuntu/Debian-style network configuration files in /etc/network, and
1224             RedHat/CentOS network configuration files in
1225             /etc/sysconfig/network-scripts.
1226              
1227             =cut
1228              
1229             sub install_etc {
1230 7     7 1 13 my $self = shift;
1231 7 50       162 return '/etc/network' if -d '/etc/network';
1232 0 0       0 return '/etc/sysconfig/network-scripts' if -d '/etc/sysconfig/network-scripts';
1233 0         0 return '/etc';
1234             }
1235              
1236             =head2 $file = Net::ISP::Balance->default_conf_file
1237              
1238             Returns the path to the default configuration file,
1239             $ETC_NETWORK/balance.conf.
1240              
1241             =cut
1242              
1243             sub default_conf_file {
1244 0     0 1 0 my $self = shift;
1245 0         0 return $self->install_etc.'/balance.conf';
1246             }
1247              
1248             =head2 $dir = Net::ISP::Balance->default_rules_directory
1249              
1250             Returns the path to the directory where the additional router and
1251             firewall rules are stored. On Ubuntu-Debian-derived systems, this is
1252             /etc/network/balance/. On RedHat/CentOS systems, this is
1253             /etc/sysconfig/network-scripts/balance/.
1254              
1255             =cut
1256              
1257             sub default_rules_directory {
1258 2     2 1 20 my $self = shift;
1259 2         16 return $self->install_etc."/balance";
1260             }
1261              
1262             =head2 $file = Net::ISP::Balance->default_lsm_conf_file
1263              
1264             Returns the path to the place where we should store lsm.conf, the file
1265             used to configure the lsm (link status monitor) application.
1266              
1267             On Ubuntu/Debian-derived systems, this will be the file
1268             /etc/network/lsm.conf. On RedHad/CentOS-derived systems, this will be
1269             /etc/sysconfig/network-scripts/lsm.conf.
1270              
1271             =cut
1272              
1273             sub default_lsm_conf_file {
1274 2     2 1 7 my $self = shift;
1275 2         6 return $self->install_etc."/balance/lsm.conf";
1276             }
1277              
1278             =head2 $dir = Net::ISP::Balance->default_lsm_scripts_dir
1279              
1280             Returns the path to the place where lsm stores its helper scripts. On
1281             Ubuntu/Debian-derived systems, this will be the directory
1282             /etc/network/lsm/. On RedHad/CentOS-derived systems, this will be
1283             /etc/sysconfig/network-scripts/lsm/.
1284              
1285             =cut
1286              
1287             sub default_lsm_scripts_dir {
1288 2     2 1 6 my $self = shift;
1289 2         6 return $self->install_etc.'/balance/lsm';
1290             }
1291              
1292             =head2 $file = $bal->bal_conf_file([$new_file])
1293              
1294             Get/set the main configuration file path, balance.conf.
1295              
1296             =cut
1297              
1298             sub bal_conf_file {
1299 0     0 1 0 my $self = shift;
1300 0         0 my $d = $self->{bal_conf_file};
1301 0 0       0 $self->{bal_conf_file} = shift if @_;
1302 0         0 $d;
1303             }
1304              
1305             =head2 $dir = $bal->rules_directory([$new_rules_directory])
1306              
1307             Get/set the route and firewall rules directory.
1308              
1309             =cut
1310              
1311             sub rules_directory {
1312 3     3 1 11 my $self = shift;
1313 3         7 my $d = $self->{rules_directory};
1314 3 100       9 $self->{rules_directory} = shift if @_;
1315 3         6 $d;
1316             }
1317              
1318             =head2 $file = $bal->lsm_conf_file([$new_conffile])
1319              
1320             Get/set the path to the lsm configuration file.
1321              
1322             =cut
1323              
1324             sub lsm_conf_file {
1325 0     0 1 0 my $self = shift;
1326 0         0 my $d = $self->{lsm_conf_file};
1327 0 0       0 $self->{lsm_conf_file} = shift if @_;
1328 0         0 $d;
1329             }
1330              
1331             =head2 $dir = $bal->lsm_scripts_dir([$new_dir])
1332              
1333             Get/set the path to the lsm scripts directory.
1334              
1335             =cut
1336              
1337             sub lsm_scripts_dir {
1338 1     1 1 2 my $self = shift;
1339 1         2 my $d = $self->{lsm_scripts_dir};
1340 1 50       3 $self->{lsm_scripts_dir} = shift if @_;
1341 1         3 $d;
1342             }
1343              
1344             =head1 INFREQUENTLY-USED METHODS
1345              
1346             These are methods that are used internally, but may be useful to
1347             applications developers.
1348              
1349             =head2 $lsm_config_text = $bal->lsm_config_file(-warn_email=>'root@localhost')
1350              
1351             This method creates the text used to create the lsm.conf configuration
1352             file. Pass it a series of -name=>value pairs to incorporate into the
1353             file.
1354              
1355             Possible switches and their defaults are:
1356              
1357             -checkip 127.0.0.1
1358             -eventscript /etc/network/load_balance.pl
1359             -long_down_eventscript /etc/network/load_balance.pl
1360             -notifyscript /etc/network/balance/lsm/default_script
1361             -max_packet_loss 15
1362             -max_successive_pkts_lost 7
1363             -min_packet_loss 5
1364             -min_successive_pkts_rcvd 10
1365             -interval_ms 1000
1366             -timeout_ms 1000
1367             -warn_email root
1368             -check_arp 0
1369             -sourceip
1370             -device -eventscript => $balance_script,
1371             -ttl 0
1372             -status 2
1373             -debug 8
1374              
1375             =cut
1376              
1377             sub lsm_config_text {
1378 1     1 0 3 my $self = shift;
1379 1         3 my %args = @_;
1380 1         4 my $scripts_dir = $self->lsm_scripts_dir;
1381 1         3 my $balance_script = $self->install_etc."/load_balance.pl";
1382 1         17 my %defaults = (
1383             -checkip => '127.0.0.1',
1384             -debug => 8,
1385             -eventscript => $balance_script,
1386             -long_down_eventscript => $balance_script,
1387             -notifyscript => "$scripts_dir/default_script",
1388             -max_packet_loss => 20,
1389             -max_successive_pkts_lost => 7,
1390             -min_packet_loss => 10,
1391             -min_successive_pkts_rcvd => 5,
1392             -interval_ms => 1000,
1393             -timeout_ms => 500,
1394             -long_down_time => 120,
1395             -warn_email => 'root',
1396             -check_arp => 0,
1397             -sourceip => undef,
1398             -device => undef,
1399             -ttl => 0,
1400             -status => 2
1401             );
1402 1         5 %defaults = (%defaults,%{$self->{lsm_config}},%args); # %args supersedes what's in %defaults
  1         6  
1403              
1404 1         3 my $result = "# This file is autogenerated by load_balancer.pl when it first runs.\n";
1405 1         3 $result .= "# Do not edit directly. Instead edit /etc/network/balance.conf.\n\n";
1406 1         3 $result .= "debug=$defaults{-debug}\n\n";
1407 1         2 delete $defaults{-debug};
1408              
1409 1         2 $result .= "defaults {\n";
1410 1         2 $result .= " name=defaults\n";
1411 1         8 for my $option (sort keys %defaults) {
1412 17         41 (my $o = $option) =~ s/^-//;
1413 17 100       29 $defaults{$option} = '' unless defined $defaults{$option}; # avoid uninit var warnings
1414 17         31 $result .= " $o=$defaults{$option}\n";
1415             }
1416 1         3 $result .= "}\n\n";
1417              
1418 1         4 for my $svc ($self->isp_services) {
1419 3         6 my $device = $self->dev($svc);
1420 3         15 my $src_ip = $self->ip($svc);
1421 3         14 my $ping = $self->ping($svc);
1422 3         5 $result .= "connection {\n";
1423 3         6 $result .= " name=$svc\n";
1424 3         4 $result .= " device=$device\n";
1425 3         5 $result .= " checkip=$ping\n";
1426 3         5 $result .= "}\n\n";
1427             }
1428              
1429 1         6 return $result;
1430             }
1431              
1432             sub _parse_configuration_file {
1433 2     2   5 my $self = shift;
1434 2         5 my $path = shift;
1435 2         4 my (%services,%lsm_options,@forwarding_group);
1436 2 50       81 open my $f,$path or die "Could not open $path: $!";
1437              
1438 2         49 while (<$f>) {
1439 32         52 chomp;
1440 32 100       93 next if /^\s*#/;
1441 26 50       49 if (/^forwarding_group\s*=\s*(.+)$/) { # routing group
1442 0 0       0 my @group = split /\s+/,$1 or next;
1443 0         0 push @forwarding_group,\@group;
1444 0         0 next;
1445             }
1446 26 100       50 if (/^mode\s*=\s*(.+)$/) { # operating mode
1447 1         6 $self->operating_mode($1);
1448 1         10 next;
1449             }
1450 25 50       48 if (/^(\w+)\s*=\s*(.*)$/) { # lsm config
1451 0         0 $lsm_options{"-${1}"} = $2;
1452 0         0 next;
1453             }
1454 25         105 my ($service,$device,$role,$ping_dest,$weight,$gateway) = split /\s+/;
1455 25 50 66     125 next unless $service && $device && $role;
      66        
1456 15 50       44 croak "load_balance.conf line $.: A service can not be named 'up' or 'down'"
1457             if $service=~/^(up|down)$/;
1458              
1459 15         32 foreach (\$ping_dest,\$weight,\$gateway) {
1460 45 100       100 undef $$_ if $$_ eq 'default';
1461             }
1462            
1463 15         52 $services{$service}{dev} = $device;
1464 15         29 $services{$service}{role} = $role;
1465 15   100     59 $services{$service}{ping} = $ping_dest || 'www.google.ca';
1466 15   100     74 $services{$service}{weight} = $weight || 1;
1467 15         53 $services{$service}{gateway}= $gateway;
1468             }
1469 2         23 close $f;
1470 2         15 $self->{svc_config} = \%services;
1471 2         6 $self->{lsm_config} = \%lsm_options;
1472 2         13 $self->{forwarding_groups} = \@forwarding_group;
1473             }
1474              
1475             sub _collect_interfaces_retry {
1476 0     0   0 my $self = shift;
1477 0         0 my $retries = $self->dev_lookup_retries;
1478 0         0 my $wait = $self->dev_lookup_retry_delay;
1479 0         0 my %ifs;
1480 0         0 for (1..$retries) {
1481 0         0 delete $self->{_interface_info_cache}; # don't want to cache partial results
1482 0 0       0 last if $self->_collect_interfaces(\%ifs);
1483 0         0 sleep $wait;
1484             }
1485 0         0 $self->{services} = \%ifs;
1486             }
1487              
1488             sub _collect_interfaces {
1489 2     2   6 my $self = shift;
1490 2         3 my $interface_info = shift;
1491              
1492 2 50       9 my $s = $self->{svc_config} or return;
1493 2         6 my $i = $self->interface_info;
1494              
1495             # print STDERR Dumper($i);
1496              
1497             # map devices to services
1498 2         6 my %devs;
1499 2         9 for my $svc (keys %$s) {
1500 15         27 my $vdev = $s->{$svc}{dev};
1501 15         30 $devs{$vdev}=$svc;
1502             }
1503              
1504 2         5 my $counter = 0;
1505 2         4 my $configured_interfaces = 0;
1506              
1507 2         13 for my $vdev (sort keys %devs) {
1508 15 100       40 my $info = $i->{$vdev} or next;
1509 13         25 my $dev = $info->{dev};
1510 13         19 my $svc = $devs{$vdev};
1511 13         24 my $role = $s->{$svc}{role};
1512 13         19 $configured_interfaces++;
1513              
1514             # copy into hash passed to us
1515             $interface_info->{$svc} = {
1516             dev => $dev, # otherwise, iptables will croak!!!
1517             running => $info->{running},
1518             gw => $s->{$svc}{gateway} || $info->{gw},
1519             net => $info->{net},
1520             ip => $info->{ip},
1521             fwmark => $role eq 'isp' ? ++$counter : undef,
1522             table => $role eq 'isp' ? $counter : undef,
1523             role => $role,
1524             ping => $s->{$svc}{ping},
1525             weight => $s->{$svc}{weight},
1526             }
1527 13 100 66     143 }
    100          
1528 2         11 return $configured_interfaces >= keys %devs;
1529             }
1530              
1531             =head2 $if_hash = $bal->interface_info
1532              
1533             =head2 $if_hash = Net::ISP::Balance->interface_info
1534              
1535             This method returns a hashref containing information about each of the
1536             network interfaces found on the system (independent of those mentioned
1537             in the configuration file). It may be called as a class method or an
1538             instance method.
1539              
1540             Each key in the hash is the name of a (virtual) interface device. The
1541             values are hashrefs with the following keys:
1542              
1543             key value
1544             --- -----
1545             dev name of the underlying physical device (usually same as vdev)
1546             running boolean, true if interface is running
1547             gw gateway, if present
1548             net subnet in xxx.xxx.xxx.xxx/xx
1549              
1550             =cut
1551              
1552              
1553             sub interface_info {
1554 2     2 1 6 my $self = shift;
1555             return $self->{_interface_info_cache}
1556 2 50 33     15 if ref $self && exists $self->{_interface_info_cache};
1557            
1558 2         4 my %results; # keyed by interface device
1559              
1560             # LOGIC TO DEAL WITH VIRTUAL INTERFACES
1561             # 1. From _ip_addr_show get all the inet XXX.XXX.XXX.XXX lines and calculate
1562             # corresponding network and virtual interface.
1563             # 2. Record mapping of network to virtual interface in a hash (%vif)
1564             # 3. When going through the routes, replace $dev with virtual interface name
1565             # 4. In (keys %devs) loop, create an inner loop for each inet found and replace
1566             # device with correct virtual device.
1567              
1568             # get interfaces with assigned addresses
1569 2         8 my $a = $self->_ip_addr_show;
1570 2         40 my (undef,@ifs) = split /^\d+: /m,$a;
1571 2         9 chomp(@ifs);
1572             my %ifs = map {
1573 2         6 my ($dev,$config) = split(/: /,$_,2);
  20         55  
1574 20         39 $dev =~ s/\@.+$//; # get rid of bonding master information
1575 20         53 ($dev,$config);
1576             } @ifs;
1577              
1578             # find virtual interfaces
1579 2         8 my (%vnet,%vif);
1580 2         9 for my $dev (keys %ifs) {
1581 20         35 my $info = $ifs{$dev};
1582 20         236 while ($info =~ /inet (\d+\.\d+\.\d+\.\d+)(?:\/(\d+))?.+?(\S+)$/mg) {
1583 18         82 my ($addr,$bits,$vdev) = ($1,$2,$3);
1584 18 50       74 $addr or next;
1585 18   100     54 $bits ||= 32;
1586 18         60 my ($peer) = $info =~ /peer\s+(\d+\.\d+\.\d+\.\d+)/;
1587 18         111 my $block = Net::Netmask->new2("$addr/$bits");
1588 18         1183 $vnet{$dev}{"$block"} = $vdev;
1589 18         249 $vif{$dev}{$vdev}{block} = $block;
1590 18         121 $vif{$dev}{$vdev}{addr} = $addr;
1591             }
1592             }
1593              
1594             # get existing routes
1595 2         6 my (%gws,%nets);
1596 2         19 my $r = $self->_ip_route_show;
1597 2         24 my @routes = split /^(?!\s)/m,$r;
1598 2         6 chomp(@routes);
1599 2         6 foreach (@routes) {
1600 16         51 while (/(\S+)\s+via\s+(\S+)\s+dev\s+(\S+)/g) {
1601 6         24 my ($net,$gateway,$dev) = ($1,$2,$3);
1602 6 100       26 ($net) = /^(\S+)/ if $net eq 'nexthop';
1603 6   33     24 my $vdev = $vnet{$dev}{$net} || $dev;
1604 6 100       30 $nets{$vdev} = $net unless $net eq 'default';
1605 6         28 $gws{$vdev} = $gateway;
1606             }
1607             }
1608              
1609 2         18 for my $dev (keys %ifs) {
1610 20         45 my $info = $ifs{$dev};
1611 20         80 my $running = $info =~ /[<,]UP[,>]/;
1612 20         61 my ($peer) = $info =~ /peer\s+(\d+\.\d+\.\d+\.\d+)/;
1613 20         30 for my $vdev (keys %{$vif{$dev}}) {
  20         84  
1614 18         38 my $addr = $vif{$dev}{$vdev}{addr};
1615 18         32 my $block = $vif{$dev}{$vdev}{block};
1616 18   66     112 my $net = $nets{$dev} || ($peer?"$peer/32":undef) || "$block";
1617 18   33     219 my $gw = $gws{$dev} || $peer
1618             || $self->_dhcp_gateway($dev)
1619             || $block->nth(1); # this guess is correct >95% of time
1620              
1621             # copy into hash passed to us
1622 18         433 $results{$vdev} = {
1623             dev => $dev, # otherwise, iptables will croak!!!
1624             running => $running,
1625             gw => $gw,
1626             net => $net,
1627             ip => $addr,
1628             }
1629             }
1630             }
1631              
1632 2 50       14 $self->{_interface_info_cache} = \%results if ref $self;
1633 2         43 return \%results;
1634             }
1635              
1636             sub _ip_addr_show {
1637 2     2   3 my $self = shift;
1638 2   33     5 return eval{$self->{dummy_data}{"ip_addr_show"}} || `ip addr show`;
1639             }
1640              
1641             sub _ip_route_show {
1642 2     2   6 my $self = shift;
1643 2   33     5 return eval{$self->{dummy_data}{"ip_route_show"}} || `ip route show all`;
1644             }
1645              
1646             # This subroutine is called for dhcp-assigned IP addresses to try to
1647             # get the gateway. It is used for those unusual cases in which the gateway
1648             # is NOT the first IP address in the net block.
1649             # In versions 1.05 and older, we tried to recover this information on static
1650             # interfaces by reading /etc/network/interfaces as well, but the file location was too
1651             # unpredictable across different Linux distros.
1652             sub _dhcp_gateway {
1653 12     12   21 my $self = shift;
1654 12         18 my $dev = shift;
1655 12 50       27 my $fh = $self->_open_dhclient_leases($dev) or return;
1656 0         0 my ($gw);
1657 0         0 while (<$fh>) {
1658 0         0 chomp;
1659 0 0       0 $gw = $1 if /option routers (\S+)[,;]/;
1660             }
1661 0         0 return $gw;
1662             }
1663              
1664             sub _open_dhclient_leases {
1665 12     12   18 my $self = shift;
1666 12         20 my $device = shift;
1667 12 50       16 if (my $dummy = eval{$self->{dummy_data}{"leases_$device"}}) {
  12         51  
1668 0 0       0 open my $fh,'<',\$dummy or die $!;
1669 0         0 return $fh;
1670             }
1671 12 50       30 my $leases = $self->_find_dhclient_leases($device) or return;
1672 0 0       0 open my $fh,$leases or die "Can't open $leases: $!";
1673 0         0 return $fh;
1674             }
1675              
1676             sub _find_dhclient_leases {
1677 12     12   19 my $self = shift;
1678 12         17 my $device = shift;
1679 12         31 my @locations = ('/var/lib/NetworkManager','/var/lib/dhcp','/var/lib/dhclient');
1680 12         21 for my $l (@locations) {
1681 36         1695 my @matches = glob("$l/dhclient*$device.lease*");
1682 36 50       175 next unless @matches;
1683 0         0 return $matches[0];
1684             }
1685 12         114 return;
1686             }
1687              
1688              
1689              
1690             #################################### here are the routing rules ###################
1691              
1692             =head2 $bal->set_routes()
1693              
1694             This method is called by set_routes_and_firewall() to emit the rules
1695             needed to create the load balancing routing tables.
1696              
1697             =cut
1698              
1699             sub set_routes {
1700 0     0 1 0 my $self = shift;
1701 0         0 $self->_initialize_routes();
1702 0         0 $self->routing_rules();
1703 0         0 $self->local_routing_rules();
1704             }
1705              
1706             =head2 $bal->set_firewall
1707              
1708             This method is called by set_routes_and_firewall() to emit the rules
1709             needed to create the balancing firewall.
1710              
1711             =cut
1712              
1713             sub set_firewall {
1714 0     0 1 0 my $self = shift;
1715 0 0       0 $self->_save_custom_chains if $self->keep_custom_chains;
1716 0         0 $self->_initialize_firewall();
1717 0         0 $self->base_fw_rules();
1718 0 0       0 $self->_restore_custom_chains if $self->keep_custom_chains;
1719 0         0 $self->balancing_fw_rules(); # WARNING: This is a null-op in "failover" mode
1720 0         0 $self->sanity_fw_rules();
1721 0         0 $self->nat_fw_rules();
1722 0         0 $self->local_fw_rules();
1723             }
1724              
1725              
1726             =head2 $bal->enable_forwarding($boolean)
1727              
1728             =cut
1729              
1730             sub enable_forwarding {
1731 1     1 1 824 my $self = shift;
1732 1 50       4 my $enable = $_[0] ? 1 : 0;
1733 1         5 $self->sh("echo $enable > /proc/sys/net/ipv4/ip_forward");
1734             }
1735             =head2 $bal->routing_rules()
1736              
1737             This method is called by set_routes() to emit the rules needed to
1738             create the routing rules.
1739              
1740             =cut
1741              
1742             sub routing_rules {
1743 2     2 0 7 my $self = shift;
1744             # main table
1745 2         5 $self->ip_route("add ",$self->net($_),'dev',$self->dev($_),'src',$self->ip($_)) foreach $self->service_names;
1746              
1747             # different handling of the default route depending on whether we are in
1748             # "balanced" or "failover" mode.
1749 2         6 my $mode = $self->operating_mode;
1750 2 100       8 if ($mode eq 'balanced') {
    50          
1751 1         3 $self->_create_default_multipath_route();
1752             } elsif ($mode eq 'failover') {
1753 1         4 $self->_create_default_failover_route();
1754             }
1755              
1756 2         6 $self->_create_service_routing_tables();
1757             }
1758              
1759             sub _initialize_routes {
1760 0     0   0 my $self = shift;
1761 0         0 $self->sh(<
1762             ip route flush all
1763             ip rule flush
1764             ip rule add from all lookup main pref 32766
1765             ip rule add from all lookup default pref 32767
1766             END
1767             ;
1768              
1769 0         0 $self->ip_route("flush table ",$self->table($_)) foreach $self->isp_services;
1770             }
1771              
1772             sub _create_default_multipath_route {
1773 1     1   1 my $self = shift;
1774              
1775 1         4 my @up = $self->up;
1776              
1777             # create multipath route
1778 1 50       4 if (@up > 1) { # multipath
1779 1 50       2 print STDERR "# setting multipath default gw\n" if $self->verbose;
1780             # EG
1781             # ip route add default scope global nexthop via 192.168.10.1 dev eth0 weight 1 \
1782             # nexthop via 192.168.11.1 dev eth1 weight 1
1783 1         2 my $hops = '';
1784 1         2 for my $svc (@up) {
1785 3 50       6 my $gw = $self->gw($svc) or next;
1786 3 50       4 my $dev = $self->dev($svc) or next;
1787 3 50       6 my $weight = $self->weight($svc) or next;
1788 3         10 $hops .= "nexthop via $gw dev $dev weight $weight ";
1789             }
1790 1 50       3 die "no valid gateways!" unless $hops;
1791 1         3 $self->ip_route("add default scope global $hops");
1792             }
1793              
1794             else {
1795 0 0       0 print STDERR "# setting single default route via $up[0]n" if $self->verbose;
1796 0         0 $self->ip_route("add default via",$self->gw($up[0]),'dev',$self->dev($up[0]));
1797             }
1798              
1799             }
1800              
1801             sub _create_default_failover_route {
1802 1     1   2 my $self = shift;
1803 1         3 my $preferred = $self->preferred_service;
1804 1 50       3 print STDERR "# setting single default route via $preferred\n" if $self->verbose;
1805 1         3 $self->ip_route("add default via",$self->gw($preferred),'dev',$self->dev($preferred));
1806             }
1807              
1808             =head2 $service = $bal->preferred_service
1809              
1810             Returns the preferred service, which is the currently running service with the highest weight. Used for
1811             failover mode.
1812              
1813             =cut
1814              
1815             sub preferred_service {
1816 1     1 1 2 my $self = shift;
1817 1         3 my @up = sort { $self->weight($b) <=> $self->weight($a) } $self->up;
  1         4  
1818 1         3 return $up[0];
1819             }
1820              
1821             sub _create_service_routing_tables {
1822 2     2   4 my $self = shift;
1823              
1824 2         4 for my $svc ($self->isp_services) {
1825 5 50       12 print STDERR "# creating routing table for $svc\n" if $self->verbose;
1826 5         12 $self->ip_route('add table',$self->table($svc),'default dev',$self->dev($svc),'via',$self->gw($svc));
1827 5         12 for my $s ($self->service_names) {
1828 36         56 $self->ip_route('add table',$self->table($svc),$self->net($s),'dev',$self->dev($s),'src',$self->ip($s));
1829             }
1830 5         12 $self->ip_rule('add from',$self->ip($svc),'table',$self->table($svc));
1831 5         10 $self->ip_rule('add fwmark',$self->fwmark($svc),'table',$self->table($svc));
1832             }
1833             }
1834              
1835             =head2 $bal->local_routing_rules()
1836              
1837             This method is called by set_routes() to process the fules and emit
1838             the commands contained in the customized route files located in
1839             $ETC_DIR/balance/routes.
1840              
1841             =cut
1842              
1843             sub local_routing_rules {
1844 1     1 1 37 my $self = shift;
1845 1         6 my $dir = $self->rules_directory;
1846 1         99 my @files = sort glob("$dir/routes/*");
1847 1         6 $self->_execute_rules_files(@files);
1848             }
1849              
1850             =head2 $bal->local_fw_rules()
1851              
1852             This method is called by set_firewall() to process the fules and emit
1853             the commands contained in the customized route files located in
1854             $ETC_DIR/balance/firewall.
1855              
1856             =cut
1857              
1858             sub local_fw_rules {
1859 1     1 1 633 my $self = shift;
1860 1         4 my $dir = $self->rules_directory;
1861 1         98 my @files = sort glob("$dir/firewall/*");
1862 1         7 $self->_execute_rules_files(@files);
1863             }
1864              
1865             =head2 $bal->pre_run_rules()
1866              
1867             This method is called by set_routes_and_firewall() to process the fules and emit
1868             the commands contained in the customized route files located in
1869             $ETC_DIR/balance/pre-run.
1870              
1871             =cut
1872              
1873             sub pre_run_rules {
1874 0     0 1 0 my $self = shift;
1875 0         0 my $dir = $self->rules_directory;
1876 0         0 my @files = sort glob("$dir/pre-run/*");
1877 0         0 $self->_execute_rules_files(@files);
1878             }
1879              
1880             =head2 $bal->post_run_rules()
1881              
1882             This method is called by set__routes_andfirewall() to process the
1883             fules and emit the commands contained in the customized route files
1884             located in $ETC_DIR/balance/post-run.
1885              
1886             =cut
1887              
1888             sub post_run_rules {
1889 0     0 1 0 my $self = shift;
1890 0         0 my $dir = $self->rules_directory;
1891 0         0 my @files = sort glob("$dir/post-run/*");
1892 0         0 $self->_execute_rules_files(@files);
1893             }
1894              
1895              
1896             sub _execute_rules_files {
1897 2     2   4 my $self = shift;
1898 2         5 my @files = @_;
1899              
1900 2         5 for my $f (@files) {
1901 5 50       16 next if $f =~ /~$/; # ignore emacs backup files
1902 5 50       10 next if $f =~ /^#/; # ignore autosave files
1903 5 50       8 print STDERR "# executing contents of $f\n" if $self->verbose;
1904 5         18 $self->sh("## Including rules from $f ##\n");
1905 5 50 33     32 next if $f =~ /(~|\.bak)$/ or $f=~/^#/;
1906              
1907 5 100       18 if ($f =~ /\.pl$/) { # perl script
1908 3         5 our $B = $self;
1909 3         966 do $f;
1910 3 50       19 warn $@ if $@;
1911             } else {
1912 2 50       74 open my $fh,$f or die "Couldn't open $f: $!";
1913 2         46 $self->sh($_) while <$fh>;
1914 2         22 close $fh;
1915             }
1916 5         18 $self->sh("## Finished $f ##\n");
1917             }
1918             }
1919              
1920             #########################
1921             # firewall rules
1922             #########################
1923              
1924             sub _initialize_firewall {
1925 0     0   0 my $self = shift;
1926 0         0 $self->sh(<
1927             iptables -F
1928             iptables -X
1929             iptables -t nat -F
1930             iptables -t nat -X
1931             iptables -t mangle -F
1932             iptables -t mangle -X
1933             END
1934             }
1935              
1936             =head2 $bal->base_fw_rules()
1937              
1938             This method is called by set_firewall() to set up basic firewall
1939             rules, including default rules and reporting.
1940              
1941             =cut
1942              
1943             sub base_fw_rules {
1944 0     0 1 0 my $self = shift;
1945 0         0 $self->sh(<
1946             iptables -P INPUT DROP
1947             iptables -P OUTPUT DROP
1948             iptables -P FORWARD DROP
1949              
1950             iptables -N REJECTPERM
1951             iptables -A REJECTPERM -j LOG -m limit --limit 1/minute --log-level 4 --log-prefix "REJECTED: "
1952             iptables -A REJECTPERM -j REJECT --reject-with icmp-net-unreachable
1953              
1954             iptables -N DROPGEN
1955             iptables -A DROPGEN -j LOG -m limit --limit 1/minute --log-level 4 --log-prefix "GENERAL: "
1956             iptables -A DROPGEN -j DROP
1957              
1958             iptables -N DROPINVAL
1959             iptables -A DROPINVAL -j LOG -m limit --limit 1/minute --log-level 4 --log-prefix "INVALID: "
1960             iptables -A DROPINVAL -j DROP
1961              
1962             iptables -N DROPPERM
1963             iptables -A DROPPERM -j LOG -m limit --limit 1/minute --log-level 4 --log-prefix "ACCESS-DENIED: "
1964             iptables -A DROPPERM -j DROP
1965              
1966             iptables -N DROPSPOOF
1967             iptables -A DROPSPOOF -j LOG -m limit --limit 1/minute --log-level 4 --log-prefix "DROP-SPOOF: "
1968             iptables -A DROPSPOOF -j DROP
1969              
1970             iptables -N DROPFLOOD
1971             iptables -A DROPFLOOD -m limit --limit 1/minute -j LOG --log-level 4 --log-prefix "DROP-FLOOD: "
1972             iptables -A DROPFLOOD -j DROP
1973              
1974             iptables -N DEBUG
1975             iptables -A DEBUG -j LOG --log-level 3 --log-prefix "DEBUG: "
1976             END
1977             ;
1978 0 0       0 if ($self->iptables_verbose) {
1979 0 0       0 print STDERR " #Setting up debugging logging\n" if $self->verbose;
1980 0         0 $self->sh(<
1981             iptables -A INPUT -j LOG --log-prefix "INPUT: "
1982             iptables -A OUTPUT -j LOG --log-prefix "OUTPUT: "
1983             iptables -A FORWARD -j LOG --log-prefix "FORWARD: "
1984             iptables -t nat -A INPUT -j LOG --log-prefix "nat INPUT: "
1985             iptables -t nat -A OUTPUT -j LOG --log-prefix "nat OUTPUT: "
1986             iptables -t nat -A FORWARD -j LOG --log-prefix "nat FORWARD: "
1987             iptables -t nat -A PREROUTING -j LOG --log-prefix "nat PREROUTING: "
1988             iptables -t nat -A POSTROUTING -j LOG --log-prefix "nat POSTROUTING: "
1989             iptables -t mangle -A INPUT -j LOG --log-prefix "mangle INPUT: "
1990             iptables -t mangle -A OUTPUT -j LOG --log-prefix "mangle OUTPUT: "
1991             iptables -t mangle -A FORWARD -j LOG --log-prefix "mangle FORWARD: "
1992             iptables -t mangle -A PREROUTING -j LOG --log-prefix "mangle PRE: "
1993             END
1994             ;
1995             }
1996             }
1997              
1998             =head2 $bal->balancing_fw_rules()
1999              
2000             This method is called by set_firewall() to set up the mangle/fwmark
2001             rules for balancing outgoing connections.
2002              
2003             =cut
2004              
2005             sub balancing_fw_rules {
2006 3     3 1 1307 my $self = shift;
2007              
2008 3 100       7 return unless $self->operating_mode eq 'balanced';
2009              
2010 2 50       5 print STDERR "# balancing FW rules\n" if $self->verbose;
2011              
2012 2         5 for my $svc ($self->isp_services) {
2013 6         11 my $table = $self->mark_table($svc);
2014 6         11 my $mark = $self->fwmark($svc);
2015 6 50 33     21 next unless defined $mark && defined $table;
2016 6         18 $self->sh(<
2017             iptables -t mangle -N $table
2018             iptables -t mangle -A $table -j MARK --set-mark $mark
2019             iptables -t mangle -A $table -j CONNMARK --save-mark
2020             END
2021             }
2022              
2023 2         6 my @up = $self->up;
2024              
2025             # packets from LAN
2026 2         4 for my $lan ($self->lan_services) {
2027 8         18 my $landev = $self->dev($lan);
2028 8         15 my $src = $self->net($lan);
2029            
2030 8 100       13 if (@up > 1) {
2031 4 50       8 print STDERR "# creating balanced mangling rules\n" if $self->verbose;
2032 4         7 my $count = @up;
2033 4         8 my $probabilities = $self->_weight_to_probability(\@up);
2034 4         9 for my $svc (sort {$probabilities->{$b} <=> $probabilities->{$a}} @up) {
  8         13  
2035 12         20 my $table = $self->mark_table($svc);
2036 12         18 my $probability = $probabilities->{$svc};
2037 12         49 $self->iptables("-t mangle -A PREROUTING -i $landev -s $src -m conntrack --ctstate NEW -m statistic --mode random --probability $probability -j $table");
2038             }
2039             }
2040              
2041             else {
2042 4         5 my $svc = $up[0];
2043 4 50       8 print STDERR "# forcing all traffic through $svc\n" if $self->verbose;
2044 4         9 my $table = $self->mark_table($svc);
2045 4         10 $self->iptables("-t mangle -A PREROUTING -i $landev -s $src -m conntrack --ctstate NEW -j $table");
2046             }
2047              
2048 8         17 $self->iptables("-t mangle -A PREROUTING -i $landev -s $src -m conntrack --ctstate ESTABLISHED,RELATED -j CONNMARK --restore-mark");
2049             }
2050              
2051             # inbound packets from WAN
2052 2         5 for my $wan ($self->isp_services) {
2053 6         8 my $dev = $self->dev($wan);
2054 6         19 my $table = $self->mark_table($wan);
2055 6         12 my $src = $self->net($wan);
2056 6         15 $self->iptables("-t mangle -A PREROUTING -i $dev -m conntrack --ctstate NEW -j $table");
2057 6         12 $self->iptables("-t mangle -A PREROUTING -i $dev -m conntrack --ctstate ESTABLISHED,RELATED -j CONNMARK --restore-mark");
2058             }
2059              
2060             }
2061              
2062             sub _weight_to_probability {
2063 4     4   7 my $self = shift;
2064 4         4 my $svcs = shift;
2065              
2066             # first turn weights into proportions of the total
2067 4         7 my %weights = map {$_ => $self->weight($_)} @$svcs;
  12         16  
2068 4         7 my $total = 0;
2069 4         13 $total += $_ foreach (values %weights);
2070 4         7 my %proportions = map {$_ => $weights{$_}/$total} keys %weights;
  12         32  
2071              
2072             # now turn them into probabilities
2073 4         8 my %probabilities;
2074              
2075 4         5 my $last = 0;
2076 4 0       12 for (sort {$proportions{$a}<=>$proportions{$b} || $a cmp $b} keys %proportions) {
  12         28  
2077 12         15 my $threshold = $proportions{$_}/(1-$last);
2078 12         15 $last += $proportions{$_};
2079 12         19 $probabilities{$_} = $threshold;
2080             }
2081 4         11 return \%probabilities;
2082             }
2083              
2084             =head2 $bal->sanity_fw_rules()
2085              
2086             This is called by set_firewall() to create a sensible series of
2087             firewall rules that seeks to prevent spoofing, flooding, and other
2088             antisocial behavior. It also enables UDP-based network time and domain
2089             name service.
2090              
2091             =cut
2092              
2093             sub sanity_fw_rules {
2094 1     1 1 22 my $self = shift;
2095              
2096             # if any of the devices are ppp, then we clamp the mss
2097 1         3 my @ppp_devices = grep {/ppp\d+/} map {$self->dev($_)} $self->isp_services;
  3         10  
  3         5  
2098             $self->iptables("-t mangle -A POSTROUTING -o $_ -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu")
2099 1         5 foreach @ppp_devices;
2100              
2101             # lo is ok
2102 1         5 $self->iptables(['-A INPUT -i lo -j ACCEPT',
2103             '-A OUTPUT -o lo -j ACCEPT',
2104             '-A INPUT -d 127.0.0.0/8 -j DROPPERM']);
2105              
2106             # accept continuing foreign traffic
2107 1         4 $self->iptables(['-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT',
2108             '-A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT',
2109             '-A INPUT -p tcp --tcp-flags SYN,ACK ACK -j ACCEPT',
2110             '-A FORWARD -p tcp --tcp-flags SYN,ACK ACK -j ACCEPT',
2111             '-A FORWARD -p tcp --tcp-flags SYN,ACK,FIN,RST RST -j ACCEPT'
2112             ]);
2113              
2114             # we allow ICMP echo, but establish flood limits
2115 1         3 $self->iptables(['-A INPUT -p icmp --icmp-type echo-request -m limit --limit 1/s -j ACCEPT',
2116             '-A INPUT -p icmp --icmp-type echo-request -j DROPFLOOD']);
2117              
2118             # allowable traffic patterns within the LAN services
2119 1         3 for my $lan ($self->lan_services) {
2120 4         9 my $dev = $self->dev($lan);
2121 4         18 my $net = $self->net($lan);
2122              
2123             # allow unlimited traffic from internal network using legit address
2124 4         13 $self->iptables("-A INPUT -i $dev -s $net -j ACCEPT");
2125              
2126             # allow locally-generated output to the LAN on the LANDEV
2127 4         12 $self->iptables("-A OUTPUT -o $dev -d $net -j ACCEPT");
2128              
2129             # and allow broadcasts to the lan
2130 4         14 $self->iptables("-A OUTPUT -o $dev -d 255.255.255.255/32 -j ACCEPT");
2131              
2132             # any outgoing udp packet is fine with me
2133 4         12 $self->iptables("-A OUTPUT -p udp -s $net -j ACCEPT");
2134             }
2135              
2136             # allow appropriate outgoing traffic via the ISPs
2137             # NOTE: we use svc_config here so that we allow outgoing traffic
2138             # on interfaces that might be down
2139             # for my $svc ($self->isp_services) {
2140             # my $ispdev = $self->dev($svc);
2141             # $self->iptables("-A OUTPUT -o $ispdev -j ACCEPT");
2142             # }
2143 1         2 for my $svc (keys %{$self->{svc_config}}) {
  1         5  
2144 8 100       17 next unless $self->{svc_config}{$svc}{role} eq 'isp';
2145 4         6 my $ispdev = $self->{svc_config}{$svc}{dev};
2146 4         9 $self->iptables("-A OUTPUT -o $ispdev -j ACCEPT");
2147             }
2148              
2149             # forwarding rules
2150 1         5 $self->_lan_wan_forwarding_rules();
2151 1         6 $self->_lan_lan_forwarding_rules();
2152              
2153             # anything else is bizarre and should be dropped
2154 1         4 $self->iptables('-A OUTPUT -j DROPSPOOF');
2155             }
2156              
2157             # establish expected traffic patterns between lan(s) and isp interfaces
2158             sub _lan_wan_forwarding_rules {
2159 1     1   1 my $self = shift;
2160              
2161 1         9 for my $lan ($self->lan_services) {
2162 4         15 my $dev = $self->dev($lan);
2163 4         7 my $net = $self->net($lan);
2164              
2165             # lan/wan forwarding
2166             # allow lan/wan forwarding
2167 4         8 for my $svc ($self->isp_services) {
2168 12         27 my $ispdev = $self->dev($svc);
2169 12 50       32 my $target = $self->_allow_forwarding($lan,$svc) ? 'ACCEPT' : 'REJECTPERM';
2170 12         48 $self->iptables("-A FORWARD -i $dev -o $ispdev -s $net -j $target");
2171             }
2172             }
2173             }
2174              
2175             # Allow forwarding between lans
2176             sub _lan_lan_forwarding_rules {
2177 1     1   2 my $self = shift;
2178              
2179             # This generates a very long list of rules if you have multiple lan services, but I think
2180             # it is the most general way to get this right.
2181 1         5 my @lans = $self->lan_services;
2182 1         7 for (my $i=0;$i<@lans;$i++) {
2183 4         13 for (my $j=0;$j<@lans;$j++) {
2184 16 100       59 next if $i == $j;
2185 12         24 my $lan1 = $lans[$i];
2186 12         22 my $lan2 = $lans[$j];
2187 12 50       31 my $target = $self->_allow_forwarding($lan1,$lan2) ? 'ACCEPT' : 'REJECTPERM';
2188 12         90 $self->iptables('-A FORWARD','-i',$self->dev($lan1),'-o',$self->dev($lan2),'-s',$self->net($lan1),'-d',$self->net($lan2),"-j $target");
2189             }
2190             }
2191             }
2192              
2193             sub _allow_forwarding {
2194 24     24   39 my $self = shift;
2195 24         50 my ($net_a,$net_b) = @_;
2196 24         48 my $forward = $self->_forwarding_groups();
2197 24         87 my $key = join ',',sort ($net_a,$net_b);
2198 24         98 return $forward->{$key};
2199             }
2200              
2201             # this returns a hashref of service pairs that are allowed to forward packets.
2202             # the keys are service name pairs, in alphabetic order, separated by a comma.
2203             sub _forwarding_groups {
2204 24     24   38 my $self = shift;
2205              
2206             # _forwarding_groups is the processed and normalized version of forwarding_groups
2207 24 100       66 return $self->{_forwarding_groups} if exists $self->{_forwarding_groups};
2208              
2209 1         1 my %allowed_pairs;
2210 1         2 my $fgs = $self->{forwarding_groups};
2211 1 50       3 unless (@$fgs) {
2212 1         3 $fgs = [[':isp',':lan']];
2213             }
2214              
2215 1         2 for my $fg (@$fgs) {
2216             my @services = map {
2217 1 50       2 /^:isp$/ ? $self->isp_services
  2 100       11  
2218             : /^:lan$/ ? $self->lan_services
2219             : $_
2220             } @$fg;
2221              
2222 1         4 for (my $i=0;$i<@services-1;$i++) {
2223 6         17 for (my $j=$i;$j<@services;$j++) {
2224 27         50 my $key = join ',',sort ($services[$i],$services[$j]);
2225 27         67 $allowed_pairs{$key}++;
2226             }
2227             }
2228             }
2229 1         4 return $self->{_forwarding_groups} = \%allowed_pairs;
2230             }
2231              
2232             =head2 $bal->nat_fw_rules()
2233              
2234             This is called by set_firewall() to set up basic NAT rules for lan traffic over ISP
2235              
2236             =cut
2237              
2238             sub nat_fw_rules {
2239 0     0 1   my $self = shift;
2240 0 0         return unless $self->lan_services;
2241             $self->iptables('-t nat -A POSTROUTING -o',$self->dev($_),'-j MASQUERADE')
2242 0           foreach $self->isp_services;
2243             }
2244              
2245             =head2 $bal->start_lsm()
2246              
2247             Start an lsm process.
2248              
2249             =cut
2250              
2251             sub start_lsm {
2252 0     0 1   my $self = shift;
2253 0           my $lsm = Net::ISP::Balance::ConfigData->config('lsm_path');
2254 0           my $lsm_conf = $self->lsm_conf_file;
2255 0           system "$lsm $lsm_conf /var/run/lsm.pid";
2256             }
2257              
2258             =head2 $bal->signal_lsm($signal)
2259              
2260             Send a signal to a running LSM and return true if successfully
2261             signalled. The signal can be numeric (e.g. 9) or a string ('TERM').
2262              
2263             =cut
2264              
2265             sub signal_lsm {
2266 0     0 1   my $self = shift;
2267 0           my $signal = shift;
2268 0   0       $signal ||= 0;
2269 0           my $pid;
2270 0 0         open my $f,'/var/run/lsm.pid' or return;
2271 0           chomp($pid = <$f>);
2272 0           close $f;
2273 0 0         return unless $pid =~ /^\d+$/;
2274 0           return kill($signal=>$pid);
2275             }
2276              
2277              
2278             1;
2279              
2280             =head1 BUGS
2281              
2282             Please report bugs to GitHub: https://github.com/lstein/Net-ISP-Balance.
2283              
2284             =head1 AUTHOR
2285              
2286             Copyright 2014, Lincoln D. Stein (lincoln.stein@gmail.com)
2287              
2288             Senior Principal Investigator,
2289             Ontario Institute for Cancer Research
2290              
2291             =head1 LICENSE
2292              
2293             This package is distributed under the terms of the Perl Artistic
2294             License 2.0. See http://www.perlfoundation.org/artistic_license_2_0.
2295              
2296             =cut
2297              
2298             __END__