[tor-commits] [stem/master] Support IPv4-mapped IPv6 addresses

atagar at torproject.org atagar at torproject.org
Sun Jan 24 00:00:50 UTC 2016


commit 195f36b382fd9825269d057fde775dcce25d482b
Author: Damian Johnson <atagar at torproject.org>
Date:   Sat Jan 23 14:25:14 2016 -0800

    Support IPv4-mapped IPv6 addresses
    
    Ohhh IPv6, how many quirks can you manage to cram into one spec? I know, I
    know, it's the new hotness but why couldn't they keep the @*!& addresses simple
    as its predecessor? </grumble>
    
    Thanks to cypherpunks for pointing this out!
    
      https://trac.torproject.org/projects/tor/ticket/18079#comment:6
---
 docs/change_log.rst          |    1 +
 stem/util/connection.py      |   38 ++++++++++++++++++++++++++++++++++++++
 test/unit/util/connection.py |   31 ++++++++++++++++++-------------
 3 files changed, 57 insertions(+), 13 deletions(-)

diff --git a/docs/change_log.rst b/docs/change_log.rst
index 1420ddf..9c6b88b 100644
--- a/docs/change_log.rst
+++ b/docs/change_log.rst
@@ -74,6 +74,7 @@ The following are only available within Stem's `git repository
   * Added :func:`~stem.util.__init__.datetime_to_unix`
   * Basic IPv6 support in :func:`~stem.util.connection.get_connections`
   * The 'ss' connection resolver didn't work on Gentoo (:trac:`18079`)
+  * Recognize IPv4-mapped IPv6 addresses in our utils (:trac:`18079`)
   * Allow :func:`stem.util.conf.Config.set` to remove values when provided with a **None** value
 
  * **Interpreter**
diff --git a/stem/util/connection.py b/stem/util/connection.py
index 0490f9c..37839cb 100644
--- a/stem/util/connection.py
+++ b/stem/util/connection.py
@@ -393,6 +393,22 @@ def is_valid_ipv6_address(address, allow_brackets = False):
     if address.startswith('[') and address.endswith(']'):
       address = address[1:-1]
 
+  if address.count('.') == 3:
+    # Likely an ipv4-mapped portion. Check that its vaild, then replace with a
+    # filler.
+
+    ipv4_start = address.rfind(':', 0, address.find('.')) + 1
+    ipv4_end = address.find(':', ipv4_start + 1)
+
+    if ipv4_end == -1:
+      ipv4_end = None  # don't crop the last character
+
+    if not is_valid_ipv4_address(address[ipv4_start:ipv4_end]):
+      return False
+
+    addr_comp = [address[:ipv4_start - 1] if ipv4_start != 0 else None, 'ff:ff', address[ipv4_end + 1:] if ipv4_end else None]
+    address = ':'.join(filter(None, addr_comp))
+
   # addresses are made up of eight colon separated groups of four hex digits
   # with leading zeros being optional
   # https://en.wikipedia.org/wiki/IPv6#Address_format
@@ -494,6 +510,9 @@ def expand_ipv6_address(address):
     >>> expand_ipv6_address('::')
     '0000:0000:0000:0000:0000:0000:0000:0000'
 
+    >>> expand_ipv6_address('::ffff:5.9.158.75')
+    '0000:0000:0000:0000:0000:ffff:0509:9e4b'
+
   :param str address: IPv6 address to be expanded
 
   :raises: **ValueError** if the address can't be expanded due to being malformed
@@ -502,6 +521,25 @@ def expand_ipv6_address(address):
   if not is_valid_ipv6_address(address):
     raise ValueError("'%s' isn't a valid IPv6 address" % address)
 
+  # expand ipv4-mapped portions of addresses
+  if address.count('.') == 3:
+    ipv4_start = address.rfind(':', 0, address.find('.')) + 1
+    ipv4_end = address.find(':', ipv4_start + 1)
+
+    if ipv4_end == -1:
+      ipv4_end = None  # don't crop the last character
+
+    # Converts ipv4 address to its hex ipv6 representation. For instance...
+    #
+    #   '5.9.158.75' => '0509:9e4b'
+
+    ipv4_bin = _get_address_binary(address[ipv4_start:ipv4_end])
+    groupings = [ipv4_bin[16 * i:16 * (i + 1)] for i in range(2)]
+    ipv6_snippet = ':'.join(['%04x' % int(group, 2) for group in groupings])
+
+    addr_comp = [address[:ipv4_start - 1] if ipv4_start != 0 else None, ipv6_snippet, address[ipv4_end + 1:] if ipv4_end else None]
+    address = ':'.join(filter(None, addr_comp))
+
   # expands collapsed groupings, there can only be a single '::' in a valid
   # address
   if '::' in address:
