fault_tolerant_router 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5d0eeae84605fd65228ba43ec1429ce65f579ed2
4
- data.tar.gz: 0bfee9df0185f24b895d7f20330eb4fdd4381345
3
+ metadata.gz: e0ae7593ff2c313398447ff2a43facc38f72af63
4
+ data.tar.gz: f293bbea0dd7ffd8e37aec2cf75cfca87379d8e2
5
5
  SHA512:
6
- metadata.gz: 0f189d0a607af22820ad949951dfd012d9f2223b4de417c16b1f11c109a45849516da27d0372dc5f6415e3219edc5d27c10940168fd170c01030e31016eb3c83
7
- data.tar.gz: 9463c6b3a497017102b63566553d9b47c4fccdd0e454dc839e8096bacaea9506ca07004e84c869a79d5f11396af6fc15f1d30827a9738b984c06412ac0c6f6c2
6
+ metadata.gz: 63cd6b8348cfb363d7a397416936c7a2a36e5e1c3f747774a2a8cf93a18aff02813cd5fa7d007caff2a48e9efc506c143d44c9da82e7353229d6344f1df6a8ce
7
+ data.tar.gz: ba8c2e775d64faa69656e30187bb645a872f16bba30ba348bc2c85afa6b109b170e8750d383b718e5b1ab5df5b475cd8bce2e654658955ee1fe4393146180dc6
data/README.md CHANGED
@@ -11,11 +11,11 @@
11
11
 
12
12
  ## In brief
13
13
 
14
- Do you have multiple internet connections (uplinks) with several providers? Do you want to transparently use all of the available bandwidth? Do you want to remain online even if some of the uplinks goes down? This tool may help you!
14
+ Do you have multiple internet connections (uplinks) with several providers? Do you want to transparently use all of the available bandwidth? Do you want to remain online even if some uplinks go down? This tool may help you!
15
15
 
16
16
  ## A more formal description
17
17
 
18
- Fault Tolerant Router is a daemon, running in background on a Linux router or firewall, monitoring the state of multiple internet uplinks/providers and changing the routing accordingly. LAN/DMZ internet traffic (outgoing connections) is load balanced between the uplinks using Linux *multipath routing*. The daemon monitors the state of the uplinks by routinely pinging well known IP addresses (Google public DNS servers, etc.) through each outgoing interface: once an uplink goes down, it is excluded from the *multipath routing*, when it comes back up, it is included again. All of the routing changes are notified to the administrator by email.
18
+ Fault Tolerant Router is a daemon, running in background on a Linux router or firewall, monitoring the state of multiple internet uplinks and changing the routing accordingly. LAN/DMZ internet traffic (outgoing connections) is load balanced between the uplinks using Linux *multipath routing*. The daemon monitors the state of the uplinks by routinely pinging well known IP addresses (Google public DNS servers, etc.) through each outgoing interface: once an uplink goes down, it is excluded from the *multipath routing*, when it comes back up, it is included again. An uplink may be assigned to a priority group: lower priority uplinks will only be used if all higher priority ones are down. That's useful to only use pay-per-traffic uplinks if no regular uplink is working. All of the routing changes are notified to the administrator by email.
19
19
 
20
20
  Fault Tolerant Router is well tested and has been used in production for several years, in several sites.
21
21
 
