commit 0d48b504566c4df7f15991465f19af15a87c0c6b Author: Damian Johnson atagar@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
tor-commits@lists.torproject.org