drsound/fault_tolerant_router

View on GitHub
lib/fault_tolerant_router/uplink.rb

Summary

Maintainability
D
2 days
Test Coverage
class Uplink
  attr_reader :description, :fwmark, :gateway, :id, :interface, :ip, :previous_gateway, :previous_ip, :previously_up, :priority_group, :rule_priority_1, :table, :type, :up, :weight
  attr_accessor :default_route, :previously_default_route, :rule_priority_2

  def initialize(config, id)
    @id = id
    @rule_priority_1 = BASE_PRIORITY + @id
    @table = BASE_TABLE + @id
    @fwmark = BASE_FWMARK + @id
    @interface = config['interface']
    unless @interface
      puts 'Error: uplink interface not specified'
      exit 1
    end
    @type = case config['type']
              when 'static'
                :static
              when 'ppp'
                :ppp
              else
                puts "Error: '#{config['type']}' is not a valid uplink type"
                exit 1
            end
    @description = config['description']
    unless @description
      puts 'Error: uplink description not specified'
      exit 1
    end
    @weight = config['weight']
    @priority_group = config['priority_group']
    @default_route = false

    if @type == :static
      @ip = config['ip']
      unless @ip
        puts 'Error: uplink IP not specified'
        exit 1
      end
      @gateway = config['gateway']
      unless @gateway
        puts 'Error: uplink gateway not specified'
        exit 1
      end
    else
      detect_ppp_ips!
    end

    @previous_ip = @ip
    @previous_gateway = @gateway
    #a new uplink is supposed to be up
    @up = true
    @previously_up = true
  end

  def detect_ppp_ips!
    @previous_ip = @ip
    @previous_gateway = @gateway
    if DEMO
      @ip = ['3.0.0.101', '3.0.0.102', nil].sample
      @gateway = ['3.0.0.1', '3.0.0.2', nil].sample
    else
      ifaddr = Socket.getifaddrs.find { |i| i.name == @interface && i.addr && i.addr.ipv4? }
      if ifaddr
        @ip = ifaddr.addr.ip_address
        @gateway = ifaddr.dstaddr.ip_address
      else
        @ip = nil
        @gateway = nil
      end
    end
    puts "Uplink #{@description}: detected ip #{@ip || 'none'}, gateway #{@gateway || 'none'}" if DEBUG
  end

  def ping(ip_address)
    if DEMO
      sleep 0.1
      rand(3) > 0
    else
      `ping -n -c 1 -W 2 -I #{@ip} #{ip_address}`
      $?.to_i == 0
    end
  end

  def test!
    #save current state
    @previously_up = @up

    successful_tests = 0
    unsuccessful_tests = 0
    commands = []

    if @type == :ppp
      detect_ppp_ips!
      if (@previous_ip != @ip) || (@previous_gateway != @gateway)
        #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
        if @ip && @gateway
          commands << "ip rule del priority #{@rule_priority_1}"
          commands << "ip rule del priority #{@rule_priority_2}"
          commands += route_add_commands
        end
      end
    end

    #do not ping if there is no ip or gateway (for example in case of a PPP interface down)
    if @ip && @gateway
      #for each test (in random order)...
      TEST_IPS.shuffle.each_with_index do |test, i|
        successful_test = false

        #retry for several times...
        PING_RETRIES.times do
          if DEBUG
            print "Uplink #{@description}: ping #{test}... "
            STDOUT.flush
          end
          if ping(test)
            successful_test = true
            puts 'ok' if DEBUG
            #avoid more pings to the same ip after a successful one
            break
          else
            puts 'error' if DEBUG
          end
        end

        if successful_test
          successful_tests += 1
        else
          unsuccessful_tests += 1
        end

        #if not currently doing the last test...
        if i + 1 < TEST_IPS.size
          if successful_tests >= REQUIRED_SUCCESSFUL_TESTS
            puts "Uplink #{@description}: avoiding more tests because there are enough positive ones" if DEBUG
            break
          elsif TEST_IPS.size - unsuccessful_tests < REQUIRED_SUCCESSFUL_TESTS
            puts "Uplink #{@description}: avoiding more tests because too many have been failed" if DEBUG
            break
          end
        end
      end
    end

    @up = successful_tests >= REQUIRED_SUCCESSFUL_TESTS

    if DEBUG
      state = @previously_up ? 'up' : 'down'
      state += " --> #{@up ? 'up' : 'down'}" if @up != @previously_up
      puts "Uplink #{@description}: #{successful_tests} successful tests, #{unsuccessful_tests} unsuccessful tests, state #{state}"
    end

    commands
  end

  def route_add_commands
    #- locally generated packets having as source ip the ethX ip
    #- returning packets of inbound connections coming from ethX
    #- non-first packets of outbound connections for which the first packet has been sent to ethX via multipath routing
    [
        "ip route replace table #{@table} default via #{@gateway} src #{@ip}",
        "ip rule add priority #{@rule_priority_1} from #{@ip} lookup #{@table}",
        "ip rule add priority #{@rule_priority_2} fwmark #{@fwmark} lookup #{@table}"
    ]
  end

end