class Scriptroute::Ally

Public Class Methods

aliases?(ip_a, ip_b) click to toggle source

Checks the cache, and if no entry is present, creates a new alias resolution test object to probe. @return [Boolean] whether the “verdict” is “ALIAS”,

ignoring the explanation.
# File lib/scriptroute/ally.rb, line 440
def Ally.aliases?(ip_a, ip_b)
  key = Ally.to_key(ip_a, ip_b)
  if(!@@result_cache.has_key?(key)) then
    Ally.new(ip_a, ip_b)
  end
  @@result_cache[key] =~ /^ALIAS/
end
before(seq1,seq2) click to toggle source

a helper function to handle comparing ipid’s, as they are unsigned short counters that can wrap. @return [Boolean]

# File lib/scriptroute/ally.rb, line 100
def Ally.before(seq1,seq2) 
  diff = seq1-seq2 
  # emulate signed short arithmetic.
  if (diff > 32767) then
    diff-=65535;
  elsif (diff < -32768) then
    diff+=65535;
  end
  # puts "#{seq1} #{diff < 0 ? "" : "not"} before #{seq2}\n"
  (diff < 0)
end
each_alias() { |split(":")| ... } click to toggle source

Iterate through the result cache, yielding each pair of IP addresses found to be aliases. @yield [String,String]

# File lib/scriptroute/ally.rb, line 47
def Ally.each_alias
  @@result_cache.each { |k,v|
    if v =~ /^ALIAS/ then 
      yield k.split(":")
    end
  }
end
make_result_cache_persistent(dbfilename) click to toggle source

use bdb41 to save the cache of results persistently, in case we would like to restore it. This sets the cache to be on disk (so we can read from it, and it will exist after the script) instead of in memory (created and destroyed with every invocation). @param [String] dbfilename where to store the cache. @return [void] @note the cache does not expire entries. The cache should probably not

be used after, say, a month, since topologies can change.

@note if bdb41 cannot be loaded, the cache will not be made persistent, and

an error will print to stderr.
# File lib/scriptroute/ally.rb, line 36
def Ally.make_result_cache_persistent(dbfilename)
  begin 
    require "bdb41"
    @@result_cache = BDB::Hash.new(dbfilename, nil, BDB::CREATE)
  rescue LoadError
    $stderr.puts "Unable to make result cache persistent: install bdb41 (libdb4.1-ruby)"
  end
end
new(ip_a, ip_b, type='udp') click to toggle source

create an object that will represent our attempt to test two addresses. the parameters may be hostnames instead of addresses. @param ip_a [String, IPaddress] The first address to test if an alias for the second. @param ip_b [String, IPaddress] The second address to test if an alias for the first. @param type [String] “udp”, “tcp” (not useful?), or “icmp”

