diff options
| author | Damian Johnson <atagar@torproject.org> | 2016-03-04 09:36:24 -0800 |
|---|---|---|
| committer | Damian Johnson <atagar@torproject.org> | 2016-03-04 09:36:31 -0800 |
| commit | e4800b022d25664579a0ee5e351fd2024662a3ba (patch) | |
| tree | 1e7dc8c7c64f033ce6678c0abe38ac389ebf337b | |
| parent | 7ee9347844f7cb28360aa0a06c97ba345be51f97 (diff) | |
| parent | 200dd8ad376257ee9e1ee4713254d6f4ba1c9f17 (diff) | |
Support accept/reject6, *4, and *6 in exit policies
Tor has expanded its exit policies quite a bit to provide more flexability in
how IPv4 and IPv6 policies can be specified. Thanks to teor for the
explanation!
https://trac.torproject.org/projects/tor/ticket/16103#comment:5
| -rw-r--r-- | docs/change_log.rst | 2 | ||||
| -rw-r--r-- | stem/exit_policy.py | 82 | ||||
| -rw-r--r-- | test/unit/exit_policy/rule.py | 50 |
3 files changed, 101 insertions, 33 deletions
diff --git a/docs/change_log.rst b/docs/change_log.rst index 57667e1f..b01602a9 100644 --- a/docs/change_log.rst +++ b/docs/change_log.rst @@ -47,6 +47,8 @@ The following are only available within Stem's `git repository * Dramatic, `300x performance improvement <https://github.com/DonnchaC/stem/pull/1>`_ for reading from the control port with python 3 * Added `stem.manual <api/manual.html>`_, which provides information available about Tor from `its manual <https://www.torproject.org/docs/tor-manual.html.en>`_ (:trac:`8251`) * :func:`~stem.connection.connect` and :func:`~stem.control.Controller.from_port` now connect to both port 9051 (relay's default) and 9151 (Tor Browser's default) (:trac:`16075`) + * :class:`~stem.exit_policy.ExitPolicy` support for *accept6* and *reject6* rules (:trac:`16103`) + * :class:`~stem.exit_policy.ExitPolicy` support for *\*4* and *\*6* wildcards (:trac:`16103`) * Added `support for NETWORK_LIVENESS events <api/response.html#stem.response.events.NetworkLivenessEvent>`_ (:spec:`44aac63`) * Added :func:`~stem.control.Controller.is_set` to the :class:`~stem.control.Controller` * Added :func:`~stem.control.Controller.is_user_traffic_allowed` to the :class:`~stem.control.Controller` diff --git a/stem/exit_policy.py b/stem/exit_policy.py index 293095b1..f71a6385 100644 --- a/stem/exit_policy.py +++ b/stem/exit_policy.py @@ -153,12 +153,6 @@ def get_config_policy(rules, ip_address = None): else: result.append(ExitPolicyRule(rule)) - # torrc policies can apply to IPv4 or IPv6, so we need to make sure /0 - # addresses aren't treated as being a full wildcard - - for rule in result: - rule._submask_wildcard = False - return ExitPolicy(*result) @@ -632,7 +626,15 @@ class ExitPolicyRule(object): This should be treated as an immutable object. + .. versionchanged:: 1.5.0 + Support for 'accept6/reject6' entries and our **is_ipv6_only** attribute. + + .. versionchanged:: 1.5.0 + Support for '\*4' and '\*6' wildcards. + :var bool is_accept: indicates if exiting is allowed or disallowed + :var bool is_ipv6_only: indicates if this is an accept6 or reject6 rule, only + matching ipv6 addresses :var str address: address that this rule is for @@ -645,17 +647,18 @@ class ExitPolicyRule(object): """ def __init__(self, rule): - # policy ::= "accept" exitpattern | "reject" exitpattern + # policy ::= "accept[6]" exitpattern | "reject[6]" exitpattern # exitpattern ::= addrspec ":" portspec - if rule.startswith('accept'): - self.is_accept = True - elif rule.startswith('reject'): - self.is_accept = False - else: - raise ValueError("An exit policy must start with either 'accept' or 'reject': %s" % rule) + self.is_accept = rule.startswith('accept') + self.is_ipv6_only = rule.startswith('accept6') or rule.startswith('reject6') - exitpattern = rule[6:] + if rule.startswith('accept6') or rule.startswith('reject6'): + exitpattern = rule[7:] + elif rule.startswith('accept') or rule.startswith('reject'): + exitpattern = rule[6:] + else: + raise ValueError("An exit policy must start with either 'accept[6]' or 'reject[6]': %s" % rule) if not exitpattern.startswith(' '): raise ValueError('An exit policy should have a space separating its accept/reject from the exit pattern: %s' % rule) @@ -677,15 +680,18 @@ class ExitPolicyRule(object): self._mask = None + # Malformed exit policies are rejected, but there's an exception where it's + # just skipped: when an accept6/reject6 rule has an IPv4 address... + # + # "Using an IPv4 address with accept6 or reject6 is ignored and generates + # a warning." + + self._skip_rule = False + addrspec, portspec = exitpattern.rsplit(':', 1) self._apply_addrspec(rule, addrspec) self._apply_portspec(rule, portspec) - # If true then a submask of /0 is treated by is_address_wildcard() as being - # a wildcard. - - self._submask_wildcard = True - # Flags to indicate if this rule seems to be expanded from the 'private' # keyword or tor's default policy suffix. @@ -694,20 +700,14 @@ class ExitPolicyRule(object): def is_address_wildcard(self): """ - **True** if we'll match against any address, **False** otherwise. + **True** if we'll match against **any** address, **False** otherwise. - Note that if this policy can apply to both IPv4 and IPv6 then this is - different from being for a /0 (since, for instance, 0.0.0.0/0 wouldn't - match against an IPv6 address). That said, /0 addresses are highly unusual - and most things citing exit policies are IPv4 specific anyway, making this - moot. + Note that this is different than \*4, \*6, or '/0' address which are + wildcards for only either IPv4 or IPv6. :returns: **bool** for if our address matching is a wildcard """ - if self._submask_wildcard and self.get_masked_bits() == 0: - return True - return self._address_type == _address_type_to_int(AddressType.WILDCARD) def is_port_wildcard(self): @@ -735,9 +735,15 @@ class ExitPolicyRule(object): :raises: **ValueError** if provided with a malformed address or port """ + if self._skip_rule: + return False + # validate our input and check if the argument doesn't match our address type if address is not None: + if self.is_ipv6_only and stem.util.connection.is_valid_ipv4_address(address): + return False # accept6/reject6 don't match ipv4 + address_type = self.get_address_type() if stem.util.connection.is_valid_ipv4_address(address): @@ -868,7 +874,10 @@ class ExitPolicyRule(object): to re-create this rule. """ - label = 'accept ' if self.is_accept else 'reject ' + if self.is_ipv6_only: + label = 'accept6 ' if self.is_accept else 'reject6 ' + else: + label = 'accept ' if self.is_accept else 'reject ' if self.is_address_wildcard(): label += '*:' @@ -906,7 +915,7 @@ class ExitPolicyRule(object): if self._hash is None: my_hash = 0 - for attr in ('is_accept', 'address', 'min_port', 'max_port'): + for attr in ('is_accept', 'is_ipv6_only', 'address', 'min_port', 'max_port'): my_hash *= 1024 attr_value = getattr(self, attr) @@ -937,6 +946,14 @@ class ExitPolicyRule(object): # Parses the addrspec... # addrspec ::= "*" | ip4spec | ip6spec + # Expand IPv4 and IPv6 specific wildcards into /0 entries so we have one + # fewer bizarre special case headaches to deal with. + + if addrspec == '*4': + addrspec = '0.0.0.0/0' + elif addrspec == '*6': + addrspec = '[0000:0000:0000:0000:0000:0000:0000:0000]/0' + if '/' in addrspec: self.address, addr_extra = addrspec.split('/', 1) else: @@ -951,6 +968,9 @@ class ExitPolicyRule(object): # ip4mask ::= an IPv4 mask in dotted-quad format # num_ip4_bits ::= an integer between 0 and 32 + if self.is_ipv6_only: + self._skip_rule = True + self._address_type = _address_type_to_int(AddressType.IPv4) if addr_extra is None: @@ -1054,10 +1074,12 @@ class MicroExitPolicyRule(ExitPolicyRule): def __init__(self, is_accept, min_port, max_port): self.is_accept = is_accept + self.is_ipv6_only = False self.address = None # wildcard address self.min_port = min_port self.max_port = max_port self._hash = None + self._skip_rule = False def is_address_wildcard(self): return True diff --git a/test/unit/exit_policy/rule.py b/test/unit/exit_policy/rule.py index ecd96a8b..59dc5d21 100644 --- a/test/unit/exit_policy/rule.py +++ b/test/unit/exit_policy/rule.py @@ -42,6 +42,9 @@ class TestExitPolicyRule(unittest.TestCase): test_inputs = ( 'accept *:*', 'reject *:*', + 'accept6 *:*', + 'reject6 *:*', + 'accept *:80', 'accept *:80-443', 'accept 127.0.0.1:80', @@ -62,6 +65,11 @@ class TestExitPolicyRule(unittest.TestCase): 'accept 192.168.0.1/255.255.255.0:80': 'accept 192.168.0.1/24:80', 'accept [::]/32:*': 'accept [0000:0000:0000:0000:0000:0000:0000:0000]/32:*', 'accept [::]/128:*': 'accept [0000:0000:0000:0000:0000:0000:0000:0000]:*', + + 'accept *4:*': 'accept 0.0.0.0/0:*', + 'accept *6:*': 'accept [0000:0000:0000:0000:0000:0000:0000:0000]/0:*', + 'accept6 *4:*': 'accept6 0.0.0.0/0:*', + 'accept6 *6:*': 'accept6 [0000:0000:0000:0000:0000:0000:0000:0000]/0:*', } for rule_arg, expected_str in test_inputs.items(): @@ -75,14 +83,23 @@ class TestExitPolicyRule(unittest.TestCase): 'accept 192.168.0.1:*': (False, True), 'accept 192.168.0.1:80': (False, False), - 'reject 127.0.0.1/0:*': (True, True), - 'reject 127.0.0.1/0.0.0.0:*': (True, True), + 'reject *4:*': (False, True), + 'reject *6:*': (False, True), + 'reject6 *4:*': (False, True), + 'reject6 *6:*': (False, True), + + 'reject 127.0.0.1/0:*': (False, True), + 'reject 127.0.0.1/0.0.0.0:*': (False, True), 'reject 127.0.0.1/16:*': (False, True), 'reject 127.0.0.1/32:*': (False, True), - 'reject [0000:0000:0000:0000:0000:0000:0000:0000]/0:80': (True, False), + 'reject [0000:0000:0000:0000:0000:0000:0000:0000]/0:80': (False, False), 'reject [0000:0000:0000:0000:0000:0000:0000:0000]/64:80': (False, False), 'reject [0000:0000:0000:0000:0000:0000:0000:0000]/128:80': (False, False), + 'reject6 *:*': (True, True), + 'reject6 *:80': (True, False), + 'reject6 [0000:0000:0000:0000:0000:0000:0000:0000]/128:80': (False, False), + 'accept 192.168.0.1:0-65535': (False, True), 'accept 192.168.0.1:1-65535': (False, True), 'accept 192.168.0.1:2-65535': (False, False), @@ -250,6 +267,14 @@ class TestExitPolicyRule(unittest.TestCase): (None, None, False): False, (None, None, True): True, }, + 'reject *4:*': { + ('192.168.0.1', 80): True, + ('FE80:0000:0000:0000:0202:B3FF:FE1E:8329', 80): False, + }, + 'reject *6:*': { + ('192.168.0.1', 80): False, + ('FE80:0000:0000:0000:0202:B3FF:FE1E:8329', 80): True, + }, } for rule_arg, matches in test_inputs.items(): @@ -352,3 +377,22 @@ class TestExitPolicyRule(unittest.TestCase): for match_args, expected_result in matches.items(): self.assertEqual(expected_result, rule.is_match(*match_args)) + + def test_ipv6_only_entries(self): + # accept6/reject6 shouldn't match anything when given an ipv4 addresses + + rule = ExitPolicyRule('accept6 192.168.0.1/0:*') + self.assertTrue(rule._skip_rule) + self.assertFalse(rule.is_match('192.168.0.1')) + self.assertFalse(rule.is_match('FE80:0000:0000:0000:0202:B3FF:FE1E:8329')) + self.assertFalse(rule.is_match()) + + rule = ExitPolicyRule('accept6 *4:*') + self.assertTrue(rule._skip_rule) + + # wildcards match all ipv6 but *not* ipv4 + + rule = ExitPolicyRule('accept6 *:*') + self.assertTrue(rule.is_ipv6_only) + self.assertTrue(rule.is_match('FE80:0000:0000:0000:0202:B3FF:FE1E:8329', 443)) + self.assertFalse(rule.is_match('192.168.0.1', 443)) |
