Lincoln D. Stein

NAME

Net::ISP::Balance - Support load balancing across multiple internet service providers

SYNOPSIS

 use Net::ISP::Balance;

 # initialize the module with its configuration file
 my $bal = Net::ISP::Balance->new('/etc/network/balance.conf');

 $bal->verbose(1);    # verbosely print routing and firewall 
                      #  commands to STDERR before running them.
 $bal->echo_only(1);  # echo commands to STDOUT; don't execute them.

 # mark the balanced services that are up
 $bal->up('CABLE','DSL','SATELLITE');

 # write out routing and firewall commands
 $bal->set_routes_and_firewall();

 # write out a forwarding rule
 $bal->forward(80 => '192.168.10.35');  # forward web requests to this host

 # write out an arbitrary routing rule
 $bal->ip_route('add 192.168.100.1  dev eth0 src 198.162.1.14');

 # write out an arbitrary iptables rule
 $bal->iptables('-A INCOMING -p tcp --dport 6000 -j REJECT');

 # get information about all services
 my @s = $bal->service_names;
 for my $s (@s) {
    print $bal->dev($s);
    print $bal->ip($s);
    print $bal->gw($s);
    print $bal->net($s);
    print $bal->fwmark($s);
    print $bal->table($s);
    print $bal->running($s);
    print $bal->weight($s);
 }

USAGE

This library supports load_balance.pl, a script to load-balance a home network across two or more Internet Service Providers (ISP). The load_balance.pl script can be found in the bin subdirectory of this distribution. Installation and configuration instructions can be found at http://lstein.github.io/Net-ISP-Balance/.

CONFIGURATION FILE

This module reads a configuration file with the following format:

 #service    device   role     ping-ip           weight    gateway
 CABLE        eth0     isp      173.194.43.95     1        173.193.43.1
 DSL          ppp0     isp      173.194.43.95     1
 LAN1         eth1     lan                        
 LAN2         eth2     lan                        
 LAN3         eth3     lan                        

The first column is a service name that is used to bring up or down the needed routes and firewall rules.

The second column is the name of the network interface device that connects to that service.

The third column is either "isp" or "lan". There may be any number of these. The script will firewall traffic passing through any of the ISPs, and will load balance traffic among them. Traffic can flow freely among any of the interfaces marked as belonging to a LAN.

The fourth column (optional) is the IP address of a host that can be periodically pinged to test the integrity of each ISP connection. If too many pings failed, the service will be brought down and all traffic routed through the remaining ISP(s). The service will continue to be monitored and will be brought up when it is once again working. Choose a host that is not likely to go offline for reasons unrelated to your network connectivity, such as google.com, or the ISP's web site. If this column is absent or marked "default", then the host will default to www.google.ca.

The fifth column (optional) is a weight to assign to the service, and is only valid for ISP rows. If weights are equal, traffic will be apportioned evenly between the two routes. Increase a weight to favor one ISP over the others. For example, if "CABLE" has a weight of 2 and "DSL" has a weight of 1, then twice as much traffic will flow through the CABLE service. If this column is omitted or marked "default", then equal weights are assumed.

The sixth column (optional) is the gateway for this service using dotted IP notation. If absent or named "default", the system will attempt to determine the proper gateway automatically. Note the algorithm relies on the fact that the gateway is almost always the first address in the IP range for the subnetwork. If this is not the case, then routing through the interface won't work properly. Add the correct gateway IP address manually to correct this.

If this package is running on a single Internet-connected host, not a router, then do not include a "lan" line.

In addition to the main table, there are several configuration options that follow the format "configuration_name=value":

forwarding_group=<space-delimited list of services>

The forwarding_group configuration option defines a set of services that the router is allowed to forward packets among. Provide a space-delimited set of service names or one or more of the abbreviations ":isp" and ":lan". ":isp" is an abbreviation for all ISP services, while ":lan" is an abbreviation for all LAN services. So for example, the two configuration lines below will allow forwarding of packets between LAN1, LAN2, LAN3 and both ISPs. LAN4 will be granted access to both ISPs but won't be able to exchange packets with LANs 1 through 3:

 forwarding_group=LAN1 LAN2 LAN3 :isp
 forwarding_group=LAN4 :isp

If no forwarding_group options are defined, then the router will forward packets among all LANs and ISP interfaces. It is equivalent to this:

 forwarding_group=:lan :isp
