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

[or-cvs] r19975: {arm} Connections panel can now list by IP, hostname, or fingerpri (in arm/trunk: . interface)



Author: atagar
Date: 2009-07-12 02:11:36 -0400 (Sun, 12 Jul 2009)
New Revision: 19975

Added:
   arm/trunk/interface/hostnameResolver.py
Modified:
   arm/trunk/arm.py
   arm/trunk/interface/connPanel.py
   arm/trunk/interface/controller.py
   arm/trunk/readme.txt
Log:
Connections panel can now list by IP, hostname, or fingerprint - reverse resolution was easy, but comparing three different implementations and making it non-blocking with a pausable thread-pool backend? Not so much.



Modified: arm/trunk/arm.py
===================================================================
--- arm/trunk/arm.py	2009-07-12 02:26:44 UTC (rev 19974)
+++ arm/trunk/arm.py	2009-07-12 06:11:36 UTC (rev 19975)
@@ -45,13 +45,6 @@
 arm -e=we -p=nemesis    use password 'nemesis' with 'WARN'/'ERR' events
 """ % (DEFAULT_CONTROL_ADDR, DEFAULT_CONTROL_PORT, DEFAULT_AUTH_COOKIE, DEFAULT_LOGGED_EVENTS, logPanel.EVENT_LISTING)
 
-EVENT_LISTING = """        d DEBUG     a ADDRMAP       l NEWDESC         u AUTHDIR_NEWDESCS
-        i INFO      b BW            m NS              v CLIENTS_SEEN
-        n NOTICE    c CIRC          o ORCONN          x STATUS_GENERAL
-        w WARN      f DESCCHANGED   s STREAM          y STATUS_CLIENT
-        e ERR       g GUARD         t STREAM_BW       z STATUS_SERVER
-        Aliases:    A All Events    U Unknown Events  R Runlevels (dinwe)"""
-
 class Input:
   "Collection of the user's command line input"
   

Modified: arm/trunk/interface/connPanel.py
===================================================================
--- arm/trunk/interface/connPanel.py	2009-07-12 02:26:44 UTC (rev 19974)
+++ arm/trunk/interface/connPanel.py	2009-07-12 06:11:36 UTC (rev 19975)
@@ -4,62 +4,39 @@
 
 import os
 import curses
-import socket
 from TorCtl import TorCtl
 
 import util
+import hostnameResolver
 
+# enums for listing types
+LIST_IP, LIST_HOSTNAME, LIST_FINGERPRINT = range(3)
+LIST_LABEL = {LIST_IP: "IP", LIST_HOSTNAME: "Hostname", LIST_FINGERPRINT: "Fingerprint"}
+
 # enums for sorting types (note: ordering corresponds to SORT_TYPES for easy lookup)
 # TODO: add ORD_BANDWIDTH -> (ORD_BANDWIDTH, "Bandwidth", lambda x, y: ???)
-ORD_TYPE, ORD_FOREIGN_IP, ORD_SRC_IP, ORD_DST_IP, ORD_COUNTRY, ORD_FOREIGN_PORT, ORD_SRC_PORT, ORD_DST_PORT = range(8)
+ORD_TYPE, ORD_FOREIGN_LISTING, ORD_SRC_LISTING, ORD_DST_LISTING, ORD_COUNTRY, ORD_FOREIGN_PORT, ORD_SRC_PORT, ORD_DST_PORT = range(8)
 SORT_TYPES = [(ORD_TYPE, "Connection Type",
-                lambda x, y: TYPE_WEIGHTS[x[0]] - TYPE_WEIGHTS[y[0]]),
-              (ORD_FOREIGN_IP, "IP (Foreign)",
-                lambda x, y: cmp(_ipToInt(x[3]), _ipToInt(y[3]))),
-              (ORD_SRC_IP, "IP (Source)",
-                lambda x, y: cmp(_ipToInt(x[3] if x[0] == "inbound" else x[1]), _ipToInt(y[3] if y[0] == "inbound" else y[1]))),
-              (ORD_DST_IP, "IP (Dest.)",
-                lambda x, y: cmp(_ipToInt(x[1] if x[0] == "inbound" else x[3]), _ipToInt(y[1] if y[0] == "inbound" else y[3]))),
+                lambda x, y: TYPE_WEIGHTS[x[CONN_TYPE]] - TYPE_WEIGHTS[y[CONN_TYPE]]),
+              (ORD_FOREIGN_LISTING, "* (Foreign)", None),
+              (ORD_SRC_LISTING, "* (Source)", None),
+              (ORD_DST_LISTING, "* (Dest.)", None),
               (ORD_COUNTRY, "Country Code",
-                lambda x, y: cmp(x[5], y[5])),
+                lambda x, y: cmp(x[CONN_COUNTRY], y[CONN_COUNTRY])),
               (ORD_FOREIGN_PORT, "Port (Foreign)",
-                lambda x, y: int(x[4]) - int(y[4])),
+                lambda x, y: int(x[CONN_F_PORT]) - int(y[CONN_F_PORT])),
               (ORD_SRC_PORT, "Port (Source)",
-                lambda x, y: int(x[4] if x[0] == "inbound" else x[2]) - int(y[4] if y[0] == "inbound" else y[2])),
+                lambda x, y: int(x[CONN_F_PORT] if x[CONN_TYPE] == "inbound" else x[CONN_L_PORT]) - int(y[CONN_F_PORT] if y[CONN_TYPE] == "inbound" else y[CONN_L_PORT])),
               (ORD_DST_PORT, "Port (Dest.)",
-                lambda x, y: int(x[2] if x[0] == "inbound" else x[4]) - int(y[2] if y[0] == "inbound" else y[4]))]
+                lambda x, y: int(x[CONN_L_PORT] if x[CONN_TYPE] == "inbound" else x[CONN_F_PORT]) - int(y[CONN_L_PORT] if y[CONN_TYPE] == "inbound" else y[CONN_F_PORT]))]
 
 TYPE_COLORS = {"inbound": "green", "outbound": "blue", "control": "red"}
 TYPE_WEIGHTS = {"inbound": 0, "outbound": 1, "control": 2}
 
-# provides bi-directional mapping of sorts with their associated labels
-def getSortLabel(sortType, withColor = False):
-  """
-  Provides label associated with a type of sorting. Throws ValueEror if no such
-  sort exists. If adding color formatting this wraps with the following mappings:
-  Connection Type     red
-  IP *                blue
-  Port *              green
-  Bandwidth           cyan
-  Country Code        yellow
-  """
-  
-  for (type, label, func) in SORT_TYPES:
-    if sortType == type:
-      color = None
-      
-      if withColor:
-        if label == "Connection Type": color = "red"
-        elif label.startswith("IP"): color = "blue"
-        elif label.startswith("Port"): color = "green"
-        elif label == "Bandwidth": color = "cyan"
-        elif label == "Country Code": color = "yellow"
-      
-      if color: return "<%s>%s</%s>" % (color, label, color)
-      else: return label
-  
-  raise ValueError(sortType)
+# enums for indexes of ConnPanel 'connections' fields
+CONN_TYPE, CONN_L_IP, CONN_L_PORT, CONN_F_IP, CONN_F_PORT, CONN_COUNTRY = range(6)
 
+# provides bi-directional mapping of sorts with their associated labels (with getSortLabel)
 def getSortType(sortLabel):
   """
   Provides sort type associated with a given label. Throws ValueEror if label
