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
from zope.interface import implementer
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 automat import MethodicalMachine
from . import _interfaces
@ -35,7 +35,7 @@ class Boss(object):
_versions = attrib(validator=instance_of(dict))
_reactor = attrib()
_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))
m = MethodicalMachine()
set_trace = getattr(m, "_setTrace", lambda self, f: None)
@ -53,7 +53,7 @@ class Boss(object):
self._R = Receive(self._side, self._timing)
self._RC = RendezvousConnector(self._url, self._appid, self._side,
self._reactor, self._journal,
self._tor_manager, self._timing)
self._tor, self._timing)
self._L = Lister(self._timing)
self._A = Allocator(self._timing)
self._I = Input(self._timing)

View File

@ -2,7 +2,7 @@ from __future__ import print_function, absolute_import, unicode_literals
import os
from six.moves.urllib_parse import urlparse
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 twisted.python import log
from twisted.internet import defer, endpoints
@ -65,7 +65,7 @@ class RendezvousConnector(object):
_side = attrib(validator=instance_of(type(u"")))
_reactor = attrib()
_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))
def __attrs_post_init__(self):
@ -86,8 +86,9 @@ class RendezvousConnector(object):
self._trace(old_state="", input=what, new_state="")
def _make_endpoint(self, hostname, port):
if self._tor_manager:
return self._tor_manager.get_endpoint_for(hostname, port)
if self._tor:
# TODO: when we enable TLS, maybe add tls=True here
return self._tor.stream_via(hostname, port)
return endpoints.HostnameEndpoint(self._reactor, hostname, port)
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 wormhole import create, input_with_completion, __version__
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,
estimate_free_space)
from .welcome import handle_welcome
@ -45,7 +45,7 @@ class Receiver:
assert isinstance(args.relay_url, type(u""))
self.args = args
self._reactor = reactor
self._tor_manager = None
self._tor = None
self._transit_receiver = None
def _msg(self, *args, **kwargs):
@ -55,29 +55,26 @@ class Receiver:
def go(self):
if self.args.tor:
with self.args.timing.add("import", which="tor_manager"):
from ..tor_manager import TorManager
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()
from ..tor_manager import get_tor
# 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
# 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,
self._reactor,
tor_manager=self._tor_manager,
tor=self._tor,
timing=self.args.timing)
self._w = w # so tests can wait on events too
# I wanted to do this instead:
#
# try:
# yield self._go(w, tor_manager)
# yield self._go(w, tor)
# finally:
# yield w.close()
#
@ -230,7 +227,7 @@ class Receiver:
def _build_transit(self, w, sender_transit):
tr = TransitReceiver(self.args.transit_helper,
no_listen=(not self.args.listen),
tor_manager=self._tor_manager,
tor=self._tor,
reactor=self._reactor,
timing=self.args.timing)
self._transit_receiver = tr

View File

@ -6,8 +6,7 @@ from twisted.python import log
from twisted.protocols import basic
from twisted.internet import reactor
from twisted.internet.defer import inlineCallbacks, returnValue
from ..errors import (TransferError, WormholeClosedError, NoTorError,
UnsendableFileError)
from ..errors import (TransferError, WormholeClosedError, UnsendableFileError)
from wormhole import create, __version__
from ..transit import TransitSender
from ..util import dict_to_bytes, bytes_to_dict, bytes_to_hexstr
@ -31,7 +30,7 @@ class Sender:
def __init__(self, args, reactor):
self._args = args
self._reactor = reactor
self._tor_manager = None
self._tor = None
self._timing = args.timing
self._fd_to_send = None
self._transit_sender = None
@ -41,22 +40,19 @@ class Sender:
assert isinstance(self._args.relay_url, type(u""))
if self._args.tor:
with self._timing.add("import", which="tor_manager"):
from ..tor_manager import TorManager
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()
from ..tor_manager import get_tor
# 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
# 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,
self._reactor,
tor_manager=self._tor_manager,
tor=self._tor,
timing=self._timing)
d = self._go(w)
@ -151,7 +147,7 @@ class Sender:
if self._fd_to_send:
ts = TransitSender(args.transit_helper,
no_listen=(not args.listen),
tor_manager=self._tor_manager,
tor=self._tor,
reactor=self._reactor,
timing=self._timing)
self._transit_sender = ts

View File

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

View File