warn_email=<email address>

Warn_email provides an email address to send notification messages to if the status of a link changes (goes down, or comes back up). You must have the "mail" program installed and configured for this to work.

interval_ms=<integer>

Indicates how often to check the ping host for each ISP.

min_packet_loss=<integer>
max_packet_loss=<integer>

These define the minimum and maximum packet losses required to declare a link up or down.

min_successive_pkts_rcvd=<integer>
max_successive_pkts_recvd=<integer>

These define the minimum and maximum numbers of successively-transmitted pings that must be returned in order to declare a link up or down.

long_down_time=<integer>

This is a value in seconds after a service that has gone down is considered to have been down for a long time. You may optionally run a series of shell scripts when this has occurred (see below).

FREQUENTLY-USED METHODS

Here are the class methods for this module that can be called on the class name.

$bal = Net::ISP::Balance->new('/path/to/config_file.conf');

Creates a new balancer object.

The first optional argument is the balancer configuration file, which defaults to /etc/network/balance.conf on Ubuntu/Debian-derived systems, and /etc/sysconfig/network-scripts/balance.conf on RedHat/CentOS-derived systems. From hereon, we'll refer to the base of the various configuration files as $ETC_NETWORK.

$bal->set_routes_and_firewall

Once the Balance objecty is created, call set_routes_and_firewall() to configure the routing tables and firewall for load balancing. These rules will either be executed on the system, or printed to standard output as a series of shell script commands if echo_only() is set to true.

The routing tables and firewall rules are based on the configuration described in $ETC_NETWORK/balance.conf. You may add custom routes and rules by creating files in $ETC_NETWORK/balance/routes and $ETC_NETWORK/balance/firewall. The former contains a series of files or perl scripts that define additional routing rules. The latter contains files or perl scripts that define additional firewall rules.

Files located in $ETC_NETWORK/balance/pre-run will be executed AFTER load_balance.pl has cleared the routing table and firewall, but before it has emitted any any route/firewall commands. Files located in in $ETC_NETWORK/balance/post-run will be run after load_balance.pl is finished.

Any files you put into these directories will be read in alphabetic order and added to the routes and/or firewall rules emitted by the load balancing script.Contained in this directory are subdirectories named "routes" and "firewall". The former contains a series of files or perl scripts that define additional routing rules. The latter contains files or perl scripts that define additional firewall rules.

Note that files ending in ~ or starting with # are treated as autosave files and ignored.

A typical routing rules file will look like the example shown below.

 # file: /etc/network/balance/01.my_routes
 ip route add 192.168.100.1  dev eth0 src 198.162.1.14
 ip route add 192.168.1.0/24 dev eth2 src 10.0.0.4

Each line will be sent to the shell, and it is intended (but not required) that these be calls to the "ip" command. General shell scripting constructs are not allowed here.

A typical firewall rules file will look like the example shown here:

 # file: /etc/network/firewall/01.my_firewall_rules

 # accept incoming telnet connections to the router
 iptable -A INPUT -p tcp --syn --dport telnet -j ACCEPT

 # masquerade connections to the DSL modem's control interface
 iptables -t nat -A POSTROUTING -o eth2 -j MASQUERADE

You may also insert routing and firewall rules via fragments of Perl code, which is convenient because you don't have to hard-code any network addresses and can make use of a variety of shortcuts. To do this, simply end the file's name with .pl and make it executable.

Here's an example that defines a series of port forwarding rules for incoming connections:

 # file: /etc/network/firewall/02.forwardings.pl 

 $B->forward(80 => '192.168.10.35'); # forward port 80 to internal web server
 $B->forward(443=> '192.168.10.35'); # forward port 443 to 
 $B->forward(23 => '192.168.10.35:22'); # forward port 23 to ssh on  web sever

The main thing to know is that on entry to the script the global variable $B will contain an initialized instance of a Net::ISP::Balance object. You may then make method calls on this object to emit firewall and routing rules.

A typical routing rules file will look like the example shown below.

 # file: /etc/network/balance/01.my_routes
 ip route add 192.168.100.1  dev eth0 src 198.162.1.14
 ip route add 192.168.1.0/24 dev eth2 src 10.0.0.4

Each line will be sent to the shell, and it is intended (but not required) that these be calls to the "ip" command. General shell scripting constructs are not allowed here.

