[tor-commits] [stem/master] Better handling for 'private' exit policy entries

atagar at torproject.org atagar at torproject.org
Wed Sep 3 16:23:10 UTC 2014


commit 0d48b504566c4df7f15991465f19af15a87c0c6b
Author: Damian Johnson <atagar at torproject.org>
Date:   Wed Sep 3 09:24:02 2014 -0700

    Better handling for 'private' exit policy entries
    
    Commonly we don't want to take 'private' exit policy entries into account...
    
      https://trac.torproject.org/projects/tor/ticket/10107
    
    Adding a method to strip them, and also identify rules that were expanded from
    the 'private' keyword.
---
 docs/change_log.rst             |    1 +
 stem/exit_policy.py             |   70 +++++++++++++++++++++++++++++++++++++++
 test/unit/exit_policy/policy.py |   30 +++++++++++++++++
 3 files changed, 101 insertions(+)

diff --git a/docs/change_log.rst b/docs/change_log.rst
index 57ccd88..130b24f 100644
--- a/docs/change_log.rst
+++ b/docs/change_log.rst
@@ -44,6 +44,7 @@ The following are only available within Stem's `git repository
 
   * Added :func:`~stem.control.BaseController.connection_time` to the :class:`~stem.control.BaseController`
   * Changed :func:`~stem.control.Controller.get_microdescriptor`, :func:`~stem.control.Controller.get_server_descriptor`, and :func:`~stem.control.Controller.get_network_status` to get our own descriptor if no fingerprint or nickname is provided.
+  * Added an :func:`~stem.exit_policy.ExitPolicy.strip_private` method to :class:`~stem.exit_policy.ExitPolicy` and :func:`~stem.exit_policy.ExitPolicy.is_private` to :class:`~stem.exit_policy.ExitPolicyRule`
 
  * **Descriptors**
 
diff --git a/stem/exit_policy.py b/stem/exit_policy.py
index 4f622d7..2cade1d 100644
--- a/stem/exit_policy.py
+++ b/stem/exit_policy.py
@@ -30,6 +30,7 @@ exiting to a destination is permissible or not. For instance...
     |- can_exit_to - check if exiting to this destination is allowed or not
     |- is_exiting_allowed - check if any exiting is allowed
     |- summary - provides a short label, similar to a microdescriptor
+    |- strip_private - provides a copy of the policy without 'private' entries
     |- __str__  - string representation
     +- __iter__ - ExitPolicyRule entries that this contains
 
@@ -42,6 +43,7 @@ exiting to a destination is permissible or not. For instance...
     |- is_match - checks if we match a given destination
     |- get_mask - provides the address representation of our mask
     |- get_masked_bits - provides the bit representation of our mask
+    |- is_private - flag indicating if this was expanded from a 'private' keyword
     +- __str__ - string representation for this rule
 
   get_config_policy - provides the ExitPolicy based on torrc rules
@@ -137,6 +139,48 @@ def get_config_policy(rules):
   return ExitPolicy(*result)
 
 
