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