Commit e4800b02 authored by Damian Johnson's avatar Damian Johnson
Browse files

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
parents 7ee93478 200dd8ad
Loading
Loading
Loading
Loading
+2 −0
Original line number Original line Diff line number Diff line
@@ -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
  * 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`)
  * 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`)
  * :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 `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_set` to the :class:`~stem.control.Controller`
  * Added :func:`~stem.control.Controller.is_user_traffic_allowed` to the :class:`~stem.control.Controller`
  * Added :func:`~stem.control.Controller.is_user_traffic_allowed` to the :class:`~stem.control.Controller`
+52 −30
Original line number Original line Diff line number Diff line
@@ -153,12 +153,6 @@ def get_config_policy(rules, ip_address = None):
    else:
    else:
      result.append(ExitPolicyRule(rule))
      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)
  return ExitPolicy(*result)




@@ -632,7 +626,15 @@ class ExitPolicyRule(object):


  This should be treated as an immutable 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_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
  :var str address: address that this rule is for


@@ -645,17 +647,18 @@ class ExitPolicyRule(object):
  """
  """


  def __init__(self, rule):
  def __init__(self, rule):
    # policy ::= "accept" exitpattern | "reject" exitpattern
    # policy ::= "accept[6]" exitpattern | "reject[6]" exitpattern
    # exitpattern ::= addrspec ":" portspec
    # exitpattern ::= addrspec ":" portspec


    if rule.startswith('accept'):
    self.is_accept = rule.startswith('accept')
      self.is_accept = True
    self.is_ipv6_only = rule.startswith('accept6') or rule.startswith('reject6')
    elif rule.startswith('reject'):
      self.is_accept = False
    else:
      raise ValueError("An exit policy must start with either 'accept' or 'reject': %s" % rule)


    if rule.startswith('accept6') or rule.startswith('reject6'):
      exitpattern = rule[7:]
    elif rule.startswith('accept') or rule.startswith('reject'):
      exitpattern = rule[6:]
      exitpattern = rule[6:]
    else:
      raise ValueError("An exit policy must start with either 'accept[6]' or 'reject[6]': %s" % rule)


    if not exitpattern.startswith(' '):
    if not exitpattern.startswith(' '):
      raise ValueError('An exit policy should have a space separating its accept/reject from the exit pattern: %s' % rule)
      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
    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)
    addrspec, portspec = exitpattern.rsplit(':', 1)
    self._apply_addrspec(rule, addrspec)
    self._apply_addrspec(rule, addrspec)
    self._apply_portspec(rule, portspec)
    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'
    # Flags to indicate if this rule seems to be expanded from the 'private'
    # keyword or tor's default policy suffix.
    # keyword or tor's default policy suffix.


@@ -694,20 +700,14 @@ class ExitPolicyRule(object):


  def is_address_wildcard(self):
  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
    Note that this is different than \*4, \*6, or '/0' address which are
    different from being for a /0 (since, for instance, 0.0.0.0/0 wouldn't
    wildcards for only either IPv4 or IPv6.
    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.


    :returns: **bool** for if our address matching is a wildcard
    :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)
    return self._address_type == _address_type_to_int(AddressType.WILDCARD)


  def is_port_wildcard(self):
  def is_port_wildcard(self):
@@ -735,9 +735,15 @@ class ExitPolicyRule(object):
    :raises: **ValueError** if provided with a malformed address or port
    :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
    # validate our input and check if the argument doesn't match our address type


    if address is not None:
    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()
      address_type = self.get_address_type()


      if stem.util.connection.is_valid_ipv4_address(address):
      if stem.util.connection.is_valid_ipv4_address(address):
@@ -868,6 +874,9 @@ class ExitPolicyRule(object):
    to re-create this rule.
    to re-create this rule.
    """
    """


    if self.is_ipv6_only:
      label = 'accept6 ' if self.is_accept else 'reject6 '
    else:
      label = 'accept ' if self.is_accept else 'reject '
      label = 'accept ' if self.is_accept else 'reject '


    if self.is_address_wildcard():
    if self.is_address_wildcard():
