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