A typical firewall rules file will look like the example shown here:

 # file: /etc/network/firewall/01.my_firewall_rules

 # accept incoming telnet connections to the router
 iptable -A INPUT -p tcp --syn --dport telnet -j ACCEPT

 # masquerade connections to the DSL modem's control interface
 iptables -t nat -A POSTROUTING -o eth2 -j MASQUERADE

You may also insert routing and firewall rules via fragments of Perl code, which is convenient because you don't have to hard-code any network addresses and can make use of a variety of shortcuts. To do this, simply end the file's name with .pl and make it executable.

Here's an example that defines a series of port forwarding rules for incoming connections:

 # file: /etc/network/firewall/02.forwardings.pl 

 $B->forward(80 => '192.168.10.35'); # forward port 80 to internal web server
 $B->forward(443=> '192.168.10.35'); # forward port 443 to 
 $B->forward(23 => '192.168.10.35:22'); # forward port 23 to ssh on  web sever

The main thing to know is that on entry to the script the global variable $B will contain an initialized instance of a Net::ISP::Balance object. You may then make method calls on this object to emit firewall and routing rules.

$verbose = $bal->verbose([boolean]);

sub bal_conf_file { my $self = shift; my $d = $self->{bal_conf_file}; $self->{bal_conf_file} = shift if @_; $d; } Get/set verbosity of the module. If verbose is true, then firewall and routing rules will be echoed to STDERR before being executed on the system.

$echo = $bal->echo_only([boolean]);

Get/set the echo_only flag. If this is true (default false), then routing and firewall rules will be printed to STDOUT rathar than being executed.

$mode = $bal->operating_mode([$mode])

Set or interrogate the operating mode. Will return one of "balanced" (currently the default) or "failover". This corresponds to the "mode" option in the configuration file. If the option is neither "balanced" nor "failover", then "balanced" is chosen (be warned!)

$retries = $bal->dev_lookup_retries([$retries])

Get/set the number of times the library will try to look up an interface that is not up or does not have an IP address. Default is 10

$seconds = $bal->dev_lookup_retry_delay([$seconds])

Get/set the number of seconds between retries when an interface is not up or is missing an IP address. Default is 1.

$boolean = $bal->keep_custom_chains([boolean]);

Get/set the keep_custom_chains flag. If this is true (default), then any custom iptables chains, such as those created by miniunpnpd or fail2ban, will be restored after execution of the firewall rules. If false, then these rules were be flushed.

$result_code = $bal->sh(@args)

Pass @args to the shell for execution. If echo_only() is set to true, the command will not be executed, but instead be printed to standard output.

Example:

 $bal->sh('ip rule flush');

The result code is the same as CORE::system().

$bal->iptables(@args)

Invoke sh() to call "iptables @args".

Example:

 $bal->iptables('-A OUTPUT -o eth0 -j DROP');

You may pass an array reference to iptables(), in which case iptables is called on each member of the array in turn.

Example:

 $bal->iptables(['-P OUTPUT  DROP',
                 '-P INPUT   DROP',
                 '-P FORWARD DROP']);

Note that the method keeps track of rules; if you try to enter the same iptables rule more than once the redundant ones will be ignored.

$bal->firewall_rule($chain,$table,@args)

Issue an iptables firewall rule.

 $chain -- The chain to apply the rule to, e.g. "INPUT". 
 
 $table -- The table to apply the rule to, e.g. "nat". Undef defaults to
           the standard "filter" table.

 @args  -- The other arguments to pass to iptables.

Here is a typical example of blocking incoming connections to port 25:

 $bal->firewall_rule(INPUT=>undef,-p=>'tcp',-dport=>25,-j=>'REJECT');

This will issue the following command:

 iptables -A INPUT -p tcp --dport 25 -j REJECT

The default operation is to append the rule to the chain using -A. This can be changed by passing $bal->firewall_op() any of the strings "append", "delete", "insert" or "check". Subsequent calls to firewall_rule() will return commands for the indicated function:

 $bal->firewall_op('delete');
 $bal->firewall_rule(INPUT=>undef,-p=>'tcp',-dport=>25,-j=>'REJECT');
 # gives  iptables -A INPUT -p tcp --dport 25 -j REJECT

