File Coverage

lib/Net/ISP/Balance.pm
Criterion Covered Total %
statement 493 739 66.7
branch 116 264 43.9
condition 34 79 43.0
subroutine 74 106 69.8
pod 61 72 84.7
total 778 1260 61.7


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