mirror of
https://github.com/tahoe-lafs/tahoe-lafs.git
synced 2025-02-21 02:01:31 +00:00
Merge branch '2490-tor-2'
This adds --listen=tor to create-node and create-server, along with .onion-address allocation at creation time, and onion-service starting (launching or connecting to tor as necessary) as node startup time. closes ticket:2490 refs ticket:2773 refs ticket:1010 refs ticket:517
This commit is contained in:
commit
5a195e2339
@ -87,22 +87,6 @@ For Tahoe-LAFS storage servers there are three use-cases:
|
||||
https://geti2p.net/en/about/intro
|
||||
|
||||
|
||||
Unresolved tickets
|
||||
==================
|
||||
|
||||
Tahoe's anonymity support does not yet include automatic configuration of
|
||||
servers. This issue is tracked by Tahoe tickets `#2490`_ and `#2773`_: until
|
||||
those are resolved, anonymous servers (running as Tor Onion services or I2P
|
||||
servers) must be configured manually, as described below.
|
||||
|
||||
See also Tahoe-LAFS Tor related tickets `#1010`_ and `#517`_.
|
||||
|
||||
.. _`#2490`: https://tahoe-lafs.org/trac/tahoe-lafs/ticket/2490
|
||||
.. _`#2773`: https://tahoe-lafs.org/trac/tahoe-lafs/ticket/2773
|
||||
.. _`#1010`: https://tahoe-lafs.org/trac/tahoe-lafs/ticket/1010
|
||||
.. _`#517`: https://tahoe-lafs.org/trac/tahoe-lafs/ticket/517
|
||||
|
||||
|
||||
Software Dependencies
|
||||
=====================
|
||||
|
||||
@ -316,10 +300,6 @@ the location announcement).
|
||||
Server anonymity, automatic configuration
|
||||
-----------------------------------------
|
||||
|
||||
(note: this is not yet implemented, see Tahoe tickets `#2490`_ and `#2773`_
|
||||
for progress)
|
||||
|
||||
|
||||
To configure a server node to listen on an anonymizing network, create the
|
||||
node with the ``--listen=tor`` option. This requires a Tor configuration that
|
||||
either launches a new Tor daemon, or has access to the Tor control port (and
|
||||
|
4
setup.py
4
setup.py
@ -273,7 +273,7 @@ setup(name="tahoe-lafs", # also set in __init__.py
|
||||
"mock",
|
||||
"tox",
|
||||
"foolscap[tor] >= 0.12.3",
|
||||
"txtorcon", # in case pip's resolver doesn't work
|
||||
"txtorcon >= 0.17.0", # in case pip's resolver doesn't work
|
||||
"foolscap[i2p]",
|
||||
"txi2p", # in case pip's resolver doesn't work
|
||||
"pytest",
|
||||
@ -281,7 +281,7 @@ setup(name="tahoe-lafs", # also set in __init__.py
|
||||
],
|
||||
"tor": [
|
||||
"foolscap[tor] >= 0.12.3",
|
||||
"txtorcon", # in case pip's resolver doesn't work
|
||||
"txtorcon >= 0.17.0", # in case pip's resolver doesn't work
|
||||
],
|
||||
"i2p": [
|
||||
"foolscap[i2p]",
|
||||
|
@ -13,14 +13,7 @@ from allmydata.util.assertutil import _assert
|
||||
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
|
||||
from allmydata.util import tor_provider
|
||||
|
||||
def _import_i2p():
|
||||
try:
|
||||
@ -59,6 +52,10 @@ def _common_config_sections():
|
||||
"launch",
|
||||
"socks.port",
|
||||
"tor.executable",
|
||||
"onion",
|
||||
"onion.local_port",
|
||||
"onion.external_port",
|
||||
"onion.private_key_file",
|
||||
),
|
||||
}
|
||||
|
||||
@ -143,6 +140,7 @@ class Node(service.MultiService):
|
||||
self.logSource="Node"
|
||||
self.setup_logging()
|
||||
|
||||
self.create_tor_provider()
|
||||
self.init_connections()
|
||||
self.set_tub_options()
|
||||
self.create_main_tub()
|
||||
@ -224,6 +222,10 @@ class Node(service.MultiService):
|
||||
def check_privacy(self):
|
||||
self._reveal_ip = self.get_config("node", "reveal-IP-address", True,
|
||||
boolean=True)
|
||||
def create_tor_provider(self):
|
||||
self._tor_provider = tor_provider.Provider(self.basedir, self, reactor)
|
||||
self._tor_provider.check_onion_config()
|
||||
self._tor_provider.setServiceParent(self)
|
||||
|
||||
def _make_tcp_handler(self):
|
||||
# this is always available
|
||||
@ -231,29 +233,7 @@ class Node(service.MultiService):
|
||||
return default()
|
||||
|
||||
def _make_tor_handler(self):
|
||||
enabled = self.get_config("tor", "enabled", True, boolean=True)
|
||||
if not enabled:
|
||||
return None
|
||||
tor = _import_tor()
|
||||
if not tor:
|
||||
return None
|
||||
|
||||
if self.get_config("tor", "launch", False, boolean=True):
|
||||
executable = self.get_config("tor", "tor.executable", None)
|
||||
datadir = os.path.join(self.basedir, "private", "tor-statedir")
|
||||
return tor.launch(data_directory=datadir, tor_binary=executable)
|
||||
|
||||
socks_endpoint_desc = self.get_config("tor", "socks.port", None)
|
||||
if socks_endpoint_desc:
|
||||
socks_ep = endpoints.clientFromString(reactor, socks_endpoint_desc)
|
||||
return tor.socks_endpoint(socks_ep)
|
||||
|
||||
controlport = self.get_config("tor", "control.port", None)
|
||||
if controlport:
|
||||
ep = endpoints.clientFromString(reactor, controlport)
|
||||
return tor.control_endpoint(ep)
|
||||
|
||||
return tor.default_socks()
|
||||
return self._tor_provider.get_tor_handler()
|
||||
|
||||
def _make_i2p_handler(self):
|
||||
enabled = self.get_config("i2p", "enabled", True, boolean=True)
|
||||
|
@ -1,11 +1,11 @@
|
||||
import os
|
||||
from twisted.internet import defer
|
||||
from twisted.internet import reactor, defer
|
||||
from twisted.python.usage import UsageError
|
||||
from allmydata.scripts.common import BasedirOptions, NoDefaultBasedirOptions
|
||||
from allmydata.scripts.default_nodedir import _default_nodedir
|
||||
from allmydata.util.assertutil import precondition
|
||||
from allmydata.util.encodingutil import listdir_unicode, argv_to_unicode, quote_local_unicode_path
|
||||
from allmydata.util import fileutil, iputil
|
||||
from allmydata.util import fileutil, iputil, tor_provider
|
||||
|
||||
|
||||
dummy_tac = """
|
||||
@ -30,6 +30,17 @@ WHERE_OPTS = [
|
||||
"Comma-separated list of listener types (tcp,tor,i2p,none)."),
|
||||
]
|
||||
|
||||
TOR_OPTS = [
|
||||
("tor-control-port", None, None,
|
||||
"Tor's control port endpoint descriptor string (e.g. tcp:127.0.0.1:9051 or unix:/var/run/tor/control)"),
|
||||
("tor-executable", None, None,
|
||||
"The 'tor' executable to run (default is to search $PATH)."),
|
||||
]
|
||||
|
||||
TOR_FLAGS = [
|
||||
("tor-launch", None, "Launch a tor instead of connecting to a tor control port."),
|
||||
]
|
||||
|
||||
def validate_where_options(o):
|
||||
if o['listen'] == "none":
|
||||
# no other arguments are accepted
|
||||
@ -68,6 +79,16 @@ def validate_where_options(o):
|
||||
if 'tcp' not in listeners and o['hostname']:
|
||||
raise UsageError("--listen= must be tcp to use --hostname")
|
||||
|
||||
def validate_tor_options(o):
|
||||
use_tor = "tor" in o["listen"].split(",")
|
||||
if not use_tor:
|
||||
if o["tor-launch"]:
|
||||
raise UsageError("--tor-launch requires --listen=tor")
|
||||
if o["tor-control-port"]:
|
||||
raise UsageError("--tor-control-port= requires --listen=tor")
|
||||
if o["tor-launch"] and o["tor-control-port"]:
|
||||
raise UsageError("use either --tor-launch or --tor-control-port=, not both")
|
||||
|
||||
class _CreateBaseOptions(BasedirOptions):
|
||||
optFlags = [
|
||||
("hide-ip", None, "prohibit any configuration that would reveal the node's IP address"),
|
||||
@ -81,6 +102,7 @@ class CreateClientOptions(_CreateBaseOptions):
|
||||
# we provide 'create-node'-time options for the most common
|
||||
# configuration knobs. The rest can be controlled by editing
|
||||
# tahoe.cfg before node startup.
|
||||
|
||||
("nickname", "n", None, "Specify the nickname for this node."),
|
||||
("introducer", "i", None, "Specify the introducer FURL to use."),
|
||||
("webport", "p", "tcp:3456:interface=127.0.0.1",
|
||||
@ -97,25 +119,28 @@ class CreateClientOptions(_CreateBaseOptions):
|
||||
class CreateNodeOptions(CreateClientOptions):
|
||||
optFlags = [
|
||||
("no-storage", None, "Do not offer storage service to other nodes."),
|
||||
]
|
||||
] + TOR_FLAGS
|
||||
|
||||
synopsis = "[options] [NODEDIR]"
|
||||
description = "Create a full Tahoe-LAFS node (client+server)."
|
||||
optParameters = CreateClientOptions.optParameters + WHERE_OPTS
|
||||
optParameters = CreateClientOptions.optParameters + WHERE_OPTS + TOR_OPTS
|
||||
|
||||
def parseArgs(self, basedir=None):
|
||||
CreateClientOptions.parseArgs(self, basedir)
|
||||
validate_where_options(self)
|
||||
validate_tor_options(self)
|
||||
|
||||
class CreateIntroducerOptions(NoDefaultBasedirOptions):
|
||||
subcommand_name = "create-introducer"
|
||||
description = "Create a Tahoe-LAFS introducer."
|
||||
optFlags = [
|
||||
("hide-ip", None, "prohibit any configuration that would reveal the node's IP address"),
|
||||
]
|
||||
optParameters = NoDefaultBasedirOptions.optParameters + WHERE_OPTS
|
||||
] + TOR_FLAGS
|
||||
optParameters = NoDefaultBasedirOptions.optParameters + WHERE_OPTS + TOR_OPTS
|
||||
def parseArgs(self, basedir=None):
|
||||
NoDefaultBasedirOptions.parseArgs(self, basedir)
|
||||
validate_where_options(self)
|
||||
validate_tor_options(self)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def write_node_config(c, config):
|
||||
@ -150,6 +175,8 @@ def write_node_config(c, config):
|
||||
c.write("web.static = public_html\n")
|
||||
|
||||
listeners = config['listen'].split(",")
|
||||
|
||||
tor_config = {}
|
||||
tub_ports = []
|
||||
tub_locations = []
|
||||
if listeners == ["none"]:
|
||||
@ -157,8 +184,10 @@ def write_node_config(c, config):
|
||||
c.write("tub.location = disabled\n")
|
||||
else:
|
||||
if "tor" in listeners:
|
||||
raise NotImplementedError("--listen=tor is under development, "
|
||||
"see ticket #2490 for details")
|
||||
(tor_config, tor_port, tor_location) = \
|
||||
yield tor_provider.create_onion(reactor, config)
|
||||
tub_ports.append(tor_port)
|
||||
tub_locations.append(tor_location)
|
||||
if "i2p" in listeners:
|
||||
raise NotImplementedError("--listen=i2p is under development, "
|
||||
"see ticket #2490 for details")
|
||||
@ -183,7 +212,13 @@ def write_node_config(c, config):
|
||||
c.write("#ssh.port = 8022\n")
|
||||
c.write("#ssh.authorized_keys_file = ~/.ssh/authorized_keys\n")
|
||||
c.write("\n")
|
||||
yield None
|
||||
|
||||
if tor_config:
|
||||
c.write("[tor]\n")
|
||||
for key, value in tor_config.items():
|
||||
c.write("%s = %s\n" % (key, value))
|
||||
c.write("\n")
|
||||
|
||||
|
||||
def write_client_config(c, config):
|
||||
c.write("[client]\n")
|
||||
|
@ -1,9 +1,11 @@
|
||||
import os
|
||||
import mock
|
||||
from twisted.trial import unittest
|
||||
from twisted.internet import defer
|
||||
from twisted.internet import defer, reactor
|
||||
from twisted.python import usage
|
||||
from allmydata.util import configutil
|
||||
from ..common_util import run_cli, parse_cli
|
||||
from ...scripts import create_node
|
||||
|
||||
def read_config(basedir):
|
||||
tahoe_cfg = os.path.join(basedir, "tahoe.cfg")
|
||||
@ -154,14 +156,6 @@ class Config(unittest.TestCase):
|
||||
basedir)
|
||||
self.assertEqual(str(e), "--listen= must be none, or one/some of: tcp, tor, i2p")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_node_listen_tor(self):
|
||||
basedir = self.mktemp()
|
||||
d = run_cli("create-node", "--listen=tor", basedir)
|
||||
e = yield self.assertFailure(d, NotImplementedError)
|
||||
self.assertEqual(str(e), "--listen=tor is under development, "
|
||||
"see ticket #2490 for details")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_node_listen_i2p(self):
|
||||
basedir = self.mktemp()
|
||||
@ -201,6 +195,19 @@ class Config(unittest.TestCase):
|
||||
self.assertIn("is not empty", err)
|
||||
self.assertIn("To avoid clobbering anything, I am going to quit now", err)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_node_slow_tor(self):
|
||||
basedir = self.mktemp()
|
||||
d = defer.Deferred()
|
||||
with mock.patch("allmydata.util.tor_provider.create_onion",
|
||||
return_value=d):
|
||||
d2 = run_cli("create-node", "--listen=tor", basedir)
|
||||
d.callback(({}, "port", "location"))
|
||||
rc, out, err = yield d2
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertIn("Node created", out)
|
||||
self.assertEqual(err, "")
|
||||
|
||||
def test_introducer_no_hostname(self):
|
||||
basedir = self.mktemp()
|
||||
e = self.assertRaises(usage.UsageError, parse_cli,
|
||||
@ -236,3 +243,77 @@ class Config(unittest.TestCase):
|
||||
self.assertIn(basedir, err)
|
||||
self.assertIn("is not empty", err)
|
||||
self.assertIn("To avoid clobbering anything, I am going to quit now", err)
|
||||
|
||||
class Tor(unittest.TestCase):
|
||||
def test_default(self):
|
||||
basedir = self.mktemp()
|
||||
tor_config = {"abc": "def"}
|
||||
tor_port = "ghi"
|
||||
tor_location = "jkl"
|
||||
onion_d = defer.succeed( (tor_config, tor_port, tor_location) )
|
||||
with mock.patch("allmydata.util.tor_provider.create_onion",
|
||||
return_value=onion_d) as co:
|
||||
rc, out, err = self.successResultOf(
|
||||
run_cli("create-node", "--listen=tor", basedir))
|
||||
self.assertEqual(len(co.mock_calls), 1)
|
||||
args = co.mock_calls[0][1]
|
||||
self.assertIdentical(args[0], reactor)
|
||||
self.assertIsInstance(args[1], create_node.CreateNodeOptions)
|
||||
self.assertEqual(args[1]["listen"], "tor")
|
||||
cfg = read_config(basedir)
|
||||
self.assertEqual(cfg.get("tor", "abc"), "def")
|
||||
self.assertEqual(cfg.get("node", "tub.port"), "ghi")
|
||||
self.assertEqual(cfg.get("node", "tub.location"), "jkl")
|
||||
|
||||
def test_launch(self):
|
||||
basedir = self.mktemp()
|
||||
tor_config = {"abc": "def"}
|
||||
tor_port = "ghi"
|
||||
tor_location = "jkl"
|
||||
onion_d = defer.succeed( (tor_config, tor_port, tor_location) )
|
||||
with mock.patch("allmydata.util.tor_provider.create_onion",
|
||||
return_value=onion_d) as co:
|
||||
rc, out, err = self.successResultOf(
|
||||
run_cli("create-node", "--listen=tor", "--tor-launch",
|
||||
basedir))
|
||||
args = co.mock_calls[0][1]
|
||||
self.assertEqual(args[1]["listen"], "tor")
|
||||
self.assertEqual(args[1]["tor-launch"], True)
|
||||
self.assertEqual(args[1]["tor-control-port"], None)
|
||||
|
||||
def test_control_port(self):
|
||||
basedir = self.mktemp()
|
||||
tor_config = {"abc": "def"}
|
||||
tor_port = "ghi"
|
||||
tor_location = "jkl"
|
||||
onion_d = defer.succeed( (tor_config, tor_port, tor_location) )
|
||||
with mock.patch("allmydata.util.tor_provider.create_onion",
|
||||
return_value=onion_d) as co:
|
||||
rc, out, err = self.successResultOf(
|
||||
run_cli("create-node", "--listen=tor", "--tor-control-port=mno",
|
||||
basedir))
|
||||
args = co.mock_calls[0][1]
|
||||
self.assertEqual(args[1]["listen"], "tor")
|
||||
self.assertEqual(args[1]["tor-launch"], False)
|
||||
self.assertEqual(args[1]["tor-control-port"], "mno")
|
||||
|
||||
def test_not_both(self):
|
||||
e = self.assertRaises(usage.UsageError,
|
||||
parse_cli,
|
||||
"create-node", "--listen=tor",
|
||||
"--tor-launch", "--tor-control-port=foo")
|
||||
self.assertEqual(str(e), "use either --tor-launch or"
|
||||
" --tor-control-port=, not both")
|
||||
|
||||
def test_launch_without_listen(self):
|
||||
e = self.assertRaises(usage.UsageError,
|
||||
parse_cli,
|
||||
"create-node", "--listen=none", "--tor-launch")
|
||||
self.assertEqual(str(e), "--tor-launch requires --listen=tor")
|
||||
|
||||
def test_control_port_without_listen(self):
|
||||
e = self.assertRaises(usage.UsageError,
|
||||
parse_cli,
|
||||
"create-node", "--listen=none",
|
||||
"--tor-control-port=foo")
|
||||
self.assertEqual(str(e), "--tor-control-port= requires --listen=tor")
|
||||
|
@ -2,7 +2,7 @@ import os
|
||||
import mock
|
||||
from io import BytesIO
|
||||
from twisted.trial import unittest
|
||||
from twisted.internet import reactor, endpoints
|
||||
from twisted.internet import reactor, endpoints, defer
|
||||
from twisted.internet.interfaces import IStreamClientEndpoint
|
||||
from ConfigParser import SafeConfigParser
|
||||
from foolscap.connections import tcp
|
||||
@ -13,6 +13,9 @@ class FakeNode(Node):
|
||||
self.config = SafeConfigParser()
|
||||
self.config.readfp(BytesIO(config_str))
|
||||
self._reveal_ip = True
|
||||
self.basedir = "BASEDIR"
|
||||
self.services = []
|
||||
self.create_tor_provider()
|
||||
|
||||
BASECONFIG = ("[client]\n"
|
||||
"introducer.furl = \n"
|
||||
@ -32,69 +35,80 @@ class Tor(unittest.TestCase):
|
||||
self.assertEqual(h, None)
|
||||
|
||||
def test_unimportable(self):
|
||||
n = FakeNode(BASECONFIG)
|
||||
with mock.patch("allmydata.node._import_tor", return_value=None):
|
||||
with mock.patch("allmydata.util.tor_provider._import_tor",
|
||||
return_value=None):
|
||||
n = FakeNode(BASECONFIG)
|
||||
h = n._make_tor_handler()
|
||||
self.assertEqual(h, None)
|
||||
|
||||
def test_default(self):
|
||||
n = FakeNode(BASECONFIG)
|
||||
h1 = mock.Mock()
|
||||
with mock.patch("foolscap.connections.tor.default_socks",
|
||||
return_value=h1) as f:
|
||||
n = FakeNode(BASECONFIG)
|
||||
h = n._make_tor_handler()
|
||||
self.assertEqual(f.mock_calls, [mock.call()])
|
||||
self.assertIdentical(h, h1)
|
||||
|
||||
def test_launch(self):
|
||||
n = FakeNode(BASECONFIG+"[tor]\nlaunch = true\n")
|
||||
n.basedir = "BASEDIR"
|
||||
def _do_test_launch(self, executable):
|
||||
# the handler is created right away
|
||||
config = BASECONFIG+"[tor]\nlaunch = true\n"
|
||||
if executable:
|
||||
config += "tor.executable = %s\n" % executable
|
||||
h1 = mock.Mock()
|
||||
with mock.patch("foolscap.connections.tor.launch",
|
||||
with mock.patch("foolscap.connections.tor.control_endpoint_maker",
|
||||
return_value=h1) as f:
|
||||
n = FakeNode(config)
|
||||
h = n._make_tor_handler()
|
||||
data_directory = os.path.join(n.basedir, "private", "tor-statedir")
|
||||
exp = mock.call(data_directory=data_directory,
|
||||
tor_binary=None)
|
||||
private_dir = os.path.join(n.basedir, "private")
|
||||
exp = mock.call(n._tor_provider._make_control_endpoint)
|
||||
self.assertEqual(f.mock_calls, [exp])
|
||||
self.assertIdentical(h, h1)
|
||||
|
||||
# later, when Foolscap first connects, Tor should be launched
|
||||
tp = n._tor_provider
|
||||
reactor = "reactor"
|
||||
tcp = object()
|
||||
tcep = object()
|
||||
launch_tor = mock.Mock(return_value=defer.succeed(("ep_desc", tcp)))
|
||||
cfs = mock.Mock(return_value=tcep)
|
||||
with mock.patch("allmydata.util.tor_provider._launch_tor", launch_tor):
|
||||
with mock.patch("allmydata.util.tor_provider.clientFromString", cfs):
|
||||
cep = self.successResultOf(tp._make_control_endpoint(reactor))
|
||||
launch_tor.assert_called_with(reactor, executable, private_dir,
|
||||
tp._txtorcon)
|
||||
cfs.assert_called_with(reactor, "ep_desc")
|
||||
self.assertIs(cep, tcep)
|
||||
|
||||
def test_launch(self):
|
||||
self._do_test_launch(None)
|
||||
|
||||
def test_launch_executable(self):
|
||||
n = FakeNode(BASECONFIG+"[tor]\nlaunch = true\ntor.executable = tor")
|
||||
n.basedir = "BASEDIR"
|
||||
h1 = mock.Mock()
|
||||
with mock.patch("foolscap.connections.tor.launch",
|
||||
return_value=h1) as f:
|
||||
h = n._make_tor_handler()
|
||||
data_directory = os.path.join(n.basedir, "private", "tor-statedir")
|
||||
exp = mock.call(data_directory=data_directory,
|
||||
tor_binary="tor")
|
||||
self.assertEqual(f.mock_calls, [exp])
|
||||
self.assertIdentical(h, h1)
|
||||
self._do_test_launch("/special/tor")
|
||||
|
||||
def test_socksport_unix_endpoint(self):
|
||||
n = FakeNode(BASECONFIG+"[tor]\nsocks.port = unix:/var/lib/fw-daemon/tor_socks.socket\n")
|
||||
h1 = mock.Mock()
|
||||
with mock.patch("foolscap.connections.tor.socks_endpoint",
|
||||
return_value=h1) as f:
|
||||
n = FakeNode(BASECONFIG+"[tor]\nsocks.port = unix:/var/lib/fw-daemon/tor_socks.socket\n")
|
||||
h = n._make_tor_handler()
|
||||
self.assertTrue(IStreamClientEndpoint.providedBy(f.mock_calls[0]))
|
||||
self.assertIdentical(h, h1)
|
||||
|
||||
def test_socksport_endpoint(self):
|
||||
n = FakeNode(BASECONFIG+"[tor]\nsocks.port = tcp:127.0.0.1:1234\n")
|
||||
h1 = mock.Mock()
|
||||
with mock.patch("foolscap.connections.tor.socks_endpoint",
|
||||
return_value=h1) as f:
|
||||
n = FakeNode(BASECONFIG+"[tor]\nsocks.port = tcp:127.0.0.1:1234\n")
|
||||
h = n._make_tor_handler()
|
||||
self.assertTrue(IStreamClientEndpoint.providedBy(f.mock_calls[0]))
|
||||
self.assertIdentical(h, h1)
|
||||
|
||||
def test_socksport_endpoint_otherhost(self):
|
||||
n = FakeNode(BASECONFIG+"[tor]\nsocks.port = tcp:otherhost:1234\n")
|
||||
h1 = mock.Mock()
|
||||
with mock.patch("foolscap.connections.tor.socks_endpoint",
|
||||
return_value=h1) as f:
|
||||
n = FakeNode(BASECONFIG+"[tor]\nsocks.port = tcp:otherhost:1234\n")
|
||||
h = n._make_tor_handler()
|
||||
self.assertTrue(IStreamClientEndpoint.providedBy(f.mock_calls[0]))
|
||||
self.assertIdentical(h, h1)
|
||||
@ -110,10 +124,10 @@ class Tor(unittest.TestCase):
|
||||
self.assertIn("invalid literal for int() with base 10: 'kumquat'", str(e))
|
||||
|
||||
def test_controlport(self):
|
||||
n = FakeNode(BASECONFIG+"[tor]\ncontrol.port = tcp:localhost:1234\n")
|
||||
h1 = mock.Mock()
|
||||
with mock.patch("foolscap.connections.tor.control_endpoint",
|
||||
return_value=h1) as f:
|
||||
n = FakeNode(BASECONFIG+"[tor]\ncontrol.port = tcp:localhost:1234\n")
|
||||
h = n._make_tor_handler()
|
||||
self.assertEqual(len(f.mock_calls), 1)
|
||||
ep = f.mock_calls[0][1][0]
|
||||
@ -230,9 +244,10 @@ class Connections(unittest.TestCase):
|
||||
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)
|
||||
with mock.patch("allmydata.util.tor_provider._import_tor",
|
||||
return_value=None):
|
||||
n = FakeNode(BASECONFIG+"[connections]\ntcp = tor\n")
|
||||
e = self.assertRaises(ValueError, n.init_connections)
|
||||
self.assertEqual(str(e),
|
||||
"'tahoe.cfg [connections] tcp='"
|
||||
" uses unavailable/unimportable handler type 'tor'."
|
||||
|
@ -355,6 +355,7 @@ class MultiplePorts(unittest.TestCase):
|
||||
n.read_config()
|
||||
n.check_privacy()
|
||||
n.services = []
|
||||
n.create_tor_provider()
|
||||
n.init_connections()
|
||||
n.set_tub_options()
|
||||
t = FakeTub()
|
||||
|
535
src/allmydata/test/test_tor_provider.py
Normal file
535
src/allmydata/test/test_tor_provider.py
Normal file
@ -0,0 +1,535 @@
|
||||
import os
|
||||
from twisted.trial import unittest
|
||||
from twisted.internet import defer, error
|
||||
from StringIO import StringIO
|
||||
import mock
|
||||
from ..util import tor_provider
|
||||
from ..scripts import create_node, runner
|
||||
from foolscap.eventual import flushEventualQueue
|
||||
|
||||
def mock_txtorcon(txtorcon):
|
||||
return mock.patch("allmydata.util.tor_provider._import_txtorcon",
|
||||
return_value=txtorcon)
|
||||
|
||||
def mock_tor(tor):
|
||||
return mock.patch("allmydata.util.tor_provider._import_tor",
|
||||
return_value=tor)
|
||||
|
||||
def make_cli_config(basedir, *argv):
|
||||
parent = runner.Options()
|
||||
cli_config = create_node.CreateNodeOptions()
|
||||
cli_config.parent = parent
|
||||
cli_config.parseOptions(argv)
|
||||
cli_config["basedir"] = basedir
|
||||
cli_config.stdout = StringIO()
|
||||
return cli_config
|
||||
|
||||
class TryToConnect(unittest.TestCase):
|
||||
def test_try(self):
|
||||
reactor = object()
|
||||
txtorcon = mock.Mock()
|
||||
tor_state = object()
|
||||
d = defer.succeed(tor_state)
|
||||
txtorcon.build_tor_connection = mock.Mock(return_value=d)
|
||||
ep = object()
|
||||
stdout = StringIO()
|
||||
with mock.patch("allmydata.util.tor_provider.clientFromString",
|
||||
return_value=ep) as cfs:
|
||||
d = tor_provider._try_to_connect(reactor, "desc", stdout, txtorcon)
|
||||
r = self.successResultOf(d)
|
||||
self.assertIs(r, tor_state)
|
||||
cfs.assert_called_with(reactor, "desc")
|
||||
txtorcon.build_tor_connection.assert_called_with(ep)
|
||||
|
||||
def test_try_handled_error(self):
|
||||
reactor = object()
|
||||
txtorcon = mock.Mock()
|
||||
d = defer.fail(error.ConnectError("oops"))
|
||||
txtorcon.build_tor_connection = mock.Mock(return_value=d)
|
||||
ep = object()
|
||||
stdout = StringIO()
|
||||
with mock.patch("allmydata.util.tor_provider.clientFromString",
|
||||
return_value=ep) as cfs:
|
||||
d = tor_provider._try_to_connect(reactor, "desc", stdout, txtorcon)
|
||||
r = self.successResultOf(d)
|
||||
self.assertIs(r, None)
|
||||
cfs.assert_called_with(reactor, "desc")
|
||||
txtorcon.build_tor_connection.assert_called_with(ep)
|
||||
self.assertEqual(stdout.getvalue(),
|
||||
"Unable to reach Tor at 'desc': "
|
||||
"An error occurred while connecting: oops.\n")
|
||||
|
||||
def test_try_unhandled_error(self):
|
||||
reactor = object()
|
||||
txtorcon = mock.Mock()
|
||||
d = defer.fail(ValueError("oops"))
|
||||
txtorcon.build_tor_connection = mock.Mock(return_value=d)
|
||||
ep = object()
|
||||
stdout = StringIO()
|
||||
with mock.patch("allmydata.util.tor_provider.clientFromString",
|
||||
return_value=ep) as cfs:
|
||||
d = tor_provider._try_to_connect(reactor, "desc", stdout, txtorcon)
|
||||
f = self.failureResultOf(d)
|
||||
self.assertIsInstance(f.value, ValueError)
|
||||
self.assertEqual(str(f.value), "oops")
|
||||
cfs.assert_called_with(reactor, "desc")
|
||||
txtorcon.build_tor_connection.assert_called_with(ep)
|
||||
self.assertEqual(stdout.getvalue(), "")
|
||||
|
||||
class LaunchTor(unittest.TestCase):
|
||||
def _do_test_launch(self, tor_executable):
|
||||
reactor = object()
|
||||
private_dir = "private"
|
||||
txtorcon = mock.Mock()
|
||||
tpp = mock.Mock
|
||||
tpp.tor_protocol = mock.Mock()
|
||||
txtorcon.launch_tor = mock.Mock(return_value=tpp)
|
||||
|
||||
with mock.patch("allmydata.util.tor_provider.allocate_tcp_port",
|
||||
return_value=999999):
|
||||
d = tor_provider._launch_tor(reactor, tor_executable, private_dir,
|
||||
txtorcon)
|
||||
tor_control_endpoint, tor_control_proto = self.successResultOf(d)
|
||||
self.assertIs(tor_control_proto, tpp.tor_protocol)
|
||||
|
||||
def test_launch(self):
|
||||
return self._do_test_launch(None)
|
||||
def test_launch_executable(self):
|
||||
return self._do_test_launch("mytor")
|
||||
|
||||
class ConnectToTor(unittest.TestCase):
|
||||
def _do_test_connect(self, endpoint, reachable):
|
||||
reactor = object()
|
||||
txtorcon = object()
|
||||
args = []
|
||||
if endpoint:
|
||||
args = ["--tor-control-port=%s" % endpoint]
|
||||
cli_config = make_cli_config("basedir", "--listen=tor", *args)
|
||||
stdout = cli_config.stdout
|
||||
expected_port = "tcp:127.0.0.1:9151"
|
||||
if endpoint:
|
||||
expected_port = endpoint
|
||||
tor_state = mock.Mock
|
||||
tor_state.protocol = object()
|
||||
tried = []
|
||||
def _try_to_connect(reactor, port, stdout, txtorcon):
|
||||
tried.append( (reactor, port, stdout, txtorcon) )
|
||||
if not reachable:
|
||||
return defer.succeed(None)
|
||||
if port == expected_port: # second one on the list
|
||||
return defer.succeed(tor_state)
|
||||
return defer.succeed(None)
|
||||
|
||||
with mock.patch("allmydata.util.tor_provider._try_to_connect",
|
||||
_try_to_connect):
|
||||
d = tor_provider._connect_to_tor(reactor, cli_config, txtorcon)
|
||||
if not reachable:
|
||||
f = self.failureResultOf(d)
|
||||
self.assertIsInstance(f.value, ValueError)
|
||||
self.assertEqual(str(f.value),
|
||||
"unable to reach any default Tor control port")
|
||||
return
|
||||
successful_port, tor_control_proto = self.successResultOf(d)
|
||||
self.assertEqual(successful_port, expected_port)
|
||||
self.assertIs(tor_control_proto, tor_state.protocol)
|
||||
expected = [(reactor, "unix:/var/run/tor/control", stdout, txtorcon),
|
||||
(reactor, "tcp:127.0.0.1:9051", stdout, txtorcon),
|
||||
(reactor, "tcp:127.0.0.1:9151", stdout, txtorcon),
|
||||
]
|
||||
if endpoint:
|
||||
expected = [(reactor, endpoint, stdout, txtorcon)]
|
||||
self.assertEqual(tried, expected)
|
||||
|
||||
def test_connect(self):
|
||||
return self._do_test_connect(None, True)
|
||||
def test_connect_endpoint(self):
|
||||
return self._do_test_connect("tcp:other:port", True)
|
||||
def test_connect_unreachable(self):
|
||||
return self._do_test_connect(None, False)
|
||||
|
||||
|
||||
class CreateOnion(unittest.TestCase):
|
||||
def test_no_txtorcon(self):
|
||||
with mock.patch("allmydata.util.tor_provider._import_txtorcon",
|
||||
return_value=None):
|
||||
d = tor_provider.create_onion("reactor", "cli_config")
|
||||
f = self.failureResultOf(d)
|
||||
self.assertIsInstance(f.value, ValueError)
|
||||
self.assertEqual(str(f.value),
|
||||
"Cannot create onion without txtorcon. "
|
||||
"Please 'pip install tahoe-lafs[tor]' to fix this.")
|
||||
def _do_test_launch(self, executable):
|
||||
basedir = self.mktemp()
|
||||
os.mkdir(basedir)
|
||||
private_dir = os.path.join(basedir, "private")
|
||||
os.mkdir(private_dir)
|
||||
reactor = object()
|
||||
args = ["--listen=tor", "--tor-launch"]
|
||||
if executable:
|
||||
args.append("--tor-executable=%s" % executable)
|
||||
cli_config = make_cli_config(basedir, *args)
|
||||
protocol = object()
|
||||
launch_tor = mock.Mock(return_value=defer.succeed(("control_endpoint",
|
||||
protocol)))
|
||||
txtorcon = mock.Mock()
|
||||
ehs = mock.Mock()
|
||||
ehs.private_key = "privkey"
|
||||
ehs.hostname = "ONION.onion"
|
||||
txtorcon.EphemeralHiddenService = mock.Mock(return_value=ehs)
|
||||
ehs.add_to_tor = mock.Mock(return_value=defer.succeed(None))
|
||||
ehs.remove_from_tor = mock.Mock(return_value=defer.succeed(None))
|
||||
|
||||
with mock_txtorcon(txtorcon):
|
||||
with mock.patch("allmydata.util.tor_provider._launch_tor",
|
||||
launch_tor):
|
||||
with mock.patch("allmydata.util.tor_provider.allocate_tcp_port",
|
||||
return_value=999999):
|
||||
d = tor_provider.create_onion(reactor, cli_config)
|
||||
tahoe_config_tor, tor_port, tor_location = self.successResultOf(d)
|
||||
|
||||
launch_tor.assert_called_with(reactor, executable,
|
||||
os.path.abspath(private_dir), txtorcon)
|
||||
txtorcon.EphemeralHiddenService.assert_called_with("3457 127.0.0.1:999999")
|
||||
ehs.add_to_tor.assert_called_with(protocol)
|
||||
ehs.remove_from_tor.assert_called_with(protocol)
|
||||
|
||||
expected = {"launch": "true",
|
||||
"onion": "true",
|
||||
"onion.local_port": "999999",
|
||||
"onion.external_port": "3457",
|
||||
"onion.private_key_file": os.path.join("private",
|
||||
"tor_onion.privkey"),
|
||||
}
|
||||
if executable:
|
||||
expected["tor.executable"] = executable
|
||||
self.assertEqual(tahoe_config_tor, expected)
|
||||
self.assertEqual(tor_port, "tcp:999999:interface=127.0.0.1")
|
||||
self.assertEqual(tor_location, "tor:ONION.onion:3457")
|
||||
fn = os.path.join(basedir, tahoe_config_tor["onion.private_key_file"])
|
||||
with open(fn, "rb") as f:
|
||||
privkey = f.read()
|
||||
self.assertEqual(privkey, "privkey")
|
||||
|
||||
def test_launch(self):
|
||||
return self._do_test_launch(None)
|
||||
def test_launch_executable(self):
|
||||
return self._do_test_launch("mytor")
|
||||
|
||||
def test_control_endpoint(self):
|
||||
basedir = self.mktemp()
|
||||
os.mkdir(basedir)
|
||||
private_dir = os.path.join(basedir, "private")
|
||||
os.mkdir(private_dir)
|
||||
reactor = object()
|
||||
cli_config = make_cli_config(basedir, "--listen=tor")
|
||||
protocol = object()
|
||||
connect_to_tor = mock.Mock(return_value=defer.succeed(("goodport",
|
||||
protocol)))
|
||||
txtorcon = mock.Mock()
|
||||
ehs = mock.Mock()
|
||||
ehs.private_key = "privkey"
|
||||
ehs.hostname = "ONION.onion"
|
||||
txtorcon.EphemeralHiddenService = mock.Mock(return_value=ehs)
|
||||
ehs.add_to_tor = mock.Mock(return_value=defer.succeed(None))
|
||||
ehs.remove_from_tor = mock.Mock(return_value=defer.succeed(None))
|
||||
|
||||
with mock_txtorcon(txtorcon):
|
||||
with mock.patch("allmydata.util.tor_provider._connect_to_tor",
|
||||
connect_to_tor):
|
||||
with mock.patch("allmydata.util.tor_provider.allocate_tcp_port",
|
||||
return_value=999999):
|
||||
d = tor_provider.create_onion(reactor, cli_config)
|
||||
tahoe_config_tor, tor_port, tor_location = self.successResultOf(d)
|
||||
|
||||
connect_to_tor.assert_called_with(reactor, cli_config, txtorcon)
|
||||
txtorcon.EphemeralHiddenService.assert_called_with("3457 127.0.0.1:999999")
|
||||
ehs.add_to_tor.assert_called_with(protocol)
|
||||
ehs.remove_from_tor.assert_called_with(protocol)
|
||||
|
||||
expected = {"control.port": "goodport",
|
||||
"onion": "true",
|
||||
"onion.local_port": "999999",
|
||||
"onion.external_port": "3457",
|
||||
"onion.private_key_file": os.path.join("private",
|
||||
"tor_onion.privkey"),
|
||||
}
|
||||
self.assertEqual(tahoe_config_tor, expected)
|
||||
self.assertEqual(tor_port, "tcp:999999:interface=127.0.0.1")
|
||||
self.assertEqual(tor_location, "tor:ONION.onion:3457")
|
||||
fn = os.path.join(basedir, tahoe_config_tor["onion.private_key_file"])
|
||||
with open(fn, "rb") as f:
|
||||
privkey = f.read()
|
||||
self.assertEqual(privkey, "privkey")
|
||||
|
||||
_None = object()
|
||||
class FakeConfig(dict):
|
||||
def get_config(self, section, option, default=_None, boolean=False):
|
||||
if section != "tor":
|
||||
raise ValueError(section)
|
||||
value = self.get(option, default)
|
||||
if value is _None:
|
||||
raise KeyError
|
||||
return value
|
||||
|
||||
class Provider(unittest.TestCase):
|
||||
def test_build(self):
|
||||
tor_provider.Provider("basedir", FakeConfig(), "reactor")
|
||||
|
||||
def test_handler_disabled(self):
|
||||
p = tor_provider.Provider("basedir", FakeConfig(enabled=False),
|
||||
"reactor")
|
||||
self.assertEqual(p.get_tor_handler(), None)
|
||||
|
||||
def test_handler_no_tor(self):
|
||||
with mock_tor(None):
|
||||
p = tor_provider.Provider("basedir", FakeConfig(), "reactor")
|
||||
self.assertEqual(p.get_tor_handler(), None)
|
||||
|
||||
def test_handler_launch_no_txtorcon(self):
|
||||
with mock_txtorcon(None):
|
||||
p = tor_provider.Provider("basedir", FakeConfig(launch=True),
|
||||
"reactor")
|
||||
self.assertEqual(p.get_tor_handler(), None)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_handler_launch(self):
|
||||
reactor = object()
|
||||
tor = mock.Mock()
|
||||
txtorcon = mock.Mock()
|
||||
handler = object()
|
||||
tor.control_endpoint_maker = mock.Mock(return_value=handler)
|
||||
with mock_tor(tor):
|
||||
with mock_txtorcon(txtorcon):
|
||||
p = tor_provider.Provider("basedir", FakeConfig(launch=True),
|
||||
reactor)
|
||||
h = p.get_tor_handler()
|
||||
self.assertIs(h, handler)
|
||||
tor.control_endpoint_maker.assert_called_with(p._make_control_endpoint)
|
||||
|
||||
# make sure Tor is launched just once, the first time an endpoint is
|
||||
# requested, and never again. The clientFromString() function is
|
||||
# called once each time.
|
||||
|
||||
ep_desc = object()
|
||||
launch_tor = mock.Mock(return_value=defer.succeed((ep_desc,None)))
|
||||
ep = object()
|
||||
cfs = mock.Mock(return_value=ep)
|
||||
with mock.patch("allmydata.util.tor_provider._launch_tor", launch_tor):
|
||||
with mock.patch("allmydata.util.tor_provider.clientFromString", cfs):
|
||||
d = p._make_control_endpoint(reactor)
|
||||
yield flushEventualQueue()
|
||||
self.assertIs(self.successResultOf(d), ep)
|
||||
launch_tor.assert_called_with(reactor, None,
|
||||
os.path.join("basedir", "private"),
|
||||
txtorcon)
|
||||
cfs.assert_called_with(reactor, ep_desc)
|
||||
|
||||
launch_tor2 = mock.Mock(return_value=defer.succeed((ep_desc,None)))
|
||||
cfs2 = mock.Mock(return_value=ep)
|
||||
with mock.patch("allmydata.util.tor_provider._launch_tor", launch_tor2):
|
||||
with mock.patch("allmydata.util.tor_provider.clientFromString", cfs2):
|
||||
d2 = p._make_control_endpoint(reactor)
|
||||
yield flushEventualQueue()
|
||||
self.assertIs(self.successResultOf(d2), ep)
|
||||
self.assertEqual(launch_tor2.mock_calls, [])
|
||||
cfs2.assert_called_with(reactor, ep_desc)
|
||||
|
||||
def test_handler_socks_endpoint(self):
|
||||
tor = mock.Mock()
|
||||
handler = object()
|
||||
tor.socks_endpoint = mock.Mock(return_value=handler)
|
||||
ep = object()
|
||||
cfs = mock.Mock(return_value=ep)
|
||||
reactor = object()
|
||||
|
||||
with mock_tor(tor):
|
||||
p = tor_provider.Provider("basedir",
|
||||
FakeConfig(**{"socks.port": "ep_desc"}),
|
||||
reactor)
|
||||
with mock.patch("allmydata.util.tor_provider.clientFromString", cfs):
|
||||
h = p.get_tor_handler()
|
||||
cfs.assert_called_with(reactor, "ep_desc")
|
||||
tor.socks_endpoint.assert_called_with(ep)
|
||||
self.assertIs(h, handler)
|
||||
|
||||
def test_handler_control_endpoint(self):
|
||||
tor = mock.Mock()
|
||||
handler = object()
|
||||
tor.control_endpoint = mock.Mock(return_value=handler)
|
||||
ep = object()
|
||||
cfs = mock.Mock(return_value=ep)
|
||||
reactor = object()
|
||||
|
||||
with mock_tor(tor):
|
||||
p = tor_provider.Provider("basedir",
|
||||
FakeConfig(**{"control.port": "ep_desc"}),
|
||||
reactor)
|
||||
with mock.patch("allmydata.util.tor_provider.clientFromString", cfs):
|
||||
h = p.get_tor_handler()
|
||||
self.assertIs(h, handler)
|
||||
cfs.assert_called_with(reactor, "ep_desc")
|
||||
tor.control_endpoint.assert_called_with(ep)
|
||||
|
||||
def test_handler_default(self):
|
||||
tor = mock.Mock()
|
||||
handler = object()
|
||||
tor.default_socks = mock.Mock(return_value=handler)
|
||||
|
||||
with mock_tor(tor):
|
||||
p = tor_provider.Provider("basedir", FakeConfig(), "reactor")
|
||||
h = p.get_tor_handler()
|
||||
self.assertIs(h, handler)
|
||||
tor.default_socks.assert_called_with()
|
||||
|
||||
class Provider_CheckOnionConfig(unittest.TestCase):
|
||||
def test_default(self):
|
||||
# default config doesn't start an onion service, so it should be
|
||||
# happy both with and without txtorcon
|
||||
|
||||
p = tor_provider.Provider("basedir", FakeConfig(), "reactor")
|
||||
p.check_onion_config()
|
||||
|
||||
with mock_txtorcon(None):
|
||||
p = tor_provider.Provider("basedir", FakeConfig(), "reactor")
|
||||
p.check_onion_config()
|
||||
|
||||
def test_no_txtorcon(self):
|
||||
with mock_txtorcon(None):
|
||||
p = tor_provider.Provider("basedir", FakeConfig(onion=True),
|
||||
"reactor")
|
||||
e = self.assertRaises(ValueError, p.check_onion_config)
|
||||
self.assertEqual(str(e), "Cannot create onion without txtorcon. "
|
||||
"Please 'pip install tahoe-lafs[tor]' to fix.")
|
||||
|
||||
def test_no_launch_no_control(self):
|
||||
p = tor_provider.Provider("basedir", FakeConfig(onion=True), "reactor")
|
||||
e = self.assertRaises(ValueError, p.check_onion_config)
|
||||
self.assertEqual(str(e), "[tor] onion = true, but we have neither "
|
||||
"launch=true nor control.port=")
|
||||
|
||||
def test_missing_keys(self):
|
||||
p = tor_provider.Provider("basedir", FakeConfig(onion=True,
|
||||
launch=True), "reactor")
|
||||
e = self.assertRaises(ValueError, p.check_onion_config)
|
||||
self.assertEqual(str(e), "[tor] onion = true, "
|
||||
"but onion.local_port= is missing")
|
||||
|
||||
p = tor_provider.Provider("basedir",
|
||||
FakeConfig(onion=True, launch=True,
|
||||
**{"onion.local_port": "x",
|
||||
}), "reactor")
|
||||
e = self.assertRaises(ValueError, p.check_onion_config)
|
||||
self.assertEqual(str(e), "[tor] onion = true, "
|
||||
"but onion.external_port= is missing")
|
||||
|
||||
p = tor_provider.Provider("basedir",
|
||||
FakeConfig(onion=True, launch=True,
|
||||
**{"onion.local_port": "x",
|
||||
"onion.external_port": "y",
|
||||
}), "reactor")
|
||||
e = self.assertRaises(ValueError, p.check_onion_config)
|
||||
self.assertEqual(str(e), "[tor] onion = true, "
|
||||
"but onion.private_key_file= is missing")
|
||||
|
||||
def test_ok(self):
|
||||
p = tor_provider.Provider("basedir",
|
||||
FakeConfig(onion=True, launch=True,
|
||||
**{"onion.local_port": "x",
|
||||
"onion.external_port": "y",
|
||||
"onion.private_key_file": "z",
|
||||
}), "reactor")
|
||||
p.check_onion_config()
|
||||
|
||||
class Provider_Service(unittest.TestCase):
|
||||
def test_no_onion(self):
|
||||
reactor = object()
|
||||
p = tor_provider.Provider("basedir", FakeConfig(onion=False), reactor)
|
||||
with mock.patch("allmydata.util.tor_provider.Provider._start_onion") as s:
|
||||
p.startService()
|
||||
self.assertEqual(s.mock_calls, [])
|
||||
self.assertEqual(p.running, True)
|
||||
|
||||
p.stopService()
|
||||
self.assertEqual(p.running, False)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_launch(self):
|
||||
basedir = self.mktemp()
|
||||
os.mkdir(basedir)
|
||||
fn = os.path.join(basedir, "keyfile")
|
||||
with open(fn, "w") as f:
|
||||
f.write("private key")
|
||||
reactor = object()
|
||||
cfg = FakeConfig(onion=True, launch=True,
|
||||
**{"onion.local_port": 123,
|
||||
"onion.external_port": 456,
|
||||
"onion.private_key_file": "keyfile",
|
||||
})
|
||||
|
||||
txtorcon = mock.Mock()
|
||||
with mock_txtorcon(txtorcon):
|
||||
p = tor_provider.Provider(basedir, cfg, reactor)
|
||||
tor_state = mock.Mock()
|
||||
tor_state.protocol = object()
|
||||
ehs = mock.Mock()
|
||||
ehs.add_to_tor = mock.Mock(return_value=defer.succeed(None))
|
||||
ehs.remove_from_tor = mock.Mock(return_value=defer.succeed(None))
|
||||
txtorcon.EphemeralHiddenService = mock.Mock(return_value=ehs)
|
||||
launch_tor = mock.Mock(return_value=defer.succeed((None,tor_state.protocol)))
|
||||
with mock.patch("allmydata.util.tor_provider._launch_tor",
|
||||
launch_tor):
|
||||
d = p.startService()
|
||||
yield flushEventualQueue()
|
||||
self.successResultOf(d)
|
||||
self.assertIs(p._onion_ehs, ehs)
|
||||
self.assertIs(p._onion_tor_control_proto, tor_state.protocol)
|
||||
launch_tor.assert_called_with(reactor, None,
|
||||
os.path.join(basedir, "private"), txtorcon)
|
||||
txtorcon.EphemeralHiddenService.assert_called_with("456 127.0.0.1:123",
|
||||
"private key")
|
||||
ehs.add_to_tor.assert_called_with(tor_state.protocol)
|
||||
|
||||
yield p.stopService()
|
||||
ehs.remove_from_tor.assert_called_with(tor_state.protocol)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_control_endpoint(self):
|
||||
basedir = self.mktemp()
|
||||
os.mkdir(basedir)
|
||||
fn = os.path.join(basedir, "keyfile")
|
||||
with open(fn, "w") as f:
|
||||
f.write("private key")
|
||||
reactor = object()
|
||||
cfg = FakeConfig(onion=True,
|
||||
**{"control.port": "ep_desc",
|
||||
"onion.local_port": 123,
|
||||
"onion.external_port": 456,
|
||||
"onion.private_key_file": "keyfile",
|
||||
})
|
||||
|
||||
txtorcon = mock.Mock()
|
||||
with mock_txtorcon(txtorcon):
|
||||
p = tor_provider.Provider(basedir, cfg, reactor)
|
||||
tor_state = mock.Mock()
|
||||
tor_state.protocol = object()
|
||||
txtorcon.build_tor_connection = mock.Mock(return_value=tor_state)
|
||||
ehs = mock.Mock()
|
||||
ehs.add_to_tor = mock.Mock(return_value=defer.succeed(None))
|
||||
ehs.remove_from_tor = mock.Mock(return_value=defer.succeed(None))
|
||||
txtorcon.EphemeralHiddenService = mock.Mock(return_value=ehs)
|
||||
tcep = object()
|
||||
cfs = mock.Mock(return_value=tcep)
|
||||
with mock.patch("allmydata.util.tor_provider.clientFromString", cfs):
|
||||
d = p.startService()
|
||||
yield flushEventualQueue()
|
||||
self.successResultOf(d)
|
||||
self.assertIs(p._onion_ehs, ehs)
|
||||
self.assertIs(p._onion_tor_control_proto, tor_state.protocol)
|
||||
cfs.assert_called_with(reactor, "ep_desc")
|
||||
txtorcon.build_tor_connection.assert_called_with(tcep)
|
||||
txtorcon.EphemeralHiddenService.assert_called_with("456 127.0.0.1:123",
|
||||
"private key")
|
||||
ehs.add_to_tor.assert_called_with(tor_state.protocol)
|
||||
|
||||
yield p.stopService()
|
||||
ehs.remove_from_tor.assert_called_with(tor_state.protocol)
|
318
src/allmydata/util/tor_provider.py
Normal file
318
src/allmydata/util/tor_provider.py
Normal file
@ -0,0 +1,318 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, print_function, with_statement
|
||||
import os
|
||||
|
||||
from twisted.internet.defer import inlineCallbacks, returnValue
|
||||
from twisted.internet.endpoints import clientFromString
|
||||
from twisted.internet.error import ConnectionRefusedError, ConnectError
|
||||
from twisted.application import service
|
||||
|
||||
from .observer import OneShotObserverList
|
||||
from .iputil import allocate_tcp_port
|
||||
|
||||
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_txtorcon():
|
||||
try:
|
||||
import txtorcon
|
||||
return txtorcon
|
||||
except ImportError: # pragma: no cover
|
||||
return None
|
||||
|
||||
def data_directory(private_dir):
|
||||
return os.path.join(private_dir, "tor-statedir")
|
||||
|
||||
# different ways we might approach this:
|
||||
|
||||
# 1: get an ITorControlProtocol, make a
|
||||
# txtorcon.EphemeralHiddenService(ports), yield ehs.add_to_tor(tcp), store
|
||||
# ehs.hostname and ehs.private_key, yield ehs.remove_from_tor(tcp)
|
||||
|
||||
def _try_to_connect(reactor, endpoint_desc, stdout, txtorcon):
|
||||
# yields a TorState, or None
|
||||
ep = clientFromString(reactor, endpoint_desc)
|
||||
d = txtorcon.build_tor_connection(ep)
|
||||
def _failed(f):
|
||||
# depending upon what's listening at that endpoint, we might get
|
||||
# various errors. If this list is too short, we might expose an
|
||||
# exception to the user (causing "tahoe create-node" to fail messily)
|
||||
# when we're supposed to just try the next potential port instead.
|
||||
# But I don't want to catch everything, because that may hide actual
|
||||
# coding errrors.
|
||||
f.trap(ConnectionRefusedError, # nothing listening on TCP
|
||||
ConnectError, # missing unix socket, or permission denied
|
||||
#ValueError,
|
||||
# connecting to e.g. an HTTP server causes an
|
||||
# UnhandledException (around a ValueError) when the handshake
|
||||
# fails to parse, but that's not something we can catch. The
|
||||
# attempt hangs, so don't do that.
|
||||
RuntimeError, # authentication failure
|
||||
)
|
||||
if stdout:
|
||||
stdout.write("Unable to reach Tor at '%s': %s\n" %
|
||||
(endpoint_desc, f.value))
|
||||
return None
|
||||
d.addErrback(_failed)
|
||||
return d
|
||||
|
||||
@inlineCallbacks
|
||||
def _launch_tor(reactor, tor_executable, private_dir, txtorcon):
|
||||
# TODO: handle default tor-executable
|
||||
# TODO: it might be a good idea to find exactly which Tor we used,
|
||||
# and record it's absolute path into tahoe.cfg . This would protect
|
||||
# us against one Tor being on $PATH at create-node time, but then a
|
||||
# different Tor being present at node startup. OTOH, maybe we don't
|
||||
# need to worry about it.
|
||||
tor_config = txtorcon.TorConfig()
|
||||
tor_config.DataDirectory = data_directory(private_dir)
|
||||
|
||||
if True: # unix-domain control socket
|
||||
tor_config.ControlPort = "unix:" + os.path.join(private_dir, "tor.control")
|
||||
tor_control_endpoint_desc = tor_config.ControlPort
|
||||
else:
|
||||
# we allocate a new TCP control port each time
|
||||
tor_config.ControlPort = allocate_tcp_port()
|
||||
tor_control_endpoint_desc = "tcp:127.0.0.1:%d" % tor_config.ControlPort
|
||||
|
||||
tor_config.SOCKSPort = allocate_tcp_port()
|
||||
|
||||
tpp = yield txtorcon.launch_tor(
|
||||
tor_config, reactor,
|
||||
tor_binary=tor_executable,
|
||||
# can be useful when debugging; mirror Tor's output to ours
|
||||
# stdout=sys.stdout,
|
||||
# stderr=sys.stderr,
|
||||
)
|
||||
|
||||
# now tor is launched and ready to be spoken to
|
||||
# as a side effect, we've got an ITorControlProtocol ready to go
|
||||
tor_control_proto = tpp.tor_protocol
|
||||
|
||||
# How/when to shut down the new process? for normal usage, the child
|
||||
# tor will exit when it notices its parent (us) quit. Unit tests will
|
||||
# mock out txtorcon.launch_tor(), so there will never be a real Tor
|
||||
# process. So I guess we don't need to track the process.
|
||||
|
||||
# If we do want to do anything with it, we can call tpp.quit()
|
||||
# (because it's a TorProcessProtocol) which returns a Deferred
|
||||
# that fires when Tor has actually exited.
|
||||
|
||||
returnValue((tor_control_endpoint_desc, tor_control_proto))
|
||||
|
||||
@inlineCallbacks
|
||||
def _connect_to_tor(reactor, cli_config, txtorcon):
|
||||
# we assume tor is already running
|
||||
ports_to_try = ["unix:/var/run/tor/control",
|
||||
"tcp:127.0.0.1:9051",
|
||||
"tcp:127.0.0.1:9151", # TorBrowserBundle
|
||||
]
|
||||
if cli_config["tor-control-port"]:
|
||||
ports_to_try = [cli_config["tor-control-port"]]
|
||||
for port in ports_to_try:
|
||||
tor_state = yield _try_to_connect(reactor, port, cli_config.stdout,
|
||||
txtorcon)
|
||||
if tor_state:
|
||||
tor_control_proto = tor_state.protocol
|
||||
returnValue((port, tor_control_proto)) ; break # helps editor
|
||||
else:
|
||||
raise ValueError("unable to reach any default Tor control port")
|
||||
|
||||
@inlineCallbacks
|
||||
def create_onion(reactor, cli_config):
|
||||
txtorcon = _import_txtorcon()
|
||||
if not txtorcon:
|
||||
raise ValueError("Cannot create onion without txtorcon. "
|
||||
"Please 'pip install tahoe-lafs[tor]' to fix this.")
|
||||
tahoe_config_tor = {} # written into tahoe.cfg:[tor]
|
||||
private_dir = os.path.abspath(os.path.join(cli_config["basedir"], "private"))
|
||||
stdout = cli_config.stdout
|
||||
if cli_config["tor-launch"]:
|
||||
tahoe_config_tor["launch"] = "true"
|
||||
tor_executable = cli_config["tor-executable"]
|
||||
if tor_executable:
|
||||
tahoe_config_tor["tor.executable"] = tor_executable
|
||||
print("launching Tor (to allocate .onion address)..", file=stdout)
|
||||
(_, tor_control_proto) = yield _launch_tor(
|
||||
reactor, tor_executable, private_dir, txtorcon)
|
||||
print("Tor launched", file=stdout)
|
||||
else:
|
||||
print("connecting to Tor (to allocate .onion address)..", file=stdout)
|
||||
(port, tor_control_proto) = yield _connect_to_tor(
|
||||
reactor, cli_config, txtorcon)
|
||||
print("Tor connection established", file=stdout)
|
||||
tahoe_config_tor["control.port"] = port
|
||||
|
||||
external_port = 3457 # TODO: pick this randomly? there's no contention.
|
||||
|
||||
local_port = allocate_tcp_port()
|
||||
ehs = txtorcon.EphemeralHiddenService(
|
||||
"%d 127.0.0.1:%d" % (external_port, local_port)
|
||||
)
|
||||
print("allocating .onion address (takes ~40s)..", file=stdout)
|
||||
yield ehs.add_to_tor(tor_control_proto)
|
||||
print(".onion address allocated", file=stdout)
|
||||
tor_port = "tcp:%d:interface=127.0.0.1" % local_port
|
||||
tor_location = "tor:%s:%d" % (ehs.hostname, external_port)
|
||||
privkey = ehs.private_key
|
||||
yield ehs.remove_from_tor(tor_control_proto)
|
||||
|
||||
# in addition to the "how to launch/connect-to tor" keys above, we also
|
||||
# record information about the onion service into tahoe.cfg.
|
||||
# * "local_port" is a server endpont string, which should match
|
||||
# "tor_port" (which will be added to tahoe.cfg [node] tub.port)
|
||||
# * "external_port" is the random "public onion port" (integer), which
|
||||
# (when combined with the .onion address) should match "tor_location"
|
||||
# (which will be added to tub.location)
|
||||
# * "private_key_file" points to the on-disk copy of the private key
|
||||
# material (although we always write it to the same place)
|
||||
|
||||
tahoe_config_tor["onion"] = "true"
|
||||
tahoe_config_tor["onion.local_port"] = str(local_port)
|
||||
tahoe_config_tor["onion.external_port"] = str(external_port)
|
||||
assert privkey
|
||||
tahoe_config_tor["onion.private_key_file"] = os.path.join("private",
|
||||
"tor_onion.privkey")
|
||||
privkeyfile = os.path.join(private_dir, "tor_onion.privkey")
|
||||
with open(privkeyfile, "wb") as f:
|
||||
f.write(privkey)
|
||||
|
||||
# tahoe_config_tor: this is a dictionary of keys/values to add to the
|
||||
# "[tor]" section of tahoe.cfg, which tells the new node how to launch
|
||||
# Tor in the right way.
|
||||
|
||||
# tor_port: a server endpoint string, it will be added to tub.port=
|
||||
|
||||
# tor_location: a foolscap connection hint, "tor:ONION:EXTERNAL_PORT"
|
||||
|
||||
# We assume/require that the Node gives us the same data_directory=
|
||||
# at both create-node and startup time. The data directory is not
|
||||
# recorded in tahoe.cfg
|
||||
|
||||
returnValue((tahoe_config_tor, tor_port, tor_location))
|
||||
|
||||
# we can always create a Provider. If foolscap.connections.tor or txtorcon
|
||||
# are not installed, then get_tor_handler() will return None. If tahoe.cfg
|
||||
# wants to start an onion service too, then check_onion_config() will throw a
|
||||
# nice error, and startService will throw an ugly error.
|
||||
|
||||
class Provider(service.MultiService):
|
||||
def __init__(self, basedir, node_for_config, reactor):
|
||||
service.MultiService.__init__(self)
|
||||
self._basedir = basedir
|
||||
self._node_for_config = node_for_config
|
||||
self._tor_launched = None
|
||||
self._onion_ehs = None
|
||||
self._onion_tor_control_proto = None
|
||||
self._tor = _import_tor()
|
||||
self._txtorcon = _import_txtorcon()
|
||||
self._reactor = reactor
|
||||
|
||||
def _get_tor_config(self, *args, **kwargs):
|
||||
return self._node_for_config.get_config("tor", *args, **kwargs)
|
||||
|
||||
def get_tor_handler(self):
|
||||
enabled = self._get_tor_config("enabled", True, boolean=True)
|
||||
if not enabled:
|
||||
return None
|
||||
if not self._tor:
|
||||
return None
|
||||
|
||||
if self._get_tor_config("launch", False, boolean=True):
|
||||
if not self._txtorcon:
|
||||
return None
|
||||
return self._tor.control_endpoint_maker(self._make_control_endpoint)
|
||||
|
||||
socks_endpoint_desc = self._get_tor_config("socks.port", None)
|
||||
if socks_endpoint_desc:
|
||||
socks_ep = clientFromString(self._reactor, socks_endpoint_desc)
|
||||
return self._tor.socks_endpoint(socks_ep)
|
||||
|
||||
controlport = self._get_tor_config("control.port", None)
|
||||
if controlport:
|
||||
ep = clientFromString(self._reactor, controlport)
|
||||
return self._tor.control_endpoint(ep)
|
||||
|
||||
return self._tor.default_socks()
|
||||
|
||||
@inlineCallbacks
|
||||
def _make_control_endpoint(self, reactor):
|
||||
# this will only be called when tahoe.cfg has "[tor] launch = true"
|
||||
(endpoint_desc, _) = yield self._get_launched_tor(reactor)
|
||||
tor_control_endpoint = clientFromString(reactor, endpoint_desc)
|
||||
returnValue(tor_control_endpoint)
|
||||
|
||||
def _get_launched_tor(self, reactor):
|
||||
# this fires with a tuple of (control_endpoint, tor_protocol)
|
||||
if not self._tor_launched:
|
||||
self._tor_launched = OneShotObserverList()
|
||||
private_dir = os.path.join(self._basedir, "private")
|
||||
tor_binary = self._get_tor_config("tor.executable", None)
|
||||
d = _launch_tor(reactor, tor_binary, private_dir, self._txtorcon)
|
||||
d.addBoth(self._tor_launched.fire)
|
||||
return self._tor_launched.when_fired()
|
||||
|
||||
def check_onion_config(self):
|
||||
if self._get_tor_config("onion", False, boolean=True):
|
||||
if not self._txtorcon:
|
||||
raise ValueError("Cannot create onion without txtorcon. "
|
||||
"Please 'pip install tahoe-lafs[tor]' to fix.")
|
||||
|
||||
# to start an onion server, we either need a Tor control port, or
|
||||
# we need to launch tor
|
||||
launch = self._get_tor_config("launch", False, boolean=True)
|
||||
controlport = self._get_tor_config("control.port", None)
|
||||
if not launch and not controlport:
|
||||
raise ValueError("[tor] onion = true, but we have neither "
|
||||
"launch=true nor control.port=")
|
||||
# check that all the expected onion-specific keys are present
|
||||
def require(name):
|
||||
if not self._get_tor_config("onion.%s" % name, None):
|
||||
raise ValueError("[tor] onion = true,"
|
||||
" but onion.%s= is missing" % name)
|
||||
require("local_port")
|
||||
require("external_port")
|
||||
require("private_key_file")
|
||||
|
||||
@inlineCallbacks
|
||||
def _start_onion(self, reactor):
|
||||
# launch tor, if necessary
|
||||
if self._get_tor_config("launch", False, boolean=True):
|
||||
(_, tor_control_proto) = yield self._get_launched_tor(reactor)
|
||||
else:
|
||||
controlport = self._get_tor_config("control.port", None)
|
||||
tcep = clientFromString(reactor, controlport)
|
||||
tor_state = yield self._txtorcon.build_tor_connection(tcep)
|
||||
tor_control_proto = tor_state.protocol
|
||||
|
||||
local_port = int(self._get_tor_config("onion.local_port"))
|
||||
external_port = int(self._get_tor_config("onion.external_port"))
|
||||
|
||||
fn = self._get_tor_config("onion.private_key_file")
|
||||
privkeyfile = os.path.join(self._basedir, fn)
|
||||
with open(privkeyfile, "rb") as f:
|
||||
privkey = f.read()
|
||||
ehs = self._txtorcon.EphemeralHiddenService(
|
||||
"%d 127.0.0.1:%d" % (external_port, local_port), privkey)
|
||||
yield ehs.add_to_tor(tor_control_proto)
|
||||
self._onion_ehs = ehs
|
||||
self._onion_tor_control_proto = tor_control_proto
|
||||
|
||||
|
||||
def startService(self):
|
||||
service.MultiService.startService(self)
|
||||
# if we need to start an onion service, now is the time
|
||||
if self._get_tor_config("onion", False, boolean=True):
|
||||
return self._start_onion(self._reactor) # so tests can synchronize
|
||||
|
||||
@inlineCallbacks
|
||||
def stopService(self):
|
||||
if self._onion_ehs and self._onion_tor_control_proto:
|
||||
yield self._onion_ehs.remove_from_tor(self._onion_tor_control_proto)
|
||||
# TODO: can we also stop tor?
|
||||
yield service.MultiService.stopService(self)
|
Loading…
x
Reference in New Issue
Block a user