Class: Scriptroute::Ally

Inherits:
Object
  • Object
show all
Defined in:
lib/scriptroute/ally.rb

Constant Summary collapse

@@result_cache =

the cache of previously seen results is a class variable.

Hash.new

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(ip_a, ip_b, type = 'udp') ⇒ Ally

create an object that will represent our attempt to test two addresses. the parameters may be hostnames instead of addresses.

Parameters:

  • ip_a (String, IPaddress)

    The first address to test if an alias for the second.

  • ip_b (String, IPaddress)

    The second address to test if an alias for the first.

  • type (String) (defaults to: 'udp')

    “udp”, “tcp” (not useful?), or “icmp”



282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
# 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

Class Method Details

.aliases?(ip_a, ip_b) ⇒ Boolean

Checks the cache, and if no entry is present, creates a new alias resolution test object to probe.

Returns:

  • (Boolean)

    whether the “verdict” is “ALIAS”, ignoring the explanation.



440
441
442
443
444
445
446
# 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) ⇒ Boolean

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

Returns:

  • (Boolean)


100
101
102
103
104
105
106
107
108
109
110
# 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 {|String, String| ... } ⇒ Object

Iterate through the result cache, yielding each pair of IP addresses found to be aliases.

Yields:

  • (String, String)


47
48
49
50
51
52
53
# 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) ⇒ 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.

This method returns an undefined value.

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).

Parameters:

  • dbfilename (String)

    where to store the cache.



36
37
38
39
40
41
42
43
# 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

.to_key(ip_a, ip_b) ⇒ String

keys in the cache are concatenations of IP addresses. since alias relations are symmetric (reflexive?), the addresses are sorted first.

Returns:

  • (String)

    a key for use in the cache hashtable.



59
60
61
# File 'lib/scriptroute/ally.rb', line 59

def Ally.to_key(ip_a, ip_b) 
  [ ip_a, ip_b ].sort.join(':') 
end

Instance Method Details

#assert_response_non_bogus(resp) ⇒ Object

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



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# 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) ⇒ Boolean

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

Returns:

  • (Boolean)


114
115
116
# File 'lib/scriptroute/ally.rb', line 114

def before(seq1,seq2)
  Ally.before(seq1,seq2)
end

#ip_to_name(ip) ⇒ String

do a reverse lookup on an IP address. Be prepared to handle an exception, as in safe mode, this is not permitted.

Parameters:

  • ip (String)

    the IP address to convert.

Returns:

  • (String)

    the output of Socket.gethostbyname

Raises:

  • (RuntimeError)

    if gethostbyname throws an exception.



123
124
125
126
127
128
129
130
# 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?Boolean

Returns whether the “verdict” is “ALIAS”, ignoring the explanation.

Returns:

  • (Boolean)

    whether the “verdict” is “ALIAS”, ignoring the explanation.



430
431
432
433
434
# 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) ⇒ void

This method returns an undefined value.

Parameters:

  • msg (String)

    why we believe this pair to be aliases



84
85
86
87
88
# File 'lib/scriptroute/ally.rb', line 84

def is_alias(msg)
  @verdict = "ALIAS! #{msg}"
  @@result_cache[my_key] = @verdict;
  throw :resolved
end

#my_keyString

a quick shorthand for finding where our object belongs in the cache.

Returns:

  • (String)

    a key for use in the cache hashtable.



66
67
68
# File 'lib/scriptroute/ally.rb', line 66

def my_key
  Ally.to_key(@a.ip_dst, @b.ip_dst)
end

#not_alias(msg) ⇒ void

This method returns an undefined value.

Parameters:

  • msg (String)

    why we believe this pair to be not aliases



91
92
93
94
95
# File 'lib/scriptroute/ally.rb', line 91

def not_alias(msg)
  @verdict = "NOT ALIAS. #{msg}"
  @@result_cache[my_key] = @verdict;
  throw :resolved 
end

#to_sString

Returns the “verdict”.

Returns:

  • (String)

    the “verdict”



425
426
427
# File 'lib/scriptroute/ally.rb', line 425

def to_s 
  @verdict
end

#try_undns(msg) ⇒ Object

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



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# 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) ⇒ void

This method returns an undefined value.

Parameters:

  • msg (String)

    why we cannot figure out this pair



77
78
79
80
81
# 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