drsound/fault_tolerant_router

View on GitHub
lib/fault_tolerant_router/uplinks.rb

Summary

Maintainability
C
1 day
Test Coverage
class Uplinks
  include Enumerable

  def initialize(config)
    @uplinks = config.each_with_index.map { |uplink, i| Uplink.new(uplink, i) }
    @uplinks.each { |uplink| uplink.rule_priority_2 = BASE_PRIORITY + @uplinks.size + uplink.id }
    @default_route_table = @uplinks.map { |uplink| uplink.table }.max + 1
  end

  def each
    @uplinks.each { |uplink| yield uplink }
  end

  def initialize_routing!
    commands = []
    rule_priorities = @uplinks.map { |uplink| [uplink.rule_priority_1, uplink.rule_priority_2] }.flatten.minmax
    tables = @uplinks.map { |uplink| uplink.table }.minmax

    #enable IP forwarding
    commands << 'echo 1 > /proc/sys/net/ipv4/ip_forward'

    #clean all previous configurations, try to clean more than needed (double) to avoid problems in case of changes in the
    #number of uplinks between different executions
    ((rule_priorities.max - rule_priorities.min + 2) * 2).times { |i| commands << "ip rule del priority #{rule_priorities.min + i} &> /dev/null" }
    ((tables.max - tables.min + 2) * 2).times { |i| commands << "ip route del table #{tables.min + i} &> /dev/null" }

    #disable "reverse path filtering" on the uplink interfaces
    commands << 'echo 0 > /proc/sys/net/ipv4/conf/all/rp_filter'
    commands += @uplinks.map { |uplink| "echo 2 > /proc/sys/net/ipv4/conf/#{uplink.interface}/rp_filter" }

    #set uplinks routes
    commands += @uplinks.map { |uplink| uplink.route_add_commands }

    #rule for first packet of outbound connections
    commands << "ip rule add priority #{rule_priorities.max + 1} from all lookup #{tables.max + 1}"

    #set default route
    commands += update_default_route!

    #apply the routing changes
    commands << 'ip route flush cache'

    commands.flatten
  end

  def update_default_route!
    #select uplinks that are up and with a specified priority group value
    selected = @uplinks.find_all { |uplink| uplink.up && uplink.priority_group }
    puts "Choosing default route: available uplinks: #{selected.map { |uplink| uplink.description }.join(', ')}" if DEBUG

    #restrict the selection to the members of highest priority group
    highest_available_priority = selected.map { |uplink| uplink.priority_group }.min
    selected = selected.find_all { |uplink| uplink.priority_group == highest_available_priority }
    puts "Choosing default route: highest priority group uplinks: #{selected.map { |uplink| uplink.description }.join(', ')}" if DEBUG

    changes = false
    #assign default route status to the uplinks and detect changes from previous configuration
    @uplinks.each do |uplink|
      uplink.previously_default_route = uplink.default_route
      uplink.default_route = selected.include?(uplink)
      changes ||= uplink.default_route != uplink.previously_default_route
    end

    #check if any default route uplink changed its gateway (for example due to a ppp update)
    if @uplinks.any? { |uplink| uplink.default_route && uplink.gateway != uplink.previous_gateway }
      changes ||= true
      puts "Choosing default route: detected gateway change in a default route uplink" if DEBUG
    end

    commands = []
    if selected.size == 0
      puts 'Choosing default route: no available uplinks, no need for an update' if DEBUG
    elsif !changes
      puts 'Choosing default route: no changes, no need for an update' if DEBUG
    else
      puts 'Choosing default route: changes detected, update needed' if DEBUG
      #do not use balancing if there is just one routing uplink
      if selected.size == 1
        nexthops = "via #{selected.first.gateway}"
      else
        nexthops = selected.map do |uplink|
          #the "weight" parameter is optional
          tail = uplink.weight ? " weight #{uplink.weight}" : ''
          "nexthop via #{uplink.gateway}#{tail}"
        end
        nexthops = nexthops.join(' ')
      end

      #set the route for first packet of outbound connections
      commands << "ip route replace table #{@default_route_table} default #{nexthops}"
    end

    commands
  end

  def test!
    commands = []
    messages = []
    @uplinks.each do |uplink|
      c = uplink.test!
      commands += c
    end

    commands += update_default_route!

    #apply the routing changes, in any
    commands << 'ip route flush cache' if commands.any?

    changes = false
    @uplinks.each do |uplink|
      current = uplink.ip || 'none'
      previous = uplink.previous_ip || 'none'
      changes ||= current != previous
      ip = current == previous ? current : "#{previous} --> #{current}"

      current = uplink.gateway || 'none'
      previous = uplink.previous_gateway || 'none'
      changes ||= current != previous
      gateway = current == previous ? current : "#{previous} --> #{current}"

      current = uplink.up ? 'up' : 'down'
      previous = uplink.previously_up ? 'up' : 'down'
      changes ||= current != previous
      up = current == previous ? current : "#{previous} --> #{current}"

      current = uplink.default_route ? 'routing' : 'standby'
      previous = uplink.previously_default_route ? 'routing' : 'standby'
      changes ||= current != previous
      default_route = current == previous ? current : "#{previous} --> #{current}"

      messages << "Uplink #{uplink.description}: ip #{ip}, gateway #{gateway}, #{up}, #{default_route}"
    end
    messages = [] unless changes

    [commands, messages]
  end

  def all_priority_group_members_down?
    @uplinks.all? { |uplink| !uplink.priority_group || !uplink.up }
  end

end