If you want to apply a series of deletes and then revert to the original append behavior, then it is easiest to localize the hash key "firewall_op":

 {
   local $bal->{firewall_op} = 'delete';
   $bal->firewall_rule(INPUT=>undef,-dport=>25,-j=>'ACCEPT');
   $bal->firewall_rule(INPUT->undef,-dport=>80,-j=>'ACCEPT');
 }
 
   $bal->firewall_rule(INPUT=>undef,-dport=>25,-j=>'DROP');
   $bal->firewall_rule(INPUT=>undef,-dport=>80,-j=>'DROP');

$bal->force_route($service_or_device,@selectors)

The force_route() method issues iptables commands that will force certain traffic to travel over a particular ISP service or network device. This is useful, for example, when one of your ISPs acts as your e-mail relay and only accepts connections from the IP address it assigns.

$service_or_device is the symbolic name of an ISP service (e.g. "CABLE") or a network device that a service is attached to (e.g. "eth0").

@selectors are a series of options that will be passed to iptables to select the routing of packets. For example, to forward all outgoing mail (destined to port 25) to the "CABLE" ISP, you would write:

    $bal->force_route('CABLE','-p'=>'tcp','--syn','--dport'=>25);

@selectors is a series of optional arguments that will be passed to iptables on the command line. They will simply be space-separated, and so the following is equivalent to the previous example:

    $bal->force_route('CABLE','-p tcp --syn --dport 25');

Bare arguments that begin with a leading hyphen and are followed by two or more alphanumeric characters are automatically converted into double-hyphen arguments. This allows you to simplify commands slightly. The following is equivalent to the previous examples:

    $bal->force_route('CABLE',-p=>'tcp',-syn,-dport=>25);

You can delete force_route rules by setting firewall_op() to 'delete':

    $bal->firewall_op('delete');
    $bal->force_route('CABLE',-p=>'tcp',-syn,-dport=>25);

$bal->add_route($address => $device, [$masquerade])

This method is used to create routing and firewall rules for a network that isn't mentioned in balance.conf. This may be necessary to route to VPNs and/or to the control interfaces of attached modems.

The first argument is the network address in CIDR format, e.g. '192.168.2.0/24'. The second is the network interface that the network can be accessed via. The third, optional, argument is a boolean. If true, then firewall rules will be set up to masquerade from the LAN into the attached network.

Note that this is pretty limited. If you want to do anything more sophisticated you're better off setting the routes and firewall rules manually.

$table_name = $bal->mark_table($service)

This returns the iptables table name for connections marked for output on a particular ISP service. The name is simply the word "MARK-" appended to the service name. For example, for a service named "DSL", the corresponding firewall table will be named "MARK-DSL".

$bal->forward($incoming_port,$destination_host,@protocols)

This method emits appropriate port/host forwarding rules using DNAT address translation. The destination host can be specified using either of these forms:

  192.168.100.1       # forward to same port as incoming
  192.168.100.1:8080  # forward to a different port on host

Protocols are one or more of 'tcp','udp'. If omitted defaults to tcp.

Examples:

    $bal->forward(80 => '192.168.100.1');
    $bal->forward(80 => '192.168.100.1:8080','tcp');

$bal->forward_with_snat($incoming_port,$destination_host,@protocols)

This method is the same as forward(), except that it also does source NATing from LAN-based requests to make the request appear to have come from the router. This is used when you expose a server, such as a web server, to the internet, but you also need to access the server from machines on the LAN. Use this if you find that the service is visible from outside the LAN but not inside the LAN.

Examples:

    $bal->forward_with_snat(80 => '192.168.100.1');
    $bal->forward_with_snat(80 => '192.168.100.1:8080','tcp');

$bal->ip_route(@args)

Shortcut for $bal->sh('ip route',@args);

$bal->ip_rule(@args)

Shortcut for $bal->sh('ip rule',@args);

$verbose = $bal->iptables_verbose([boolean])

Makes iptables send an incredible amount of debugging information to syslog.

QUERYING THE CONFIGURATION

These methods allow you to get information about the Net::ISP::Balance object's configuration, including settings and other characteristics of the various network interfaces.

@names = $bal->service_names

Return the list of service names defined in balance.conf.

@names = $bal->isp_services

Return list of service names that correspond to load-balanced ISPs.

@names = $bal->lan_services

Return list of service names that correspond to lans.

$state = $bal->event($service => $new_state)

Record a transition between "up" and "down" for a named service. The first argument is the name of the ISP service that has changed, e.g. "CABLE". The second argument is either "up" or "down".