@@ -68,19 +45,27 @@
   
   for (type, label, func) in SORT_TYPES:
     if sortLabel == label: return type
+    elif label.startswith("*"):
+      if sortLabel in [label.replace("*", listingType) for listingType in LIST_LABEL.values()]: return type
   raise ValueError(sortLabel)
 
-class ConnPanel(util.Panel):
+class ConnPanel(TorCtl.PostEventListener, util.Panel):
   """
   Lists netstat provided network data of tor.
   """
   
   def __init__(self, lock, conn, logger):
+    TorCtl.PostEventListener.__init__(self)
     util.Panel.__init__(self, lock, -1)
     self.scroll = 0
-    self.conn = conn            # tor connection for querrying country codes
-    self.logger = logger        # notified in case of problems
-    self.sortOrdering = [ORD_TYPE, ORD_SRC_IP, ORD_SRC_PORT]
+    self.conn = conn                # tor connection for querrying country codes
+    self.logger = logger            # notified in case of problems
+    self.listingType = LIST_IP      # information used in listing entries
+    self.allowDNS = True            # permits hostname resolutions if true
+    self.sortOrdering = [ORD_TYPE, ORD_FOREIGN_LISTING, ORD_FOREIGN_PORT]
+    self.isPaused = False
+    self.resolver = hostnameResolver.HostnameResolver()
+    self.fingerprintMappings = _getFingerprintMappings(self.conn) # mappings of ip -> [(port, OR identity), ...]
     
     # gets process id to make sure we get the correct netstat data
     psCall = os.popen('ps -C tor -o pid')
@@ -103,22 +88,24 @@
     # count of total inbound, outbound, and control connections
     self.connectionCount = [0, 0, 0]
     
-    # cache of DNS lookups, IP Address => hostname (None if couldn't be resolved)
-    # TODO: implement
-    self.hostnameResolution = {}
-    
     self.reset()
   