# File lib/scriptroute/ally.rb, line 282
def initialize(ip_a, ip_b, type='udp')
  @a = packet_creator(ip_a, type)
  @b = packet_creator(ip_b, type)
  
  # for now, we don't know.
  @verdict = "UNKNOWN: Failed to complete"
  
  # we'll throw :resolved when we've figured it out; this is
  # to simplify the if/elsif/elsif insanity
  catch :resolved do

    # the quick test; handling this test through the packet
    # test causes confusion.  It is after packet construction
    # so that the names are looked up to addresses
    if( @a.ip_dst == @b.ip_dst ) then
      is_alias "trivial, #{@a.ip_dst} = #{@b.ip_dst}"
    end

    ## this is entirely too complicated, and needs a rewrite.
    packets = Scriptroute::send_train([ Struct::DelayedPacket.new(0,@a), 
                                        Struct::DelayedPacket.new(0.001,@b) ])

    ## try again if we had a pcap overload style problem
    if(packets.length < 2 || 
                       packets[0] == nil || packets[0].probe == nil ||
                       packets[1] == nil || packets[1].probe == nil) then
      packets = Scriptroute::send_train([ Struct::DelayedPacket.new(0,@a), 
          Struct::DelayedPacket.new(0.001,@b) ])
      if(packets.length < 2 || 
                         packets[0] == nil || packets[0].probe == nil ||
                         packets[1] == nil || packets[1].probe == nil) then
        try_undns "Internal error: #{@a.ip_dst} and #{@b.ip_dst}"
      end
    end
    
    if(packets[0].response && packets[1].response) then
      
      assert_response_non_bogus(packets[0])
      assert_response_non_bogus(packets[1])
      
      id0 = packets[0].response.packet.ip_id
      id1 = packets[1].response.packet.ip_id
      
      if(packets[0].response.packet.ip_src == 
         packets[1].response.packet.ip_src) then
        is_alias "mercator/source address: #{merc(packets[0])} #{merc(packets[1])}"
        
      elsif( id0 == id1 ) then
        # when they're the same, it's either:
        if ( id0 == 0 ) then
          # a) a lack of implementation
          try_undns "IPIDs not used: both are zero"
        else
          # b) not aliases.
          not_alias "Same IPID."
        end
        
      elsif(before(id0-10, id1) && before(id1, id0+200)) then
        # adding a delay here (the 0.40) seems to increase the likelihood
        # of a response.
        packetz = Scriptroute::send_train([ Struct::DelayedPacket.new(0.40,@b), 
                                            Struct::DelayedPacket.new(0.001,@a) ])
        if(packetz[0] == nil || packetz[1] == nil)  then
          packetz = Scriptroute::send_train([ Struct::DelayedPacket.new(0.40,@b), 
                                             Struct::DelayedPacket.new(0.001,@a) ])
          if(packetz[0] == nil || packetz[1] == nil)  then
            raise "couldn't send the second set of packets"
          end
        end
        if(packetz[0].response && packetz[1].response) then
          id2 = packetz[0].response.packet.ip_id
          id3 = packetz[1].response.packet.ip_id
          assert_response_non_bogus(packetz[0])
          assert_response_non_bogus(packetz[1])
          if(before(id2-10, id3) &&
             before(id3, id2+200) && 
             before(id0, id2) &&
             before(id1, id3) &&
             unique_ids( [ id0, id1, id2, id3 ] ) ) then
            is_alias "ally/ipid: #{[id0, id1, id2, id3].join(', ')}"
          else 
            not_alias "disparate ids: #{[id0, id1, id2, id3].join(', ')}"
          end
        elsif(packetz[0].response || packetz[1].response) then
          last = packetz[ (packetz[0].response) ? 0 : 1 ] 
          
          assert_response_non_bogus(last)
          id2 = last.response.packet.ip_id
          if(before(id0, id2) &&
             before(id1, id2) &&
             unique_ids( [ id0, id1, id2 ] )) then
            is_alias "ally/ipid; less response: #{[id0, id1, id2].join(', ')}"
          else 
            not_alias "disparate ids (3): #{[id0, id1, id2].join(', ')}"
          end
        else 
          is_alias "ally/ipid; presumptive (second round had no responses): #{[id0, id1].join(', ')}"
        end
      else
        not_alias "quick (2): #{[id0, id1].join(', ')}"
        ## #{[id0, id1].map {|v| (((v&0xff)*256) + v/256)}.join(', ')} "
      end
    elsif(packets[0].response || packets[1].response) then
      first = packets[ (packets[0].response) ? 0 : 1 ] 
      
      # we received only one response.
      # try sending again, reordered.
      packetz = Scriptroute::send_train([ Struct::DelayedPacket.new(0,@b), 
                                          Struct::DelayedPacket.new(0.001,@a) ])
      if(packetz[0].response || packetz[1].response) then
        # we received at least one response
        second = packetz[ (packetz[0].response) ? 0 : 1 ] 
        assert_response_non_bogus(second)
        if((second.probe.packet.ip_dst != second.response.packet.ip_src ||
            first.probe.packet.ip_dst != first.response.packet.ip_src) &&
           first.response.packet.ip_src == second.response.packet.ip_src) then
          # shows the signature of a cisco ( responds with a different source address )
          if(second.probe.packet.ip_dst != first.probe.packet.ip_dst) then
            # and responses to two different requests
            # puts second.response.packet
            # puts first.response.packet
            is_alias "mercator/source address rate limited: #{merc(first)} #{merc(second)}"
          else
            # the destination of both probes we got answers to was the same.
            # the other destination was unresponsive.
            unresponsive(packets[packets[0].response ? 1 : 0].probe.packet.ip_dst)
          end
        else
          # not necessarily true? might have just lost the first packet in the
          # first round.
          unresponsive(packets[packets[0].response ? 1 : 0].probe.packet.ip_dst)
        end
      else
        unresponsive(packets[packets[0].response ? 1 : 0].probe.packet.ip_dst)
      end
    else
      unresponsive(@a.ip_dst, @b.ip_dst)
    end
    fail "shouldn't get here under any circumstances."
  end # catch.