@@ -26,13 +26,14 @@ Fault Tolerant Router has been featured on Slashdot, see [article](http://linux.
26
26
  ## Interaction between *multipath routing*, *iptables* and *ip policy routing*
27
27
 
28
28
  The system is based on the interaction between Linux *multipath routing*, *iptables* and *ip policy routing*. Outgoing (from LAN/DMZ to WAN) and incoming (from WAN to LAN/DMZ) connections have a different behaviour:
29
+
29
30
  * **Outgoing connections (from LAN/DMZ to WAN)**:
30
- * **New connections**:
31
- The outgoing interface (uplink) is decided by the Linux *multipath routing*, in a round-robin fashion. Then, just before the packet leaves the router (in the *iptables* POSTROUTING chain), *iptables* marks the connection with the outgoing interface id, so that all subsequent connection packets will be sent through the same interface.
31
+ * **New connections**:
32
+ The outgoing interface (uplink) is decided by the Linux *multipath routing*, in a round-robin fashion. Then, just before the packet leaves the router (in the *iptables* POSTROUTING chain), *iptables* marks the connection with the outgoing interface id, so that all subsequent connection packets will be sent through the same interface.
32
33
  NB: all the packets of the same connection should be originating from the same IP address, otherwise the server you are connecting to would refuse them, unless you are using specific protocols.
33
- * **Established connections**:
34
+ * **Established connections**:
34
35
  Before the packet is routed (in the *iptables* PREROUTING chain), *iptables* marks it with the outgoing interface id that was previously assigned to the connection. This way, thanks to *ip policy routing*, the packet will pass through a specific routing table directing it to the connection outgoing interface.
35
- * **Incoming connections (from WAN to LAN/DMZ)**:
36
+ * **Incoming connections (from WAN to LAN/DMZ)**:
36
37
  The incoming interface is obviously decided by the connecting host, connecting to one of the IP addresses assigned to an uplink interface. Just after the packet enters the router (in the *iptables* PREROUTING chain), *iptables* marks the connection with the incoming interface id. Then, when the packet reaches the LAN or DMZ, a return packet is generated by the receiving host and sent back to the connecting host. Once this return packet hits the router, before it is actually routed (in the *iptables* PREROUTING chain), *iptables* marks it with the outgoing interface id that was previously assigned to that connection. This way, thanks to *ip policy routing*, the return packet will pass through a specific routing table directing it to the connection outgoing interface.
37
38
 
38
39
  ## The uplink monitor daemon
@@ -40,15 +41,14 @@ The incoming interface is obviously decided by the connecting host, connecting t
40
41
  The daemon monitors the state of the uplinks by routinely pinging well known IP addresses through each uplink: if enough pings are successful the uplink is considered up, if not it's considered down. If an uplink state change is detected, the default *multipath routing* table (used for LAN/DMZ to WAN new connections) is changed accordingly and the administrator is notified by email.
41
42
 
42
43
  The IP addresses to ping and the number of required successful pings are configurable. Here are some things to consider in order not to get false positives or negatives:
43
- * Some ping packets can randomly get lost along the way: don't require 100% of the pings to be successful!
44
+
45
+ * Some ping packets can randomly get lost along the way: do not require 100% of the pings to be successful!
44
46
  * Some of the hosts you are pinging (see *tests/ips* configuration parameter) may be temporarily down.
45
- * It's better not to ping too near hosts (for example your provider routers), because your provider could be temporarily disconnected from the rest of the internet (it happened...), so the uplink would result as up while it's actually unusable.
46
- * Sometimes an uplink can be not completely up or down, it can be just "disturbed", losing a high percentage of packets and being almost unusable: it's better to consider such uplink as down, so don't require too few successful pings, otherwise it may be considered up, because a few pings may pass through a "disturbed" link.
47
+ * It's better not to ping too near hosts (for example your provider routers), because your provider could be temporarily disconnected from the rest of the internet (it happened to me), so the uplink would look as up while it's actually unusable.
48
+ * Sometimes an uplink can be not completely up or down, it can be just "disturbed", losing a high percentage of packets and being almost unusable: it's better to consider such uplink as down, so do not require too few successful pings, otherwise it may be considered up, because a few pings may pass through a "disturbed" link.
47
49
 
48
50
  The order of IP addresses listed in *tests/ips* configuration parameter is not important, because the list is shuffled before every uplink check.
49
51
 
50
- If no uplink is up, all of them are added to the default *multipath routing* table, to get some bandwidth as soon as one uplink comes back up.
51
-
52
52
  ## Requirements
53
53
 
54
54
  * [Ruby](https://www.ruby-lang.org)
@@ -66,35 +66,37 @@ If no uplink is up, all of them are added to the default *multipath routing* tab
66
66
  Fault Tolerant Router should be run **as root**, or as an high privileges user, able to modify routing, etc.
67
67
 
68
68
  1. Configure your router interfaces as usual, with every uplink connected to it's own physical interface. An interface may have more than one IP address if needed (from the same uplink of course). **Don't** set any default route.
69
- 2. Save an example configuration file in /etc/fault_tolerant_router.conf (use the `--config` option to set another location):
69
+ 2. Save an example configuration file in /etc/fault_tolerant_router.conf (use the `--config` option to set another location):
70
70
  `$ fault_tolerant_router generate_config`
71
71
  3. Edit /etc/fault_tolerant_router.conf
72
- 4. _(Optional)_ Demo how Fault Tolerant Router works, to familiarize with it:
72
+ 4. _(Optional)_ Demo how Fault Tolerant Router works, to familiarize with it:
73
73
  `$ fault_tolerant_router --demo monitor`
74
74
  5. Generate *iptables* rules and integrate them with your existing ones:
75
75
  `$ fault_tolerant_router generate_iptables`
76
- 6. _(Optional)_ Test email notification, to be sure SMTP parameters are correct and the administrator will get notifications:
76
+ 6. _(Optional)_ Test email notification, to be sure SMTP parameters are correct and the administrator will get notifications:
77
77
  `$ fault_tolerant_router email_test`
78
- 7. Run the daemon:
79
- `$ fault_tolerant_router monitor`
78
+ 7. Run the daemon:
79
+ `$ fault_tolerant_router monitor`
80
80
  Previous command will actually run Fault Tolerant Router in foreground. To run it in background you should use your Linux distribution specific method to start it as a system service. See for example [start-stop-daemon](http://manned.org/start-stop-daemon).
81
- If you want a quick and dirty way to run the program in background, just add an ampersand at the end of the command line:
81
+ If you want a quick and dirty way to run the program in background, just add an ampersand at the end of the command line:
82
82
  `$ fault_tolerant_router monitor &`
83
83
 
84
84
  ## Configuration file
85
85
 
86
- The fault_tolerant_router.conf configuration file is in [YAML](http://en.wikipedia.org/wiki/YAML) format. Here is the explanation of the options:
86
+ The fault_tolerant_router.conf configuration file is in [YAML](http://en.wikipedia.org/wiki/YAML) format. Here is the explanation of the parameters:
87
+
87
88
  * **uplinks**: Array of uplinks. The example configuration has 3 uplinks, but you can have from 2 to as many as you wish.
88
89
  * **interface**: The network interface where the uplink is connected. Until today Fault Tolerant Router has always been used with each uplink on it's own physical interface, never tried it with VLAN interfaces (it's in the to do list).
89
90
  * **type**: Specify *static* for any kind of static IP interface or *ppp* for a PPP dynamic IP interface.
90
91
  * **ip**: Primary IP address of the network interface. You can have more than one IP address assigned to the interface, just specify here the primary one that will be used as standard SNAT source. Omit this parameter in case of a PPP dynamic IP interface.
91
92
  * **gateway**: The uplink gateway, usually the provider's router IP address. Omit this parameter in case of a PPP dynamic IP interface.
92
93
  * **description**: Uplink name, used in notifications.
93
- * **weight**: Optional parameter, it's the preference to assign to this uplink when choosing one for a new outgoing connection. Use when you have uplinks with different bandwidths. See http://www.policyrouting.org/PolicyRoutingBook/ONLINE/CH05.web.html
94
- * **default_route**: Optional parameter, default value is *true*. If set to *false* the uplink is excluded from the *multipath routing*, i.e. the uplink will never be selected when choosing one for a new outgoing connection. There's an exception to this if some kind of outgoing connection is forced to pass through this uplink, see [Iptables rules](#iptables-rules) section. Note this parameter only affects outgoing connections, even if set to *false* incoming connections are still possible. Use cases to set it to *false*:
94
+ * **priority_group**: An integer value, representing the priority group the uplink is assigned to. Priority groups with lower values have higher priority. A priority group is considered available when at least one of its members is up. When choosing a default route, available priority groups are selected, then the highest priority of these is choosen and it's members are load balanced: lower priority group members are not used. That's useful for example to only use pay-per-traffic uplinks if no regular uplink is working: just set the pay-per-traffic uplinks to a lower priority then the regular.
95
+ If no value is specified, the uplink is excluded from the *multipath routing*, i.e. the uplink will never be selected when choosing one for a new outgoing connection. There's an exception to this if some kind of outgoing connection is forced to pass through this uplink, see [Iptables rules](#iptables-rules) section. Note this parameter only affects outgoing connections: even if no value is specified incoming connections are still possible. Use cases to left this parameter empty:
95
96
  * Want to reserve an uplink for incoming connections only, excluding it from outgoing LAN internet traffic. Tipically you may want this because you have a mail server, web server, VPN server, etc. listening on an uplink.
96
97
  * Temporarily force all of the outgoing LAN internet traffic to pass through the other uplinks, to stress test them and determine their bandwidth.
97
98
  * Temporarily exclude an uplink to reconfigure it, for example because of and internet provider change.
99
+ * **weight**: Optional parameter, it's the preference to assign to this uplink when choosing one for a new outgoing connection. Use when you have uplinks with different bandwidths. See http://www.policyrouting.org/PolicyRoutingBook/ONLINE/CH05.web.html
98
100
  * **downlinks**
99
101
  * **lan**: LAN interface
100
102
  * **dmz**: DMZ interface, leave blank if you have no DMZ
@@ -111,15 +113,15 @@ The fault_tolerant_router.conf configuration file is in [YAML](http://en.wikiped
111
113
  * **send**: Set to *true* or *false* to enable or disable email notification
112
114
  * **sender**: Email sender
113
115
  * **recipients**: An array of email recipients
114
- * **smtp_parameters**: See http://ruby-doc.org/stdlib-2.2.0/libdoc/net/smtp/rdoc/Net/SMTP.html
116
+ * **smtp_parameters**: See http://ruby-doc.org/stdlib-2.3.1/libdoc/net/smtp/rdoc/Net/SMTP.html
115
117
  * **base_table**: Base IP route table number, just need to change if you are already using [multiple routing tables](http://lartc.org/howto/lartc.rpdb.html), to avoid overlapping.
116
- * **base_priority**: Just need to change if you are already using [ip policy routing](http://lartc.org/howto/lartc.rpdb.html), to avoid overlapping. Must be higher than 32767 (default priority, see `ip rule` command output).
118
+ * **base_priority**: Just need to change if you are already using [ip policy routing](http://lartc.org/howto/lartc.rpdb.html), to avoid overlapping. Must be higher than 32767 (the default routing table priority, see `ip rule` command output).
117
119
  * **base_fwmark**: Just need to change if you are already using packet marking, to avoid overlapping.
118
120
 
119
121
  ## *Iptables* rules
120
122
 
121
- *Iptables* rules are generated with the command:
122
- `$ fault_tolerant_router generate_iptables`
123
+ *Iptables* rules are generated with the command:
124
+ `$ fault_tolerant_router generate_iptables`
123
125
  Rules are in [iptables-save](http://manned.org/iptables-save.8) format, you should integrate them with your existing ones.
124
126
  Documentation is included as comments in the output, here is a dump using the standard example configuration:
125
127
  ```
@@ -243,13 +245,9 @@ COMMIT
243
245
  :LAN_WAN - [0:0]
244
246
  :WAN_LAN - [0:0]
245
247
 
246
- #This is just a very basic example, add your own rules for the INPUT chain.
247
-
248
- [0:0] -A INPUT -i lo -j ACCEPT
249
- [0:0] -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
248
+ #This is just a very basic example, add your own rules for the FORWARD chain.
250
249
 
251
250
  [0:0] -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
252
-
253
251
  [0:0] -A FORWARD -i eth0 -o eth1 -j LAN_WAN
254
252
  [0:0] -A FORWARD -i eth0 -o eth2 -j LAN_WAN
255
253
  [0:0] -A FORWARD -i eth0 -o ppp0 -j LAN_WAN
@@ -257,14 +255,18 @@ COMMIT
257
255
  [0:0] -A FORWARD -i eth2 -o eth0 -j WAN_LAN
258
256
  [0:0] -A FORWARD -i ppp0 -o eth0 -j WAN_LAN
259
257
 
260
- #This is just a very basic example, add your own rules for the FORWARD chain.
261
-
262
258
  [0:0] -A LAN_WAN -j ACCEPT
263
259
  [0:0] -A WAN_LAN -j REJECT
264
260
 
265
261
  COMMIT
266
262
  ```
267
263
 
264
+ ## Changelog
265
+
266
+ * v1.0.0: First release
267
+ * v1.1.0: Dynamic IP PPP interfaces support
268
+ * v1.2.0: Uplink priority groups
269
+
268
270
  ## To do
269
271
 
270
272
  See [issues](https://github.com/drsound/fault_tolerant_router/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement) tagged as *enhancement* on GitHub.
@@ -275,5 +277,5 @@ GNU General Public License v2.0, see LICENSE file
275
277
 
276
278
  ## Author
277
279
 
278
- Alessandro Zarrilli (Poggibonsi - Italy)
280
+ Alessandro Zarrilli (Firenze - Italy)
279
281
  alessandro@zarrilli.net
@@ -13,6 +13,64 @@ require 'fault_tolerant_router/monitor'
13
13
  require 'fault_tolerant_router/uplink'
14
14
  require 'fault_tolerant_router/uplinks'
15
15
 
16
+ CONFIG_TEMPLATE = {
17
+ 'uplinks' => {
18
+ 'interface' => {},
19
+ 'type' => {},
20
+ 'ip' => {},
21
+ 'gateway' => {},
22
+ 'description' => {},
23
+ 'weight' => {},
24
+ 'priority_group' => {}
25
+ },
26
+ 'downlinks' => {
27
+ 'lan' => {},
28
+ 'dmz' => {}
29
+ },
30
+ 'tests' => {
31
+ 'ips' => {},
32
+ 'required_successful' => {},
33
+ 'ping_retries' => {},
34
+ 'interval' => {}
35
+ },
36
+ 'log' => {
37
+ 'file' => {},
38
+ 'max_size' => {},
39
+ 'old_files' => {}
40
+ },
41
+ 'email' => {
42
+ 'send' => {},
43
+ 'sender' => {},
44
+ 'recipients' => {},
45
+ 'smtp_parameters' => {
46
+ 'address' => {},
47
+ 'port' => {},
48
+ 'authentication' => {},
49
+ 'enable_starttls_auto' => {},
50
+ 'user_name' => {},
51
+ 'password' => {}
52
+ }
53
+ },
54
+ 'base_table' => {},
55
+ 'base_priority' => {},
56
+ 'base_fwmark' => {}
57
+ }
58
+
59
+ def check_config(config, template, path = [])
60
+ config.each do |k, v|
61
+ unless template.keys.include?(k)
62
+ puts "Error: unknown configuration parameter '#{(path + [k]).join('/')}'"
63
+ exit 1
64
+ end
65
+ case v
66
+ when Hash
67
+ check_config(v, template[k], path + [k])
68
+ when Array
69
+ v.each { |v2| check_config(v2, template[k], path + [k]) if v2.is_a?(Hash) }
70
+ end
71
+ end
72
+ end
73
+
16
74
  options = {
17
75
  configuration_file: '/etc/fault_tolerant_router.conf',
18
76
  debug: false,
@@ -71,6 +129,8 @@ unless File.exists?(options[:configuration_file])
71
129
  end
72
130
 
73
131
  config = YAML.load_file(options[:configuration_file])
132
+ check_config(config, CONFIG_TEMPLATE)
133
+
74
134
  LAN_INTERFACE = config['downlinks']['lan']
75
135
  DMZ_INTERFACE = config['downlinks']['dmz']
76
136
  TEST_IPS = config['tests']['ips']
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ['Alessandro Zarrilli']
10
10
  spec.email = ['alessandro@zarrilli.net']
11
11
  spec.summary = %q{Multiple uplinks Linux routing supervising daemon}
12
- spec.description = %q{A daemon, running in background on a Linux router or firewall, monitoring the state of multiple internet uplinks/providers and changing the routing accordingly. LAN/DMZ internet traffic (outgoing connections) is load balanced between the uplinks using Linux multipath routing. The daemon monitors the state of the uplinks by routinely pinging well known IP addresses (Google public DNS servers, etc.) through each outgoing interface: once an uplink goes down, it is excluded from the multipath routing, when it comes back up, it is included again. All of the routing changes are notified to the administrator by email. Fault Tolerant Router is well tested and has been used in production for several years, in several sites. See https://github.com/drsound/fault_tolerant_router for full documentation.}
12
+ spec.description = %q{A daemon, running in background on a Linux router or firewall, monitoring the state of multiple internet uplinks and changing the routing accordingly. LAN/DMZ internet traffic (outgoing connections) is load balanced between the uplinks using Linux multipath routing. The daemon monitors the state of the uplinks by routinely pinging well known IP addresses (Google public DNS servers, etc.) through each outgoing interface: once an uplink goes down, it is excluded from the multipath routing, when it comes back up, it is included again. An uplink may be assigned to a priority group: lower priority uplinks will only be used if all higher priority ones are down. That's useful to only use pay-per-traffic uplinks if no regular uplink is working. All of the routing changes are notified to the administrator by email. Fault Tolerant Router is well tested and has been used in production for several years, in several sites. See https://github.com/drsound/fault_tolerant_router for full documentation.}
13
13
  spec.homepage = 'https://github.com/drsound/fault_tolerant_router'
14
14
  spec.license = 'GPL-2'
15
15
 
@@ -1,5 +1,5 @@
1
1
  def generate_config(file_path)
2
- if File.exists?(file_path)
2
+ if File.exist?(file_path)
3
3
  puts "Configuration file #{file_path} already exists, will not overwrite!"
4
4
  exit 1
5
5
  end
@@ -9,33 +9,30 @@ def generate_config(file_path)
9
9
  #see https://github.com/drsound/fault_tolerant_router for a complete parameter
10
10
  #description
11
11
 
12
- #add as many uplinks as needed
12
+ #add as many uplinks as needed, in this example ppp0 is used as default route only if both eth1 and eth2 are down
13
13
  uplinks:
14
14
  - interface: eth1
15
15
  type: static
16
16
  ip: 1.0.0.2
17
17
  gateway: 1.0.0.1
18
18
  description: Example Provider 1
19
+ priority_group: 1
19
20
  #optional parameter
20
21
  weight: 1
21
- #optional parameter, default is true
22
- default_route: true
23
22
  - interface: eth2
24
23
  type: static
25
24
  ip: 2.0.0.2
26
25
  gateway: 2.0.0.1
27
26
  description: Example Provider 2
27
+ priority_group: 1
28
28
  #optional parameter
29
29
  weight: 2
30
- #optional parameter, default is true
31
- default_route: true
32
30
  - interface: ppp0
33
31
  type: ppp
34
32
  description: Example Provider 3
33
+ priority_group: 2
35
34
  #optional parameter
36
35
  weight: 1
37
- #optional parameter, default is true
38
- default_route: true
39
36
 
40
37
  downlinks:
41
38
  lan: eth0
@@ -78,7 +75,7 @@ email:
78
75
  - user1@domain.com
79
76
  - user2@domain.com
80
77
  - user3@domain.com
81
- #see http://ruby-doc.org/stdlib-2.2.0/libdoc/net/smtp/rdoc/Net/SMTP.html
78
+ #see http://ruby-doc.org/stdlib-2.3.1/libdoc/net/smtp/rdoc/Net/SMTP.html
82
79
  smtp_parameters:
83
80
  address: smtp.gmail.com
84
81
  port: 587
@@ -93,8 +90,8 @@ email:
93
90
  base_table: 1
94
91
 
95
92
  #just need to change if you are already using ip policy routing, to avoid
96
- #overlapping, must be higher than 32767 (default priority, see output of
97
- #"ip rule" command)
93
+ #overlapping, must be higher than 32767 (the default routing table priority,
94
+ #see output of "ip rule" command)
98
95
  base_priority: 40000
99
96
 
100
97
  #just need to change if you are already using packet marking, to avoid
@@ -140,13 +140,9 @@ END
140
140
 
141
141
  puts <<END
142
142
 
143
- #This is just a very basic example, add your own rules for the INPUT chain.
144
-
145
- [0:0] -A INPUT -i lo -j ACCEPT
146
- [0:0] -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
143
+ #This is just a very basic example, add your own rules for the FORWARD chain.
147
144
 
148
145
  [0:0] -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
149
-
150
146
  END
151
147
  UPLINKS.each do |uplink|
152
148
  puts "[0:0] -A FORWARD -i #{LAN_INTERFACE} -o #{uplink.interface} -j LAN_WAN"
@@ -164,8 +160,6 @@ END
164
160
  end
165
161
  puts <<END
166
162
 
167
- #This is just a very basic example, add your own rules for the FORWARD chain.
168
-
169
163
  [0:0] -A LAN_WAN -j ACCEPT
170
164
  [0:0] -A WAN_LAN -j REJECT
171
165
  END
@@ -22,28 +22,27 @@ end
22
22
 
23
23
  def monitor
24
24
  logger = Logger.new(LOG_FILE, LOG_OLD_FILES, LOG_MAX_SIZE)
25
- command UPLINKS.initialize_routing_commands
25
+ command(UPLINKS.initialize_routing!)
26
26
 
27
- loop do
28
- routing_commands, ip_change_messages = UPLINKS.detect_ip_changes!
29
- command routing_commands
30
- log(logger, ip_change_messages)
27
+ list = UPLINKS.select { |uplink| uplink.default_route }.map { |uplink| uplink.description }.join(', ')
28
+ log(logger, ["Monitor started, initial default route uplinks: #{list}"])
31
29
 
32
- routing_commands, up_state_change_messages, all_default_route_uplinks_down = UPLINKS.test_routing!
33
- command routing_commands
34
- log(logger, up_state_change_messages)
30
+ loop do
31
+ commands, messages = UPLINKS.test!
32
+ command(commands)
33
+ log(logger, messages)
35
34
 
36
- if SEND_EMAIL && (ip_change_messages.any? || up_state_change_messages.any?)
35
+ if SEND_EMAIL && messages.any?
37
36
  begin
38
- send_email((ip_change_messages + up_state_change_messages).join("\n"))
37
+ send_email(messages.join("\n"))
39
38
  rescue Exception => e
40
39
  puts "Problem sending email: #{e}" if DEBUG
41
40
  logger.error("Problem sending email: #{e}")
42
41
  end
43
42
  end
44
43
 
45
- if all_default_route_uplinks_down
46
- puts 'No waiting, because all of the default route uplinks are down' if DEBUG
44
+ if UPLINKS.all_priority_group_members_down?
45
+ puts 'No waiting, because all of the priority group members are down' if DEBUG
47
46
  elsif DEMO
48
47
  puts "Waiting just 5 seconds because in demo mode, otherwise would be #{TEST_INTERVAL} seconds..." if DEBUG
49
48
  sleep 5
@@ -1,81 +1,74 @@
1
1
  class Uplink
2
- attr_reader :default_route, :description, :fwmark, :gateway, :id, :interface, :ip, :priority1, :table, :type, :up, :weight
3
- attr_accessor :priority2, :routing
2
+ attr_reader :description, :fwmark, :gateway, :id, :interface, :ip, :previous_gateway, :previous_ip, :previously_up, :priority_group, :rule_priority_1, :table, :type, :up, :weight
3
+ attr_accessor :default_route, :previously_default_route, :rule_priority_2
4
4
 
5
5
  def initialize(config, id)
6
6
  @id = id
7
- @priority1 = BASE_PRIORITY + @id
7
+ @rule_priority_1 = BASE_PRIORITY + @id
8
8
  @table = BASE_TABLE + @id
9
9
  @fwmark = BASE_FWMARK + @id
10
10
  @interface = config['interface']
11
- raise "Uplink interface not specified: #{config}" unless @interface
11
+ unless @interface
12
+ puts 'Error: uplink interface not specified'
13
+ exit 1
14
+ end
12
15
  @type = case config['type']
13
16
  when 'static'
14
17
  :static
15
18
  when 'ppp'
16
19
  :ppp
17
20
  else
18
- raise "Uplink type not valid: #{config}"
21
+ puts "Error: '#{config['type']}' is not a valid uplink type"
22
+ exit 1
19
23
  end
20
24
  @description = config['description']
21
- raise "Uplink description not specified: #{config}" unless @description
25
+ unless @description
26
+ puts 'Error: uplink description not specified'
27
+ exit 1
28
+ end
22
29
  @weight = config['weight']
23
- @default_route = config['default_route'].nil? ? true : config['default_route']
24
-
25
- #a new uplink is supposed to be up
26
- @up = true
27
- #a new uplink starts as routing if it's marked as a default route
28
- @routing = @default_route
30
+ @priority_group = config['priority_group']
31
+ @default_route = false
29
32
 
30
33
  if @type == :static
31
34
  @ip = config['ip']
32
- raise "Uplink IP address not specified: #{config}" unless @ip
35
+ unless @ip
36
+ puts 'Error: uplink IP not specified'
37
+ exit 1
38
+ end
33
39
  @gateway = config['gateway']
34
- raise "Uplink gateway not specified: #{config}" unless @gateway
40
+ unless @gateway
41
+ puts 'Error: uplink gateway not specified'
42
+ exit 1
43
+ end
35
44
  else
36
45
  detect_ppp_ips!
37
- puts "Uplink #{@description}: initialized with [ip: #{@ip}, gateway: #{@gateway}]" if DEBUG
38
46
  end
47
+
48
+ @previous_ip = @ip
49
+ @previous_gateway = @gateway
50
+ #a new uplink is supposed to be up
51
+ @up = true
52
+ @previously_up = true
39
53
  end
40
54
 
41
55
  def detect_ppp_ips!
42
56
  @previous_ip = @ip
43
57
  @previous_gateway = @gateway
44
58
  if DEMO
45
- @ip = %w(3.0.0.101 3.0.0.102).sample
46
- @gateway = %w(3.0.0.1 3.0.0.2).sample
59
+ @ip = ['3.0.0.101', '3.0.0.102', nil].sample
60
+ @gateway = ['3.0.0.1', '3.0.0.2', nil].sample
47
61
  else
48
62
  ifaddr = Socket.getifaddrs.find { |i| i.name == @interface && i.addr && i.addr.ipv4? }
49
63
  if ifaddr
50
64
  @ip = ifaddr.addr.ip_address
51
65
  @gateway = ifaddr.dstaddr.ip_address
52
66
  else
53
- #todo: what to do if it happens?
54
- raise 'PPP IP address not found'
55
- end
56
- end
57
- end
58
-
59
- def detect_ip_changes!
60
- commands = []
61
- need_default_route_update = false
62
- message = nil
63
- if @type == :ppp
64
- detect_ppp_ips!
65
- if (@previous_ip != @ip) || (@previous_gateway != @gateway)
66
- message = "Uplink #{@description}: IP change [ip: #{@previous_ip}, gateway: #{@previous_gateway}] --> [ip: #{@ip}, gateway: #{@gateway}]"
67
- puts message if DEBUG
68
- commands = [
69
- [
70
- "ip rule del priority #{@priority1}",
71
- "ip rule del priority #{@priority2}"
72
- ],
73
- route_add_commands
74
- ].flatten
67
+ @ip = nil
68
+ @gateway = nil
75
69
  end
76
- need_default_route_update = @routing && (@previous_gateway != @gateway)
77
70
  end
78
- [commands, need_default_route_update, message]
71
+ puts "Uplink #{@description}: detected ip #{@ip || 'none'}, gateway #{@gateway || 'none'}" if DEBUG
79
72
  end
80
73
 
81
74
  def ping(ip_address)
@@ -88,66 +81,76 @@ class Uplink
88
81
  end
89
82
  end
90
83
 
91
- def test_routing!
84
+ def test!
92
85
  #save current state
93
86
  @previously_up = @up
94
- @previously_routing = @routing
95
87
 
96
- @successful_tests = 0
97
- @unsuccessful_tests = 0
88
+ successful_tests = 0
89
+ unsuccessful_tests = 0
90
+ commands = []
98
91
 
99
- #for each test (in random order)...
100
- TEST_IPS.shuffle.each_with_index do |test, i|
101
- successful_test = false
92
+ if @type == :ppp
93
+ detect_ppp_ips!
94
+ if (@previous_ip != @ip) || (@previous_gateway != @gateway)
95
+ #only apply routing commands if there are an ip and gateway, else they will be applied on next checks, whenever new ip and gateway will be available
96
+ if @ip && @gateway
97
+ commands << "ip rule del priority #{@rule_priority_1}"
98
+ commands << "ip rule del priority #{@rule_priority_2}"
99
+ commands += route_add_commands
100
+ end
101
+ end
102
+ end
102
103
 
103
- #retry for several times...
104
- PING_RETRIES.times do
105
- if DEBUG
106
- print "Uplink #{@description}: ping #{test}... "
107
- STDOUT.flush
104
+ #do not ping if there is no ip or gateway (for example in case of a PPP interface down)
105
+ if @ip && @gateway
106
+ #for each test (in random order)...
107
+ TEST_IPS.shuffle.each_with_index do |test, i|
108
+ successful_test = false
109
+
110
+ #retry for several times...
111
+ PING_RETRIES.times do
112
+ if DEBUG
113
+ print "Uplink #{@description}: ping #{test}... "
114
+ STDOUT.flush
115
+ end
116
+ if ping(test)
117
+ successful_test = true
118
+ puts 'ok' if DEBUG
119
+ #avoid more pings to the same ip after a successful one
120
+ break
121
+ else
122
+ puts 'error' if DEBUG
123
+ end
108
124
  end
109
- if ping(test)
110
- successful_test = true
111
- puts 'ok' if DEBUG
112
- #avoid more pings to the same ip after a successful one
113
- break
125
+
126
+ if successful_test
127
+ successful_tests += 1
114
128
  else
115
- puts 'error' if DEBUG
129
+ unsuccessful_tests += 1
116
130
  end
117
- end
118
-
119
- if successful_test
120
- @successful_tests += 1
121
- else
122
- @unsuccessful_tests += 1
123
- end
124
131
 
125
- #if not currently doing the last test...
126
- if i + 1 < TEST_IPS.size
127
- if @successful_tests >= REQUIRED_SUCCESSFUL_TESTS
128
- puts "Uplink #{@description}: avoiding more tests because there are enough positive ones" if DEBUG
129
- break
130
- elsif TEST_IPS.size - @unsuccessful_tests < REQUIRED_SUCCESSFUL_TESTS
131
- puts "Uplink #{@description}: avoiding more tests because too many have been failed" if DEBUG
132
- break
132
+ #if not currently doing the last test...
133
+ if i + 1 < TEST_IPS.size
134
+ if successful_tests >= REQUIRED_SUCCESSFUL_TESTS
135
+ puts "Uplink #{@description}: avoiding more tests because there are enough positive ones" if DEBUG
136
+ break
137
+ elsif TEST_IPS.size - unsuccessful_tests < REQUIRED_SUCCESSFUL_TESTS
138
+ puts "Uplink #{@description}: avoiding more tests because too many have been failed" if DEBUG
139
+ break
140
+ end
133
141
  end
134
142
  end
135
-
136
143
  end
137
144
 
138
- @up = @successful_tests >= REQUIRED_SUCCESSFUL_TESTS
139
- up_state_changed = @up != @previously_up
140
- @routing = @up && @default_route
141
- routing_state_changed = @routing != @previously_routing
145
+ @up = successful_tests >= REQUIRED_SUCCESSFUL_TESTS
142
146
 
143
- state = @previously_up ? 'up' : 'down'
144
- state += " --> #{@up ? 'up' : 'down'}" if up_state_changed
145
- routing = @previously_routing ? 'enabled' : 'disabled'
146
- routing += " --> #{@routing ? 'enabled' : 'disabled'}" if routing_state_changed
147
- message = "Uplink #{@description}: #{state}"
148
- puts "Uplink #{@description}: #{@successful_tests} successful tests, #{@unsuccessful_tests} unsuccessful tests, state #{state}, routing #{routing}" if DEBUG
147
+ if DEBUG
148
+ state = @previously_up ? 'up' : 'down'
149
+ state += " --> #{@up ? 'up' : 'down'}" if @up != @previously_up
150
+ puts "Uplink #{@description}: #{successful_tests} successful tests, #{unsuccessful_tests} unsuccessful tests, state #{state}"
151
+ end
149
152
 
150
- [up_state_changed, routing_state_changed, message]
153
+ commands
151
154
  end
152
155
 
153
156
  def route_add_commands
@@ -155,9 +158,9 @@ class Uplink
155
158
  #- returning packets of inbound connections coming from ethX
156
159
  #- non-first packets of outbound connections for which the first packet has been sent to ethX via multipath routing
157
160
  [
158
- "ip route replace table #{table} default via #{@gateway} src #{@ip}",
159
- "ip rule add priority #{@priority1} from #{@ip} lookup #{table}",
160
- "ip rule add priority #{@priority2} fwmark #{fwmark} lookup #{table}"
161
+ "ip route replace table #{@table} default via #{@gateway} src #{@ip}",
162
+ "ip rule add priority #{@rule_priority_1} from #{@ip} lookup #{@table}",
163
+ "ip rule add priority #{@rule_priority_2} fwmark #{@fwmark} lookup #{@table}"
161
164
  ]
162
165
  end
163
166
 
@@ -1,118 +1,142 @@
1
1
  class Uplinks
2
+ include Enumerable
2
3
 
3
4
  def initialize(config)
4
5
  @uplinks = config.each_with_index.map { |uplink, i| Uplink.new(uplink, i) }
5
- @uplinks.each { |uplink| uplink.priority2 = BASE_PRIORITY + @uplinks.size + uplink.id }
6
+ @uplinks.each { |uplink| uplink.rule_priority_2 = BASE_PRIORITY + @uplinks.size + uplink.id }
7
+ @default_route_table = @uplinks.map { |uplink| uplink.table }.max + 1
6
8
  end
7
9
 
8
10
  def each
9
11
  @uplinks.each { |uplink| yield uplink }
10
12
  end
11
13
 
12
- def initialize_routing_commands
14
+ def initialize_routing!
13
15
  commands = []
14
- priorities = @uplinks.map { |uplink| [uplink.priority1, uplink.priority2] }.flatten.minmax
16
+ rule_priorities = @uplinks.map { |uplink| [uplink.rule_priority_1, uplink.rule_priority_2] }.flatten.minmax
15
17
  tables = @uplinks.map { |uplink| uplink.table }.minmax
16
18
 
17
19
  #enable IP forwarding
18
- commands += ['echo 1 > /proc/sys/net/ipv4/ip_forward']
20
+ commands << 'echo 1 > /proc/sys/net/ipv4/ip_forward'
19
21
 
20
22
  #clean all previous configurations, try to clean more than needed (double) to avoid problems in case of changes in the
21
23
  #number of uplinks between different executions
22
- ((priorities.max - priorities.min + 2) * 2).times { |i| commands += ["ip rule del priority #{priorities.min + i} &> /dev/null"] }
23
- ((tables.max - tables.min + 2) * 2).times { |i| commands += ["ip route del table #{tables.min + i} &> /dev/null"] }
24
+ ((rule_priorities.max - rule_priorities.min + 2) * 2).times { |i| commands << "ip rule del priority #{rule_priorities.min + i} &> /dev/null" }
25
+ ((tables.max - tables.min + 2) * 2).times { |i| commands << "ip route del table #{tables.min + i} &> /dev/null" }
24
26
 
25
27
  #disable "reverse path filtering" on the uplink interfaces
26
- commands += ['echo 0 > /proc/sys/net/ipv4/conf/all/rp_filter']
28
+ commands << 'echo 0 > /proc/sys/net/ipv4/conf/all/rp_filter'
27
29
  commands += @uplinks.map { |uplink| "echo 2 > /proc/sys/net/ipv4/conf/#{uplink.interface}/rp_filter" }
28
30
 
29
31
  #set uplinks routes
30
32
  commands += @uplinks.map { |uplink| uplink.route_add_commands }
31
33
 
32
34
  #rule for first packet of outbound connections
33
- commands += ["ip rule add priority #{priorities.max + 1} from all lookup #{tables.max + 1}"]
35
+ commands << "ip rule add priority #{rule_priorities.max + 1} from all lookup #{tables.max + 1}"
34
36
 
35
37
  #set default route
36
- commands += set_default_route_commands
38
+ commands += update_default_route!
37
39
 
38
40
  #apply the routing changes
39
- commands += ['ip route flush cache']
41
+ commands << 'ip route flush cache'
40
42
 
41
43
  commands.flatten
42
44
  end
43
45
 
44
- def set_default_route_commands
45
- routing_uplinks = @uplinks.find_all { |uplink| uplink.routing }
46
+ def update_default_route!
47
+ #select uplinks that are up and with a specified priority group value
48
+ selected = @uplinks.find_all { |uplink| uplink.up && uplink.priority_group }
49
+ puts "Choosing default route: available uplinks: #{selected.map { |uplink| uplink.description }.join(', ')}" if DEBUG
46
50
 
47
- #do not use balancing if there is just one routing uplink
48
- if routing_uplinks.size == 1
49
- nexthops = "via #{routing_uplinks.first.gateway}"
50
- else
51
- nexthops = routing_uplinks.map do |uplink|
52
- #the "weight" parameter is optional
53
- tail = uplink.weight ? " weight #{uplink.weight}" : ''
54
- "nexthop via #{uplink.gateway}#{tail}"
55
- end
56
- nexthops = nexthops.join(' ')
57
- end
58
- #set the route for first packet of outbound connections
59
- ["ip route replace table #{@uplinks.map { |uplink| uplink.table }.max + 1} default #{nexthops}"]
60
- end
61
-
62
- def detect_ip_changes!
63
- commands = []
64
- need_default_route_update = false
65
- messages = []
51
+ #restrict the selection to the members of highest priority group
52
+ highest_available_priority = selected.map { |uplink| uplink.priority_group }.min
53
+ selected = selected.find_all { |uplink| uplink.priority_group == highest_available_priority }
54
+ puts "Choosing default route: highest priority group uplinks: #{selected.map { |uplink| uplink.description }.join(', ')}" if DEBUG
66
55
 
56
+ changes = false
57
+ #assign default route status to the uplinks and detect changes from previous configuration
67
58
  @uplinks.each do |uplink|
68
- c, n, m = uplink.detect_ip_changes!
69
- commands += c if c.any?
70
- need_default_route_update ||= n
71
- messages << m if m
59
+ uplink.previously_default_route = uplink.default_route
60
+ uplink.default_route = selected.include?(uplink)
61
+ changes ||= uplink.default_route != uplink.previously_default_route
72
62
  end
73
63
 
74
- if need_default_route_update
75
- puts 'Will update default route because some of its gateways changed' if DEBUG
76
- commands += set_default_route_commands
64
+ #check if any default route uplink changed its gateway (for example due to a ppp update)
65
+ if @uplinks.any? { |uplink| uplink.default_route && uplink.gateway != uplink.previous_gateway }
66
+ changes ||= true
67
+ puts "Choosing default route: detected gateway change in a default route uplink" if DEBUG
77
68
  end
78
69
 
79
- #apply the routing changes, in any
80
- commands += ['ip route flush cache'] if commands.any?
70
+ commands = []
71
+ if selected.size == 0
72
+ puts 'Choosing default route: no available uplinks, no need for an update' if DEBUG
73
+ elsif !changes
74
+ puts 'Choosing default route: no changes, no need for an update' if DEBUG
75
+ else
76
+ puts 'Choosing default route: changes detected, update needed' if DEBUG
77
+ #do not use balancing if there is just one routing uplink
78
+ if selected.size == 1
79
+ nexthops = "via #{selected.first.gateway}"
80
+ else
81
+ nexthops = selected.map do |uplink|
82
+ #the "weight" parameter is optional
83
+ tail = uplink.weight ? " weight #{uplink.weight}" : ''
84
+ "nexthop via #{uplink.gateway}#{tail}"
85
+ end
86
+ nexthops = nexthops.join(' ')
87
+ end
81
88
 
82
- [commands, messages]
89
+ #set the route for first packet of outbound connections
90
+ commands << "ip route replace table #{@default_route_table} default #{nexthops}"
91
+ end
92
+
93
+ commands
83
94
  end
84
95
 
85
- def test_routing!
86
- any_up_state_changed = false
87
- any_routing_state_changed = false
88
- messages = []
89
- all_default_route_uplinks_down = false
96
+ def test!
90
97
  commands = []
91
-
98
+ messages = []
92
99
  @uplinks.each do |uplink|
93
- up_state_changed, routing_state_changed, message = uplink.test_routing!
94
- any_up_state_changed ||= up_state_changed
95
- any_routing_state_changed ||= routing_state_changed
96
- messages << message
100
+ c = uplink.test!
101
+ commands += c
97
102
  end
98
103
 
99
- default_route_uplinks = @uplinks.find_all { |uplink| uplink.default_route }
100
- if default_route_uplinks.all? { |uplink| !uplink.up }
101
- default_route_uplinks.each { |uplink| uplink.routing = true }
102
- puts 'No default route uplink seems to be up: enabling them all!' if DEBUG
103
- all_default_route_uplinks_down = true
104
- end
104
+ commands += update_default_route!
105
105
 
106
- #change default route if any uplink changed its routing state
107
- if any_routing_state_changed
108
- commands = set_default_route_commands
109
- #apply the routing changes
110
- commands += ['ip route flush cache']
106
+ #apply the routing changes, in any
107
+ commands << 'ip route flush cache' if commands.any?
108
+
109
+ changes = false
110
+ @uplinks.each do |uplink|
111
+ current = uplink.ip || 'none'
112
+ previous = uplink.previous_ip || 'none'
113
+ changes ||= current != previous
114
+ ip = current == previous ? current : "#{previous} --> #{current}"
115
+
116
+ current = uplink.gateway || 'none'
117
+ previous = uplink.previous_gateway || 'none'
118
+ changes ||= current != previous
119
+ gateway = current == previous ? current : "#{previous} --> #{current}"
120
+
121
+ current = uplink.up ? 'up' : 'down'
122
+ previous = uplink.previously_up ? 'up' : 'down'
123
+ changes ||= current != previous
124
+ up = current == previous ? current : "#{previous} --> #{current}"
125
+
126
+ current = uplink.default_route ? 'routing' : 'standby'
127
+ previous = uplink.previously_default_route ? 'routing' : 'standby'
128
+ changes ||= current != previous
129
+ default_route = current == previous ? current : "#{previous} --> #{current}"
130
+
131
+ messages << "Uplink #{uplink.description}: ip #{ip}, gateway #{gateway}, #{up}, #{default_route}"
111
132
  end
133
+ messages = [] unless changes
112
134
 
113
- messages = [] unless any_up_state_changed
135
+ [commands, messages]
136
+ end
114
137
 
115
- [commands, messages, all_default_route_uplinks_down]
138
+ def all_priority_group_members_down?
139
+ @uplinks.all? { |uplink| !uplink.priority_group || !uplink.up }
116
140
  end
117
141
 
118
142
  end
@@ -1,3 +1,3 @@
1
1
  module FaultTolerantRouter
2
- VERSION = '1.1.0'
2
+ VERSION = '1.2.0'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fault_tolerant_router
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alessandro Zarrilli
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-06-28 00:00:00.000000000 Z
11
+ date: 2016-07-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -53,15 +53,18 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '2.6'
55
55
  description: 'A daemon, running in background on a Linux router or firewall, monitoring
56
- the state of multiple internet uplinks/providers and changing the routing accordingly.
57
- LAN/DMZ internet traffic (outgoing connections) is load balanced between the uplinks
58
- using Linux multipath routing. The daemon monitors the state of the uplinks by routinely
56
+ the state of multiple internet uplinks and changing the routing accordingly. LAN/DMZ
57
+ internet traffic (outgoing connections) is load balanced between the uplinks using
58
+ Linux multipath routing. The daemon monitors the state of the uplinks by routinely
59
59
  pinging well known IP addresses (Google public DNS servers, etc.) through each outgoing
60
60
  interface: once an uplink goes down, it is excluded from the multipath routing,
61
- when it comes back up, it is included again. All of the routing changes are notified
62
- to the administrator by email. Fault Tolerant Router is well tested and has been
63
- used in production for several years, in several sites. See https://github.com/drsound/fault_tolerant_router
64
- for full documentation.'
61
+ when it comes back up, it is included again. An uplink may be assigned to a priority
62
+ group: lower priority uplinks will only be used if all higher priority ones are
63
+ down. That''s useful to only use pay-per-traffic uplinks if no regular uplink is
64
+ working. All of the routing changes are notified to the administrator by email.
65
+ Fault Tolerant Router is well tested and has been used in production for several
66
+ years, in several sites. See https://github.com/drsound/fault_tolerant_router for
67
+ full documentation.'
65
68
  email:
66
69
  - alessandro@zarrilli.net
67
70
  executables:
@@ -102,7 +105,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
102
105
  version: '0'
103
106
  requirements: []
104
107
  rubyforge_project:
105
- rubygems_version: 2.4.5
108
+ rubygems_version: 2.4.8
106
109
  signing_key:
107
110
  specification_version: 4
108
111
  summary: Multiple uplinks Linux routing supervising daemon