+  # when consensus changes update fingerprint mappings
+  def new_consensus_event(self, n):
+    self.fingerprintMappings = _getFingerprintMappings(self.conn)
+  
+  def new_desc_event(self, d):
+    self.fingerprintMappings = _getFingerprintMappings(self.conn)
+  
   def reset(self):
     """
     Reloads netstat results.
     """
     
+    if self.isPaused or self.pid == -1: return
     self.connections = []
     self.connectionCount = [0, 0, 0]
     
-    if self.pid == -1: return # initilization had warned of failure - abandon
-    
     # looks at netstat for tor with stderr redirected to /dev/null, options are:
     # n = prevents dns lookups, p = include process (say if it's tor), t = tcp only
     netstatCall = os.popen("netstat -npt 2> /dev/null | grep %s/tor" % self.pid)
@@ -128,14 +115,10 @@
       for line in results:
         if not line.startswith("tcp"): continue
         param = line.split()
-        local = param[3]
-        foreign = param[4]
+        local, foreign = param[3], param[4]
+        localIP, foreignIP = local[:local.find(":")], foreign[:foreign.find(":")]
+        localPort, foreignPort = local[len(localIP) + 1:], foreign[len(foreignIP) + 1:]
         
-        localIP = local[:local.find(":")]
-        localPort = local[len(localIP) + 1:]
-        foreignIP = foreign[:foreign.find(":")]
-        foreignPort = foreign[len(foreignIP) + 1:]
-        
         if localPort in (self.orPort, self.dirPort):
           type = "inbound"
           self.connectionCount[0] += 1
@@ -149,7 +132,7 @@
         try:
           countryCodeQuery = "ip-to-country/%s" % foreign[:foreign.find(":")]
           countryCode = self.conn.get_info(countryCodeQuery)[countryCodeQuery]
-        except socket.error: countryCode = None
+        except socket.error: countryCode = "??"
         
         self.connections.append((type, localIP, localPort, foreignIP, foreignPort, countryCode))
     except IOError:
@@ -157,7 +140,9 @@
       self.logger.monitor_event("WARN", "Unable to query netstat for new connections")
     
     netstatCall.close()
-    self.sortConnections()
+    
+    # hostnames are sorted at redraw - otherwise now's a good time
+    if self.listingType != LIST_HOSTNAME: self.sortConnections()
   
   def handleKey(self, key):
     self._resetBounds()
@@ -166,12 +151,20 @@
     elif key == curses.KEY_DOWN: self.scroll = max(0, self.scroll + 1)
     elif key == curses.KEY_PPAGE: self.scroll = max(self.scroll - pageHeight, 0)
     elif key == curses.KEY_NPAGE: self.scroll = max(0, self.scroll + pageHeight)
+    elif key == ord('r') or key == ord('R'):
+      self.allowDNS = not self.allowDNS
+      if not self.allowDNS: self.resolver.setPaused(True)
+      elif self.listingType == LIST_HOSTNAME: self.resolver.setPaused(False)
+    else: return # skip following redraw
     self.redraw()
   
   def redraw(self):
     if self.win:
       if not self.lock.acquire(False): return
       try:
+        # hostnames frequently get updated so frequent sorting needed
+        if self.listingType == LIST_HOSTNAME: self.sortConnections()
+        
         self.clear()
         self.addstr(0, 0, "Connections (%i inbound, %i outbound, %i control):" % tuple(self.connectionCount), util.LABEL_ATTR)
         
@@ -179,18 +172,89 @@
         lineNum = (-1 * self.scroll) + 1
         for entry in self.connections:
           if lineNum >= 1:
-            type = entry[0]
+            type = entry[CONN_TYPE]
             color = TYPE_COLORS[type]
-            src = "%s:%s" % (entry[1], entry[2])
-            dst = "%s:%s %s" % (entry[3], entry[4], "" if type == "control" else "(%s)" % entry[5])
+            
+            if self.listingType == LIST_IP:
+              src = "%s:%s" % (entry[CONN_L_IP], entry[CONN_L_PORT])
+              dst = "%s:%s %s" % (entry[CONN_F_IP], entry[CONN_F_PORT], "" if type == "control" else "(%s)" % entry[CONN_COUNTRY])
+              src, dst = "%-26s" % src, "%-26s" % dst
+            elif self.listingType == LIST_HOSTNAME:
+              src = "localhost:%-5s" % entry[CONN_L_PORT]
+              hostname = self.resolver.resolve(entry[CONN_F_IP])
+              
+              # truncates long hostnames
+              portDigits = len(str(entry[CONN_F_PORT]))
+              if hostname and (len(hostname) + portDigits) > 36: hostname = hostname[:(33 - portDigits)] + "..."
+              
+              dst = "%s:%s" % (hostname if hostname else entry[CONN_F_IP], entry[CONN_F_PORT])
+              dst = "%-37s" % dst
+            else:
+              src = "localhost "
+              if entry[CONN_TYPE] == "control": dst = "localhost"
+              else: dst = self.getFingerprint(entry[CONN_F_IP], entry[CONN_F_PORT])
+              dst = "%-41s" % dst
+            
             if type == "inbound": src, dst = dst, src
