rewrite tor support

This shifts most reponsibility to the new txtorcon "Controller" object, where
it belongs. We no longer need a list of likely control-port locations, nor do
we need to keep track of the SOCKS port ourselves.

The one downside is that if a control-port is not reachable, then this does
not fall back to using a plain SOCKS port (usually tcp:localhost:9050).
txtorcon no longer uses txsocksx, so it no longer advertises a simple way to
use Tor without the control port. This shouldn't affect users who run the
TorBrowserBundle, or who are running a tor daemon which they can control
directly, but it may break for users who want to use a pre-existing tor
daemon that they don't have permissions to speak control-port to.
This commit is contained in:
Brian Warner 2017-05-23 00:45:02 -07:00
parent 805e07cd97
commit 46a9c9eeb9
11 changed files with 194 additions and 552 deletions

View File

@ -3,7 +3,7 @@ import re
import six import six
from zope.interface import implementer from zope.interface import implementer
from attr import attrs, attrib from attr import attrs, attrib
from attr.validators import provides, instance_of from attr.validators import provides, instance_of, optional
from twisted.python import log from twisted.python import log
from automat import MethodicalMachine from automat import MethodicalMachine
from . import _interfaces from . import _interfaces
@ -35,7 +35,7 @@ class Boss(object):
_versions = attrib(validator=instance_of(dict)) _versions = attrib(validator=instance_of(dict))
_reactor = attrib() _reactor = attrib()
_journal = attrib(validator=provides(_interfaces.IJournal)) _journal = attrib(validator=provides(_interfaces.IJournal))
_tor_manager = attrib() # TODO: ITorManager or None _tor = attrib(validator=optional(provides(_interfaces.ITorManager)))
_timing = attrib(validator=provides(_interfaces.ITiming)) _timing = attrib(validator=provides(_interfaces.ITiming))
m = MethodicalMachine() m = MethodicalMachine()
set_trace = getattr(m, "_setTrace", lambda self, f: None) set_trace = getattr(m, "_setTrace", lambda self, f: None)
@ -53,7 +53,7 @@ class Boss(object):
self._R = Receive(self._side, self._timing) self._R = Receive(self._side, self._timing)
self._RC = RendezvousConnector(self._url, self._appid, self._side, self._RC = RendezvousConnector(self._url, self._appid, self._side,
self._reactor, self._journal, self._reactor, self._journal,
self._tor_manager, self._timing) self._tor, self._timing)
self._L = Lister(self._timing) self._L = Lister(self._timing)
self._A = Allocator(self._timing) self._A = Allocator(self._timing)
self._I = Input(self._timing) self._I = Input(self._timing)

View File

@ -2,7 +2,7 @@ from __future__ import print_function, absolute_import, unicode_literals
import os import os
from six.moves.urllib_parse import urlparse from six.moves.urllib_parse import urlparse
from attr import attrs, attrib from attr import attrs, attrib
from attr.validators import provides, instance_of from attr.validators import provides, instance_of, optional
from zope.interface import implementer from zope.interface import implementer
from twisted.python import log from twisted.python import log
from twisted.internet import defer, endpoints from twisted.internet import defer, endpoints
@ -65,7 +65,7 @@ class RendezvousConnector(object):
_side = attrib(validator=instance_of(type(u""))) _side = attrib(validator=instance_of(type(u"")))
_reactor = attrib() _reactor = attrib()
_journal = attrib(validator=provides(_interfaces.IJournal)) _journal = attrib(validator=provides(_interfaces.IJournal))
_tor_manager = attrib() # TODO: ITorManager or None _tor = attrib(validator=optional(provides(_interfaces.ITorManager)))
_timing = attrib(validator=provides(_interfaces.ITiming)) _timing = attrib(validator=provides(_interfaces.ITiming))
def __attrs_post_init__(self): def __attrs_post_init__(self):
@ -86,8 +86,9 @@ class RendezvousConnector(object):
self._trace(old_state="", input=what, new_state="") self._trace(old_state="", input=what, new_state="")
def _make_endpoint(self, hostname, port): def _make_endpoint(self, hostname, port):
if self._tor_manager: if self._tor:
return self._tor_manager.get_endpoint_for(hostname, port) # TODO: when we enable TLS, maybe add tls=True here
return self._tor.stream_via(hostname, port)
return endpoints.HostnameEndpoint(self._reactor, hostname, port) return endpoints.HostnameEndpoint(self._reactor, hostname, port)
def wire(self, boss, nameplate, mailbox, allocator, lister, terminator): def wire(self, boss, nameplate, mailbox, allocator, lister, terminator):

View File

@ -7,7 +7,7 @@ from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.python import log from twisted.python import log
from wormhole import create, input_with_completion, __version__ from wormhole import create, input_with_completion, __version__
from ..transit import TransitReceiver from ..transit import TransitReceiver
from ..errors import TransferError, WormholeClosedError, NoTorError from ..errors import TransferError, WormholeClosedError
from ..util import (dict_to_bytes, bytes_to_dict, bytes_to_hexstr, from ..util import (dict_to_bytes, bytes_to_dict, bytes_to_hexstr,
estimate_free_space) estimate_free_space)
from .welcome import handle_welcome from .welcome import handle_welcome
@ -45,7 +45,7 @@ class Receiver:
assert isinstance(args.relay_url, type(u"")) assert isinstance(args.relay_url, type(u""))
self.args = args self.args = args
self._reactor = reactor self._reactor = reactor
self._tor_manager = None self._tor = None
self._transit_receiver = None self._transit_receiver = None
def _msg(self, *args, **kwargs): def _msg(self, *args, **kwargs):
@ -55,29 +55,26 @@ class Receiver:
def go(self): def go(self):
if self.args.tor: if self.args.tor:
with self.args.timing.add("import", which="tor_manager"): with self.args.timing.add("import", which="tor_manager"):
from ..tor_manager import TorManager from ..tor_manager import get_tor
self._tor_manager = TorManager(self._reactor,
self.args.launch_tor,
self.args.tor_control_port,
timing=self.args.timing)
if not self._tor_manager.tor_available():
raise NoTorError()
# For now, block everything until Tor has started. Soon: launch # For now, block everything until Tor has started. Soon: launch
# tor in parallel with everything else, make sure the TorManager # tor in parallel with everything else, make sure the Tor object
# can lazy-provide an endpoint, and overlap the startup process # can lazy-provide an endpoint, and overlap the startup process
# with the user handing off the wormhole code # with the user handing off the wormhole code
yield self._tor_manager.start() self._tor = yield get_tor(self._reactor,
self.args.launch_tor,
self.args.tor_control_port,
timing=self.args.timing)
w = create(self.args.appid or APPID, self.args.relay_url, w = create(self.args.appid or APPID, self.args.relay_url,
self._reactor, self._reactor,
tor_manager=self._tor_manager, tor=self._tor,
timing=self.args.timing) timing=self.args.timing)
self._w = w # so tests can wait on events too self._w = w # so tests can wait on events too
# I wanted to do this instead: # I wanted to do this instead:
# #
# try: # try:
# yield self._go(w, tor_manager) # yield self._go(w, tor)
# finally: # finally:
# yield w.close() # yield w.close()
# #
@ -230,7 +227,7 @@ class Receiver:
def _build_transit(self, w, sender_transit): def _build_transit(self, w, sender_transit):
tr = TransitReceiver(self.args.transit_helper, tr = TransitReceiver(self.args.transit_helper,
no_listen=(not self.args.listen), no_listen=(not self.args.listen),
tor_manager=self._tor_manager, tor=self._tor,
reactor=self._reactor, reactor=self._reactor,
timing=self.args.timing) timing=self.args.timing)
self._transit_receiver = tr self._transit_receiver = tr