+def _flag_private_rules(rules):
+  """
+  Determine if part of our policy was expanded from the 'private' keyword. This
+  doesn't differentiate if this actually came from the 'private' keyword or a
+  series of rules exactly matching it.
+  """
+
+  matches = []
+
+  for i, rule in enumerate(rules):
+    if i + len(PRIVATE_ADDRESSES) > len(rules):
+      break
+
+    rule_str = '%s/%s' % (rule.address, rule.get_masked_bits())
+
+    if rule_str == PRIVATE_ADDRESSES[0]:
+      matches.append(i)
+
+  for start_index in matches:
+    # To match the private policy the following must all be true...
+    #
+    #   * series of addresses and bit masks match PRIVATE_ADDRESSES
+    #   * all these rules have the same port range and acceptance
+    #   * all these rules must be either accept or reject entries
+
+    rule_set = rules[start_index:start_index + len(PRIVATE_ADDRESSES)]
+    min_port, max_port = rule_set[0].min_port, rule_set[0].max_port
+    is_accept = rule_set[0].is_accept
+    is_match = True
+
+    for i, rule in enumerate(rule_set):
+      rule_str = '%s/%s' % (rule.address, rule.get_masked_bits())
+
+      if rule_str != PRIVATE_ADDRESSES[i] or rule.min_port != min_port or rule.max_port != max_port or rule.is_accept != is_accept:
+        is_match = False
+        break
+
+    if is_match:
+      for rule in rule_set:
+        rule._is_private = True
+
+
 class ExitPolicy(object):
   """
   Policy for the destinations that a relay allows or denies exiting to. This
@@ -149,6 +193,7 @@ class ExitPolicy(object):
 
   def __init__(self, *rules):
     # sanity check the types
+
     for rule in rules:
       if not isinstance(rule, (bytes, unicode, ExitPolicyRule)):
         raise TypeError('Exit policy rules can only contain strings or ExitPolicyRules, got a %s (%s)' % (type(rule), rules))
@@ -300,6 +345,15 @@ class ExitPolicy(object):
 
     return (label_prefix + ', '.join(display_ranges)).strip()
 
+  def strip_private(self):
+    """
+    Provides a copy of this policy without 'private' policy entries.
+
+    :returns: **ExitPolicy** without private rules
+    """
+
+    return ExitPolicy(*[rule for rule in self._get_rules() if not rule.is_private()])
+
   def _get_rules(self):
     if self._rules is None:
       rules = []
@@ -346,6 +400,8 @@ class ExitPolicy(object):
         elif is_all_reject:
           rules = [ExitPolicyRule('reject *:*')]
 
+      _flag_private_rules(rules)
+
       self._rules = rules
       self._input_rules = None
 
@@ -526,6 +582,12 @@ class ExitPolicyRule(object):
 
     self._submask_wildcard = True
 
+    # Flags to indicate if this rule seems to be expanded from the 'private'
+    # keyword or tor's default policy suffix.
+
+    self._is_private = False
+    self._is_default_suffix = False  # TODO: implement
+
   def is_address_wildcard(self):
     """
     **True** if we'll match against any address, **False** otherwise.
@@ -570,6 +632,7 @@ class ExitPolicyRule(object):
     """
 
     # validate our input and check if the argument doesn't match our address type
+
     if address is not None:
       address_type = self.get_address_type()
 
@@ -660,6 +723,13 @@ class ExitPolicyRule(object):
 
     return self._masked_bits
 
+  def is_private(self):
+    """
+    True if this rule was expanded from the 'private' keyword, False otherwise.
+    """
+
+    return self._is_private
+
   @lru_cache()
   def __str__(self):
     """
diff --git a/test/unit/exit_policy/policy.py b/test/unit/exit_policy/policy.py
index 70d6ac1..d00954d 100644
--- a/test/unit/exit_policy/policy.py
+++ b/test/unit/exit_policy/policy.py
@@ -96,6 +96,36 @@ class TestExitPolicy(unittest.TestCase):
     policy = ExitPolicy('reject *:80-65535', 'accept *:1-65533', 'reject *:*')
     self.assertEquals('accept 1-79', policy.summary())
 
+  def test_all_private_policy(self):
+    for port in ('*', '80', '1-1024'):
+      private_policy = get_config_policy('reject private:%s' % port)
+
+      for rule in private_policy:
+        self.assertTrue(rule.is_private())
+
+      self.assertEqual(ExitPolicy(), private_policy.strip_private())
+
+    # though not commonly done, technically private policies can be accept rules too
+
+    private_policy = get_config_policy('accept private:*')
+    self.assertEqual(ExitPolicy(), private_policy.strip_private())
+
+  def test_all_non_private_policy(self):
+    nonprivate_policy = get_config_policy('reject *:80-65535, accept *:1-65533, reject *:*')
+
+    for rule in nonprivate_policy:
+      self.assertFalse(rule.is_private())
+
+    self.assertEqual(nonprivate_policy, nonprivate_policy.strip_private())
+
+  def test_mixed_private_policy(self):
+    policy = get_config_policy('accept *:80, reject private:1-65533, accept *:*')
+
+    for rule in policy:
+      self.assertTrue(rule.is_accept != rule.is_private())  # only reject rules are the private ones
+
+    self.assertEqual(get_config_policy('accept *:80, accept *:*'), policy.strip_private())
+
   def test_str(self):
     # sanity test for our __str__ method
 



More information about the tor-commits mailing list