-            self.addfstr(lineNum, 0, "<%s>%-30s-->     %-26s(<b>%s</b>)</%s>" % (color, src, dst, type.upper(), color))
+            self.addfstr(lineNum, 0, "<%s>%s -->   %s   (<b>%s</b>)</%s>" % (color, src, dst, type.upper(), color))
           lineNum += 1
         
         self.refresh()
       finally:
         self.lock.release()
   
+  def getFingerprint(self, ipAddr, port):
+    """
+    Makes an effort to match connection to fingerprint - if there's multiple
+    potential matches or the IP address isn't found in the discriptor then
+    returns "UNKNOWN".
+    """
+    
+    if ipAddr in self.fingerprintMappings.keys():
+      potentialMatches = self.fingerprintMappings[ipAddr]
+      
+      if len(potentialMatches) == 1: return potentialMatches[0][1]
+      else:
+        for (entryPort, entryFingerprint) in potentialMatches:
+          if entryPort == port: return entryFingerprint
+    
+    return "UNKNOWN"
+  
+  def setPaused(self, isPause):
+    """
+    If true, prevents connection listing from being updated.
+    """
+    
+    self.isPaused = isPause
+  
+  def getSortLabel(self, sortType, withColor = False):
+    """
+    Provides label associated with a type of sorting. Throws ValueEror if no such
+    sort exists. If adding color formatting this wraps with the following mappings:
+    Connection Type     red
+    [Listing] *         blue
+    Port *              green
+    Bandwidth           cyan
+    Country Code        yellow
+    """
+    
+    for (type, label, func) in SORT_TYPES:
+      if sortType == type:
+        color = None
+        
+        if withColor:
+          if label == "Connection Type": color = "red"
+          elif label.startswith("*"): color = "blue"
+          elif label.startswith("Port"): color = "green"
+          elif label == "Bandwidth": color = "cyan"
+          elif label == "Country Code": color = "yellow"
+        
+        if label.startswith("*"): label = label.replace("*", LIST_LABEL[self.listingType])
+        if color: return "<%s>%s</%s>" % (color, label, color)
+        else: return label
+    
+    raise ValueError(sortType)
+  
   def sortConnections(self):
     """
     Sorts connections according to currently set ordering. This takes into
@@ -200,11 +264,33 @@
     # Current implementation is very inefficient, but since connection lists
     # are decently small (count get up to arounk 1k) this shouldn't be a big
     # whoop. Suggestions for improvements are welcome!
-    self.connections.sort(lambda x, y: _multisort(x, y, self.sortOrdering))
+    
+    sorts = []
+    
+    # wrapper function for using current listed data (for 'LISTING' sorts)
+    if self.listingType == LIST_IP:
+      listingWrapper = lambda ip, port: _ipToInt(ip)
+    elif self.listingType == LIST_HOSTNAME:
+      # alphanumeric hostnames followed by unresolved IP addresses
+      listingWrapper = lambda ip, port: self.resolver.resolve(ip).upper() if self.resolver.resolve(ip) else "ZZZZZ" + ip
+    elif self.listingType == LIST_FINGERPRINT:
+      # alphanumeric fingerprints followed by UNKNOWN entries
+      listingWrapper = lambda ip, port: self.getFingerprint(ip, port) if self.getFingerprint(ip, port) != "UNKNOWN" else "ZZZZZ" + ip
+    
+    for entry in self.sortOrdering:
+      if entry == ORD_FOREIGN_LISTING:
+        sorts.append(lambda x, y: cmp(listingWrapper(x[CONN_F_IP], x[CONN_F_PORT]), listingWrapper(y[CONN_F_IP], y[CONN_F_PORT])))
+      elif entry == ORD_SRC_LISTING:
+        sorts.append(lambda x, y: cmp(listingWrapper(x[CONN_F_IP] if x[CONN_TYPE] == "inbound" else x[CONN_L_IP], x[CONN_F_PORT]), listingWrapper(y[CONN_F_IP] if y[CONN_TYPE] == "inbound" else y[CONN_L_IP], y[CONN_F_PORT])))
+      elif entry == ORD_DST_LISTING:
+        sorts.append(lambda x, y: cmp(listingWrapper(x[CONN_L_IP] if x[CONN_TYPE] == "inbound" else x[CONN_F_IP], x[CONN_F_PORT]), listingWrapper(y[CONN_L_IP] if y[CONN_TYPE] == "inbound" else y[CONN_F_IP], y[CONN_F_PORT])))
+      else: sorts.append(SORT_TYPES[entry][2])
+    
+    self.connections.sort(lambda x, y: _multisort(x, y, sorts))
 
+# recursively checks primary, secondary, and tertiary sorting parameter in ties
 def _multisort(conn1, conn2, sorts):
-  # recursively checks primary, secondary, and tertiary sorting parameter in ties
-  comp = SORT_TYPES[sorts[0]][2](conn1, conn2)
+  comp = sorts[0](conn1, conn2)
   if comp or len(sorts) == 1: return comp
   else: return _multisort(conn1, conn2, sorts[1:])
 
@@ -216,3 +302,19 @@
     total += int(comp)
   return total
 
+# uses consensus data to map IP addresses to port / fingerprint combinations
+def _getFingerprintMappings(conn):
+  ipToFingerprint = {}
+  
+  try:
+    lastIp, lastPort = None, None
+    for line in conn.get_info("desc/all-recent")["desc/all-recent"].split("\n"):
+      if line.startswith("router "): lastIp, lastPort = line.split()[2], line.split()[3]
+      elif line.startswith("opt fingerprint "):
+        fingerprint = "".join(line.split()[2:])
+        if lastIp in ipToFingerprint.keys(): ipToFingerprint[lastIp].append((lastPort, fingerprint))
+        else: ipToFingerprint[lastIp] = [(lastPort, fingerprint)]
+  except TorCtl.TorCtlClosed: pass
+  
+  return ipToFingerprint
+

Modified: arm/trunk/interface/controller.py
===================================================================
--- arm/trunk/interface/controller.py	2009-07-12 02:26:44 UTC (rev 19974)
+++ arm/trunk/interface/controller.py	2009-07-12 06:11:36 UTC (rev 19975)
@@ -31,18 +31,20 @@
   ["bandwidth", "log"],
   ["conn"],
   ["torrc"]]
-PAUSEABLE = ["header", "bandwidth", "log"]
+PAUSEABLE = ["header", "bandwidth", "log", "conn"]
 PAGE_COUNT = 3 # all page numbering is internally represented as 0-indexed
 # TODO: page for configuration information
 
 class ControlPanel(util.Panel):
   """ Draws single line label for interface controls. """
   
-  def __init__(self, lock):
+  def __init__(self, lock, resolver):
     util.Panel.__init__(self, lock, 1)
     self.msgText = CTL_HELP           # message text to be displyed
     self.msgAttr = curses.A_NORMAL    # formatting attributes
     self.page = 1                     # page number currently being displayed
+    self.resolver = resolver          # dns resolution thread
+    self.resolvingBatchSize = 0       # number of entries in batch being resolved
   
   def setMsg(self, msgText, msgAttr=curses.A_NORMAL):
     """