@@ -906,7 +915,7 @@ class ExitPolicyRule(object):
    if self._hash is None:
    if self._hash is None:
      my_hash = 0
      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
        my_hash *= 1024


        attr_value = getattr(self, attr)
        attr_value = getattr(self, attr)
@@ -937,6 +946,14 @@ class ExitPolicyRule(object):
    # Parses the addrspec...
    # Parses the addrspec...
    # addrspec ::= "*" | ip4spec | ip6spec
    # 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:
    if '/' in addrspec:
      self.address, addr_extra = addrspec.split('/', 1)
      self.address, addr_extra = addrspec.split('/', 1)
    else:
    else:
@@ -951,6 +968,9 @@ class ExitPolicyRule(object):
      # ip4mask ::= an IPv4 mask in dotted-quad format
      # ip4mask ::= an IPv4 mask in dotted-quad format
      # num_ip4_bits ::= an integer between 0 and 32
      # 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)
      self._address_type = _address_type_to_int(AddressType.IPv4)


      if addr_extra is None:
      if addr_extra is None:
@@ -1054,10 +1074,12 @@ class MicroExitPolicyRule(ExitPolicyRule):


  def __init__(self, is_accept, min_port, max_port):
  def __init__(self, is_accept, min_port, max_port):
    self.is_accept = is_accept
    self.is_accept = is_accept
    self.is_ipv6_only = False
    self.address = None  # wildcard address
    self.address = None  # wildcard address
    self.min_port = min_port
    self.min_port = min_port
    self.max_port = max_port
    self.max_port = max_port
    self._hash = None
    self._hash = None
    self._skip_rule = False


  def is_address_wildcard(self):
  def is_address_wildcard(self):
    return True
    return True
+47 −3
Original line number Original line Diff line number Diff line
@@ -42,6 +42,9 @@ class TestExitPolicyRule(unittest.TestCase):
    test_inputs = (
    test_inputs = (
      'accept *:*',
      'accept *:*',
      'reject *:*',
      'reject *:*',
      'accept6 *:*',
      'reject6 *:*',

      'accept *:80',
      'accept *:80',
      'accept *:80-443',
      'accept *:80-443',
      'accept 127.0.0.1:80',
      '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 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 [::]/32:*': 'accept [0000:0000:0000:0000:0000:0000:0000:0000]/32:*',
      'accept [::]/128:*': 'accept [0000:0000:0000:0000:0000:0000:0000:0000]:*',
      '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():
    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:*': (False, True),
      'accept 192.168.0.1:80': (False, False),
      'accept 192.168.0.1:80': (False, False),


      'reject 127.0.0.1/0:*': (True, True),
      'reject *4:*': (False, True),
      'reject 127.0.0.1/0.0.0.0:*': (True, 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/16:*': (False, True),
      'reject 127.0.0.1/32:*': (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]/64:80': (False, False),
      'reject [0000:0000:0000:0000:0000:0000:0000:0000]/128: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:0-65535': (False, True),
      'accept 192.168.0.1:1-65535': (False, True),
      'accept 192.168.0.1:1-65535': (False, True),
      'accept 192.168.0.1:2-65535': (False, False),
      'accept 192.168.0.1:2-65535': (False, False),
@@ -250,6 +267,14 @@ class TestExitPolicyRule(unittest.TestCase):
        (None, None, False): False,
        (None, None, False): False,
        (None, None, True): True,
        (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():
    for rule_arg, matches in test_inputs.items():
@@ -352,3 +377,22 @@ class TestExitPolicyRule(unittest.TestCase):


      for match_args, expected_result in matches.items():
      for match_args, expected_result in matches.items():
        self.assertEqual(expected_result, rule.is_match(*match_args))
        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))