@ -3,335 +3,78 @@ import mock, io
from twisted.trial import unittest
from twisted.internet import defer
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):
def test_create(self):
tm = TorManager(None)
del tm
def test_no_txtorcon(self):
with mock.patch("wormhole.tor_manager.txtorcon", None):
self.failureResultOf(get_tor(None), NoTorError)
def test_bad_args(self):
e = self.assertRaises(TypeError,
TorManager, None, launch_tor="not boolean")
self.assertEqual(str(e), "launch_tor= must be boolean")
e = self.assertRaises(TypeError,
TorManager, None, tor_control_port=1234)
self.assertEqual(str(e), "tor_control_port= must be str or None")
e = self.assertRaises(ValueError,
TorManager, None, launch_tor=True,
tor_control_port="tcp:127.0.0.1:1234")
self.assertEqual(str(e),
f = self.failureResultOf(get_tor(None, launch_tor="not boolean"),
TypeError)
self.assertEqual(str(f.value), "launch_tor= must be boolean")
f = self.failureResultOf(get_tor(None, tor_control_port=1234),
TypeError)
self.assertEqual(str(f.value), "tor_control_port= must be str or None")
f = self.failureResultOf(get_tor(None, launch_tor=True,
tor_control_port="tcp:127.0.0.1:1234"),
ValueError)
self.assertEqual(str(f.value),
"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):
reactor = object()
my_tor = X() # object() didn't like providedBy()
launch_d = defer.Deferred()
stderr = io.StringIO()
tc = mock.Mock()
mock_TorConfig = mock.patch("wormhole.tor_manager.TorConfig",
return_value=tc)
lt_d = defer.Deferred()
mock_launch_tor = mock.patch("wormhole.tor_manager.launch_tor",
return_value=lt_d)
mock_allocate_tcp_port = mock.patch("wormhole.tor_manager.allocate_tcp_port",
return_value=12345)
mock_clientFromString = mock.patch("wormhole.tor_manager.clientFromString")
with mock_TorConfig as mtc:
with mock_launch_tor as mlt:
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")])
with mock.patch("wormhole.tor_manager.txtorcon.launch",
side_effect=launch_d) as launch:
d = get_tor(reactor, launch_tor=True, stderr=stderr)
self.assertNoResult(d)
self.assertEqual(launch.mock_calls, [mock.call(reactor)])
launch_d.callback(my_tor)
tor = self.successResultOf(d)
self.assertIs(tor, my_tor)
self.assert_(ITorManager.providedBy(tor))
self.assertEqual(stderr.getvalue(),
" launching a new Tor process, this may take a while..\n")
def _do_test_try_control_port(self, socks_ports, exp_socks_desc,
btc_exception=None, tcfp_exception=None):
def test_connect(self):
reactor = object()
my_tor = X() # object() didn't like providedBy()
tcp = "port"
connect_d = defer.Deferred()
stderr = io.StringIO()
ep = object()
mock_clientFromString = mock.patch("wormhole.tor_manager.clientFromString",
return_value=ep)
tproto = mock.Mock()
btc_d = defer.Deferred()
mock_build_tor_connection = mock.patch("wormhole.tor_manager.build_tor_connection", return_value=btc_d)
torconfig = mock.Mock()
tc = mock.Mock()
tc.SocksPort = iter(socks_ports)
tc_d = defer.Deferred()
torconfig.from_protocol = mock.Mock(return_value=tc_d)
mock_torconfig = mock.patch("wormhole.tor_manager.TorConfig", torconfig)
with mock.patch("wormhole.tor_manager.txtorcon.connect",
side_effect=connect_d) as connect:
d = get_tor(reactor, tor_control_port=tcp, stderr=stderr)
self.assertNoResult(d)
self.assertEqual(connect.mock_calls, [mock.call(reactor, tcp)])
connect_d.callback(my_tor)
tor = self.successResultOf(d)
self.assertIs(tor, my_tor)
self.assert_(ITorManager.providedBy(tor))
self.assertEqual(stderr.getvalue(), " using Tor\n")
control_port = object()
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):
def test_connect_fails(self):
reactor = object()
tcp = "port"
connect_d = defer.Deferred()
stderr = io.StringIO()
ep = object()
mock_clientFromString = mock.patch("wormhole.tor_manager.clientFromString",
return_value=ep)
tproto = mock.Mock()
btc_d = defer.Deferred()
mock_build_tor_connection = mock.patch("wormhole.tor_manager.build_tor_connection", return_value=btc_d)
torconfig = mock.Mock()
tcfp_d = defer.Deferred()
torconfig.from_protocol = mock.Mock(return_value=tcfp_d)
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, [])
with mock.patch("wormhole.tor_manager.txtorcon.connect",
side_effect=connect_d) as connect:
d = get_tor(reactor, tor_control_port=tcp, stderr=stderr)
self.assertNoResult(d)
self.assertEqual(connect.mock_calls, [mock.call(reactor, tcp)])
connect_d.errback(ConnectError())
self.failureResultOf(d, ConnectError)
self.assertEqual(stderr.getvalue(),
" unable to find control port, bailing\n")