@@ -61,8 +63,21 @@
       msgAttr = self.msgAttr
       
       if msgText == CTL_HELP:
-        msgText = "page %i / %i - q: quit, p: pause, h: page help" % (self.page, PAGE_COUNT)
         msgAttr = curses.A_NORMAL
+        
+        if self.resolvingBatchSize > 0:
+          if self.resolver.unresolvedQueue.empty() or self.resolver.isPaused:
+            # done resolving dns batch
+            self.resolvingBatchSize = 0
+            curses.halfdelay(REFRESH_RATE * 10) # revert to normal refresh rate
+          else:
+            entryCount = self.resolvingBatchSize - self.resolver.unresolvedQueue.qsize()
+            progress = 100 * entryCount / self.resolvingBatchSize
+            additive = "(or l) " if self.page == 2 else ""
+            msgText = "Resolving hostnames (%i / %i, %i%%) - press esc %sto cancel" % (entryCount, self.resolvingBatchSize, progress, additive)
+        
+        if self.resolvingBatchSize == 0:
+          msgText = "page %i / %i - q: quit, p: pause, h: page help" % (self.page, PAGE_COUNT)
       elif msgText == CTL_PAUSED:
         msgText = "Paused"
         msgAttr = curses.A_STANDOUT
@@ -127,16 +142,17 @@
   
   panels = {
     "header": headerPanel.HeaderPanel(cursesLock, conn),
-    "control": ControlPanel(cursesLock),
     "popup": util.Panel(cursesLock, 9),
     "bandwidth": bandwidthPanel.BandwidthMonitor(cursesLock, conn),
     "log": logPanel.LogMonitor(cursesLock, loggedEvents),
     "torrc": confPanel.ConfPanel(cursesLock, conn.get_info("config-file")["config-file"])}
   panels["conn"] = connPanel.ConnPanel(cursesLock, conn, panels["log"])