end
to_key(ip_a, ip_b) click to toggle source

keys in the cache are concatenations of IP addresses. since alias relations are symmetric (reflexive?), the addresses are sorted first. @return [String] a key for use in the cache hashtable.

# File lib/scriptroute/ally.rb, line 59
def Ally.to_key(ip_a, ip_b) 
  [ ip_a, ip_b ].sort.join(':') 
end

Public Instance Methods

assert_response_non_bogus(resp) click to toggle source

responses we can get include address unreachable, which means we’re not talking to the intended router. Check first.

# File lib/scriptroute/ally.rb, line 199
def assert_response_non_bogus(resp)
  if( ! resp.response ) then
    raise "(bug!) assert_response_non_bogus shouldn't check that a response was received"
  end
  if(@a.is_a?(Scriptroute::UDP)) then
    # if we tried to probe using udp traceroute-like, we're expecting a port unreach.
    # if we don't get it, likely filtered, try undns
    if((! resp.response.packet.is_a?(Scriptroute::ICMP)) or
       ( resp.response.packet.icmp_type != Scriptroute::ICMP::ICMP_UNREACH ) or
       ( resp.response.packet.icmp_code != Scriptroute::ICMP::ICMP_UNREACH_PORT )) then
      try_undns "filtered: #{resp.probe.packet.ip_dst}"
    end
  elsif(@a.is_a?(Scriptroute::TCP)) then
    # if we tried to probe using tcp 80, we're expecting a tcp rst.
    # if we don't get it, likely filtered, try undns
    if(! resp.response.packet.is_a?(Scriptroute::TCP)) then
      try_undns "filtered: #{resp.probe.packet.ip_dst}"
    end
  elsif(@a.is_a?(Scriptroute::ICMP) &&
        @a.icmp_type == Scriptroute::ICMP_ECHO ) then
    # if we tried to probe using tcp 80, we're expecting a tcp rst.
    # if we don't get it, likely filtered, try undns
    if((! resp.response.packet.is_a?(Scriptroute::ICMP)) or
       resp.response.packet.icmp_type != Scriptroute::ICMP_ECHOREPLY) then
      try_undns "filtered: #{resp.probe.packet.ip_dst}"
    end
  else
    # we don't seem to know what to do.
    fail "Can't recognize expected response to #{@a.to_s}."
  end
end
before(seq1,seq2) click to toggle source

a helper function to handle comparing ipid’s, as they are unsigned short counters that can wrap. @return [Boolean]

# File lib/scriptroute/ally.rb, line 114
def before(seq1,seq2)
  Ally.before(seq1,seq2)
end
ip_to_name(ip) click to toggle source

do a reverse lookup on an IP address. Be prepared to handle an exception, as in safe mode, this is not permitted. @param [String] ip the IP address to convert. @return [String] the output of Socket.gethostbyname @raise [RuntimeError] if gethostbyname throws an exception.