The method returns a hashref in which the keys are the ISP service names and the values are one of 'up' or 'down'.

The persistent state information is stored in /var/lib/lsm/ under a series of files named <SERVICE_NAME>.state.

$bal->run_eventd(@args)

Runs scripts in response to lsm events. The scripts are stored in directories named after the events, e.g.:

 /etc/network/lsm/up.d/*
 /etc/network/lsm/down.d/*
 /etc/network/lsm/long_down.d/*

Scripts are called with the following arguments:

  0. STATE
  1. SERVICE NAME
  2. CHECKIP
  3. DEVICE
  4. WARN_EMAIL
  5. REPLIED
  6. WAITING
  7. TIMEOUT
  8. REPLY_LATE
  9. CONS_RCVD
 10. CONS_WAIT
 11. CONS_MISS
 12. AVG_RTT
 13. SRCIP
 14. PREVSTATE
 15. TIMESTAMP

@up = $bal->up(@up_services)

Get or set the list of ISP interfaces that are currently active and should be used for balancing.

$services = $bal->services

Return a hash containing the configuration information for each service. The keys are the service names. Here's an example:

 {
 0  HASH(0x91201e8)
   'CABLE' => HASH(0x9170500)
      'dev' => 'eth0'
      'fwmark' => 2
      'gw' => '191.3.88.1'
      'ip' => '191.3.88.152'
      'net' => '191.3.88.128/27'
      'ping' => 'www.google.ca'
      'role' => 'isp'
      'running' => 1
      'table' => 2
   'DSL' => HASH(0x9113e00)
      'dev' => 'ppp0'
      'fwmark' => 1
      'gw' => '112.211.154.198'
      'ip' => '11.120.199.108'
      'net' => '112.211.154.198/32'
      'ping' => 'www.google.ca'
      'role' => 'isp'
      'running' => 1
      'table' => 1
   'LAN' => HASH(0x913ce58)
      'dev' => 'eth1'
      'fwmark' => undef
      'gw' => '192.168.10.1'
      'ip' => '192.168.10.1'
      'net' => '192.168.10.0/24'
      'ping' => ''
      'role' => 'lan'
      'running' => 1
 }

$service = $bal->service('CABLE')

Return the subhash describing the single named service (see services() above).

$dev = $bal->dev('CABLE')

$ip = $bal->ip('CABLE')

$gateway = $bal->gw('CABLE')

$network = $bal->net('CABLE')

$role = $bal->role('CABLE')

$running = $bal->running('CABLE')

$mark_number = $bal->fwmark('CABLE')

$routing_table_number = $bal->table('CABLE')

$ping_dest = $bal->ping('CABLE')

These methods pull out the named information from the configuration data. fwmark() returns a small integer that will be used for marking connections for routing through one of the ISP connections when an outgoing connection originates on the LAN and is routed through the router. table() returns a small integer corresponding to a routing table used to route connections originating on the router itself.

FILES AND PATHS

These are methods that determine where Net::ISP::Balance finds its configuration files.

$path = Net::ISP::Balance->install_etc

Returns the path to where the network configuration files reside on this system, e.g. /etc/network. Note that this only knows about Ubuntu/Debian-style network configuration files in /etc/network, and RedHat/CentOS network configuration files in /etc/sysconfig/network-scripts.

$file = Net::ISP::Balance->default_conf_file

Returns the path to the default configuration file, $ETC_NETWORK/balance.conf.

$dir = Net::ISP::Balance->default_rules_directory

Returns the path to the directory where the additional router and firewall rules are stored. On Ubuntu-Debian-derived systems, this is /etc/network/balance/. On RedHat/CentOS systems, this is /etc/sysconfig/network-scripts/balance/.

$file = Net::ISP::Balance->default_lsm_conf_file

Returns the path to the place where we should store lsm.conf, the file used to configure the lsm (link status monitor) application.

On Ubuntu/Debian-derived systems, this will be the file /etc/network/lsm.conf. On RedHad/CentOS-derived systems, this will be /etc/sysconfig/network-scripts/lsm.conf.

$dir = Net::ISP::Balance->default_lsm_scripts_dir

Returns the path to the place where lsm stores its helper scripts. On Ubuntu/Debian-derived systems, this will be the directory /etc/network/lsm/. On RedHad/CentOS-derived systems, this will be /etc/sysconfig/network-scripts/lsm/.

$file = $bal->bal_conf_file([$new_file])

Get/set the main configuration file path, balance.conf.

$dir = $bal->rules_directory([$new_rules_directory])

Get/set the route and firewall rules directory.

$file = $bal->lsm_conf_file([$new_conffile])

Get/set the path to the lsm configuration file.

$dir = $bal->lsm_scripts_dir([$new_dir])

Get/set the path to the lsm scripts directory.

INFREQUENTLY-USED METHODS

These are methods that are used internally, but may be useful to applications developers.

$lsm_config_text = $bal->lsm_config_file(-warn_email=>'root@localhost')

This method creates the text used to create the lsm.conf configuration file. Pass it a series of -name=>value pairs to incorporate into the file.

Possible switches and their defaults are:

    -checkip                    127.0.0.1
    -eventscript                /etc/network/load_balance.pl
    -long_down_eventscript      /etc/network/load_balance.pl
    -notifyscript               /etc/network/balance/lsm/default_script
    -max_packet_loss            15
    -max_successive_pkts_lost    7
    -min_packet_loss             5
    -min_successive_pkts_rcvd   10
    -interval_ms              1000
    -timeout_ms               1000
    -warn_email               root
    -check_arp                   0
    -sourceip                 <autodiscovered>
    -device                   <autodiscovered>                      -eventscript          => $balance_script,
    -ttl                      0 <use system value>
    -status                   2 <no assumptions>
    -debug                    8 <moderate verbosity from scale of 0 to 100>

$if_hash = $bal->interface_info

$if_hash = Net::ISP::Balance->interface_info

This method returns a hashref containing information about each of the network interfaces found on the system (independent of those mentioned in the configuration file). It may be called as a class method or an instance method.

Each key in the hash is the name of a (virtual) interface device. The values are hashrefs with the following keys:

  key       value
  ---       -----
  dev       name of the underlying physical device (usually same as vdev)
  running   boolean, true if interface is running
  gw        gateway, if present
  net       subnet in xxx.xxx.xxx.xxx/xx

$bal->set_routes()

This method is called by set_routes_and_firewall() to emit the rules needed to create the load balancing routing tables.

$bal->set_firewall

This method is called by set_routes_and_firewall() to emit the rules needed to create the balancing firewall.

$bal->enable_forwarding($boolean)

$bal->routing_rules()

This method is called by set_routes() to emit the rules needed to create the routing rules.

$service = $bal->preferred_service

Returns the preferred service, which is the currently running service with the highest weight. Used for failover mode.

$bal->local_routing_rules()

This method is called by set_routes() to process the fules and emit the commands contained in the customized route files located in $ETC_DIR/balance/routes.

$bal->local_fw_rules()

This method is called by set_firewall() to process the fules and emit the commands contained in the customized route files located in $ETC_DIR/balance/firewall.

$bal->pre_run_rules()

This method is called by set_routes_and_firewall() to process the fules and emit the commands contained in the customized route files located in $ETC_DIR/balance/pre-run.

$bal->post_run_rules()

This method is called by set__routes_andfirewall() to process the fules and emit the commands contained in the customized route files located in $ETC_DIR/balance/post-run.

$bal->base_fw_rules()

This method is called by set_firewall() to set up basic firewall rules, including default rules and reporting.

$bal->balancing_fw_rules()

This method is called by set_firewall() to set up the mangle/fwmark rules for balancing outgoing connections.

$bal->sanity_fw_rules()

This is called by set_firewall() to create a sensible series of firewall rules that seeks to prevent spoofing, flooding, and other antisocial behavior. It also enables UDP-based network time and domain name service.

$bal->nat_fw_rules()

This is called by set_firewall() to set up basic NAT rules for lan traffic over ISP

$bal->start_lsm()

Start an lsm process.

$bal->signal_lsm($signal)

Send a signal to a running LSM and return true if successfully signalled. The signal can be numeric (e.g. 9) or a string ('TERM').

BUGS

Please report bugs to GitHub: https://github.com/lstein/Net-ISP-Balance.

AUTHOR

Copyright 2014, Lincoln D. Stein (lincoln.stein@gmail.com)

Senior Principal Investigator, Ontario Institute for Cancer Research

LICENSE

This package is distributed under the terms of the Perl Artistic License 2.0. See http://www.perlfoundation.org/artistic_license_2_0.