class Object

Public Instance Methods

command(input) click to toggle source
# File lib/fault_tolerant_router/monitor.rb, line 1
def command(input)
  input = [input] if input.is_a?(String)
  input.each do |c|
    `#{c}` unless DEMO
    puts "Command: #{c}" if DEBUG
  end
end
generate_config(file_path) click to toggle source
# File lib/fault_tolerant_router/generate_config.rb, line 1
def generate_config(file_path)
  if File.exist?(file_path)
    puts "Configuration file #{file_path} already exists, will not overwrite!"
    exit 1
  end
  begin
    open(file_path, 'w') do |file|
      file.puts <<END
#see https://github.com/drsound/fault_tolerant_router for a complete parameter
#description

#add as many uplinks as needed, in this example ppp0 is used as default route only if both eth1 and eth2 are down
uplinks:
- interface: eth1
  type: static
  ip: 1.0.0.2
  gateway: 1.0.0.1
  description: Example Provider 1
  priority_group: 1
  #optional parameter
  weight: 1
- interface: eth2
  type: static
  ip: 2.0.0.2
  gateway: 2.0.0.1
  description: Example Provider 2
  priority_group: 1
  #optional parameter
  weight: 2
- interface: ppp0
  type: ppp
  description: Example Provider 3
  priority_group: 2
  #optional parameter
  weight: 1

downlinks:
  lan: eth0
  #leave blank if you have no DMZ
  dmz:

tests:
  #an array of IP addresses to ping to verify the uplinks state. You can add as
  #many as you wish. Predefined ones are Google DNS, OpenDNS DNS, other public
  #DNS. Every time an uplink is tested the IP addresses are shuffled, so listing
  #order is not important.
  ips:
  - 8.8.8.8
  - 8.8.4.4
  - 208.67.222.222
  - 208.67.220.220
  - 4.2.2.2
  - 4.2.2.3
  #number of successfully pinged IP addresses to consider an uplink to be
  #functional
  required_successful: 4
  #number of ping retries before giving up on an IP
  ping_retries: 1
  #seconds between a check of the uplinks and the next one
  interval: 60

log:
  #file: "/var/log/fault_tolerant_router.log"
  file: "/tmp/fault_tolerant_router.log"
  #maximum log file size (in bytes). Once reached this size, the log file will
  #be rotated.
  max_size: 1024000
  #number of old rotated files to keep
  old_files: 10

email:
  send: false
  sender: router@domain.com
  recipients:
  - user1@domain.com
  - user2@domain.com
  - user3@domain.com
  #see http://ruby-doc.org/stdlib-2.3.1/libdoc/net/smtp/rdoc/Net/SMTP.html
  smtp_parameters:
    address: smtp.gmail.com
    port: 587
    #domain: domain.com
    authentication: :login
    enable_starttls_auto: true
    user_name: user@gmail.com
    password: secret-password

#base IP route table number, just need to change if you are already using
#multiple routing tables
base_table: 1

#just need to change if you are already using ip policy routing, to avoid
#overlapping, must be higher than 32767 (the default routing table priority,
#see output of "ip rule" command)
base_priority: 40000

#just need to change if you are already using packet marking, to avoid
#overlapping
base_fwmark: 1
END
    end
    puts "Example configuration saved to #{file_path}"
  rescue
    puts "Error while saving configuration file #{file_path}!"
    exit 1
  end
end
generate_iptables() click to toggle source
# File lib/fault_tolerant_router/generate_iptables.rb, line 1
def generate_iptables
  puts <<END
#Integrate with your existing "iptables-save" configuration, or adapt to work
#with any other iptables configuration system

*mangle
:PREROUTING ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:INPUT ACCEPT [0:0]

#New outbound connections: force a connection to use a specific uplink instead
#of participating in the multipath routing. This can be useful if you have an
#SMTP server that should always send emails originating from a specific IP
#address (because of PTR DNS records), or if you have some service that you want
#always to use a particular slow/fast uplink.
#
#Uncomment if needed.
#
#NB: these are just examples, you can add as many options as needed: -s, -d,
#    --sport, etc.

END
  UPLINKS.each do |uplink|
    puts "##{uplink.description}"
    puts "#[0:0] -A PREROUTING -i #{LAN_INTERFACE} -m conntrack --ctstate NEW -p tcp --dport XXX -j CONNMARK --set-mark #{uplink.fwmark}"
    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
  end
  puts <<END

#Mark packets with the outgoing interface:
#
#- Established outbound connections: mark non-first packets (first packet will
#  be marked as 0, as a standard unmerked packet, because the connection has not
#  yet been marked with CONNMARK --set-mark)
#
#- New outbound connections: mark first packet, only effective if marking has
#  been done in the section above
#
#- Inbound connections: mark returning packets (from LAN/DMZ to WAN)

[0:0] -A PREROUTING -i #{LAN_INTERFACE} -j CONNMARK --restore-mark
END
  puts "[0:0] -A PREROUTING -i #{DMZ_INTERFACE} -j CONNMARK --restore-mark" if DMZ_INTERFACE
  puts <<END

#New inbound connections: mark the connection with the incoming interface.

END
  UPLINKS.each do |uplink|
    puts "##{uplink.description}"
    puts "[0:0] -A PREROUTING -i #{uplink.interface} -m conntrack --ctstate NEW -j CONNMARK --set-mark #{uplink.fwmark}"
  end
  puts <<END

#New outbound connections: mark the connection with the outgoing interface
#(chosen by the multipath routing).