View File

@ -6,8 +6,7 @@ from twisted.python import log
from twisted.protocols import basic from twisted.protocols import basic
from twisted.internet import reactor from twisted.internet import reactor
from twisted.internet.defer import inlineCallbacks, returnValue from twisted.internet.defer import inlineCallbacks, returnValue
from ..errors import (TransferError, WormholeClosedError, NoTorError, from ..errors import (TransferError, WormholeClosedError, UnsendableFileError)
UnsendableFileError)
from wormhole import create, __version__ from wormhole import create, __version__
from ..transit import TransitSender from ..transit import TransitSender
from ..util import dict_to_bytes, bytes_to_dict, bytes_to_hexstr from ..util import dict_to_bytes, bytes_to_dict, bytes_to_hexstr
@ -31,7 +30,7 @@ class Sender:
def __init__(self, args, reactor): def __init__(self, args, reactor):
self._args = args self._args = args
self._reactor = reactor self._reactor = reactor
self._tor_manager = None self._tor = None
self._timing = args.timing self._timing = args.timing
self._fd_to_send = None self._fd_to_send = None
self._transit_sender = None self._transit_sender = None
@ -41,22 +40,19 @@ class Sender:
assert isinstance(self._args.relay_url, type(u"")) assert isinstance(self._args.relay_url, type(u""))
if self._args.tor: if self._args.tor:
with self._timing.add("import", which="tor_manager"): with self._timing.add("import", which="tor_manager"):
from ..tor_manager import TorManager from ..tor_manager import get_tor
self._tor_manager = TorManager(reactor,
self._args.launch_tor,
self._args.tor_control_port,
timing=self._timing)
if not self._tor_manager.tor_available():
raise NoTorError()
# For now, block everything until Tor has started. Soon: launch # For now, block everything until Tor has started. Soon: launch
# tor in parallel with everything else, make sure the TorManager # tor in parallel with everything else, make sure the Tor object
# can lazy-provide an endpoint, and overlap the startup process # can lazy-provide an endpoint, and overlap the startup process
# with the user handing off the wormhole code # with the user handing off the wormhole code
yield self._tor_manager.start() self._tor = yield get_tor(reactor,
self._args.launch_tor,
self._args.tor_control_port,
timing=self._timing)
w = create(self._args.appid or APPID, self._args.relay_url, w = create(self._args.appid or APPID, self._args.relay_url,
self._reactor, self._reactor,
tor_manager=self._tor_manager, tor=self._tor,
timing=self._timing) timing=self._timing)
d = self._go(w) d = self._go(w)
@ -151,7 +147,7 @@ class Sender:
if self._fd_to_send: if self._fd_to_send:
ts = TransitSender(args.transit_helper, ts = TransitSender(args.transit_helper,
no_listen=(not args.listen), no_listen=(not args.listen),
tor_manager=self._tor_manager, tor=self._tor,
reactor=self._reactor, reactor=self._reactor,
timing=self._timing) timing=self._timing)
self._transit_sender = ts self._transit_sender = ts

View File

@ -4,9 +4,10 @@ from textwrap import fill, dedent
from humanize import naturalsize from humanize import naturalsize
import mock import mock
import click.testing import click.testing
from zope.interface import implementer
from twisted.trial import unittest from twisted.trial import unittest
from twisted.python import procutils, log from twisted.python import procutils, log
from twisted.internet import defer, endpoints, reactor from twisted.internet import endpoints, reactor
from twisted.internet.utils import getProcessOutputAndValue from twisted.internet.utils import getProcessOutputAndValue
from twisted.internet.defer import gatherResults, inlineCallbacks, returnValue from twisted.internet.defer import gatherResults, inlineCallbacks, returnValue
from .. import __version__ from .. import __version__
@ -14,6 +15,7 @@ from .common import ServerBase, config
from ..cli import cmd_send, cmd_receive, welcome, cli from ..cli import cmd_send, cmd_receive, welcome, cli
from ..errors import (TransferError, WrongPasswordError, WelcomeError, from ..errors import (TransferError, WrongPasswordError, WelcomeError,
UnsendableFileError) UnsendableFileError)
from .._interfaces import ITorManager
from wormhole.server.cmd_server import MyPlugin from wormhole.server.cmd_server import MyPlugin
from wormhole.server.cli import server from wormhole.server.cli import server
@ -297,15 +299,12 @@ class ScriptVersion(ServerBase, ScriptsBase, unittest.TestCase):
self.failUnlessEqual(ver.strip(), "magic-wormhole {}".format(__version__)) self.failUnlessEqual(ver.strip(), "magic-wormhole {}".format(__version__))
self.failUnlessEqual(rc, 0) self.failUnlessEqual(rc, 0)
class FakeTorManager: @implementer(ITorManager)
class FakeTor:
# use normal endpoints, but record the fact that we were asked # use normal endpoints, but record the fact that we were asked
def __init__(self): def __init__(self):
self.endpoints = [] self.endpoints = []
def tor_available(self): def stream_via(self, host, port):
return True
def start(self):
return defer.succeed(None)
def get_endpoint_for(self, host, port):
self.endpoints.append((host, port)) self.endpoints.append((host, port))
return endpoints.HostnameEndpoint(reactor, host, port) return endpoints.HostnameEndpoint(reactor, host, port)
@ -456,16 +455,16 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase):
if fake_tor: if fake_tor:
send_cfg.tor = True send_cfg.tor = True
send_cfg.transit_helper = self.transit send_cfg.transit_helper = self.transit
tx_tm = FakeTorManager() tx_tm = FakeTor()
with mock.patch("wormhole.tor_manager.TorManager", with mock.patch("wormhole.tor_manager.get_tor",
return_value=tx_tm, return_value=tx_tm,
) as mtx_tm: ) as mtx_tm:
send_d = cmd_send.send(send_cfg) send_d = cmd_send.send(send_cfg)
recv_cfg.tor = True recv_cfg.tor = True
recv_cfg.transit_helper = self.transit recv_cfg.transit_helper = self.transit
rx_tm = FakeTorManager() rx_tm = FakeTor()
with mock.patch("wormhole.tor_manager.TorManager", with mock.patch("wormhole.tor_manager.get_tor",
return_value=rx_tm, return_value=rx_tm,
) as mrx_tm: ) as mrx_tm:
receive_d = cmd_receive.receive(recv_cfg) receive_d = cmd_receive.receive(recv_cfg)