diff --git a/test/unit/util/connection.py b/test/unit/util/connection.py
index 776182d..793bcc6 100644
--- a/test/unit/util/connection.py
+++ b/test/unit/util/connection.py
@@ -53,7 +53,7 @@ tcp    ESTAB      0      0              127.0.0.1:22            127.0.0.1:56673
 tcp    ESTAB      0      0           192.168.0.1:44415        38.229.79.2:443    users:(("tor",15843,9))
 """
 
-SS_GENTOO_OUTPUT = """\
+SS_IPV6_OUTPUT = """\
 Netid  State      Recv-Q Send-Q Local Address:Port               Peer Address:Port
 tcp    ESTAB      0      0      5.9.158.75:443                107.170.93.13:56159               users:(("tor",pid=25056,fd=997))
 tcp    ESTAB      0      0      5.9.158.75:443                159.203.97.91:37802               users:(("tor",pid=25056,fd=77))
@@ -241,15 +241,16 @@ class TestConnection(unittest.TestCase):
     self.assertRaises(IOError, stem.util.connection.get_connections, Resolver.SS, process_pid = 1111)
 
   @patch('stem.util.system.call')
-  def test_get_connections_by_ss_on_gentoo(self, call_mock):
+  def test_get_connections_by_ss_ipv6(self, call_mock):
     """
-    Checks the get_connections function with the ss resolver results on a
-    hardened Gentoo system...
+    Checks the get_connections function with the ss resolver results on IPv6
+    conections. This also checks with the output from a hardened Gentoo system
+    which has subtle differences...
 
       https://trac.torproject.org/projects/tor/ticket/18079
     """
 
-    call_mock.return_value = SS_GENTOO_OUTPUT.split('\n')
+    call_mock.return_value = SS_IPV6_OUTPUT.split('\n')
     expected = [
       Connection('5.9.158.75', 443, '107.170.93.13', 56159, 'tcp'),
       Connection('5.9.158.75', 443, '159.203.97.91', 37802, 'tcp'),
@@ -257,12 +258,7 @@ class TestConnection(unittest.TestCase):
       Connection('2a01:4f8:190:514a::2', 443, '2001:858:2:2:aabb:0:563b:1526', 51428, 'tcp'),
     ]
     self.assertEqual(expected, stem.util.connection.get_connections(Resolver.SS, process_pid = 25056, process_name = 'tor'))
-
-    # TODO: This example ss output has entries like "::ffff:5.9.158.75:5222".
-    # What is this? Looks like a IPv6 *and* IPv4 address mashed together. Our
-    # resolver understandably thinks this is invalid right now.
-
-    self.assertRaises(IOError, stem.util.connection.get_connections, Resolver.SS, process_name = 'beam')
+    self.assertEqual(2, len(stem.util.connection.get_connections(Resolver.SS, process_name = 'beam')))
 
   @patch('stem.util.system.call')
   def test_get_connections_by_lsof(self, call_mock):
@@ -399,6 +395,9 @@ class TestConnection(unittest.TestCase):
       'fe80:0000:0000:0000:0202:b3ff:fe1e:8329',
       'fe80:0:0:0:202:b3ff:fe1e:8329',
       'fe80::202:b3ff:fe1e:8329',
+      '::ffff:5.9.158.75',
+      '5.9.158.75::ffff',
+      '::5.9.158.75:ffff',
       '::',
     )
 
@@ -406,16 +405,19 @@ class TestConnection(unittest.TestCase):
       'fe80:0000:0000:0000:0202:b3ff:fe1e:829g',
       'fe80:0000:0000:0000:0202:b3ff:fe1e: 8329',
       '2001:db8::aaaa::1',
+      '::ffff:5.9.158.75.12',
+      '::ffff:5.9.158',
+      '::ffff:5.9',
       ':::',
       ':',
       '',
     )
 
     for address in valid_addresses:
-      self.assertTrue(stem.util.connection.is_valid_ipv6_address(address))
+      self.assertTrue(stem.util.connection.is_valid_ipv6_address(address), "%s isn't a valid IPv6 address" % address)
 
     for address in invalid_addresses:
-      self.assertFalse(stem.util.connection.is_valid_ipv6_address(address))
+      self.assertFalse(stem.util.connection.is_valid_ipv6_address(address), '%s should be an invalid IPv6 address' % address)
 
   def test_is_valid_port(self):
     """
@@ -464,6 +466,9 @@ class TestConnection(unittest.TestCase):
       '::': '0000:0000:0000:0000:0000:0000:0000:0000',
       '::1': '0000:0000:0000:0000:0000:0000:0000:0001',
       '1::1': '0001:0000:0000:0000:0000:0000:0000:0001',
+      '::ffff:5.9.158.75': '0000:0000:0000:0000:0000:ffff:0509:9e4b',
+      '5.9.158.75::ffff': '0509:9e4b:0000:0000:0000:0000:0000:ffff',
+      '::5.9.158.75:ffff': '0000:0000:0000:0000:0000:0509:9e4b:ffff',
     }
 
     for test_arg, expected in test_values.items():





More information about the tor-commits mailing list