class Scriptroute::Ally
Public Class Methods
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
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
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
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
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
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
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
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
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
@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
@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
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
@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
@return [String] the “verdict”
# File lib/scriptroute/ally.rb, line 425 def to_s @verdict end
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
@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
# File lib/scriptroute/ally.rb, line 232 def merc(exchange) exchange.probe.packet.ip_dst + '->' + exchange.response.packet.ip_src end
@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
@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
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