diff --git a/docs/releases.rst b/docs/releases.rst index 8de19077..0f0b7b7e 100644 --- a/docs/releases.rst +++ b/docs/releases.rst @@ -21,6 +21,9 @@ unreleased `git master `_ *will likely become v19.0.0* + * introduce `non_anonymous_mode=` kwarg for :func:`txtorcon.launch` + + v18.3.0 ------- diff --git a/test/test_controller.py b/test/test_controller.py index 618f2d17..41ed508a 100644 --- a/test/test_controller.py +++ b/test/test_controller.py @@ -236,6 +236,20 @@ def test_launch_tor_unix_controlport_no_directory(self): ) self.assertTrue("must exist" in str(ctx.exception)) + @defer.inlineCallbacks + def test_launch_tor_non_anonymous_and_socks(self): + reactor = FakeReactor(self, Mock(), None, [9050]) + with self.assertRaises(ValueError) as ctx: + yield launch( + reactor, + non_anonymous_mode=True, + socks_port=1234, + tor_binary="/bin/echo", + stdout=Mock(), + stderr=Mock(), + ) + self.assertIn("Cannot use SOCKS", str(ctx.exception)) + @patch('txtorcon.controller.find_tor_binary', return_value='/bin/echo') @defer.inlineCallbacks def test_launch_fails(self, ftb): @@ -290,6 +304,35 @@ def foo(*args, **kw): tor = yield launch(reactor, _tor_config=config) self.assertTrue(isinstance(tor, Tor)) + @patch('txtorcon.controller.find_tor_binary', return_value='/bin/echo') + @patch('txtorcon.controller.TorProcessProtocol') + @defer.inlineCallbacks + def test_successful_launch_non_anonymous(self, tpp, ftb): + trans = FakeProcessTransport() + reactor = FakeReactor(self, trans, lambda p: None, [1, 2, 3]) + config = TorConfig() + + def boot(arg=None): + config.post_bootstrap.callback(config) + config.__dict__['bootstrap'] = Mock(side_effect=boot) + config.__dict__['attach_protocol'] = Mock(return_value=defer.succeed(None)) + + def foo(*args, **kw): + rtn = Mock() + rtn.post_bootstrap = defer.succeed(None) + rtn.when_connected = Mock(return_value=defer.succeed(rtn)) + return rtn + tpp.side_effect = foo + + tor = yield launch(reactor, _tor_config=config, non_anonymous_mode=True) + self.assertTrue(isinstance(tor, Tor)) + self.assertTrue(config.HiddenServiceNonAnonymousMode) + + with self.assertRaises(Exception): + yield tor.web_agent() + with self.assertRaises(Exception): + yield tor.dns_resolve('meejah.ca') + @defer.inlineCallbacks def test_quit(self): tor = Tor(Mock(), Mock()) diff --git a/txtorcon/controller.py b/txtorcon/controller.py index a1004a15..5fc6017e 100644 --- a/txtorcon/controller.py +++ b/txtorcon/controller.py @@ -55,6 +55,7 @@ def launch(reactor, control_port=None, data_directory=None, socks_port=None, + non_anonymous_mode=None, stdout=None, stderr=None, timeout=None, @@ -110,6 +111,14 @@ def launch(reactor, faster. If ``None`` (the default), we create a tempdir for this **and delete it on exit**. It is recommended you pass something here. + :param non_anonymous_mode: sets the Tor options + `HiddenServiceSingleHopMode` and + `HiddenServiceNonAnonymousMode` to 1 and un-sets any + `SOCKSPort` config, thus putting this Tor client into + "non-anonymous mode" which allows starting so-called Single + Onion services -- which use single-hop circuits to rendezvous + points. See WARNINGs in Tor manual! + :param stdout: a file-like object to which we write anything that Tor prints on stdout (just needs to support write()). @@ -221,9 +230,18 @@ def launch(reactor, except KeyError: socks_port = None - if socks_port is None: - socks_port = yield available_tcp_port(reactor) - config.SOCKSPort = socks_port + if non_anonymous_mode: + if socks_port is not None: + raise ValueError( + "Cannot use SOCKS options with non_anonymous_mode=True" + ) + config.HiddenServiceNonAnonymousMode = 1 + config.HiddenServiceSingleHopMode = 1 + config.SOCKSPort = 0 + else: + if socks_port is None: + socks_port = yield available_tcp_port(reactor) + config.SOCKSPort = socks_port try: our_user = user or config.User @@ -350,6 +368,7 @@ def launch(reactor, config.protocol, _tor_config=config, _process_proto=process_protocol, + _non_anonymous=True if non_anonymous_mode else False, ) ) @@ -474,7 +493,7 @@ def main(reactor): print(port.getHost()) """ - def __init__(self, reactor, control_protocol, _tor_config=None, _process_proto=None): + def __init__(self, reactor, control_protocol, _tor_config=None, _process_proto=None, _non_anonymous=None): """ don't instantiate this class yourself -- instead use the factory methods :func:`txtorcon.launch` or :func:`txtorcon.connect` @@ -487,6 +506,8 @@ def __init__(self, reactor, control_protocol, _tor_config=None, _process_proto=N # cache our preferred socks port (please use # self._default_socks_endpoint() to get one) self._socks_endpoint = None + # True if we've turned on non-anonymous mode / Onion services + self._non_anonymous = _non_anonymous @inlineCallbacks def quit(self): @@ -552,6 +573,10 @@ def web_agent(self, pool=None, socks_endpoint=None): :param pool: passed on to the Agent (as ``pool=``) """ + if self._non_anonymous: + raise Exception( + "Cannot use web_agent when in non_anonymous mode" + ) # local import since not all platforms have this from txtorcon import web @@ -932,6 +957,10 @@ def _default_socks_endpoint(self): (which might mean setting one up in our attacked Tor if it doesn't have one) """ + if self._non_anonymous: + raise Exception( + "Cannot use SOCKS when in non_anonymous mode" + ) if self._socks_endpoint is None: self._socks_endpoint = yield _create_socks_endpoint(self._reactor, self._protocol) returnValue(self._socks_endpoint) diff --git a/txtorcon/onion.py b/txtorcon/onion.py index 6817a84d..52650d49 100644 --- a/txtorcon/onion.py +++ b/txtorcon/onion.py @@ -170,7 +170,11 @@ class FilesystemOnionService(object): @staticmethod @defer.inlineCallbacks - def create(reactor, config, hsdir, ports, version=3, group_readable=False, progress=None, await_all_uploads=None): + def create(reactor, config, hsdir, ports, + version=3, + group_readable=False, + progress=None, + await_all_uploads=None): """ returns a new FilesystemOnionService after adding it to the provided config and ensuring at least one of its descriptors @@ -1076,7 +1080,12 @@ class FilesystemAuthenticatedOnionService(object): @staticmethod @defer.inlineCallbacks - def create(reactor, config, hsdir, ports, auth=None, version=3, group_readable=False, progress=None, await_all_uploads=None): + def create(reactor, config, hsdir, ports, + auth=None, + version=3, + group_readable=False, + progress=None, + await_all_uploads=None): """ returns a new FilesystemAuthenticatedOnionService after adding it to the provided config and ensureing at least one of its