# File lib/scriptroute/ally.rb, line 123
def ip_to_name(ip)
  begin
    (Socket.gethostbyname(ip)[0]).gsub(/\"/,'')
  rescue => e
    # provide a more informative exception.
    raise "#{ip} #{e}"
  end
end
is?() click to toggle source

@return [Boolean] whether the “verdict” is “ALIAS”, ignoring the explanation.

# File lib/scriptroute/ally.rb, line 430
def is? 
  # redundancy is for testing.  want to be able to compare
  # true == true.
  true & (@verdict =~ /^ALIAS/)
end
is_alias(msg) click to toggle source

@param msg [String] why we believe this pair to be aliases @return [void]

# File lib/scriptroute/ally.rb, line 84
def is_alias(msg)
  @verdict = "ALIAS! #{msg}"
  @@result_cache[my_key] = @verdict;
  throw :resolved
end
my_key() click to toggle source

a quick shorthand for finding where our object belongs in the cache. @return [String] a key for use in the cache hashtable.

# File lib/scriptroute/ally.rb, line 66
def my_key
  Ally.to_key(@a.ip_dst, @b.ip_dst)
end
not_alias(msg) click to toggle source

@param msg [String] why we believe this pair to be not aliases @return [void]

# File lib/scriptroute/ally.rb, line 91
def not_alias(msg)
  @verdict = "NOT ALIAS. #{msg}"
  @@result_cache[my_key] = @verdict;
  throw :resolved 
end
to_s() click to toggle source

@return [String] the “verdict”

# File lib/scriptroute/ally.rb, line 425
def to_s 
  @verdict
end
try_undns(msg) click to toggle source

if we can’t tell using packets, try using undns to guess using the names attached to these interfaces.

# File lib/scriptroute/ally.rb, line 134
def try_undns(msg) 
  # we can't tell using packets whether the two are aliases.

  if(!$have_undns) then
    # throws out (return implied)
    unknown "#{msg}; undns not loaded."
  end
  
  # try a reverse lookup, then match the names using established
  # rules.   This requires access to the file system so won't
  # work if run remotely.  (it will jump to the rescue line and
  # print unknown).
  begin
    # the next fragment is designed to try both lookups first,
    # then try both through undns second.  this means we won't
    # complain about undns unless we would have had a chance of
    # using it.
    begin 
      a_name, b_name = [ @a.ip_dst, @b.ip_dst ].map { |dst|
        ip_to_name(dst) 
      }
      begin 
        a_uniq, b_uniq = [ a_name, b_name ].map { |name|
          uniq = Undns.get_identifier(0, name)
          if( uniq == nil || uniq == '') then
            raise "#{name} lacks unique fragments"
          end
          uniq
        }
        if(a_uniq == b_uniq) then
          is_alias "name: #{a_uniq}, otherwise #{msg}"
        else
          not_alias "name: #{a_uniq} != #{b_uniq} and #{msg}"
        end
      rescue => e
        # rule them out if the names say different cities, even
        # if we can't tell the specific pattern to prove that
        # addresses are aliases.  this is just an optimization --
        # unknown is usually treated as "no" anyway.
        a_city, b_city = [ a_name, b_name ].map { |name|
          city = Undns.get_loc(0, name)
          if( city == nil || city == '') then
            raise "#{name} lacks unique fragments and city location"
          end
          city
        }
        if(a_city == b_city) then
          unknown "#{msg}; undns failed and cities are the same: #{e}"
        else
          not_alias "name: cities #{a_city} != #{b_city} and #{msg}"
        end
      end
    end
  rescue SecurityError => e
    unknown "#{msg}; undns failed (securityerror): #{e}"
  rescue LoadError => e
    unknown "#{msg}; loaderror undns failed: #{e}"
  rescue => e
    # unable to lookup, don't have undns, etc.
    unknown "#{msg}; undns failed: #{e}"
  end
end
unknown(msg) click to toggle source

@param msg [String] why we cannot figure out this pair @return [void]

# File lib/scriptroute/ally.rb, line 77
def unknown(msg)
  @verdict = "UNKNOWN. #{msg}"
  @@result_cache[my_key] = @verdict;
  throw :resolved # much like a return in the calling scope.
end

Private Instance Methods

merc(exchange) click to toggle source
# File lib/scriptroute/ally.rb, line 232
def merc(exchange)
  exchange.probe.packet.ip_dst + '->' + exchange.response.packet.ip_src
end
packet_creator(destination, type) click to toggle source

@param destination [String, IPaddress] a destination for assignment using {Scriptroute::IPv4.ip_dst=} @param type [String] “udp”, “tcp”, or “ping” to determine what to send. @return [IPv4] a packet of specified type

# File lib/scriptroute/ally.rb, line 239
def packet_creator(destination, type)
  case type
  when 'udp' 
    probe = Scriptroute::UDP.new(12) 
  when 'tcp' 
    probe = Scriptroute::TCP.new(0) 
  when 'ping' 
    @seq = @seq ? @seq + 1 : 0
    probe = Scriptroute::ICMPecho.new(0) 
    # probe.icmp_type = Scriptroute::Icmp::ICMP_ECHO
    # probe.icmp_code = 0
    probe.icmp_seq = @seq
  else 
    raise "unknown probe type #{type}"
  end
  probe.ip_dst = destination
  probe
end
unique_ids(id_array) click to toggle source

@param [Array] id_array an array of IP IDs to check whether unique (duplicates suggest non-aliases.) @return [Boolean] whether the id_array is unique

# File lib/scriptroute/ally.rb, line 260
def unique_ids(id_array)
  id_array.uniq.length == id_array.length
end
unresponsive(*all) click to toggle source

Handles the case where one or both of the candidate addresses are unresponsive, thus, Ally won’t work.

# File lib/scriptroute/ally.rb, line 266
def unresponsive(*all)
  if all.length > 1 then
    try_undns "two unresponsive: #{all.join(' and ')}"
  else
    try_undns "one unresponsive: #{all.join(' and ')}"
  end
end