summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDamian Johnson <atagar@torproject.org>2016-03-04 09:36:24 -0800
committerDamian Johnson <atagar@torproject.org>2016-03-04 09:36:31 -0800
commite4800b022d25664579a0ee5e351fd2024662a3ba (patch)
tree1e7dc8c7c64f033ce6678c0abe38ac389ebf337b
parent7ee9347844f7cb28360aa0a06c97ba345be51f97 (diff)
parent200dd8ad376257ee9e1ee4713254d6f4ba1c9f17 (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.rst2
-rw-r--r--stem/exit_policy.py82
-rw-r--r--test/unit/exit_policy/rule.py50
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))