+  panels["control"] = ControlPanel(cursesLock, panels["conn"].resolver)
   
   # listeners that update bandwidth and log panels with Tor status
   conn.add_event_listener(panels["log"])
   conn.add_event_listener(panels["bandwidth"])
+  conn.add_event_listener(panels["conn"])
   
   # tells Tor to listen to the events we're interested
   loggedEvents = setEventListening(loggedEvents, conn, panels["log"])
@@ -183,7 +199,7 @@
       
       # if it's been at least five seconds since the last refresh of connection listing, update
       currentTime = time.time()
-      if currentTime - netstatRefresh >= 5:
+      if not panels["conn"].isPaused and currentTime - netstatRefresh >= 5:
         panels["conn"].reset()
         netstatRefresh = currentTime
       
@@ -195,7 +211,7 @@
       cursesLock.release()
     
     key = stdscr.getch()
-    if key == 27 or key == ord('q') or key == ord('Q'): break # quits (also on esc)
+    if key == ord('q') or key == ord('Q'): break # quits
     elif key == curses.KEY_LEFT or key == curses.KEY_RIGHT:
       # switch page
       if key == curses.KEY_LEFT: page = (page - 1) % PAGE_COUNT
@@ -230,18 +246,25 @@
         
         if page == 0:
           bwVisibleLabel = "visible" if panels["bandwidth"].isVisible else "hidden"
-          popup.addfstr(1, 2, "b: toggle <u>b</u>andwidth panel (<b>%s</b>)" % bwVisibleLabel)
-          popup.addfstr(1, 41, "e: change logged <u>e</u>vents")
+          popup.addfstr(1, 2, "b: toggle bandwidth panel (<b>%s</b>)" % bwVisibleLabel)
+          popup.addstr(1, 41, "e: change logged events")
         if page == 1:
           popup.addstr(1, 2, "up arrow: scroll up a line")
           popup.addstr(1, 41, "down arrow: scroll down a line")
           popup.addstr(2, 2, "page up: scroll up a page")
           popup.addstr(2, 41, "page down: scroll down a page")
-          popup.addstr(3, 2, "s: sort ordering")
-          #popup.addstr(4, 2, "r: resolve hostnames")
-          #popup.addstr(4, 41, "R: hostname auto-resolution")
-          #popup.addstr(5, 2, "h: show IP/hostnames")
-          #popup.addstr(5, 41, "c: clear hostname cache")
+          
+          listingEnum = panels["conn"].listingType
+          if listingEnum == connPanel.LIST_IP: listingType = "ip address"
+          elif listingEnum == connPanel.LIST_HOSTNAME: listingType = "hostname"
+          else: listingType = "fingerprint"
+          
+          popup.addfstr(3, 2, "l: listed identity (<b>%s</b>)" % listingType)
+          
+          allowDnsLabel = "allow" if panels["conn"].allowDNS else "disallow"
+          popup.addfstr(3, 41, "r: permit DNS resolution (<b>%s</b>)" % allowDnsLabel)
+          
+          popup.addstr(4, 2, "s: sort ordering")
         elif page == 2:
           popup.addstr(1, 2, "up arrow: scroll up a line")
           popup.addstr(1, 41, "down arrow: scroll down a line")
@@ -249,12 +272,12 @@
           popup.addstr(2, 41, "page down: scroll down a page")
           
           strippingLabel = "on" if panels["torrc"].stripComments else "off"
-          popup.addfstr(3, 2, "s: comment <u>s</u>tripping (<b>%s</b>)" % strippingLabel)
+          popup.addfstr(3, 2, "s: comment stripping (<b>%s</b>)" % strippingLabel)
           
           lineNumLabel = "on" if panels["torrc"].showLineNum else "off"
-          popup.addfstr(3, 41, "n: line <u>n</u>umbering (<b>%s</b>)" % lineNumLabel)
+          popup.addfstr(3, 41, "n: line numbering (<b>%s</b>)" % lineNumLabel)
           
-          popup.addfstr(4, 2, "r: <u>r</u>eload torrc")
+          popup.addfstr(4, 2, "r: reload torrc")
         
         popup.addstr(7, 2, "Press any key...")
         
