fault_tolerant_router 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +10 -9
- data/bin/fault_tolerant_router +10 -7
- data/lib/fault_tolerant_router/generate_config.rb +4 -3
- data/lib/fault_tolerant_router/generate_iptables.rb +28 -20
- data/lib/fault_tolerant_router/monitor.rb +27 -157
- data/lib/fault_tolerant_router/uplink.rb +164 -0
- data/lib/fault_tolerant_router/uplinks.rb +118 -0
- data/lib/fault_tolerant_router/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5d0eeae84605fd65228ba43ec1429ce65f579ed2
|
4
|
+
data.tar.gz: 0bfee9df0185f24b895d7f20330eb4fdd4381345
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0f189d0a607af22820ad949951dfd012d9f2223b4de417c16b1f11c109a45849516da27d0372dc5f6415e3219edc5d27c10940168fd170c01030e31016eb3c83
|
7
|
+
data.tar.gz: 9463c6b3a497017102b63566553d9b47c4fccdd0e454dc839e8096bacaea9506ca07004e84c869a79d5f11396af6fc15f1d30827a9738b984c06412ac0c6f6c2
|
data/README.md
CHANGED
@@ -86,8 +86,9 @@ If you want a quick and dirty way to run the program in background, just add an
|
|
86
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:
|
87
87
|
* **uplinks**: Array of uplinks. The example configuration has 3 uplinks, but you can have from 2 to as many as you wish.
|
88
88
|
* **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
|
-
* **
|
89
|
+
* **type**: Specify *static* for any kind of static IP interface or *ppp* for a PPP dynamic IP interface.
|
90
|
+
* **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
|
+
* **gateway**: The uplink gateway, usually the provider's router IP address. Omit this parameter in case of a PPP dynamic IP interface.
|
91
92
|
* **description**: Uplink name, used in notifications.
|
92
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
|
93
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*:
|
@@ -169,7 +170,7 @@ Documentation is included as comments in the output, here is a dump using the st
|
|
169
170
|
#Example Provider 2
|
170
171
|
[0:0] -A PREROUTING -i eth2 -m conntrack --ctstate NEW -j CONNMARK --set-mark 2
|
171
172
|
#Example Provider 3
|
172
|
-
[0:0] -A PREROUTING -i
|
173
|
+
[0:0] -A PREROUTING -i ppp0 -m conntrack --ctstate NEW -j CONNMARK --set-mark 3
|
173
174
|
|
174
175
|
#New outbound connections: mark the connection with the outgoing interface
|
175
176
|
#(chosen by the multipath routing).
|
@@ -179,7 +180,7 @@ Documentation is included as comments in the output, here is a dump using the st
|
|
179
180
|
#Example Provider 2
|
180
181
|
[0:0] -A POSTROUTING -o eth2 -m conntrack --ctstate NEW -j CONNMARK --set-mark 2
|
181
182
|
#Example Provider 3
|
182
|
-
[0:0] -A POSTROUTING -o
|
183
|
+
[0:0] -A POSTROUTING -o ppp0 -m conntrack --ctstate NEW -j CONNMARK --set-mark 3
|
183
184
|
|
184
185
|
COMMIT
|
185
186
|
|
@@ -203,7 +204,7 @@ COMMIT
|
|
203
204
|
#Example Provider 2
|
204
205
|
#[0:0] -A PREROUTING -i eth2 -d 2.0.0.2 -j DNAT --to-destination XXX.XXX.XXX.XXX
|
205
206
|
#Example Provider 3
|
206
|
-
#[0:0] -A PREROUTING -i
|
207
|
+
#[0:0] -A PREROUTING -i ppp0 -j DNAT --to-destination XXX.XXX.XXX.XXX
|
207
208
|
|
208
209
|
#SNAT: LAN/DMZ --> WAN. Force an outgoing connection to use a specific source
|
209
210
|
#address instead of the default one of the outgoing interface. Of course this
|
@@ -220,7 +221,7 @@ COMMIT
|
|
220
221
|
#Example Provider 2
|
221
222
|
#[0:0] -A POSTROUTING -s XXX.XXX.XXX.XXX -o eth2 -j SNAT --to-source YYY.YYY.YYY.YYY
|
222
223
|
#Example Provider 3
|
223
|
-
#[0:0] -A POSTROUTING -s XXX.XXX.XXX.XXX -o
|
224
|
+
#[0:0] -A POSTROUTING -s XXX.XXX.XXX.XXX -o ppp0 -j SNAT --to-source YYY.YYY.YYY.YYY
|
224
225
|
|
225
226
|
#SNAT: LAN --> WAN
|
226
227
|
|
@@ -229,7 +230,7 @@ COMMIT
|
|
229
230
|
#Example Provider 2
|
230
231
|
[0:0] -A POSTROUTING -o eth2 -j SNAT --to-source 2.0.0.2
|
231
232
|
#Example Provider 3
|
232
|
-
[0:0] -A POSTROUTING -o
|
233
|
+
[0:0] -A POSTROUTING -o ppp0 -j MASQUERADE
|
233
234
|
|
234
235
|
COMMIT
|
235
236
|
|
@@ -251,10 +252,10 @@ COMMIT
|
|
251
252
|
|
252
253
|
[0:0] -A FORWARD -i eth0 -o eth1 -j LAN_WAN
|
253
254
|
[0:0] -A FORWARD -i eth0 -o eth2 -j LAN_WAN
|
254
|
-
[0:0] -A FORWARD -i eth0 -o
|
255
|
+
[0:0] -A FORWARD -i eth0 -o ppp0 -j LAN_WAN
|
255
256
|
[0:0] -A FORWARD -i eth1 -o eth0 -j WAN_LAN
|
256
257
|
[0:0] -A FORWARD -i eth2 -o eth0 -j WAN_LAN
|
257
|
-
[0:0] -A FORWARD -i
|
258
|
+
[0:0] -A FORWARD -i ppp0 -o eth0 -j WAN_LAN
|
258
259
|
|
259
260
|
#This is just a very basic example, add your own rules for the FORWARD chain.
|
260
261
|
|
data/bin/fault_tolerant_router
CHANGED
@@ -4,11 +4,14 @@ require 'optparse'
|
|
4
4
|
require 'net/smtp'
|
5
5
|
require 'mail'
|
6
6
|
require 'logger'
|
7
|
+
require 'socket'
|
7
8
|
require 'yaml'
|
8
9
|
require 'fault_tolerant_router/version'
|
9
10
|
require 'fault_tolerant_router/generate_config'
|
10
11
|
require 'fault_tolerant_router/generate_iptables'
|
11
12
|
require 'fault_tolerant_router/monitor'
|
13
|
+
require 'fault_tolerant_router/uplink'
|
14
|
+
require 'fault_tolerant_router/uplinks'
|
12
15
|
|
13
16
|
options = {
|
14
17
|
configuration_file: '/etc/fault_tolerant_router.conf',
|
@@ -22,14 +25,14 @@ parser = OptionParser.new do |opts|
|
|
22
25
|
opts.separator ''
|
23
26
|
opts.separator ' generate_config: save an example configuration'
|
24
27
|
opts.separator ' generate_iptables: generate an "iptables-save" configuration'
|
25
|
-
opts.separator ' email_test: send a test email to verify the correctness of
|
28
|
+
opts.separator ' email_test: send a test email to verify the correctness of SMTP parameters'
|
26
29
|
opts.separator ' monitor: monitor uplinks and change routing accordingly'
|
27
30
|
opts.separator ''
|
31
|
+
opts.separator 'OPTIONS:'
|
28
32
|
opts.on('--config=FILE', 'Configuration file (default /etc/fault_tolerant_router.conf)') do |configuration_file|
|
29
33
|
options[:configuration_file] = configuration_file
|
30
34
|
end
|
31
|
-
opts.separator ''
|
32
|
-
opts.separator '"monitor" specific options:'
|
35
|
+
opts.separator ' "monitor" specific options:'
|
33
36
|
opts.on('--debug', 'Print debug output') do |debug|
|
34
37
|
options[:debug] = debug
|
35
38
|
end
|
@@ -49,7 +52,7 @@ rescue OptionParser::ParseError
|
|
49
52
|
end
|
50
53
|
|
51
54
|
DEMO = options[:demo]
|
52
|
-
#activate debug if
|
55
|
+
#activate debug if in demo mode
|
53
56
|
DEBUG = options[:debug] || DEMO
|
54
57
|
|
55
58
|
unless ARGV.size == 1 && %w(generate_config generate_iptables email_test monitor).include?(ARGV[0])
|
@@ -68,14 +71,13 @@ unless File.exists?(options[:configuration_file])
|
|
68
71
|
end
|
69
72
|
|
70
73
|
config = YAML.load_file(options[:configuration_file])
|
71
|
-
UPLINKS = config['uplinks'].map { |uplink| Hash[uplink.map { |k, v| [k.to_sym, v] }] }
|
72
74
|
LAN_INTERFACE = config['downlinks']['lan']
|
73
75
|
DMZ_INTERFACE = config['downlinks']['dmz']
|
74
76
|
TEST_IPS = config['tests']['ips']
|
75
77
|
REQUIRED_SUCCESSFUL_TESTS = config['tests']['required_successful']
|
76
78
|
PING_RETRIES = config['tests']['ping_retries']
|
77
79
|
TEST_INTERVAL = config['tests']['interval']
|
78
|
-
LOG_FILE = config['log']['
|
80
|
+
LOG_FILE = config['log']['file']
|
79
81
|
LOG_MAX_SIZE = config['log']['max_size']
|
80
82
|
LOG_OLD_FILES = config['log']['old_files']
|
81
83
|
SEND_EMAIL = config['email']['send']
|
@@ -85,6 +87,7 @@ SMTP_PARAMETERS = Hash[config['email']['smtp_parameters'].map { |k, v| [k.to_sym
|
|
85
87
|
BASE_TABLE = config['base_table']
|
86
88
|
BASE_PRIORITY = config['base_priority']
|
87
89
|
BASE_FWMARK = config['base_fwmark']
|
90
|
+
UPLINKS = Uplinks.new(config['uplinks'])
|
88
91
|
|
89
92
|
case ARGV[0]
|
90
93
|
when 'generate_iptables'
|
@@ -94,7 +97,7 @@ case ARGV[0]
|
|
94
97
|
send_email('fault_tolerant_router test')
|
95
98
|
puts "Test email sent to #{EMAIL_RECIPIENTS.join(', ')}"
|
96
99
|
rescue Exception => e
|
97
|
-
puts "
|
100
|
+
puts "Error sending email: #{e}"
|
98
101
|
end
|
99
102
|
else
|
100
103
|
monitor
|
@@ -12,6 +12,7 @@ def generate_config(file_path)
|
|
12
12
|
#add as many uplinks as needed
|
13
13
|
uplinks:
|
14
14
|
- interface: eth1
|
15
|
+
type: static
|
15
16
|
ip: 1.0.0.2
|
16
17
|
gateway: 1.0.0.1
|
17
18
|
description: Example Provider 1
|
@@ -20,6 +21,7 @@ uplinks:
|
|
20
21
|
#optional parameter, default is true
|
21
22
|
default_route: true
|
22
23
|
- interface: eth2
|
24
|
+
type: static
|
23
25
|
ip: 2.0.0.2
|
24
26
|
gateway: 2.0.0.1
|
25
27
|
description: Example Provider 2
|
@@ -27,9 +29,8 @@ uplinks:
|
|
27
29
|
weight: 2
|
28
30
|
#optional parameter, default is true
|
29
31
|
default_route: true
|
30
|
-
- interface:
|
31
|
-
|
32
|
-
gateway: 3.0.0.1
|
32
|
+
- interface: ppp0
|
33
|
+
type: ppp
|
33
34
|
description: Example Provider 3
|
34
35
|
#optional parameter
|
35
36
|
weight: 1
|
@@ -21,10 +21,10 @@ def generate_iptables
|
|
21
21
|
# --sport, etc.
|
22
22
|
|
23
23
|
END
|
24
|
-
UPLINKS.
|
25
|
-
puts "##{uplink
|
26
|
-
puts "#[0:0] -A PREROUTING -i #{LAN_INTERFACE} -m conntrack --ctstate NEW -p tcp --dport XXX -j CONNMARK --set-mark #{
|
27
|
-
puts "#[0:0] -A PREROUTING -i #{DMZ_INTERFACE} -m conntrack --ctstate NEW -p tcp --dport XXX -j CONNMARK --set-mark #{
|
24
|
+
UPLINKS.each do |uplink|
|
25
|
+
puts "##{uplink.description}"
|
26
|
+
puts "#[0:0] -A PREROUTING -i #{LAN_INTERFACE} -m conntrack --ctstate NEW -p tcp --dport XXX -j CONNMARK --set-mark #{uplink.fwmark}"
|
27
|
+
puts "#[0:0] -A PREROUTING -i #{DMZ_INTERFACE} -m conntrack --ctstate NEW -p tcp --dport XXX -j CONNMARK --set-mark #{uplink.fwmark}" if DMZ_INTERFACE
|
28
28
|
end
|
29
29
|
puts <<END
|
30
30
|
|
@@ -47,9 +47,9 @@ END
|
|
47
47
|
#New inbound connections: mark the connection with the incoming interface.
|
48
48
|
|
49
49
|
END
|
50
|
-
UPLINKS.
|
51
|
-
puts "##{uplink
|
52
|
-
puts "[0:0] -A PREROUTING -i #{uplink
|
50
|
+
UPLINKS.each do |uplink|
|
51
|
+
puts "##{uplink.description}"
|
52
|
+
puts "[0:0] -A PREROUTING -i #{uplink.interface} -m conntrack --ctstate NEW -j CONNMARK --set-mark #{uplink.fwmark}"
|
53
53
|
end
|
54
54
|
puts <<END
|
55
55
|
|
@@ -57,9 +57,9 @@ END
|
|
57
57
|
#(chosen by the multipath routing).
|
58
58
|
|
59
59
|
END
|
60
|
-
UPLINKS.
|
61
|
-
puts "##{uplink
|
62
|
-
puts "[0:0] -A POSTROUTING -o #{uplink
|
60
|
+
UPLINKS.each do |uplink|
|
61
|
+
puts "##{uplink.description}"
|
62
|
+
puts "[0:0] -A POSTROUTING -o #{uplink.interface} -m conntrack --ctstate NEW -j CONNMARK --set-mark #{uplink.fwmark}"
|
63
63
|
end
|
64
64
|
puts <<END
|
65
65
|
|
@@ -82,8 +82,12 @@ COMMIT
|
|
82
82
|
|
83
83
|
END
|
84
84
|
UPLINKS.each do |uplink|
|
85
|
-
puts "##{uplink
|
86
|
-
|
85
|
+
puts "##{uplink.description}"
|
86
|
+
if uplink.type == :ppp
|
87
|
+
puts "#[0:0] -A PREROUTING -i #{uplink.interface} -j DNAT --to-destination XXX.XXX.XXX.XXX"
|
88
|
+
else
|
89
|
+
puts "#[0:0] -A PREROUTING -i #{uplink.interface} -d #{uplink.ip} -j DNAT --to-destination XXX.XXX.XXX.XXX"
|
90
|
+
end
|
87
91
|
end
|
88
92
|
puts <<END
|
89
93
|
|
@@ -99,8 +103,8 @@ END
|
|
99
103
|
|
100
104
|
END
|
101
105
|
UPLINKS.each do |uplink|
|
102
|
-
puts "##{uplink
|
103
|
-
puts "#[0:0] -A POSTROUTING -s XXX.XXX.XXX.XXX -o #{uplink
|
106
|
+
puts "##{uplink.description}"
|
107
|
+
puts "#[0:0] -A POSTROUTING -s XXX.XXX.XXX.XXX -o #{uplink.interface} -j SNAT --to-source YYY.YYY.YYY.YYY"
|
104
108
|
end
|
105
109
|
puts <<END
|
106
110
|
|
@@ -108,8 +112,12 @@ END
|
|
108
112
|
|
109
113
|
END
|
110
114
|
UPLINKS.each do |uplink|
|
111
|
-
puts "##{uplink
|
112
|
-
|
115
|
+
puts "##{uplink.description}"
|
116
|
+
if uplink.type == :ppp
|
117
|
+
puts "[0:0] -A POSTROUTING -o #{uplink.interface} -j MASQUERADE"
|
118
|
+
else
|
119
|
+
puts "[0:0] -A POSTROUTING -o #{uplink.interface} -j SNAT --to-source #{uplink.ip}"
|
120
|
+
end
|
113
121
|
end
|
114
122
|
puts <<END
|
115
123
|
|
@@ -141,17 +149,17 @@ END
|
|
141
149
|
|
142
150
|
END
|
143
151
|
UPLINKS.each do |uplink|
|
144
|
-
puts "[0:0] -A FORWARD -i #{LAN_INTERFACE} -o #{uplink
|
152
|
+
puts "[0:0] -A FORWARD -i #{LAN_INTERFACE} -o #{uplink.interface} -j LAN_WAN"
|
145
153
|
end
|
146
154
|
UPLINKS.each do |uplink|
|
147
|
-
puts "[0:0] -A FORWARD -i #{uplink
|
155
|
+
puts "[0:0] -A FORWARD -i #{uplink.interface} -o #{LAN_INTERFACE} -j WAN_LAN"
|
148
156
|
end
|
149
157
|
if DMZ_INTERFACE
|
150
158
|
UPLINKS.each do |uplink|
|
151
|
-
puts "[0:0] -A FORWARD -i #{DMZ_INTERFACE} -o #{uplink
|
159
|
+
puts "[0:0] -A FORWARD -i #{DMZ_INTERFACE} -o #{uplink.interface} -j DMZ_WAN"
|
152
160
|
end
|
153
161
|
UPLINKS.each do |uplink|
|
154
|
-
puts "[0:0] -A FORWARD -i #{uplink
|
162
|
+
puts "[0:0] -A FORWARD -i #{uplink.interface} -o #{DMZ_INTERFACE} -j WAN_DMZ"
|
155
163
|
end
|
156
164
|
end
|
157
165
|
puts <<END
|
@@ -1,36 +1,13 @@
|
|
1
|
-
def command(
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
def ping(ip, source)
|
7
|
-
if DEMO
|
8
|
-
sleep 0.1
|
9
|
-
rand(3) > 0
|
10
|
-
else
|
11
|
-
`ping -n -c 1 -W 2 -I #{source} #{ip}`
|
12
|
-
$?.to_i == 0
|
1
|
+
def command(input)
|
2
|
+
input = [input] if input.is_a?(String)
|
3
|
+
input.each do |c|
|
4
|
+
`#{c}` unless DEMO
|
5
|
+
puts "Command: #{c}" if DEBUG
|
13
6
|
end
|
14
7
|
end
|
15
8
|
|
16
|
-
def
|
17
|
-
|
18
|
-
enabled_uplinks = UPLINKS.find_all { |uplink| uplink[:enabled] }
|
19
|
-
#do not use balancing if there is just one enabled uplink
|
20
|
-
if enabled_uplinks.size == 1
|
21
|
-
nexthops = "via #{enabled_uplinks.first[:gateway]}"
|
22
|
-
else
|
23
|
-
nexthops = enabled_uplinks.collect do |uplink|
|
24
|
-
#the "weight" parameter is optional
|
25
|
-
weight = uplink[:weight] ? " weight #{uplink[:weight]}" : ''
|
26
|
-
"nexthop via #{uplink[:gateway]}#{weight}"
|
27
|
-
end
|
28
|
-
nexthops = nexthops.join(' ')
|
29
|
-
end
|
30
|
-
#set the route for first packet of outbound connections
|
31
|
-
command "ip route replace table #{BASE_TABLE + UPLINKS.size} default #{nexthops}"
|
32
|
-
#apply the routing changes
|
33
|
-
command 'ip route flush cache'
|
9
|
+
def log(logger, messages)
|
10
|
+
logger.warn(messages.join('; ')) if messages.any?
|
34
11
|
end
|
35
12
|
|
36
13
|
def send_email(body)
|
@@ -45,141 +22,34 @@ end
|
|
45
22
|
|
46
23
|
def monitor
|
47
24
|
logger = Logger.new(LOG_FILE, LOG_OLD_FILES, LOG_MAX_SIZE)
|
48
|
-
|
49
|
-
#enable all the uplinks
|
50
|
-
UPLINKS.each do |uplink|
|
51
|
-
uplink[:working] = true
|
52
|
-
uplink[:default_route] ||= uplink[:default_route].nil?
|
53
|
-
uplink[:enabled] = uplink[:default_route]
|
54
|
-
end
|
55
|
-
|
56
|
-
#clean all previous configurations, try to clean more than needed (double) to avoid problems in case of changes in the
|
57
|
-
#number of uplinks between different executions
|
58
|
-
((UPLINKS.size * 2 + 1) * 2).times do |i|
|
59
|
-
command "ip rule del priority #{BASE_PRIORITY + i} &> /dev/null"
|
60
|
-
end
|
61
|
-
((UPLINKS.size + 1) * 2).times do |i|
|
62
|
-
command "ip route del table #{BASE_TABLE + i} &> /dev/null"
|
63
|
-
end
|
64
|
-
|
65
|
-
#enable IP forwarding
|
66
|
-
command 'echo 1 > /proc/sys/net/ipv4/ip_forward'
|
67
|
-
|
68
|
-
#disable "reverse path filtering" on the uplink interfaces
|
69
|
-
command 'echo 0 > /proc/sys/net/ipv4/conf/all/rp_filter'
|
70
|
-
UPLINKS.each do |uplink|
|
71
|
-
command "echo 2 > /proc/sys/net/ipv4/conf/#{uplink[:interface]}/rp_filter"
|
72
|
-
end
|
73
|
-
|
74
|
-
#- locally generated packets having as source ip the ethX ip
|
75
|
-
#- returning packets of inbound connections coming from ethX
|
76
|
-
#- non-first packets of outbound connections for which the first packet has been sent to ethX via multipath routing
|
77
|
-
UPLINKS.each_with_index do |uplink, i|
|
78
|
-
command "ip route add table #{BASE_TABLE + i} default via #{uplink[:gateway]} src #{uplink[:ip]}"
|
79
|
-
command "ip rule add priority #{BASE_PRIORITY + i} from #{uplink[:ip]} lookup #{BASE_TABLE + i}"
|
80
|
-
command "ip rule add priority #{BASE_PRIORITY + UPLINKS.size + i} fwmark #{BASE_FWMARK + i} lookup #{BASE_TABLE + i}"
|
81
|
-
end
|
82
|
-
#first packet of outbound connections
|
83
|
-
command "ip rule add priority #{BASE_PRIORITY + UPLINKS.size * 2} from all lookup #{BASE_TABLE + UPLINKS.size}"
|
84
|
-
set_default_route
|
25
|
+
command UPLINKS.initialize_routing_commands
|
85
26
|
|
86
27
|
loop do
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
print "Uplink #{uplink[:description]}: ping #{test}... "
|
102
|
-
STDOUT.flush
|
103
|
-
end
|
104
|
-
if ping(test, uplink[:ip])
|
105
|
-
successful_test = true
|
106
|
-
puts 'ok' if DEBUG
|
107
|
-
#avoid more pings to the same ip after a successful one
|
108
|
-
break
|
109
|
-
else
|
110
|
-
puts 'error' if DEBUG
|
111
|
-
end
|
112
|
-
end
|
113
|
-
if successful_test
|
114
|
-
uplink[:successful_tests] += 1
|
115
|
-
else
|
116
|
-
uplink[:unsuccessful_tests] += 1
|
117
|
-
end
|
118
|
-
#if not currently doing the last test...
|
119
|
-
if i + 1 < TEST_IPS.size
|
120
|
-
if uplink[:successful_tests] >= REQUIRED_SUCCESSFUL_TESTS
|
121
|
-
puts "Uplink #{uplink[:description]}: avoiding more tests because there are enough positive ones" if DEBUG
|
122
|
-
break
|
123
|
-
elsif TEST_IPS.size - uplink[:unsuccessful_tests] < REQUIRED_SUCCESSFUL_TESTS
|
124
|
-
puts "Uplink #{uplink[:description]}: avoiding more tests because too many have been failed" if DEBUG
|
125
|
-
break
|
126
|
-
end
|
127
|
-
end
|
128
|
-
end
|
129
|
-
uplink[:working] = uplink[:successful_tests] >= REQUIRED_SUCCESSFUL_TESTS
|
130
|
-
uplink[:enabled] = uplink[:working] && uplink[:default_route]
|
131
|
-
end
|
132
|
-
|
133
|
-
#only consider uplinks flagged as default route
|
134
|
-
if UPLINKS.find_all { |uplink| uplink[:default_route] }.all? { |uplink| !uplink[:working] }
|
135
|
-
UPLINKS.find_all { |uplink| uplink[:default_route] }.each { |uplink| uplink[:enabled] = true }
|
136
|
-
puts 'No uplink seems to be working, enabling all of them' if DEBUG
|
137
|
-
end
|
138
|
-
|
139
|
-
UPLINKS.each do |uplink|
|
140
|
-
description = case
|
141
|
-
when uplink[:enabled] && !uplink[:previously_enabled] then
|
142
|
-
', enabled'
|
143
|
-
when !uplink[:enabled] && uplink[:previously_enabled] then
|
144
|
-
', disabled'
|
145
|
-
else
|
146
|
-
''
|
147
|
-
end
|
148
|
-
puts "Uplink #{uplink[:description]}: #{uplink[:successful_tests]} successful tests, #{uplink[:unsuccessful_tests]} unsuccessful tests#{description}"
|
149
|
-
end if DEBUG
|
150
|
-
|
151
|
-
#set a new default route if there are changes between the previous and the current uplinks situation
|
152
|
-
set_default_route if UPLINKS.any? { |uplink| uplink[:enabled] != uplink[:previously_enabled] }
|
153
|
-
|
154
|
-
if UPLINKS.any? { |uplink| uplink[:working] != uplink[:previously_working] }
|
155
|
-
body = ''
|
156
|
-
UPLINKS.each do |uplink|
|
157
|
-
body += "Uplink #{uplink[:description]}: #{uplink[:previously_working] ? 'up' : 'down'}"
|
158
|
-
if uplink[:previously_working] == uplink[:working]
|
159
|
-
body += "\n"
|
160
|
-
else
|
161
|
-
body += " --> #{uplink[:working] ? 'up' : 'down'}\n"
|
162
|
-
end
|
163
|
-
end
|
164
|
-
|
165
|
-
logger.warn(body.gsub("\n", ';'))
|
166
|
-
|
167
|
-
if SEND_EMAIL
|
168
|
-
begin
|
169
|
-
send_email(body)
|
170
|
-
rescue Exception => e
|
171
|
-
puts "Problem sending email: #{e}" if DEBUG
|
172
|
-
logger.error("Problem sending email: #{e}")
|
173
|
-
end
|
28
|
+
routing_commands, ip_change_messages = UPLINKS.detect_ip_changes!
|
29
|
+
command routing_commands
|
30
|
+
log(logger, ip_change_messages)
|
31
|
+
|
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)
|
35
|
+
|
36
|
+
if SEND_EMAIL && (ip_change_messages.any? || up_state_change_messages.any?)
|
37
|
+
begin
|
38
|
+
send_email((ip_change_messages + up_state_change_messages).join("\n"))
|
39
|
+
rescue Exception => e
|
40
|
+
puts "Problem sending email: #{e}" if DEBUG
|
41
|
+
logger.error("Problem sending email: #{e}")
|
174
42
|
end
|
175
43
|
end
|
176
44
|
|
177
|
-
if
|
178
|
-
puts
|
45
|
+
if all_default_route_uplinks_down
|
46
|
+
puts 'No waiting, because all of the default route uplinks are down' if DEBUG
|
47
|
+
elsif DEMO
|
48
|
+
puts "Waiting just 5 seconds because in demo mode, otherwise would be #{TEST_INTERVAL} seconds..." if DEBUG
|
179
49
|
sleep 5
|
180
50
|
else
|
181
51
|
puts "Waiting #{TEST_INTERVAL} seconds..." if DEBUG
|
182
52
|
sleep TEST_INTERVAL
|
183
53
|
end
|
184
54
|
end
|
185
|
-
end
|
55
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
class Uplink
|
2
|
+
attr_reader :default_route, :description, :fwmark, :gateway, :id, :interface, :ip, :priority1, :table, :type, :up, :weight
|
3
|
+
attr_accessor :priority2, :routing
|
4
|
+
|
5
|
+
def initialize(config, id)
|
6
|
+
@id = id
|
7
|
+
@priority1 = BASE_PRIORITY + @id
|
8
|
+
@table = BASE_TABLE + @id
|
9
|
+
@fwmark = BASE_FWMARK + @id
|
10
|
+
@interface = config['interface']
|
11
|
+
raise "Uplink interface not specified: #{config}" unless @interface
|
12
|
+
@type = case config['type']
|
13
|
+
when 'static'
|
14
|
+
:static
|
15
|
+
when 'ppp'
|
16
|
+
:ppp
|
17
|
+
else
|
18
|
+
raise "Uplink type not valid: #{config}"
|
19
|
+
end
|
20
|
+
@description = config['description']
|
21
|
+
raise "Uplink description not specified: #{config}" unless @description
|
22
|
+
@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
|
29
|
+
|
30
|
+
if @type == :static
|
31
|
+
@ip = config['ip']
|
32
|
+
raise "Uplink IP address not specified: #{config}" unless @ip
|
33
|
+
@gateway = config['gateway']
|
34
|
+
raise "Uplink gateway not specified: #{config}" unless @gateway
|
35
|
+
else
|
36
|
+
detect_ppp_ips!
|
37
|
+
puts "Uplink #{@description}: initialized with [ip: #{@ip}, gateway: #{@gateway}]" if DEBUG
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def detect_ppp_ips!
|
42
|
+
@previous_ip = @ip
|
43
|
+
@previous_gateway = @gateway
|
44
|
+
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
|
47
|
+
else
|
48
|
+
ifaddr = Socket.getifaddrs.find { |i| i.name == @interface && i.addr && i.addr.ipv4? }
|
49
|
+
if ifaddr
|
50
|
+
@ip = ifaddr.addr.ip_address
|
51
|
+
@gateway = ifaddr.dstaddr.ip_address
|
52
|
+
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
|
75
|
+
end
|
76
|
+
need_default_route_update = @routing && (@previous_gateway != @gateway)
|
77
|
+
end
|
78
|
+
[commands, need_default_route_update, message]
|
79
|
+
end
|
80
|
+
|
81
|
+
def ping(ip_address)
|
82
|
+
if DEMO
|
83
|
+
sleep 0.1
|
84
|
+
rand(3) > 0
|
85
|
+
else
|
86
|
+
`ping -n -c 1 -W 2 -I #{@ip} #{ip_address}`
|
87
|
+
$?.to_i == 0
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_routing!
|
92
|
+
#save current state
|
93
|
+
@previously_up = @up
|
94
|
+
@previously_routing = @routing
|
95
|
+
|
96
|
+
@successful_tests = 0
|
97
|
+
@unsuccessful_tests = 0
|
98
|
+
|
99
|
+
#for each test (in random order)...
|
100
|
+
TEST_IPS.shuffle.each_with_index do |test, i|
|
101
|
+
successful_test = false
|
102
|
+
|
103
|
+
#retry for several times...
|
104
|
+
PING_RETRIES.times do
|
105
|
+
if DEBUG
|
106
|
+
print "Uplink #{@description}: ping #{test}... "
|
107
|
+
STDOUT.flush
|
108
|
+
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
|
114
|
+
else
|
115
|
+
puts 'error' if DEBUG
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
if successful_test
|
120
|
+
@successful_tests += 1
|
121
|
+
else
|
122
|
+
@unsuccessful_tests += 1
|
123
|
+
end
|
124
|
+
|
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
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
|
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
|
142
|
+
|
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
|
149
|
+
|
150
|
+
[up_state_changed, routing_state_changed, message]
|
151
|
+
end
|
152
|
+
|
153
|
+
def route_add_commands
|
154
|
+
#- locally generated packets having as source ip the ethX ip
|
155
|
+
#- returning packets of inbound connections coming from ethX
|
156
|
+
#- non-first packets of outbound connections for which the first packet has been sent to ethX via multipath routing
|
157
|
+
[
|
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
|
+
]
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
class Uplinks
|
2
|
+
|
3
|
+
def initialize(config)
|
4
|
+
@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
|
+
end
|
7
|
+
|
8
|
+
def each
|
9
|
+
@uplinks.each { |uplink| yield uplink }
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize_routing_commands
|
13
|
+
commands = []
|
14
|
+
priorities = @uplinks.map { |uplink| [uplink.priority1, uplink.priority2] }.flatten.minmax
|
15
|
+
tables = @uplinks.map { |uplink| uplink.table }.minmax
|
16
|
+
|
17
|
+
#enable IP forwarding
|
18
|
+
commands += ['echo 1 > /proc/sys/net/ipv4/ip_forward']
|
19
|
+
|
20
|
+
#clean all previous configurations, try to clean more than needed (double) to avoid problems in case of changes in the
|
21
|
+
#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
|
+
|
25
|
+
#disable "reverse path filtering" on the uplink interfaces
|
26
|
+
commands += ['echo 0 > /proc/sys/net/ipv4/conf/all/rp_filter']
|
27
|
+
commands += @uplinks.map { |uplink| "echo 2 > /proc/sys/net/ipv4/conf/#{uplink.interface}/rp_filter" }
|
28
|
+
|
29
|
+
#set uplinks routes
|
30
|
+
commands += @uplinks.map { |uplink| uplink.route_add_commands }
|
31
|
+
|
32
|
+
#rule for first packet of outbound connections
|
33
|
+
commands += ["ip rule add priority #{priorities.max + 1} from all lookup #{tables.max + 1}"]
|
34
|
+
|
35
|
+
#set default route
|
36
|
+
commands += set_default_route_commands
|
37
|
+
|
38
|
+
#apply the routing changes
|
39
|
+
commands += ['ip route flush cache']
|
40
|
+
|
41
|
+
commands.flatten
|
42
|
+
end
|
43
|
+
|
44
|
+
def set_default_route_commands
|
45
|
+
routing_uplinks = @uplinks.find_all { |uplink| uplink.routing }
|
46
|
+
|
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 = []
|
66
|
+
|
67
|
+
@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
|
72
|
+
end
|
73
|
+
|
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
|
77
|
+
end
|
78
|
+
|
79
|
+
#apply the routing changes, in any
|
80
|
+
commands += ['ip route flush cache'] if commands.any?
|
81
|
+
|
82
|
+
[commands, messages]
|
83
|
+
end
|
84
|
+
|
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
|
90
|
+
commands = []
|
91
|
+
|
92
|
+
@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
|
97
|
+
end
|
98
|
+
|
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
|
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']
|
111
|
+
end
|
112
|
+
|
113
|
+
messages = [] unless any_up_state_changed
|
114
|
+
|
115
|
+
[commands, messages, all_default_route_uplinks_down]
|
116
|
+
end
|
117
|
+
|
118
|
+
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.0
|
4
|
+
version: 1.1.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-
|
11
|
+
date: 2015-06-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -79,6 +79,8 @@ files:
|
|
79
79
|
- lib/fault_tolerant_router/generate_config.rb
|
80
80
|
- lib/fault_tolerant_router/generate_iptables.rb
|
81
81
|
- lib/fault_tolerant_router/monitor.rb
|
82
|
+
- lib/fault_tolerant_router/uplink.rb
|
83
|
+
- lib/fault_tolerant_router/uplinks.rb
|
82
84
|
- lib/fault_tolerant_router/version.rb
|
83
85
|
homepage: https://github.com/drsound/fault_tolerant_router
|
84
86
|
licenses:
|