END
  UPLINKS.each do |uplink|
    puts "##{uplink.description}"
    puts "[0:0] -A POSTROUTING -o #{uplink.interface} -m conntrack --ctstate NEW -j CONNMARK --set-mark #{uplink.fwmark}"
  end
  puts <<END

COMMIT


*nat
:PREROUTING ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]

#DNAT: WAN --> LAN/DMZ. The original destination IP (-d) can be any of the IP
#addresses assigned to the uplink interface. XXX.XXX.XXX.XXX can be any of your
#LAN/DMZ IPs.
#
#Uncomment if needed.
#
#NB: these are just examples, you can add as many options as you wish: -s,
#    --sport, --dport, etc.

END
  UPLINKS.each do |uplink|
    puts "##{uplink.description}"
    if uplink.type == :ppp
      puts "#[0:0] -A PREROUTING -i #{uplink.interface} -j DNAT --to-destination XXX.XXX.XXX.XXX"
    else
      puts "#[0:0] -A PREROUTING -i #{uplink.interface} -d #{uplink.ip} -j DNAT --to-destination XXX.XXX.XXX.XXX"
    end
  end
  puts <<END

#SNAT: LAN/DMZ --> WAN. Force an outgoing connection to use a specific source
#address instead of the default one of the outgoing interface. Of course this
#only makes sense if more than one IP address is assigned to the uplink
#interface.
#
#Uncomment if needed.
#
#NB: these are just examples, you can add as many options as needed: -d,
#    --sport, --dport, etc.

END
  UPLINKS.each do |uplink|
    puts "##{uplink.description}"
    puts "#[0:0] -A POSTROUTING -s XXX.XXX.XXX.XXX -o #{uplink.interface} -j SNAT --to-source YYY.YYY.YYY.YYY"
  end
  puts <<END

#SNAT: LAN --> WAN

END
  UPLINKS.each do |uplink|
    puts "##{uplink.description}"
    if uplink.type == :ppp
      puts "[0:0] -A POSTROUTING -o #{uplink.interface} -j MASQUERADE"
    else
      puts "[0:0] -A POSTROUTING -o #{uplink.interface} -j SNAT --to-source #{uplink.ip}"
    end
  end
  puts <<END

COMMIT


*filter

:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:LAN_WAN - [0:0]
:WAN_LAN - [0:0]
END

  if DMZ_INTERFACE
    puts ':DMZ_WAN - [0:0]'
    puts ':WAN_DMZ - [0:0]'
  end

  puts <<END

#This is just a very basic example, add your own rules for the FORWARD chain.

[0:0] -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
END
  UPLINKS.each do |uplink|
    puts "[0:0] -A FORWARD -i #{LAN_INTERFACE} -o #{uplink.interface} -j LAN_WAN"
  end
  UPLINKS.each do |uplink|
    puts "[0:0] -A FORWARD -i #{uplink.interface} -o #{LAN_INTERFACE} -j WAN_LAN"
  end
  if DMZ_INTERFACE
    UPLINKS.each do |uplink|
      puts "[0:0] -A FORWARD -i #{DMZ_INTERFACE} -o #{uplink.interface} -j DMZ_WAN"
    end
    UPLINKS.each do |uplink|
      puts "[0:0] -A FORWARD -i #{uplink.interface} -o #{DMZ_INTERFACE} -j WAN_DMZ"
    end
  end
  puts <<END

[0:0] -A LAN_WAN -j ACCEPT
[0:0] -A WAN_LAN -j REJECT
END
  if DMZ_INTERFACE
    puts '[0:0] -A DMZ_WAN -j ACCEPT'
    puts '[0:0] -A WAN_DMZ -j ACCEPT'
  end
  puts <<END

COMMIT
END
end
log(logger, messages) click to toggle source
# File lib/fault_tolerant_router/monitor.rb, line 9
def log(logger, messages)
  logger.warn(messages.join('; ')) if messages.any?
end
monitor() click to toggle source
# File lib/fault_tolerant_router/monitor.rb, line 23
def monitor
  logger = Logger.new(LOG_FILE, LOG_OLD_FILES, LOG_MAX_SIZE)
  command(UPLINKS.initialize_routing!)

  list = UPLINKS.select { |uplink| uplink.default_route }.map { |uplink| uplink.description }.join(', ')
  log(logger, ["Monitor started, initial default route uplinks: #{list}"])

  loop do
    commands, messages = UPLINKS.test!
    command(commands)
    log(logger, messages)

    if SEND_EMAIL && messages.any?
      begin
        send_email(messages.join("\n"))
      rescue Exception => e
        puts "Problem sending email: #{e}" if DEBUG
        logger.error("Problem sending email: #{e}")
      end
    end

    if UPLINKS.all_priority_group_members_down?
      puts 'No waiting, because all of the priority group members are down' if DEBUG
    elsif DEMO
      puts "Waiting just 5 seconds because in demo mode, otherwise would be #{TEST_INTERVAL} seconds..." if DEBUG
      sleep 5
    else
      puts "Waiting #{TEST_INTERVAL} seconds..." if DEBUG
      sleep TEST_INTERVAL
    end
  end
end
send_email(body) click to toggle source
# File lib/fault_tolerant_router/monitor.rb, line 13
def send_email(body)
  mail = Mail.new
  mail.from = EMAIL_SENDER
  mail.to = EMAIL_RECIPIENTS
  mail.subject = 'Uplinks status change'
  mail.body = body
  mail.delivery_method :smtp, SMTP_PARAMETERS
  mail.deliver
end