@@ -322,6 +345,22 @@
         for key in PAUSEABLE: panels[key].setPaused(isPaused or key not in PAGES[page])
       finally:
         cursesLock.release()
+    elif (page == 1 and (key == ord('l') or key == ord('L'))) or (key == 27 and panels["conn"].listingType == connPanel.LIST_HOSTNAME and panels["control"].resolvingBatchSize > 0):
+      # either pressed 'l' on connection listing or canceling hostname resolution (esc on any page)
+      panels["conn"].listingType = (panels["conn"].listingType + 1) % 3
+      
+      if panels["conn"].listingType == connPanel.LIST_HOSTNAME:
+        curses.halfdelay(10) # refreshes display every second until done resolving
+        panels["control"].resolvingBatchSize = len(panels["conn"].connections)
+        
+        resolver = panels["conn"].resolver
+        resolver.setPaused(not panels["conn"].allowDNS)
+        for connEntry in panels["conn"].connections: resolver.resolve(connEntry[connPanel.CONN_F_IP])
+      else:
+        panels["control"].resolvingBatchSize = 0
+        resolver.setPaused(True)
+      
+      panels["conn"].sortConnections()
     elif page == 1 and (key == ord('s') or key == ord('S')):
       # set ordering for connection listing
       cursesLock.acquire()
@@ -336,12 +375,18 @@
         
         # listing of inital ordering
         prevOrdering = "<b>Current Order: "
-        for sort in panels["conn"].sortOrdering: prevOrdering += connPanel.getSortLabel(sort, True) + ", "
+        for sort in panels["conn"].sortOrdering: prevOrdering += panels["conn"].getSortLabel(sort, True) + ", "
         prevOrdering = prevOrdering[:-2] + "</b>"
         
         # Makes listing of all options
         options = []
-        for (type, label, func) in connPanel.SORT_TYPES: options.append(label)
+        for (type, label, func) in connPanel.SORT_TYPES:
+          label = panels["conn"].getSortLabel(type)
+          
+          # replaces 'Fingerprint' listings with shorter description
+          if label.startswith("Fingerprint"): label = label.replace("Fingerprint", "Tor ID")
+          
+          options.append(label)
         options.append("Cancel")
         
         while len(selections) < 3:
@@ -353,7 +398,7 @@
           # provides new ordering
           newOrdering = "<b>New Order: "
           if selections:
-            for sort in selections: newOrdering += connPanel.getSortLabel(sort, True) + ", "
+            for sort in selections: newOrdering += panels["conn"].getSortLabel(sort, True) + ", "
             newOrdering = newOrdering[:-2] + "</b>"
           else: newOrdering += "</b>"
           popup.addfstr(2, 2, newOrdering)
@@ -377,7 +422,7 @@
             selection = options[cursorLoc]
             if selection == "Cancel": break
             else:
-              selections.append(connPanel.getSortType(selection))
+              selections.append(connPanel.getSortType(selection.replace("Tor ID", "Fingerprint")))
               options.remove(selection)
               cursorLoc = min(cursorLoc, len(options) - 1)
           

