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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2c619776dd495528564bc6177a684c7ab5f1da84
4
- data.tar.gz: 6b0434ab1d89c616f0e63e22600ffcb6c6f818cd
3
+ metadata.gz: 5d0eeae84605fd65228ba43ec1429ce65f579ed2
4
+ data.tar.gz: 0bfee9df0185f24b895d7f20330eb4fdd4381345
5
5
  SHA512:
6
- metadata.gz: 102eb31be1f36deb2a01c195280aebb8d13b371c24c9e538845604be7dc310b54772926abe3c0f2e029983c1bdbd09a383157dbfaf7fcf59c9374d321d73b499
7
- data.tar.gz: 713419088384c7486c4994cfb21afa6272a110258cf067cb5ec9d7c441017cf3d2ccf0f6327d04e2f1eef5d738f0dbeb04137a945db60e070644e82fc8e025b3
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
- * **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.
90
- * **gateway**: The uplink gateway, usually the provider's router IP address.
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 eth3 -m conntrack --ctstate NEW -j CONNMARK --set-mark 3
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 eth3 -m conntrack --ctstate NEW -j CONNMARK --set-mark 3
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 eth3 -d 3.0.0.2 -j DNAT --to-destination XXX.XXX.XXX.XXX
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 eth3 -j SNAT --to-source YYY.YYY.YYY.YYY
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 eth3 -j SNAT --to-source 3.0.0.2
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 eth3 -j LAN_WAN
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 eth3 -o eth0 -j WAN_LAN
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
 
@@ -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 smtp parameters'
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 we are in demo mode
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']['log_file']
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 "Problem sending email: #{e}"
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: eth3
31
- ip: 3.0.0.2
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.each_with_index do |uplink, i|
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 #{BASE_FWMARK + i}"
27
- puts "#[0:0] -A PREROUTING -i #{DMZ_INTERFACE} -m conntrack --ctstate NEW -p tcp --dport XXX -j CONNMARK --set-mark #{BASE_FWMARK + i}" if DMZ_INTERFACE
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.each_with_index do |uplink, i|
51
- puts "##{uplink[:description]}"
52
- puts "[0:0] -A PREROUTING -i #{uplink[:interface]} -m conntrack --ctstate NEW -j CONNMARK --set-mark #{BASE_FWMARK + i}"
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.each_with_index do |uplink, i|
61
- puts "##{uplink[:description]}"
62
- puts "[0:0] -A POSTROUTING -o #{uplink[:interface]} -m conntrack --ctstate NEW -j CONNMARK --set-mark #{BASE_FWMARK + i}"
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[:description]}"
86
- puts "#[0:0] -A PREROUTING -i #{uplink[:interface]} -d #{uplink[:ip]} -j DNAT --to-destination XXX.XXX.XXX.XXX"
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[:description]}"
103
- puts "#[0:0] -A POSTROUTING -s XXX.XXX.XXX.XXX -o #{uplink[:interface]} -j SNAT --to-source YYY.YYY.YYY.YYY"
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[:description]}"
112
- puts "[0:0] -A POSTROUTING -o #{uplink[:interface]} -j SNAT --to-source #{uplink[:ip]}"
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[:interface]} -j LAN_WAN"
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[:interface]} -o #{LAN_INTERFACE} -j WAN_LAN"
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[:interface]} -j DMZ_WAN"
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[:interface]} -o #{DMZ_INTERFACE} -j WAN_DMZ"
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(c)
2
- `#{c}` unless DEMO
3
- puts "Command: #{c}" if DEBUG
4
- end
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 set_default_route
17
- #find the enabled uplinks
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
- #for each uplink...
88
- UPLINKS.each do |uplink|
89
- #set current "working" state as the previous one
90
- uplink[:previously_working] = uplink[:working]
91
- #set current "enabled" state as the previous one
92
- uplink[:previously_enabled] = uplink[:enabled]
93
- uplink[:successful_tests] = 0
94
- uplink[:unsuccessful_tests] = 0
95
- #for each test (in random order)...
96
- TEST_IPS.shuffle.each_with_index do |test, i|
97
- successful_test = false
98
- #retry for several times...
99
- PING_RETRIES.times do
100
- if DEBUG
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 DEMO
178
- puts "Waiting just 5 seconds because we are in demo mode, otherwise would wait #{TEST_INTERVAL} seconds..."
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
@@ -1,3 +1,3 @@
1
1
  module FaultTolerantRouter
2
- VERSION = '1.0.1'
2
+ VERSION = '1.1.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.0.1
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-03-10 00:00:00.000000000 Z
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: