From 325028c96765f6060d29eab8dffa62d9535fa9e9 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 31 Aug 2016 01:50:13 -0700 Subject: [PATCH 1/2] error if tcp=tor is requested but tor is unimportable This only catches txtorcon not being installed (which should be fixed by doing `pip install tahoe-lafs[tor]`). It doesn't notice that the Tor daemon is not running (which we can't detect during startup, only afterwards, when it's harder to notify the user), in which case Tor connections (and all connections when "tcp = tor" is enabled) will just fail silently. --- src/allmydata/node.py | 30 ++++++++++++++++++++------ src/allmydata/test/test_connections.py | 21 ++++++++++++++++++ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/allmydata/node.py b/src/allmydata/node.py index baadb004f..656c83848 100644 --- a/src/allmydata/node.py +++ b/src/allmydata/node.py @@ -14,6 +14,21 @@ from allmydata.util.fileutil import abspath_expanduser_unicode from allmydata.util.encodingutil import get_filesystem_encoding, quote_output from allmydata.util import configutil +def _import_tor(): + # this exists to be overridden by unit tests + try: + from foolscap.connections import tor + return tor + except ImportError: # pragma: no cover + return None + +def _import_i2p(): + try: + from foolscap.connections import i2p + return i2p + except ImportError: # pragma: no cover + return None + # Add our application versions to the data that Foolscap's LogPublisher # reports. for thing, things_version in get_package_versions().iteritems(): @@ -175,9 +190,8 @@ class Node(service.MultiService): enabled = self.get_config("tor", "enable", True, boolean=True) if not enabled: return None - try: - from foolscap.connections import tor - except ImportError: + tor = _import_tor() + if not tor: return None if self.get_config("tor", "launch", False, boolean=True): @@ -215,9 +229,8 @@ class Node(service.MultiService): enabled = self.get_config("i2p", "enable", True, boolean=True) if not enabled: return None - try: - from foolscap.connections import i2p - except ImportError: + i2p = _import_i2p() + if not i2p: return None samport = self.get_config("i2p", "sam.port", None) @@ -259,6 +272,11 @@ class Node(service.MultiService): raise ValueError("'tahoe.cfg [connections] tcp='" " uses unknown handler type '%s'" % tcp_handler_name) + if not handlers[tcp_handler_name]: + raise ValueError("'tahoe.cfg [connections] tcp=' uses " + "unavailable/unimportable handler type '%s'. " + "Please pip install tahoe-lafs[%s] to fix." + % (tcp_handler_name, tcp_handler_name)) self._default_connection_handlers["tcp"] = tcp_handler_name def set_tub_options(self): diff --git a/src/allmydata/test/test_connections.py b/src/allmydata/test/test_connections.py index a20b15888..e3f846785 100644 --- a/src/allmydata/test/test_connections.py +++ b/src/allmydata/test/test_connections.py @@ -29,6 +29,12 @@ class Tor(unittest.TestCase): h = n._make_tor_handler() self.assertEqual(h, None) + def test_unimportable(self): + n = FakeNode(BASECONFIG) + with mock.patch("allmydata.node._import_tor", return_value=None): + h = n._make_tor_handler() + self.assertEqual(h, None) + def test_default(self): n = FakeNode(BASECONFIG) h1 = mock.Mock() @@ -109,6 +115,12 @@ class I2P(unittest.TestCase): h = n._make_i2p_handler() self.assertEqual(h, None) + def test_unimportable(self): + n = FakeNode(BASECONFIG) + with mock.patch("allmydata.node._import_i2p", return_value=None): + h = n._make_i2p_handler() + self.assertEqual(h, None) + def test_default(self): n = FakeNode(BASECONFIG) h1 = mock.Mock() @@ -204,6 +216,15 @@ class Connections(unittest.TestCase): self.assertEqual(n._default_connection_handlers["tor"], "tor") self.assertEqual(n._default_connection_handlers["i2p"], "i2p") + def test_tor_unimportable(self): + n = FakeNode(BASECONFIG+"[connections]\ntcp = tor\n") + with mock.patch("allmydata.node._import_tor", return_value=None): + e = self.assertRaises(ValueError, n.init_connections) + self.assertEqual(str(e), + "'tahoe.cfg [connections] tcp='" + " uses unavailable/unimportable handler type 'tor'." + " Please pip install tahoe-lafs[tor] to fix.") + def test_unknown(self): n = FakeNode(BASECONFIG+"[connections]\ntcp = unknown\n") e = self.assertRaises(ValueError, n.init_connections) From d47fc0fd2705a208b08701ae9ff7ddadc9050150 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 31 Aug 2016 02:44:27 -0700 Subject: [PATCH 2/2] config: add reveal-IP-address=False This adds a safety flag named `[node] reveal-IP-address`, for which the default value is True. When this is set to False, any configuration that might reveal the node's IP address (to servers, or the external network) will cause a PrivacyError to be raised at startup, terminating the node before it gets a chance to betray the user's privacy. It also adds docs and tests. refs ticket:1010 --- docs/configuration.rst | 21 ++++++++++ src/allmydata/node.py | 18 ++++++++ src/allmydata/test/test_connections.py | 58 +++++++++++++++++++++++++- 3 files changed, 96 insertions(+), 1 deletion(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index 3ca9e98c9..915b30b33 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -324,6 +324,23 @@ set the ``tub.location`` option described below. used for files that usually (on a Unix system) go into ``/tmp``. The string will be interpreted relative to the node's base directory. +``reveal-IP-address = (boolean, optional, defaults to True)`` + + This is a safety flag. If False, any of the following configuration + problems will cause ``tahoe start`` to throw a PrivacyError instead of + starting the node: + + * ``[node] tub.location`` contains any ``tcp:`` hints + + * ``[node] tub.location`` uses ``AUTO``, or is missing/empty (because + that defaults to AUTO) + + * ``[connections] tcp =`` is set to ``tcp`` (or left as the default), + rather than being set to ``tor`` + + These configuration problems would reveal the node's IP address to + servers and external networks. + Connection Management ===================== @@ -334,6 +351,10 @@ also controls when Tor and I2P are used: for all TCP connections (to hide your IP address), or only when necessary (just for servers which declare that they need Tor, because they use ``.onion`` addresses). +Note that if you want to protect your node's IP address, you should set +``[node] reveal-IP-address = False``, which will refuse to launch the node if +any of the other configuration settings might violate this privacy property. + ``[connections]`` ----------------- diff --git a/src/allmydata/node.py b/src/allmydata/node.py index 656c83848..48441524f 100644 --- a/src/allmydata/node.py +++ b/src/allmydata/node.py @@ -76,6 +76,9 @@ class UnescapedHashError(Exception): return ("The configuration entry %s contained an unescaped '#' character." % quote_output("[%s]%s = %s" % self.args)) +class PrivacyError(Exception): + """reveal-IP-address = false, but the node is configured in such a way + that the IP address could be revealed""" class Node(service.MultiService): # this implements common functionality of both Client nodes and Introducer @@ -99,6 +102,7 @@ class Node(service.MultiService): assert type(self.nickname) is unicode self.init_tempdir() + self.check_privacy() self.init_connections() self.set_tub_options() self.create_main_tub() @@ -181,6 +185,10 @@ class Node(service.MultiService): twlog.msg(e) raise e + def check_privacy(self): + self._reveal_ip = self.get_config("node", "reveal-IP-address", True, + boolean=True) + def _make_tcp_handler(self): # this is always available from foolscap.connections.tcp import default @@ -279,6 +287,10 @@ class Node(service.MultiService): % (tcp_handler_name, tcp_handler_name)) self._default_connection_handlers["tcp"] = tcp_handler_name + if not self._reveal_ip: + if self._default_connection_handlers["tcp"] == "tcp": + raise PrivacyError("tcp = tcp, must be set to 'tor'") + def set_tub_options(self): self.tub_options = { "logLocalFailures": True, @@ -339,6 +351,8 @@ class Node(service.MultiService): # addresses. Don't probe for local addresses unless necessary. split_location = location.split(",") if "AUTO" in split_location: + if not self._reveal_ip: + raise PrivacyError("tub.location uses AUTO") local_addresses = iputil.get_local_addresses_sync() # tubport must be like "tcp:12345" or "tcp:12345:morestuff" local_portnum = int(tubport.split(":")[1]) @@ -348,6 +362,10 @@ class Node(service.MultiService): new_locations.extend(["tcp:%s:%d" % (ip, local_portnum) for ip in local_addresses]) else: + if not self._reveal_ip: + hint_type = loc.split(":")[0] + if hint_type == "tcp": + raise PrivacyError("tub.location includes tcp: hint") new_locations.append(loc) return ",".join(new_locations) diff --git a/src/allmydata/test/test_connections.py b/src/allmydata/test/test_connections.py index e3f846785..2ff32011c 100644 --- a/src/allmydata/test/test_connections.py +++ b/src/allmydata/test/test_connections.py @@ -5,12 +5,13 @@ from twisted.trial import unittest from twisted.internet import reactor, endpoints from ConfigParser import SafeConfigParser from foolscap.connections import tcp -from ..node import Node +from ..node import Node, PrivacyError class FakeNode(Node): def __init__(self, config_str): self.config = SafeConfigParser() self.config.readfp(BytesIO(config_str)) + self._reveal_ip = True BASECONFIG = ("[client]\n" "introducer.furl = \n" @@ -230,3 +231,58 @@ class Connections(unittest.TestCase): e = self.assertRaises(ValueError, n.init_connections) self.assertIn("'tahoe.cfg [connections] tcp='", str(e)) self.assertIn("uses unknown handler type 'unknown'", str(e)) + +class Privacy(unittest.TestCase): + def test_flag(self): + n = FakeNode(BASECONFIG) + n.check_privacy() + self.assertTrue(n._reveal_ip) + + n = FakeNode(BASECONFIG+"[node]\nreveal-IP-address = true\n") + n.check_privacy() + self.assertTrue(n._reveal_ip) + + n = FakeNode(BASECONFIG+"[node]\nreveal-IP-address = false\n") + n.check_privacy() + self.assertFalse(n._reveal_ip) + + n = FakeNode(BASECONFIG+"[node]\nreveal-ip-address = false\n") + n.check_privacy() + self.assertFalse(n._reveal_ip) + + def test_connections(self): + n = FakeNode(BASECONFIG+"[node]\nreveal-IP-address = false\n") + n.check_privacy() + e = self.assertRaises(PrivacyError, n.init_connections) + self.assertEqual(str(e), "tcp = tcp, must be set to 'tor'") + + def test_tub_location_auto(self): + n = FakeNode(BASECONFIG+"[node]\nreveal-IP-address = false\n") + n._portnumfile = "missing" + n.check_privacy() + e = self.assertRaises(PrivacyError, n.get_tub_location, None) + self.assertEqual(str(e), "tub.location uses AUTO") + + n = FakeNode(BASECONFIG+"[node]\nreveal-IP-address = false\n" + + "tub.location = AUTO\n") + n._portnumfile = "missing" + n.check_privacy() + e = self.assertRaises(PrivacyError, n.get_tub_location, None) + self.assertEqual(str(e), "tub.location uses AUTO") + + n = FakeNode(BASECONFIG+"[node]\nreveal-IP-address = false\n" + + "tub.location = AUTO,tcp:hostname:1234\n") + n._portnumfile = "missing" + n.check_privacy() + e = self.assertRaises(PrivacyError, n.get_tub_location, None) + self.assertEqual(str(e), "tub.location uses AUTO") + + def test_tub_location_tcp(self): + n = FakeNode(BASECONFIG+"[node]\nreveal-IP-address = false\n" + + "tub.location = tcp:hostname:1234\n") + n._portnumfile = "missing" + n.check_privacy() + e = self.assertRaises(PrivacyError, n.get_tub_location, None) + self.assertEqual(str(e), "tub.location includes tcp: hint") + +