Added: arm/trunk/interface/hostnameResolver.py
===================================================================
--- arm/trunk/interface/hostnameResolver.py	                        (rev 0)
+++ arm/trunk/interface/hostnameResolver.py	2009-07-12 06:11:36 UTC (rev 19975)
@@ -0,0 +1,129 @@
+#!/usr/bin/env python
+# hostnameResolver.py -- Background thread for performing reverse DNS resolution.
+# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+
+import os
+import time
+import itertools
+import Queue
+from threading import Thread
+
+RESOLVER_THREAD_POOL_SIZE = 5     # upping to around 30 causes the program to intermittently seize
+RESOLVER_MAX_CACHE_SIZE = 5000
+RESOLVER_CACHE_TRIM_SIZE = 2000   # entries removed when max cache size reached
+DNS_ERROR_CODES = ("1(FORMERR)", "2(SERVFAIL)", "3(NXDOMAIN)", "4(NOTIMP)", "5(REFUSED)", "6(YXDOMAIN)", "7(YXRRSET)", "8(NXRRSET)", "9(NOTAUTH)", "10(NOTZONE)", "16(BADVERS)")
+
+class HostnameResolver(Thread):
+  """
+  Background thread that quietly performs reverse DNS lookup of address with
+  caching. This is non-blocking, providing None in the case of errors or
+  new requests.
+  """
+  
+  # Resolutions are made using os 'host' calls as opposed to 'gethostbyaddr' in
+  # the socket module because the later appears to be a blocking call (ie, serial
+  # requests which vastly reduces performance). In theory this shouldn't be the
+  # case if your system has the gethostbyname_r function, which you can check
+  # for with:
+  # import distutils.sysconfig
+  # distutils.sysconfig.get_config_var("HAVE_GETHOSTBYNAME_R")
+  # however, I didn't find this to be the case. As always, suggestions welcome!
+  
+  def __init__(self):
+    Thread.__init__(self)
+    self.resolvedCache = {}           # IP Address => (hostname, age) (None if couldn't be resolved)
+    self.unresolvedQueue = Queue.Queue()
+    self.recentQueries = []           # recent resolution requests to prevent duplicate requests
+    self.counter = itertools.count()  # atomic counter to track age of entries (for trimming)
+    self.threadPool = []              # worker threads that process requests
+    self.isPaused = True
+    
+    for i in range(RESOLVER_THREAD_POOL_SIZE):
+      t = _ResolverWorker(self.resolvedCache, self.unresolvedQueue, self.counter)
+      t.setDaemon(True)
+      t.setPaused(self.isPaused)
+      t.start()
+      self.threadPool.append(t)
+  
+  def resolve(self, ipAddr):
+    """
+    Provides hostname associated with an IP address. If not found this returns
+    None and performs a reverse DNS lookup for future reference. This also
+    provides None if the address couldn't be resolved.
+    """
+    
+    # if outstanding requests are done then clear recentQueries so we can run erronious requests again
+    if self.unresolvedQueue.empty(): self.recentQueries = []
+    
+    if ipAddr in self.resolvedCache.keys():
+      return self.resolvedCache[ipAddr][0]
+    elif ipAddr not in self.recentQueries:
+      self.recentQueries.append(ipAddr)
+      self.unresolvedQueue.put(ipAddr)
+      
+      if len(self.resolvedCache) > RESOLVER_MAX_CACHE_SIZE:
+        # trims cache (clean out oldest entries)
+        currentCount = self.counter.next()
+        threshold = currentCount - (RESOLVER_MAX_CACHE_SIZE - RESOLVER_CACHE_TRIM_SIZE) # max count of entries being removed
+        toDelete = []
+        
+        for (entryAddr, (entryHostname, entryAge)) in self.resolvedCache:
+          if entryAge < threshold: toDelete.append(entryAddr)
+        
+        for entryAddr in toDelete: del self.resolvedCache[entryAddr]
+  
+  def setPaused(self, isPause):
+    """
+    If true, prevents further dns requests.
+    """
+    
+    if isPause == self.isPaused: return
+    self.isPaused = isPause
+    for t in self.threadPool: t.setPaused(self.isPaused)
+
+class _ResolverWorker(Thread):
+  """
+  Helper thread for HostnameResolver, performing lookups on unresolved IP
+  addresses and adding the results to the resolvedCache.
+  """
+  
+  def __init__(self, resolvedCache, unresolvedQueue, counter):
+    Thread.__init__(self)
+    self.resolvedCache = resolvedCache
+    self.unresolvedQueue = unresolvedQueue
+    self.counter = counter
+    self.isPaused = False
+  
+  def run(self):
+    while True:
+      while self.isPaused: time.sleep(1)
+      
+      ipAddr = self.unresolvedQueue.get() # snag next available ip
+      resolutionFailed = False            # if true don't cache results
+      hostCall = os.popen("host %s" % ipAddr)
+      
+      try:
+        hostname = hostCall.read().split()[-1:][0]
+        
+        if hostname == "reached":
+          # got message: ";; connection timed out; no servers could be reached"
+          resolutionFailed = True
+        elif hostname in DNS_ERROR_CODES:
+          # got error response (can't do resolution on address)
+          hostname = None
+        else:
+          # strips off ending period
+          hostname = hostname[:-1]
+      except IOError: resolutionFailed = True # host call failed
+      
+      hostCall.close()
+      if not resolutionFailed: self.resolvedCache[ipAddr] = (hostname, self.counter.next())
+      self.unresolvedQueue.task_done() # signals that job's done
+  
+  def setPaused(self, isPause):
+    """
+    Puts further work on hold if true.
+    """
+    
+    self.isPaused = isPause
+

Modified: arm/trunk/readme.txt
===================================================================
--- arm/trunk/readme.txt	2009-07-12 02:26:44 UTC (rev 19974)
+++ arm/trunk/readme.txt	2009-07-12 06:11:36 UTC (rev 19975)
@@ -14,3 +14,5 @@
 
 This is started via 'arm' (use the '--help' argument for usage).
 
+Warning: The second page (connections) provides the hostnames of Tor relays you're connected to. This means reverse DNS lookups which, if monitored, could leak your current connections to an eavesdropper. You can disable lookups with 'r' (see the page's help for the current status).
+