View File

@ -149,14 +149,14 @@ class Hints(unittest.TestCase):
self.assertIsInstance(efho(transit.DirectTCPV1Hint("host", 1234, 0.0)),
endpoints.HostnameEndpoint)
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)
c._tor_manager = mock.Mock()
c._tor = mock.Mock()
def tor_ep(hostname, port):
if hostname == "non-public":
return None
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)),
("tor_ep", "host", 1234))
self.assertEqual(efho(transit.TorTCPV1Hint("host2.onion", 1234, 0.0)),
@ -1470,7 +1470,7 @@ class Transit(unittest.TestCase):
@inlineCallbacks
def test_success_direct_tor(self):
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")
hints = yield s.get_connection_hints() # start the listener
del hints
@ -1491,7 +1491,7 @@ class Transit(unittest.TestCase):
@inlineCallbacks
def test_success_direct_tor_relay(self):
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")
hints = yield s.get_connection_hints() # start the listener
del hints

View File

@ -1,171 +1,85 @@
from __future__ import print_function, unicode_literals
import sys, re
import six
from zope.interface import implementer
import sys
from zope.interface.declarations import directlyProvides
from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.internet.error import ConnectError
from twisted.internet.endpoints import clientFromString
try:
from txtorcon import (TorConfig, launch_tor, build_tor_connection,
DEFAULT_VALUE, TorClientEndpoint)
import txtorcon
except ImportError:
TorConfig = None
launch_tor = None
build_tor_connection = None
TorClientEndpoint = None
DEFAULT_VALUE = "DEFAULT_VALUE"
import ipaddress
from . import _interfaces
txtorcon = None
from . import _interfaces, errors
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)
class TorManager:
def __init__(self, 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).
Otherwise if tor_control_port is provided, I will attempt to connect
to an existing Tor's control port at the endpoint it specifies. I'll
ask that Tor for its SOCKS port.
Otherwise if tor_control_port is provided, I will attempt to connect
to an existing Tor's control port at the endpoint it specifies. I'll
ask that Tor for its SOCKS port.
With no arguments, I will try to connect to an existing Tor's control
port at the usual places: [unix:/var/run/tor/control,
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
port at the usual places: [unix:/var/run/tor/control,
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
the Rendezvous Server will fail, and the program will terminate.
If I am unable to make a SOCKS connection, the initial connection to
the Rendezvous Server will fail, and the program will terminate.
Control-port connections can only succeed if I can authenticate (by
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
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.
if not txtorcon:
raise errors.NoTorError()
self._reactor = reactor
if not isinstance(launch_tor, bool): # note: False is int
raise TypeError("launch_tor= must be boolean")
if not isinstance(tor_control_port, (type(""), type(None))):
raise TypeError("tor_control_port= must be str or None")
if launch_tor and tor_control_port is not None:
raise ValueError("cannot combine --launch-tor and --tor-control-port=")
self._launch_tor = launch_tor
self._tor_control_port = tor_control_port
self._timing = timing or DebugTiming()
self._stderr = stderr
if not isinstance(launch_tor, bool): # note: False is int
raise TypeError("launch_tor= must be boolean")
if not isinstance(tor_control_port, (type(""), type(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:
raise ValueError("cannot combine --launch-tor and --tor-control-port=")
timing = timing or DebugTiming()
def tor_available(self):
# unit tests mock out everything we get from txtorcon, so we can test
# this class under py3 even if txtorcon isn't installed. But the real
# commands need to know if they have Tor or not.
return bool(TorConfig)
# Connect to an existing Tor, or create a new one. If we need to
# launch an onion service, then we need a working control port (and
# authentication cookie). If we're only acting as a client, we don't
# need the control port.
@inlineCallbacks
def start(self):
# Connect to an existing Tor, or create a new one. If we need to
# launch an onion service, then we need a working control port (and
# authentication cookie). If we're only acting as a client, we don't
# need the control port.
if self._launch_tor:
print(" launching a new Tor process, this may take a while..",
file=self._stderr)
with self._timing.add("launch tor"):
(tproto, tconfig, socks_desc) = yield self._do_launch_tor()
else:
control_ports = ["unix:/var/run/tor/control", # debian tor package
"tcp:127.0.0.1:9051", # standard Tor
"tcp:127.0.0.1:9151", # TorBrowser
]
if self._tor_control_port:
control_ports = [self._tor_control_port]
with self._timing.add("find tor"):
for control_port in control_ports:
(tproto, tconfig,
socks_desc) = yield self._try_control_port(control_port)
if tproto:
print(" using Tor (control port %s) (SOCKS port %s)"
% (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
if launch_tor:
print(" launching a new Tor process, this may take a while..",
file=stderr)
with timing.add("launch tor"):
tor = yield txtorcon.launch(reactor,
#data_directory=,
#tor_binary=,
)
else:
with timing.add("find tor"):
try:
tor = yield txtorcon.connect(reactor, tor_control_port)
print(" using Tor", file=stderr)
except Exception:
#socks_desc = "tcp:127.0.0.1:9050" # fallback
#print(" using Tor (SOCKS port %s)" % socks_desc,
# file=stderr)
print(" unable to find control port, bailing",
file=stderr)
# TODO: something nicer. I think connect() is likely to throw
# a reactor.connectTCP -type error, like ConnectionFailed or
# ConnectionRefused or something
raise
directlyProvides(tor, _interfaces.ITorManager)
returnValue(tor)

View File

@ -589,7 +589,7 @@ class Common:
RELAY_DELAY = 2.0
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):
self._side = bytes_to_hexstr(os.urandom(8)) # unicode
if transit_relay:
@ -603,7 +603,7 @@ class Common:
self._transit_relays = []
self._their_direct_hints = [] # hintobjs
self._our_relay_hints = set(self._transit_relays)
self._tor_manager = tor_manager
self._tor = tor
self._transit_key = None
self._no_listen = no_listen
self._waiting_for_transit_key = []
@ -614,7 +614,7 @@ class Common:
self._timing.add("transit")
def _build_listener(self):
if self._no_listen or self._tor_manager:
if self._no_listen or self._tor:
return ([], None)
portnum = allocate_tcp_port()
addresses = ipaddrs.find_addresses()
@ -820,7 +820,7 @@ class Common:
if not ep:
continue
description = "->%s" % describe_hint_obj(hint_obj)
if self._tor_manager:
if self._tor:
description = "tor" + description
d = self._start_connector(ep, description)
contenders.append(d)
@ -847,7 +847,7 @@ class Common:
if not ep:
continue
description = "->relay:%s" % describe_hint_obj(hint_obj)
if self._tor_manager:
if self._tor:
description = "tor" + description
d = task.deferLater(self._reactor, relay_delay,
self._start_connector, ep, description,
@ -887,12 +887,11 @@ class Common:
return d
def _endpoint_from_hint_obj(self, hint):
if self._tor_manager:
if self._tor:
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
return self._tor_manager.get_endpoint_for(hint.hostname,
hint.port)
return self._tor.stream_via(hint.hostname, hint.port)
return None
if isinstance(hint, DirectTCPV1Hint):
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
versions={},
delegate=None, journal=None, tor_manager=None,
delegate=None, journal=None, tor=None,
timing=None,
stderr=sys.stderr):
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["app_versions"] = versions # app-specific capabilities
b = Boss(w, side, relay_url, appid, wormhole_versions,
reactor, journal, tor_manager, timing)
reactor, journal, tor, timing)
w._set_boss(b)
b.start()
return w
## def from_serialized(serialized, reactor, delegate,
## journal=None, tor_manager=None,
## journal=None, tor=None,
## timing=None, stderr=sys.stderr):
## assert serialized["serialized_wormhole_version"] == 1
## timing = timing or DebugTiming()

View File

@ -2,8 +2,7 @@ import json
from twisted.internet.defer import inlineCallbacks, returnValue
from . import wormhole
from .tor_manager import TorManager
from .errors import NoTorError
from .tor_manager import get_tor
@inlineCallbacks
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)
:type on_code: single-argument callable
"""
tm = None
tor = None
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
# 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
# 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:
wh.allocate_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)
:type on_code: single-argument callable
"""
tm = None
tor = None
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
# 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
# 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:
wh.allocate_code()
code = yield wh.get_code()