[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[or-cvs] r24119: {arm} Much cleaner util version of the ip address -> fingerprint m (arm/trunk/src/util)



Author: atagar
Date: 2011-01-25 17:46:57 +0000 (Tue, 25 Jan 2011)
New Revision: 24119

Modified:
   arm/trunk/src/util/torTools.py
Log:
Much cleaner util version of the ip address -> fingerprint mapping functionality. This is lazily loaded to improve startup time, and far better designed/documented than the previous connPanel implementation.



Modified: arm/trunk/src/util/torTools.py
===================================================================
--- arm/trunk/src/util/torTools.py	2011-01-22 22:56:52 UTC (rev 24118)
+++ arm/trunk/src/util/torTools.py	2011-01-25 17:46:57 UTC (rev 24119)
@@ -229,6 +229,9 @@
     self.torctlListeners = []           # callback functions for TorCtl events
     self.statusListeners = []           # callback functions for tor's state changes
     self.controllerEvents = []          # list of successfully set controller events
+    self._fingerprintMappings = None    # mappings of ip -> [(port, fingerprint), ...]
+    self._fingerprintLookupCache = {}   # lookup cache with (ip, port) -> fingerprint mappings
+    self._fingerprintsAttachedCache = None # cache of relays we're connected to
     self._isReset = False               # internal flag for tracking resets
     self._status = TOR_CLOSED           # current status of the attached control port
     self._statusTime = 0                # unix time-stamp for the duration of the status
@@ -273,6 +276,11 @@
       self.conn.add_event_listener(self)
       for listener in self.eventListeners: self.conn.add_event_listener(listener)
       
+      # reset caches for ip -> fingerprint lookups
+      self._fingerprintMappings = None
+      self._fingerprintLookupCache = {}
+      self._fingerprintsAttachedCache = None
+      
       # sets the events listened for by the new controller (incompatible events
       # are dropped with a logged warning)
       self.setControllerEvents(self.controllerEvents)
@@ -687,6 +695,34 @@
     
     return (self._status, self._statusTime)
   
+  def getRelayFingerprint(self, relayAddress, relayPort = None):
+    """
+    Provides the fingerprint associated with the given address. If there's
+    multiple potential matches or the mapping is unknown then this returns
+    None. This disambiguates the fingerprint if there's multiple relays on
+    the same ip address by several methods, one of them being to pick relays
+    we have a connection with.
+    
+    Arguments:
+      relayAddress - address of relay to be returned
+      relayPort    - orport of relay (to further narrow the results)
+    """
+    
+    self.connLock.acquire()
+    
+    result = None
+    if self.isAlive():
+      # query the fingerprint if it isn't yet cached
+      if not (relayAddress, relayPort) in self._fingerprintLookupCache:
+        relayFingerprint = self._getRelayFingerprint(relayAddress, relayPort)
+        self._fingerprintLookupCache[(relayAddress, relayPort)] = relayFingerprint
+      
+      result = self._fingerprintLookupCache[(relayAddress, relayPort)]
+    
+    self.connLock.release()
+    
+    return result
+  
   def addEventListener(self, listener):
     """
     Directs further tor controller events to callback functions of the
@@ -945,20 +981,72 @@
   def new_consensus_event(self, event):
     self._updateHeartbeat()
     
+    self.connLock.acquire()
+    
     self._cachedParam["nsEntry"] = None
     self._cachedParam["flags"] = None
     self._cachedParam["bwMeasured"] = None
+    
+    # reconstructs ip -> relay data mappings
+    self._fingerprintLookupCache = {}
+    self._fingerprintsAttachedCache = None
+    
+    if self._fingerprintMappings != None:
+      self._fingerprintMappings = self._getFingerprintMappings(event.nslist)
+    
+    self.connLock.release()
   
   def new_desc_event(self, event):
     self._updateHeartbeat()
     
+    self.connLock.acquire()
+    
     myFingerprint = self.getInfo("fingerprint")
     if not myFingerprint or myFingerprint in event.idlist:
       self._cachedParam["descEntry"] = None
       self._cachedParam["bwObserved"] = None
+    
+    # If we're tracking ip address -> fingerprint mappings then update with
+    # the new relays.
+    self._fingerprintLookupCache = {}
+    self._fingerprintsAttachedCache = None
+    
+    if self._fingerprintMappings != None:
+      for fingerprint in event.idlist:
+        # gets consensus data for the new descriptor
+        try: nsLookup = self.conn.get_network_status("id/%s" % fingerprint)
+        except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed): continue
+        
+        if len(nsLookup) > 1:
+          # multiple records for fingerprint (shouldn't happen)
+          log.log(log.WARN, "Multiple consensus entries for fingerprint: %s" % fingerprint)
+          continue
+        
+        # updates fingerprintMappings with new data
+        newRelay = nsLookup[0]
+        if newRelay.ip in self._fingerprintMappings:
+          # if entry already exists with the same orport, remove it
+          orportMatch = None
+          for entryPort, entryFingerprint in self._fingerprintMappings[newRelay.ip]:
+            if entryPort == newRelay.orport:
+              orportMatch = (entryPort, entryFingerprint)
+              break
+          
+          if orportMatch: self._fingerprintMappings[newRelay.ip].remove(orportMatch)
+          
+          # add the new entry
+          self._fingerprintMappings[newRelay.ip].append((newRelay.orport, newRelay.idhex))
+        else:
+          self._fingerprintMappings[newRelay.ip] = [(newRelay.orport, newRelay.idhex)]
+    
+    self.connLock.release()
   
   def circ_status_event(self, event):
     self._updateHeartbeat()
+    
+    # CIRC events aren't required, but if one's received then flush this cache
+    # since it uses circuit-status results.
+    self._fingerprintsAttachedCache = None
   
   def buildtimeout_set_event(self, event):
     self._updateHeartbeat()
@@ -1001,6 +1089,132 @@
     # alternative is to use the event's timestamp (via event.arrived_at)
     self.lastHeartbeat = time.time()
   
+  def _getFingerprintMappings(self, nsList = None):
+    """
+    Provides IP address to (port, fingerprint) tuple mappings for all of the
+    currently cached relays.
+    
+    Arguments:
+      nsList - network status listing (fetched if not provided)
+    """
+    
+    results = {}
+    if self.isAlive():
+      # fetch the current network status if not provided
+      if not nsList:
+        try: nsList = self.conn.get_network_status()
+        except (socket.error, TorCtl.TorCtlClosed, TorCtl.ErrorReply): nsList = []
+      
+      # construct mappings of ips to relay data
+      for relay in nsList:
+        if relay.ip in results: results[relay.ip].append((relay.orport, relay.idhex))
+        else: results[relay.ip] = [(relay.orport, relay.idhex)]
+    
+    return results
+  
+  def _getRelayFingerprint(self, relayAddress, relayPort):
+    """
+    Provides the fingerprint associated with the address/port combination.
+    
+    Arguments:
+      relayAddress - address of relay to be returned
+      relayPort    - orport of relay (to further narrow the results)
+    """
+    
+    # If we were provided with a string port then convert to an int (so
+    # lookups won't mismatch based on type).
+    if isinstance(relayPort, str): relayPort = int(relayPort)
+    
+    # checks if this matches us
+    if relayAddress == self.getInfo("address"):
+      if not relayPort or relayPort == self.getOption("ORPort"):
+        return self.getInfo("fingerprint")
+    
+    # if we haven't yet populated the ip -> fingerprint mappings then do so
+    if self._fingerprintMappings == None:
+      self._fingerprintMappings = self._getFingerprintMappings()
+    
+    potentialMatches = self._fingerprintMappings.get(relayAddress)
+    if not potentialMatches: return None # no relay matches this ip address
+    
+    if len(potentialMatches) == 1:
+      # There's only one relay belonging to this ip address. If the port
+      # matches then we're done.
+      match = potentialMatches[0]
+      
+      if relayPort and match[0] != relayPort: return None
+      else: return match[1]
+    elif relayPort:
+      # Multiple potential matches, so trying to match based on the port.
+      for entryPort, entryFingerprint in potentialMatches:
+        if entryPort == relayPort:
+          return entryFingerprint
+    
+    # Disambiguates based on our orconn-status and circuit-status results.
+    # This only includes relays we're connected to, so chances are pretty
+    # slim that we'll still have a problem narrowing this down. Note that we
+    # aren't necessarily checking for events that can create new client
+    # circuits (so this cache might be a little dirty).
+    
+    # populates the cache
+    if self._fingerprintsAttachedCache == None:
+      self._fingerprintsAttachedCache = []
+      
+      # orconn-status has entries of the form:
+      # $33173252B70A50FE3928C7453077936D71E45C52=shiven CONNECTED
+      orconnResults = self.getInfo("orconn-status")
+      if orconnResults:
+        for line in orconnResults.split("\n"):
+          self._fingerprintsAttachedCache.append(line[1:line.find("=")])
+      
+      # circuit-status has entries of the form:
+      # 7 BUILT $33173252B70A50FE3928C7453077936D71E45C52=shiven,...
+      circStatusResults = self.getInfo("circuit-status")
+      if circStatusResults:
+        for line in circStatusResults.split("\n"):
+          clientEntries = line.split(" ")[2].split(",")
+          
+          for entry in clientEntries:
+            self._fingerprintsAttachedCache.append(entry[1:entry.find("=")])
+    
+    # narrow to only relays we have a connection to
+    attachedMatches = []
+    for _, entryFingerprint in potentialMatches:
+      if entryFingerprint in self._fingerprintsAttachedCache:
+        attachedMatches.append(entryFingerprint)
+    
+    if len(attachedMatches) == 1:
+      return attachedMatches[0]
+    
+    # Highly unlikely, but still haven't found it. Last we'll use some
+    # tricks from Mike's ConsensusTracker, excluding possiblities that
+    # have...
+    # - lost their Running flag
+    # - list a bandwidth of 0
+    # - have 'opt hibernating' set
+    # 
+    # This involves constructing a TorCtl Router and checking its 'down'
+    # flag (which is set by the three conditions above). This is the last
+    # resort since it involves a couple GETINFO queries.
+    
+    for entryPort, entryFingerprint in list(potentialMatches):
+      try:
+        nsCall = self.conn.get_network_status("id/%s" % entryFingerprint)
+        if not nsCall: raise TorCtl.ErrorReply() # network consensus couldn't be fetched
+        nsEntry = nsCall[0]
+        
+        descEntry = self.getInfo("desc/id/%s" % entryFingerprint)
+        if not descEntry: raise TorCtl.ErrorReply() # relay descriptor couldn't be fetched
+        descLines = descEntry.split("\n")
+        
+        isDown = TorCtl.Router.build_from_desc(descLines, nsEntry).down
+        if isDown: potentialMatches.remove((entryPort, entryFingerprint))
+      except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed): pass
+    
+    if len(potentialMatches) == 1:
+      return potentialMatches[0][1]
+    else: return None
+  
   def _getRelayAttr(self, key, default, cacheUndefined = True):
     """
     Provides information associated with this relay, using the cached value if