[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[or-cvs] r24143: {arm} Utility for checking if destinations can be exited to or not (in arm/trunk/src: . util)
Author: atagar
Date: 2011-01-27 16:58:39 +0000 (Thu, 27 Jan 2011)
New Revision: 24143
Modified:
arm/trunk/src/test.py
arm/trunk/src/util/torTools.py
Log:
Utility for checking if destinations can be exited to or not.
Modified: arm/trunk/src/test.py
===================================================================
--- arm/trunk/src/test.py 2011-01-27 08:00:17 UTC (rev 24142)
+++ arm/trunk/src/test.py 2011-01-27 16:58:39 UTC (rev 24143)
@@ -11,6 +11,7 @@
1. Resolver Performance Test
2. Resolver Dump
3. Glyph Demo
+ 4. Exit Policy Check
q. Quit
Selection: """
@@ -23,7 +24,7 @@
userInput = raw_input(MENU)
# initiate the TorCtl connection if the test needs it
- if userInput in ("1", "2") and not conn:
+ if userInput in ("1", "2", "4") and not conn:
conn = torTools.getConn()
conn.init()
@@ -101,6 +102,42 @@
# Switching to a curses context and back repeatedly seems to screw up the
# terminal. Just to be safe this ends the process after the demo.
break
+ elif userInput == "4":
+ # display the current exit policy and query if destinations are allowed by it
+ exitPolicy = conn.getExitPolicy()
+ print("Exit Policy: %s" % exitPolicy)
+ printDivider()
+
+ while True:
+ # provide the selection options
+ userSelection = raw_input("\nCheck if destination is allowed (q to go back): ")
+ userSelection = userSelection.replace(" ", "").strip() # removes all whitespace
+
+ isValidQuery, isExitAllowed = True, False
+ if userSelection == "q":
+ printDivider()
+ break
+ elif connections.isValidIpAddress(userSelection):
+ # just an ip address (use port 80)
+ isExitAllowed = exitPolicy.check(userSelection, 80)
+ elif userSelection.isdigit():
+ # just a port (use a common ip like 4.2.2.2)
+ isExitAllowed = exitPolicy.check("4.2.2.2", userSelection)
+ elif ":" in userSelection:
+ # ip/port combination
+ ipAddr, port = userSelection.split(":", 1)
+
+ if connections.isValidIpAddress(ipAddr) and port.isdigit():
+ isExitAllowed = exitPolicy.check(ipAddr, port)
+ else: isValidQuery = False
+ else: isValidQuery = False # invalid input
+
+ if isValidQuery:
+ resultStr = "is" if isExitAllowed else "is *not*"
+ print("Exiting %s allowed to that destination" % resultStr)
+ else:
+ print("'%s' isn't a valid destination (should be an ip, port, or ip:port)\n" % userSelection)
+
else:
print("'%s' isn't a valid selection\n" % userInput)
Modified: arm/trunk/src/util/torTools.py
===================================================================
--- arm/trunk/src/util/torTools.py 2011-01-27 08:00:17 UTC (rev 24142)
+++ arm/trunk/src/util/torTools.py 2011-01-27 16:58:39 UTC (rev 24143)
@@ -81,6 +81,9 @@
# provides int -> str mappings for torctl event runlevels
TORCTL_RUNLEVELS = dict([(val, key) for (key, val) in TorUtil.loglevels.items()])
+# ip address ranges substituted by the 'private' keyword
+PRIVATE_IP_RANGES = ("0.0.0.0/8", "169.254.0.0/16", "127.0.0.0/8", "192.168.0.0/16", "10.0.0.0/8", "172.16.0.0/12")
+
# This prevents controllers from spawning worker threads (and by extension
# notifying status listeners). This is important when shutting down to prevent
# rogue threads from being alive during shutdown.
@@ -238,6 +241,9 @@
self._statusTime = 0 # unix time-stamp for the duration of the status
self.lastHeartbeat = 0 # time of the last tor event
+ self._exitPolicyChecker = None
+ self._exitPolicyLookupCache = {} # mappings of ip/port tuples to if they were accepted by the policy or not
+
# Logs issues and notices when fetching the path prefix if true. This is
# only done once for the duration of the application to avoid pointless
# messages.
@@ -283,6 +289,9 @@
self._fingerprintsAttachedCache = None
self._nicknameLookupCache = {}
+ self._exitPolicyChecker = self.getExitPolicy()
+ self._exitPolicyLookupCache = {}
+
# sets the events listened for by the new controller (incompatible events
# are dropped with a logged warning)
self.setControllerEvents(self.controllerEvents)
@@ -471,9 +480,9 @@
result = {} if fetchType == "map" else []
if self.isAlive():
- if (param, fetchType) in self._cachedConf:
+ if (param.lower(), fetchType) in self._cachedConf:
isFromCache = True
- result = self._cachedConf[(param, fetchType)]
+ result = self._cachedConf[(param.lower(), fetchType)]
else:
try:
if fetchType == "str":
@@ -494,7 +503,7 @@
cacheValue = result
if fetchType == "list": cacheValue = list(result)
elif fetchType == "map": cacheValue = dict(result)
- self._cachedConf[(param, fetchType)] = cacheValue
+ self._cachedConf[(param.lower(), fetchType)] = cacheValue
runtimeLabel = "cache fetch" if isFromCache else "runtime: %0.4f" % (time.time() - startTime)
msg = "GETCONF %s (%s)" % (param, runtimeLabel)
@@ -528,10 +537,15 @@
# flushing cached values (needed until we can detect SETCONF calls)
for fetchType in ("str", "list", "map"):
- entry = (param, fetchType)
+ entry = (param.lower(), fetchType)
if entry in self._cachedConf:
del self._cachedConf[entry]
+
+ # special caches for the exit policy
+ if param.lower() == "exitpolicy":
+ self._exitPolicyChecker = self.getExitPolicy()
+ self._exitPolicyLookupCache = {}
except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed), exc:
if type(exc) == TorCtl.TorCtlClosed: self.close()
elif type(exc) == TorCtl.ErrorReply:
@@ -697,6 +711,67 @@
return (self._status, self._statusTime)
+ def isExitingAllowed(self, ipAddress, port):
+ """
+ Checks if the given destination can be exited to by this relay, returning
+ True if so and False otherwise.
+ """
+
+ self.connLock.acquire()
+
+ result = False
+ if self.isAlive():
+ # query the policy if it isn't yet cached
+ if not (ipAddress, port) in self._exitPolicyLookupCache:
+ isAccepted = self._exitPolicyChecker.check(ipAddress, port)
+ self._exitPolicyLookupCache[(ipAddress, port)] = isAccepted
+
+ result = self._exitPolicyLookupCache[(ipAddress, port)]
+
+ self.connLock.release()
+
+ return result
+
+ def getExitPolicy(self):
+ """
+ Provides an ExitPolicy instance for the head of this relay's exit policy
+ chain. If there's no active connection then this provides None.
+ """
+
+ self.connLock.acquire()
+
+ result = None
+ if self.isAlive():
+ policyEntries = []
+ for exitPolicy in self.getOption("ExitPolicy", [], True):
+ policyEntries += [policy.strip() for policy in exitPolicy.split(",")]
+
+ # appends the default exit policy
+ defaultExitPolicy = self.getInfo("exit-policy/default")
+
+ if defaultExitPolicy:
+ policyEntries += defaultExitPolicy.split(",")
+
+ # construct the policy chain backwards
+ policyEntries.reverse()
+
+ for entry in policyEntries:
+ result = ExitPolicy(entry, result)
+
+ # Checks if we are rejecting private connections. If set, this appends
+ # 'reject private' and 'reject <my ip>' to the start of our policy chain.
+ isPrivateRejected = self.getOption("ExitPolicyRejectPrivate", True)
+
+ if isPrivateRejected:
+ result = ExitPolicy("reject private", result)
+
+ myAddress = self.getInfo("address")
+ if myAddress: result = ExitPolicy("reject %s" % myAddress, result)
+
+ self.connLock.release()
+
+ return result
+
def getRelayFingerprint(self, relayAddress, relayPort = None):
"""
Provides the fingerprint associated with the given address. If there's
@@ -1422,3 +1497,136 @@
for callback in self.statusListeners:
callback(self, eventType)
+class ExitPolicy:
+ """
+ Single rule from the user's exit policy. These are chained together to form
+ complete policies.
+ """
+
+ def __init__(self, ruleEntry, nextRule):
+ """
+ Exit policy rule constructor.
+
+ Arguments:
+ ruleEntry - tor exit policy rule (for instance, "reject *:135-139")
+ nextRule - next rule to be checked when queries don't match this policy
+ """
+
+ # sanitize the input a bit, cleaning up tabs and stripping quotes
+ ruleEntry = ruleEntry.replace("\\t", " ").replace("\"", "")
+
+ self.ruleEntry = ruleEntry
+ self.nextRule = nextRule
+ self.isAccept = ruleEntry.startswith("accept")
+
+ # strips off "accept " or "reject " and extra spaces
+ ruleEntry = ruleEntry[7:].replace(" ", "")
+
+ # split ip address (with mask if provided) and port
+ if ":" in ruleEntry: entryIp, entryPort = ruleEntry.split(":", 1)
+ else: entryIp, entryPort = ruleEntry, "*"
+
+ # sets the ip address component
+ self.isIpWildcard = entryIp == "*" or entryIp.endswith("/0")
+
+ # checks for the private alias (which expands this to a chain of entries)
+ if entryIp.lower() == "private":
+ entryIp = PRIVATE_IP_RANGES[0]
+
+ # constructs the chain backwards (last first)
+ lastHop = self.nextRule
+ prefix = "accept " if self.isAccept else "reject "
+ suffix = ":" + entryPort
+ for addr in PRIVATE_IP_RANGES[-1:0:-1]:
+ lastHop = ExitPolicy(prefix + addr + suffix, lastHop)
+
+ self.nextRule = lastHop # our next hop is the start of the chain
+
+ if "/" in entryIp:
+ ipComp = entryIp.split("/", 1)
+ self.ipAddress = ipComp[0]
+ self.ipMask = int(ipComp[1])
+ else:
+ self.ipAddress = entryIp
+ self.ipMask = 32
+
+ # constructs the binary address just in case of comparison with a mask
+ if self.ipAddress != "*":
+ self.ipAddressBin = ""
+ for octet in self.ipAddress.split("."):
+ # bin converts the int to a binary string, then we pad with zeros
+ self.ipAddressBin += ("%8s" % bin(int(octet))[2:]).replace(" ", "0")
+ else:
+ self.ipAddressBin = "0" * 32
+
+ # sets the port component
+ self.minPort, self.maxPort = 0, 0
+ self.isPortWildcard = entryPort == "*"
+
+ if entryPort != "*":
+ if "-" in entryPort:
+ portComp = entryPort.split("-", 1)
+ self.minPort = int(portComp[0])
+ self.maxPort = int(portComp[1])
+ else:
+ self.minPort = int(entryPort)
+ self.maxPort = int(entryPort)
+
+ # if both the address and port are wildcards then we're effectively the
+ # last entry so cut off the remaining chain
+ if self.isIpWildcard and self.isPortWildcard:
+ self.nextRule = None
+
+ def check(self, ipAddress, port):
+ """
+ Checks if the rule chain allows exiting to this address, returning true if
+ so and false otherwise.
+ """
+
+ port = int(port)
+
+ # does the port check first since comparing ip masks is more work
+ isPortMatch = self.isPortWildcard or (port >= self.minPort and port <= self.maxPort)
+
+ if isPortMatch:
+ isIpMatch = self.isIpWildcard or self.ipAddress == ipAddress
+
+ # expands the check to include the mask if it has one
+ if not isIpMatch and self.ipMask != 32:
+ inputAddressBin = ""
+ for octet in ipAddress.split("."):
+ inputAddressBin += ("%8s" % bin(int(octet))[2:]).replace(" ", "0")
+
+ cropSize = 32 - self.ipMask
+ isIpMatch = self.ipAddressBin[:cropSize] == inputAddressBin[:cropSize]
+
+ if isIpMatch: return self.isAccept
+
+ # our policy doesn't concern this address, move on to the next one
+ if self.nextRule: return self.nextRule.check(ipAddress, port)
+ else: return True # fell off the chain without a conclusion (shouldn't happen...)
+
+ def __str__(self):
+ # This provides the actual policy rather than the entry used to construct
+ # it so the 'private' keyword is expanded.
+
+ acceptanceLabel = "accept" if self.isAccept else "reject"
+
+ if self.isIpWildcard:
+ ipLabel = "*"
+ elif self.ipMask != 32:
+ ipLabel = "%s/%i" % (self.ipAddress, self.ipMask)
+ else: ipLabel = self.ipAddress
+
+ if self.isPortWildcard:
+ portLabel = "*"
+ elif self.minPort != self.maxPort:
+ portLabel = "%i-%i" % (self.minPort, self.maxPort)
+ else: portLabel = str(self.minPort)
+
+ myPolicy = "%s %s:%s" % (acceptanceLabel, ipLabel, portLabel)
+
+ if self.nextRule:
+ return myPolicy + ", " + str(self.nextRule)
+ else: return myPolicy
+