View File

@ -3,335 +3,78 @@ import mock, io
from twisted.trial import unittest from twisted.trial import unittest
from twisted.internet import defer from twisted.internet import defer
from twisted.internet.error import ConnectError from twisted.internet.error import ConnectError
from six import next
from ..tor_manager import TorManager, DEFAULT_VALUE from ..tor_manager import get_tor
from ..errors import NoTorError
from .._interfaces import ITorManager
class X():
pass
class Tor(unittest.TestCase): class Tor(unittest.TestCase):
def test_create(self): def test_no_txtorcon(self):
tm = TorManager(None) with mock.patch("wormhole.tor_manager.txtorcon", None):
del tm self.failureResultOf(get_tor(None), NoTorError)
def test_bad_args(self): def test_bad_args(self):
e = self.assertRaises(TypeError, f = self.failureResultOf(get_tor(None, launch_tor="not boolean"),
TorManager, None, launch_tor="not boolean") TypeError)
self.assertEqual(str(e), "launch_tor= must be boolean") self.assertEqual(str(f.value), "launch_tor= must be boolean")
e = self.assertRaises(TypeError,
TorManager, None, tor_control_port=1234) f = self.failureResultOf(get_tor(None, tor_control_port=1234),
self.assertEqual(str(e), "tor_control_port= must be str or None") TypeError)
e = self.assertRaises(ValueError, self.assertEqual(str(f.value), "tor_control_port= must be str or None")
TorManager, None, launch_tor=True, f = self.failureResultOf(get_tor(None, launch_tor=True,
tor_control_port="tcp:127.0.0.1:1234") tor_control_port="tcp:127.0.0.1:1234"),
self.assertEqual(str(e), ValueError)
self.assertEqual(str(f.value),
"cannot combine --launch-tor and --tor-control-port=") "cannot combine --launch-tor and --tor-control-port=")
def test_start_launch_tor(self):
reactor = object()
stderr = io.StringIO()
tm = TorManager(reactor, launch_tor=True, stderr=stderr)
dlt_d = defer.Deferred()
tm._do_launch_tor = mock.Mock(return_value=dlt_d)
tm._try_control_port = mock.Mock()
d = tm.start()
self.assertNoResult(d)
tsep = object()
with mock.patch("wormhole.tor_manager.clientFromString",
return_value=tsep) as cfs:
dlt_d.callback(("tproto", "tconfig", "socks_desc"))
res = self.successResultOf(d)
self.assertEqual(res, None)
self.assertEqual(tm._tor_protocol, "tproto")
self.assertEqual(tm._tor_config, "tconfig")
self.assertEqual(tm._tor_socks_endpoint, tsep)
self.assertEqual(tm._do_launch_tor.mock_calls, [mock.call()])
self.assertEqual(tm._try_control_port.mock_calls, [])
self.assertEqual(cfs.mock_calls, [mock.call(reactor, "socks_desc")])
def test_start_control_port_default_failure(self):
reactor = object()
stderr = io.StringIO()
tm = TorManager(reactor, stderr=stderr)
tm._do_launch_tor = mock.Mock()
tcp_ds = [defer.Deferred() for i in range(5)]
tcp_ds_iter = iter(tcp_ds)
attempted_control_ports = []
def next_d(control_port):
attempted_control_ports.append(control_port)
return next(tcp_ds_iter)
tm._try_control_port = mock.Mock(side_effect=next_d)
d = tm.start()
tsep = object()
with mock.patch("wormhole.tor_manager.clientFromString",
return_value=tsep) as cfs:
self.assertNoResult(d)
self.assertEqual(attempted_control_ports,
["unix:/var/run/tor/control"])
self.assertEqual(tm._try_control_port.mock_calls,
[mock.call("unix:/var/run/tor/control")])
tcp_ds[0].callback((None, None, None))
self.assertNoResult(d)
self.assertEqual(attempted_control_ports,
["unix:/var/run/tor/control",
"tcp:127.0.0.1:9051",
])
self.assertEqual(tm._try_control_port.mock_calls,
[mock.call("unix:/var/run/tor/control"),
mock.call("tcp:127.0.0.1:9051"),
])
tcp_ds[1].callback((None, None, None))
self.assertNoResult(d)
self.assertEqual(attempted_control_ports,
["unix:/var/run/tor/control",
"tcp:127.0.0.1:9051",
"tcp:127.0.0.1:9151",
])
self.assertEqual(tm._try_control_port.mock_calls,
[mock.call("unix:/var/run/tor/control"),
mock.call("tcp:127.0.0.1:9051"),
mock.call("tcp:127.0.0.1:9151"),
])
tcp_ds[2].callback((None, None, None))
res = self.successResultOf(d)
self.assertEqual(res, None)
self.assertEqual(tm._tor_protocol, None)
self.assertEqual(tm._tor_config, None)
self.assertEqual(tm._tor_socks_endpoint, tsep)
self.assertEqual(tm._do_launch_tor.mock_calls, [])
self.assertEqual(cfs.mock_calls,
[mock.call(reactor, "tcp:127.0.0.1:9050")])
def test_start_control_port_default(self):
reactor = object()
stderr = io.StringIO()
tm = TorManager(reactor, stderr=stderr)
tm._do_launch_tor = mock.Mock()
tcp_d = defer.Deferred()
# let it succeed on the first try
tm._try_control_port = mock.Mock(return_value=tcp_d)
d = tm.start()
self.assertNoResult(d)
tsep = object()
with mock.patch("wormhole.tor_manager.clientFromString",
return_value=tsep) as cfs:
tcp_d.callback(("tproto", "tconfig", "socks_desc"))
res = self.successResultOf(d)
self.assertEqual(res, None)
self.assertEqual(tm._tor_protocol, "tproto")
self.assertEqual(tm._tor_config, "tconfig")
self.assertEqual(tm._tor_socks_endpoint, tsep)
self.assertEqual(tm._do_launch_tor.mock_calls, [])
self.assertEqual(tm._try_control_port.mock_calls,
[mock.call("unix:/var/run/tor/control")])
self.assertEqual(cfs.mock_calls, [mock.call(reactor, "socks_desc")])
def test_start_control_port_non_default_failure(self):
reactor = object()
my_port = "my_port"
stderr = io.StringIO()
tm = TorManager(reactor, tor_control_port=my_port, stderr=stderr)
tm._do_launch_tor = mock.Mock()
tcp_ds = [defer.Deferred() for i in range(5)]
tcp_ds_iter = iter(tcp_ds)
attempted_control_ports = []
def next_d(control_port):
attempted_control_ports.append(control_port)
return next(tcp_ds_iter)
tm._try_control_port = mock.Mock(side_effect=next_d)
d = tm.start()
tsep = object()
with mock.patch("wormhole.tor_manager.clientFromString",
return_value=tsep) as cfs:
self.assertNoResult(d)
self.assertEqual(attempted_control_ports, [my_port])
self.assertEqual(tm._try_control_port.mock_calls,
[mock.call(my_port)])
tcp_ds[0].callback((None, None, None))
res = self.successResultOf(d)
self.assertEqual(res, None)
self.assertEqual(tm._tor_protocol, None)
self.assertEqual(tm._tor_config, None)
self.assertEqual(tm._tor_socks_endpoint, tsep)
self.assertEqual(tm._do_launch_tor.mock_calls, [])
self.assertEqual(cfs.mock_calls,
[mock.call(reactor, "tcp:127.0.0.1:9050")])
def test_start_control_port_non_default(self):
reactor = object()
my_port = "my_port"
stderr = io.StringIO()
tm = TorManager(reactor, tor_control_port=my_port, stderr=stderr)
tm._do_launch_tor = mock.Mock()
tcp_d = defer.Deferred()
tm._try_control_port = mock.Mock(return_value=tcp_d)
d = tm.start()
self.assertNoResult(d)
tsep = object()
with mock.patch("wormhole.tor_manager.clientFromString",
return_value=tsep) as cfs:
tcp_d.callback(("tproto", "tconfig", "socks_desc"))
res = self.successResultOf(d)
self.assertEqual(res, None)
self.assertEqual(tm._tor_protocol, "tproto")
self.assertEqual(tm._tor_config, "tconfig")
self.assertEqual(tm._tor_socks_endpoint, tsep)
self.assertEqual(tm._do_launch_tor.mock_calls, [])
self.assertEqual(tm._try_control_port.mock_calls,
[mock.call(my_port)])
self.assertEqual(cfs.mock_calls, [mock.call(reactor, "socks_desc")])
def test_launch(self): def test_launch(self):
reactor = object() reactor = object()
my_tor = X() # object() didn't like providedBy()
launch_d = defer.Deferred()
stderr = io.StringIO() stderr = io.StringIO()
tc = mock.Mock() with mock.patch("wormhole.tor_manager.txtorcon.launch",
mock_TorConfig = mock.patch("wormhole.tor_manager.TorConfig", side_effect=launch_d) as launch:
return_value=tc) d = get_tor(reactor, launch_tor=True, stderr=stderr)
lt_d = defer.Deferred() self.assertNoResult(d)
mock_launch_tor = mock.patch("wormhole.tor_manager.launch_tor", self.assertEqual(launch.mock_calls, [mock.call(reactor)])
return_value=lt_d) launch_d.callback(my_tor)
mock_allocate_tcp_port = mock.patch("wormhole.tor_manager.allocate_tcp_port", tor = self.successResultOf(d)
return_value=12345) self.assertIs(tor, my_tor)
mock_clientFromString = mock.patch("wormhole.tor_manager.clientFromString") self.assert_(ITorManager.providedBy(tor))
with mock_TorConfig as mtc: self.assertEqual(stderr.getvalue(),
with mock_launch_tor as mlt: " launching a new Tor process, this may take a while..\n")
with mock_allocate_tcp_port as matp:
with mock_clientFromString as mcfs:
tm = TorManager(reactor, launch_tor=True, stderr=stderr)
d = tm.start()
self.assertNoResult(d)
tp = mock.Mock()
lt_d.callback(tp)
res = self.successResultOf(d)
self.assertEqual(res, None)
self.assertIs(tm._tor_protocol, tp)
self.assertIs(tm._tor_config, tc)
self.assertEqual(mtc.mock_calls, [mock.call()])
self.assertEqual(mlt.mock_calls, [mock.call(tc, reactor)])
self.assertEqual(matp.mock_calls, [mock.call()])
self.assertEqual(mcfs.mock_calls,
[mock.call(reactor, "tcp:127.0.0.1:12345")])
def _do_test_try_control_port(self, socks_ports, exp_socks_desc, def test_connect(self):
btc_exception=None, tcfp_exception=None):
reactor = object() reactor = object()
my_tor = X() # object() didn't like providedBy()
tcp = "port"
connect_d = defer.Deferred()
stderr = io.StringIO() stderr = io.StringIO()
ep = object() with mock.patch("wormhole.tor_manager.txtorcon.connect",
mock_clientFromString = mock.patch("wormhole.tor_manager.clientFromString", side_effect=connect_d) as connect:
return_value=ep) d = get_tor(reactor, tor_control_port=tcp, stderr=stderr)
tproto = mock.Mock() self.assertNoResult(d)
btc_d = defer.Deferred() self.assertEqual(connect.mock_calls, [mock.call(reactor, tcp)])
mock_build_tor_connection = mock.patch("wormhole.tor_manager.build_tor_connection", return_value=btc_d) connect_d.callback(my_tor)
torconfig = mock.Mock() tor = self.successResultOf(d)
tc = mock.Mock() self.assertIs(tor, my_tor)
tc.SocksPort = iter(socks_ports) self.assert_(ITorManager.providedBy(tor))
tc_d = defer.Deferred() self.assertEqual(stderr.getvalue(), " using Tor\n")
torconfig.from_protocol = mock.Mock(return_value=tc_d)
mock_torconfig = mock.patch("wormhole.tor_manager.TorConfig", torconfig)
control_port = object() def test_connect_fails(self):
with mock_clientFromString as cfs:
with mock_build_tor_connection as btc:
with mock_torconfig:
tm = TorManager(reactor, stderr=stderr)
d = tm._try_control_port(control_port)
# waiting in 'tproto = yield build_tor_connection(..)'
self.assertNoResult(d)
self.assertEqual(cfs.mock_calls,
[mock.call(reactor, control_port)])
self.assertEqual(btc.mock_calls,
[mock.call(ep, build_state=False)])
self.assertEqual(torconfig.from_protocol.mock_calls, [])
btc_d.callback(tproto)
# waiting in 'tconfig = yield TorConfig.from_protocol(..)'
self.assertNoResult(d)
self.assertEqual(torconfig.from_protocol.mock_calls,
[mock.call(tproto)])
tc_d.callback(tc)
res = self.successResultOf(d)
self.assertEqual(res, (tproto, tc, exp_socks_desc))
def test_try_control_port(self):
self._do_test_try_control_port(["1234 ignorestuff",
"unix:/foo WorldWritable"],
"tcp:127.0.0.1:1234")
self._do_test_try_control_port(["unix:/foo WorldWritable",
"1234 ignorestuff"],
"unix:/foo")
self._do_test_try_control_port([DEFAULT_VALUE,
"1234"],
"tcp:127.0.0.1:9050")
def _do_test_try_control_port_exception(self, btc_exc=None, tcfp_exc=None):
reactor = object() reactor = object()
tcp = "port"
connect_d = defer.Deferred()
stderr = io.StringIO() stderr = io.StringIO()
ep = object() with mock.patch("wormhole.tor_manager.txtorcon.connect",
mock_clientFromString = mock.patch("wormhole.tor_manager.clientFromString", side_effect=connect_d) as connect:
return_value=ep) d = get_tor(reactor, tor_control_port=tcp, stderr=stderr)
tproto = mock.Mock() self.assertNoResult(d)
btc_d = defer.Deferred() self.assertEqual(connect.mock_calls, [mock.call(reactor, tcp)])
mock_build_tor_connection = mock.patch("wormhole.tor_manager.build_tor_connection", return_value=btc_d) connect_d.errback(ConnectError())
torconfig = mock.Mock() self.failureResultOf(d, ConnectError)
tcfp_d = defer.Deferred() self.assertEqual(stderr.getvalue(),
torconfig.from_protocol = mock.Mock(return_value=tcfp_d) " unable to find control port, bailing\n")
mock_torconfig = mock.patch("wormhole.tor_manager.TorConfig", torconfig)
control_port = object()
with mock_clientFromString:
with mock_build_tor_connection:
with mock_torconfig:
tm = TorManager(reactor, stderr=stderr)
d = tm._try_control_port(control_port)
# waiting in 'tproto = yield build_tor_connection(..)'
self.assertNoResult(d)
if btc_exc:
btc_d.errback(btc_exc)
else:
btc_d.callback(tproto)
assert tcfp_exc
tcfp_d.errback(tcfp_exc)
res = self.successResultOf(d)
self.assertEqual(res, (None, None, None))
def test_try_control_port_error(self):
self._do_test_try_control_port_exception(btc_exc=ValueError())
self._do_test_try_control_port_exception(btc_exc=ConnectError())
self._do_test_try_control_port_exception(tcfp_exc=ValueError())
self._do_test_try_control_port_exception(tcfp_exc=ConnectError())
def test_badaddr(self):
tm = TorManager(None)
isnon = tm.is_non_public_numeric_address
self.assertTrue(isnon("10.0.0.1"))
self.assertTrue(isnon("127.0.0.1"))
self.assertTrue(isnon("192.168.78.254"))
self.assertTrue(isnon("::1"))
self.assertFalse(isnon("8.8.8.8"))
self.assertFalse(isnon("example.org"))
def test_endpoint(self):
reactor = object()
stderr = io.StringIO()
tm = TorManager(reactor, stderr=stderr)
tm._tor_socks_endpoint = tse = object()
exp_ep = object()
with mock.patch("wormhole.tor_manager.TorClientEndpoint",
return_value=exp_ep) as tce:
ep = tm.get_endpoint_for("example.com", 1234)
self.assertIs(ep, exp_ep)
self.assertEqual(tce.mock_calls,
[mock.call(b"example.com", 1234,
socks_endpoint=tse)])
with mock.patch("wormhole.tor_manager.TorClientEndpoint",
return_value=exp_ep) as tce:
ep = tm.get_endpoint_for("127.0.0.1", 1234)
self.assertEqual(ep, None)
self.assertEqual(tce.mock_calls, [])

View File

@ -149,14 +149,14 @@ class Hints(unittest.TestCase):
self.assertIsInstance(efho(transit.DirectTCPV1Hint("host", 1234, 0.0)), self.assertIsInstance(efho(transit.DirectTCPV1Hint("host", 1234, 0.0)),
endpoints.HostnameEndpoint) endpoints.HostnameEndpoint)
self.assertEqual(efho("unknown:stuff:yowza:pivlor"), None) self.assertEqual(efho("unknown:stuff:yowza:pivlor"), None)
# c._tor_manager is currently None # c._tor is currently None
self.assertEqual(efho(transit.TorTCPV1Hint("host", "port", 0)), None) self.assertEqual(efho(transit.TorTCPV1Hint("host", "port", 0)), None)
c._tor_manager = mock.Mock() c._tor = mock.Mock()
def tor_ep(hostname, port): def tor_ep(hostname, port):
if hostname == "non-public": if hostname == "non-public":
return None return None
return ("tor_ep", hostname, port) return ("tor_ep", hostname, port)
c._tor_manager.get_endpoint_for = mock.Mock(side_effect=tor_ep) c._tor.stream_via = mock.Mock(side_effect=tor_ep)
self.assertEqual(efho(transit.DirectTCPV1Hint("host", 1234, 0.0)), self.assertEqual(efho(transit.DirectTCPV1Hint("host", 1234, 0.0)),
("tor_ep", "host", 1234)) ("tor_ep", "host", 1234))
self.assertEqual(efho(transit.TorTCPV1Hint("host2.onion", 1234, 0.0)), self.assertEqual(efho(transit.TorTCPV1Hint("host2.onion", 1234, 0.0)),
@ -1470,7 +1470,7 @@ class Transit(unittest.TestCase):
@inlineCallbacks @inlineCallbacks
def test_success_direct_tor(self): def test_success_direct_tor(self):
clock = task.Clock() clock = task.Clock()
s = transit.TransitSender("", tor_manager=mock.Mock(), reactor=clock) s = transit.TransitSender("", tor=mock.Mock(), reactor=clock)
s.set_transit_key(b"key") s.set_transit_key(b"key")
hints = yield s.get_connection_hints() # start the listener hints = yield s.get_connection_hints() # start the listener
del hints del hints
@ -1491,7 +1491,7 @@ class Transit(unittest.TestCase):
@inlineCallbacks @inlineCallbacks
def test_success_direct_tor_relay(self): def test_success_direct_tor_relay(self):
clock = task.Clock() clock = task.Clock()
s = transit.TransitSender("", tor_manager=mock.Mock(), reactor=clock) s = transit.TransitSender("", tor=mock.Mock(), reactor=clock)
s.set_transit_key(b"key") s.set_transit_key(b"key")
hints = yield s.get_connection_hints() # start the listener hints = yield s.get_connection_hints() # start the listener
del hints del hints

View File

@ -1,171 +1,85 @@
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import sys, re import sys
import six from zope.interface.declarations import directlyProvides
from zope.interface import implementer
from twisted.internet.defer import inlineCallbacks, returnValue from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.internet.error import ConnectError
from twisted.internet.endpoints import clientFromString
try: try:
from txtorcon import (TorConfig, launch_tor, build_tor_connection, import txtorcon
DEFAULT_VALUE, TorClientEndpoint)
except ImportError: except ImportError:
TorConfig = None txtorcon = None
launch_tor = None from . import _interfaces, errors
build_tor_connection = None
TorClientEndpoint = None
DEFAULT_VALUE = "DEFAULT_VALUE"
import ipaddress
from . import _interfaces
from .timing import DebugTiming from .timing import DebugTiming
from .transit import allocate_tcp_port
@inlineCallbacks
def get_tor(reactor, launch_tor=False, tor_control_port=None,
timing=None, stderr=sys.stderr):
"""
If launch_tor=True, I will try to launch a new Tor process, ask it
for its SOCKS and control ports, and use those for outbound
connections (and inbound onion-service listeners, if necessary).
@implementer(_interfaces.ITorManager) Otherwise if tor_control_port is provided, I will attempt to connect
class TorManager: to an existing Tor's control port at the endpoint it specifies. I'll
def __init__(self, reactor, launch_tor=False, tor_control_port=None, ask that Tor for its SOCKS port.
timing=None, stderr=sys.stderr):
"""
If launch_tor=True, I will try to launch a new Tor process, ask it
for its SOCKS and control ports, and use those for outbound
connections (and inbound onion-service listeners, if necessary).
Otherwise if tor_control_port is provided, I will attempt to connect With no arguments, I will try to connect to an existing Tor's control
to an existing Tor's control port at the endpoint it specifies. I'll port at the usual places: [unix:/var/run/tor/control,
ask that Tor for its SOCKS port. tcp:127.0.0.1:9051, tcp:127.0.0.1:9151]. If any are successful, I'll
ask that Tor for its SOCKS port. If none are successful, I'll attempt
to do SOCKS to tcp:127.0.0.1:9050.
With no arguments, I will try to connect to an existing Tor's control If I am unable to make a SOCKS connection, the initial connection to
port at the usual places: [unix:/var/run/tor/control, the Rendezvous Server will fail, and the program will terminate.
tcp:127.0.0.1:9051, tcp:127.0.0.1:9151]. If any are successful, I'll
ask that Tor for its SOCKS port. If none are successful, I'll attempt
to do SOCKS to tcp:127.0.0.1:9050.
If I am unable to make a SOCKS connection, the initial connection to Control-port connections can only succeed if I can authenticate (by
the Rendezvous Server will fail, and the program will terminate. reading a cookie file named by the Tor process), so the current user
must have permission to read that file (either they started Tor, e.g.
TorBrowser, or they are in a unix group that's been given access,
e.g. debian-tor).
"""
# rationale: launching a new Tor takes a long time, so only do it if
# the user specifically asks for it with --launch-tor. Using an
# existing Tor should be much faster, but still requires general
# permission via --tor.
Control-port connections can only succeed if I can authenticate (by if not txtorcon:
reading a cookie file named by the Tor process), so the current user raise errors.NoTorError()
must have permission to read that file (either they started Tor, e.g.
TorBrowser, or they are in a unix group that's been given access,
e.g. debian-tor).
"""
# rationale: launching a new Tor takes a long time, so only do it if
# the user specifically asks for it with --launch-tor. Using an
# existing Tor should be much faster, but still requires general
# permission via --tor.
self._reactor = reactor if not isinstance(launch_tor, bool): # note: False is int
if not isinstance(launch_tor, bool): # note: False is int raise TypeError("launch_tor= must be boolean")
raise TypeError("launch_tor= must be boolean") if not isinstance(tor_control_port, (type(""), type(None))):
if not isinstance(tor_control_port, (type(""), type(None))): raise TypeError("tor_control_port= must be str or None")
raise TypeError("tor_control_port= must be str or None") assert tor_control_port != ""
if launch_tor and tor_control_port is not None: if launch_tor and tor_control_port is not None:
raise ValueError("cannot combine --launch-tor and --tor-control-port=") raise ValueError("cannot combine --launch-tor and --tor-control-port=")
self._launch_tor = launch_tor timing = timing or DebugTiming()
self._tor_control_port = tor_control_port
self._timing = timing or DebugTiming()
self._stderr = stderr
def tor_available(self): # Connect to an existing Tor, or create a new one. If we need to
# unit tests mock out everything we get from txtorcon, so we can test # launch an onion service, then we need a working control port (and
# this class under py3 even if txtorcon isn't installed. But the real # authentication cookie). If we're only acting as a client, we don't
# commands need to know if they have Tor or not. # need the control port.
return bool(TorConfig)
@inlineCallbacks if launch_tor:
def start(self): print(" launching a new Tor process, this may take a while..",
# Connect to an existing Tor, or create a new one. If we need to file=stderr)
# launch an onion service, then we need a working control port (and with timing.add("launch tor"):
# authentication cookie). If we're only acting as a client, we don't tor = yield txtorcon.launch(reactor,
# need the control port. #data_directory=,
#tor_binary=,
if self._launch_tor: )
print(" launching a new Tor process, this may take a while..", else:
file=self._stderr) with timing.add("find tor"):
with self._timing.add("launch tor"): try:
(tproto, tconfig, socks_desc) = yield self._do_launch_tor() tor = yield txtorcon.connect(reactor, tor_control_port)
else: print(" using Tor", file=stderr)
control_ports = ["unix:/var/run/tor/control", # debian tor package except Exception:
"tcp:127.0.0.1:9051", # standard Tor #socks_desc = "tcp:127.0.0.1:9050" # fallback
"tcp:127.0.0.1:9151", # TorBrowser #print(" using Tor (SOCKS port %s)" % socks_desc,
] # file=stderr)
if self._tor_control_port: print(" unable to find control port, bailing",
control_ports = [self._tor_control_port] file=stderr)
with self._timing.add("find tor"): # TODO: something nicer. I think connect() is likely to throw
for control_port in control_ports: # a reactor.connectTCP -type error, like ConnectionFailed or
(tproto, tconfig, # ConnectionRefused or something
socks_desc) = yield self._try_control_port(control_port) raise
if tproto: directlyProvides(tor, _interfaces.ITorManager)
print(" using Tor (control port %s) (SOCKS port %s)" returnValue(tor)
% (control_port, socks_desc),
file=self._stderr)
break
else:
tproto = None
tconfig = None
socks_desc = "tcp:127.0.0.1:9050" # fallback
print(" using Tor (SOCKS port %s)" % socks_desc,
file=self._stderr)
self._tor_protocol = tproto
self._tor_config = tconfig
self._tor_socks_endpoint = clientFromString(self._reactor, socks_desc)
@inlineCallbacks
def _do_launch_tor(self):
tconfig = TorConfig()
#tconfig.ControlPort = allocate_tcp_port() # defaults to 9052
tconfig.SocksPort = allocate_tcp_port()
socks_desc = "tcp:127.0.0.1:%d" % tconfig.SocksPort
# this could take tor_binary=
tproto = yield launch_tor(tconfig, self._reactor)
returnValue((tproto, tconfig, socks_desc))
@inlineCallbacks
def _try_control_port(self, control_port):
NOPE = (None, None, None)
ep = clientFromString(self._reactor, control_port)
try:
tproto = yield build_tor_connection(ep, build_state=False)
# now wait for bootstrap
tconfig = yield TorConfig.from_protocol(tproto)
except (ValueError, ConnectError):
returnValue(NOPE)
socks_ports = list(tconfig.SocksPort)
socks_port = socks_ports[0] # TODO: when might there be multiple?
# I've seen "9050", and "unix:/var/run/tor/socks WorldWritable"
pieces = socks_port.split()
p = pieces[0]
if p == DEFAULT_VALUE:
socks_desc = "tcp:127.0.0.1:9050"
elif re.search('^\d+$', p):
socks_desc = "tcp:127.0.0.1:%s" % p
else:
socks_desc = p
returnValue((tproto, tconfig, socks_desc))
def is_non_public_numeric_address(self, host):
# for numeric hostnames, skip RFC1918 addresses, since no Tor exit
# node will be able to reach those. Likewise ignore IPv6 addresses.
try:
a = ipaddress.ip_address(host)
except ValueError:
return False # non-numeric, let Tor try it
if a.version != 4:
return True # IPv6 gets ignored
if (a.is_loopback or a.is_multicast or a.is_private or a.is_reserved
or a.is_unspecified):
return True # too weird, don't connect
return False
def get_endpoint_for(self, host, port):
assert isinstance(port, six.integer_types)
if self.is_non_public_numeric_address(host):
return None
# txsocksx doesn't like unicode: it concatenates some binary protocol
# bytes with the hostname when talking to the SOCKS server, so the
# py2 automatic unicode promotion blows up
host = host.encode("ascii")
ep = TorClientEndpoint(host, port,
socks_endpoint=self._tor_socks_endpoint)
return ep

View File

@ -589,7 +589,7 @@ class Common:
RELAY_DELAY = 2.0 RELAY_DELAY = 2.0
TRANSIT_KEY_LENGTH = SecretBox.KEY_SIZE TRANSIT_KEY_LENGTH = SecretBox.KEY_SIZE
def __init__(self, transit_relay, no_listen=False, tor_manager=None, def __init__(self, transit_relay, no_listen=False, tor=None,
reactor=reactor, timing=None): reactor=reactor, timing=None):
self._side = bytes_to_hexstr(os.urandom(8)) # unicode self._side = bytes_to_hexstr(os.urandom(8)) # unicode
if transit_relay: if transit_relay:
@ -603,7 +603,7 @@ class Common:
self._transit_relays = [] self._transit_relays = []
self._their_direct_hints = [] # hintobjs self._their_direct_hints = [] # hintobjs
self._our_relay_hints = set(self._transit_relays) self._our_relay_hints = set(self._transit_relays)
self._tor_manager = tor_manager self._tor = tor
self._transit_key = None self._transit_key = None
self._no_listen = no_listen self._no_listen = no_listen
self._waiting_for_transit_key = [] self._waiting_for_transit_key = []
@ -614,7 +614,7 @@ class Common:
self._timing.add("transit") self._timing.add("transit")
def _build_listener(self): def _build_listener(self):
if self._no_listen or self._tor_manager: if self._no_listen or self._tor:
return ([], None) return ([], None)
portnum = allocate_tcp_port() portnum = allocate_tcp_port()
addresses = ipaddrs.find_addresses() addresses = ipaddrs.find_addresses()
@ -820,7 +820,7 @@ class Common:
if not ep: if not ep:
continue continue
description = "->%s" % describe_hint_obj(hint_obj) description = "->%s" % describe_hint_obj(hint_obj)
if self._tor_manager: if self._tor:
description = "tor" + description description = "tor" + description
d = self._start_connector(ep, description) d = self._start_connector(ep, description)
contenders.append(d) contenders.append(d)
@ -847,7 +847,7 @@ class Common:
if not ep: if not ep:
continue continue
description = "->relay:%s" % describe_hint_obj(hint_obj) description = "->relay:%s" % describe_hint_obj(hint_obj)
if self._tor_manager: if self._tor:
description = "tor" + description description = "tor" + description
d = task.deferLater(self._reactor, relay_delay, d = task.deferLater(self._reactor, relay_delay,
self._start_connector, ep, description, self._start_connector, ep, description,
@ -887,12 +887,11 @@ class Common:
return d return d
def _endpoint_from_hint_obj(self, hint): def _endpoint_from_hint_obj(self, hint):
if self._tor_manager: if self._tor:
if isinstance(hint, (DirectTCPV1Hint, TorTCPV1Hint)): if isinstance(hint, (DirectTCPV1Hint, TorTCPV1Hint)):
# our TorManager will return None for non-public IPv4 # this Tor object will return None for non-public IPv4
# addresses and any IPv6 address # addresses and any IPv6 address
return self._tor_manager.get_endpoint_for(hint.hostname, return self._tor.stream_via(hint.hostname, hint.port)
hint.port)
return None return None
if isinstance(hint, DirectTCPV1Hint): if isinstance(hint, DirectTCPV1Hint):
return endpoints.HostnameEndpoint(self._reactor, return endpoints.HostnameEndpoint(self._reactor,

View File

@ -279,7 +279,7 @@ class _DeferredWormhole(object):
def create(appid, relay_url, reactor, # use keyword args for everything else def create(appid, relay_url, reactor, # use keyword args for everything else
versions={}, versions={},
delegate=None, journal=None, tor_manager=None, delegate=None, journal=None, tor=None,
timing=None, timing=None,
stderr=sys.stderr): stderr=sys.stderr):
timing = timing or DebugTiming() timing = timing or DebugTiming()
@ -292,13 +292,13 @@ def create(appid, relay_url, reactor, # use keyword args for everything else
wormhole_versions = {} # will be used to indicate Wormhole capabilities wormhole_versions = {} # will be used to indicate Wormhole capabilities
wormhole_versions["app_versions"] = versions # app-specific capabilities wormhole_versions["app_versions"] = versions # app-specific capabilities
b = Boss(w, side, relay_url, appid, wormhole_versions, b = Boss(w, side, relay_url, appid, wormhole_versions,
reactor, journal, tor_manager, timing) reactor, journal, tor, timing)
w._set_boss(b) w._set_boss(b)
b.start() b.start()
return w return w
## def from_serialized(serialized, reactor, delegate, ## def from_serialized(serialized, reactor, delegate,
## journal=None, tor_manager=None, ## journal=None, tor=None,
## timing=None, stderr=sys.stderr): ## timing=None, stderr=sys.stderr):
## assert serialized["serialized_wormhole_version"] == 1 ## assert serialized["serialized_wormhole_version"] == 1
## timing = timing or DebugTiming() ## timing = timing or DebugTiming()

View File

@ -2,8 +2,7 @@ import json
from twisted.internet.defer import inlineCallbacks, returnValue from twisted.internet.defer import inlineCallbacks, returnValue
from . import wormhole from . import wormhole
from .tor_manager import TorManager from .tor_manager import get_tor
from .errors import NoTorError
@inlineCallbacks @inlineCallbacks
def receive(reactor, appid, relay_url, code, def receive(reactor, appid, relay_url, code,
@ -27,18 +26,15 @@ def receive(reactor, appid, relay_url, code,
:param on_code: if not None, this is called when we have a code (even if you passed in one explicitly) :param on_code: if not None, this is called when we have a code (even if you passed in one explicitly)
:type on_code: single-argument callable :type on_code: single-argument callable
""" """
tm = None tor = None
if use_tor: if use_tor:
tm = TorManager(reactor, launch_tor, tor_control_port) tor = yield get_tor(reactor, launch_tor, tor_control_port)
# For now, block everything until Tor has started. Soon: launch # For now, block everything until Tor has started. Soon: launch
# tor in parallel with everything else, make sure the TorManager # tor in parallel with everything else, make sure the Tor object
# can lazy-provide an endpoint, and overlap the startup process # can lazy-provide an endpoint, and overlap the startup process
# with the user handing off the wormhole code # with the user handing off the wormhole code
if not tm.tor_available():
raise NoTorError()
yield tm.start()
wh = wormhole.create(appid, relay_url, reactor, tor_manager=tm) wh = wormhole.create(appid, relay_url, reactor, tor=tor)
if code is None: if code is None:
wh.allocate_code() wh.allocate_code()
code = yield wh.get_code() code = yield wh.get_code()
@ -92,17 +88,14 @@ def send(reactor, appid, relay_url, data, code,
:param on_code: if not None, this is called when we have a code (even if you passed in one explicitly) :param on_code: if not None, this is called when we have a code (even if you passed in one explicitly)
:type on_code: single-argument callable :type on_code: single-argument callable
""" """
tm = None tor = None
if use_tor: if use_tor:
tm = TorManager(reactor, launch_tor, tor_control_port) tor = yield get_tor(reactor, launch_tor, tor_control_port)
# For now, block everything until Tor has started. Soon: launch # For now, block everything until Tor has started. Soon: launch
# tor in parallel with everything else, make sure the TorManager # tor in parallel with everything else, make sure the Tor object
# can lazy-provide an endpoint, and overlap the startup process # can lazy-provide an endpoint, and overlap the startup process
# with the user handing off the wormhole code # with the user handing off the wormhole code
if not tm.tor_available(): wh = wormhole.create(appid, relay_url, reactor, tor=tor)
raise NoTorError()
yield tm.start()
wh = wormhole.create(appid, relay_url, reactor, tor_manager=tm)
if code is None: if code is None:
wh.allocate_code() wh.allocate_code()
code = yield wh.get_code() code = yield wh.get_code()