| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | from __future__ import annotations |
|---|
| 3 | |
|---|
| 4 | from typing import Any |
|---|
| 5 | from typing_extensions import Literal |
|---|
| 6 | import os |
|---|
| 7 | |
|---|
| 8 | from zope.interface import ( |
|---|
| 9 | implementer, |
|---|
| 10 | ) |
|---|
| 11 | |
|---|
| 12 | from twisted.internet.defer import inlineCallbacks, returnValue |
|---|
| 13 | from twisted.internet.endpoints import clientFromString, TCP4ServerEndpoint |
|---|
| 14 | from twisted.internet.error import ConnectionRefusedError, ConnectError |
|---|
| 15 | from twisted.application import service |
|---|
| 16 | from twisted.python.usage import Options |
|---|
| 17 | |
|---|
| 18 | from .observer import OneShotObserverList |
|---|
| 19 | from .iputil import allocate_tcp_port |
|---|
| 20 | from ..interfaces import ( |
|---|
| 21 | IAddressFamily, |
|---|
| 22 | ) |
|---|
| 23 | from ..listeners import ListenerConfig |
|---|
| 24 | |
|---|
| 25 | |
|---|
| 26 | def _import_tor(): |
|---|
| 27 | try: |
|---|
| 28 | from foolscap.connections import tor |
|---|
| 29 | return tor |
|---|
| 30 | except ImportError: # pragma: no cover |
|---|
| 31 | return None |
|---|
| 32 | |
|---|
| 33 | def _import_txtorcon(): |
|---|
| 34 | try: |
|---|
| 35 | import txtorcon |
|---|
| 36 | return txtorcon |
|---|
| 37 | except ImportError: # pragma: no cover |
|---|
| 38 | return None |
|---|
| 39 | |
|---|
| 40 | def can_hide_ip() -> Literal[True]: |
|---|
| 41 | return True |
|---|
| 42 | |
|---|
| 43 | def is_available() -> bool: |
|---|
| 44 | return not (_import_tor() is None or _import_txtorcon() is None) |
|---|
| 45 | |
|---|
| 46 | def create(reactor, config, import_tor=None, import_txtorcon=None) -> _Provider: |
|---|
| 47 | """ |
|---|
| 48 | Create a new _Provider service (this is an IService so must be |
|---|
| 49 | hooked up to a parent or otherwise started). |
|---|
| 50 | |
|---|
| 51 | If foolscap.connections.tor or txtorcon are not installed, then |
|---|
| 52 | Provider.get_tor_handler() will return None. If tahoe.cfg wants |
|---|
| 53 | to start an onion service too, then this `create()` method will |
|---|
| 54 | throw a nice error (and startService will throw an ugly error). |
|---|
| 55 | """ |
|---|
| 56 | if import_tor is None: |
|---|
| 57 | import_tor = _import_tor |
|---|
| 58 | if import_txtorcon is None: |
|---|
| 59 | import_txtorcon = _import_txtorcon |
|---|
| 60 | provider = _Provider(config, reactor, import_tor(), import_txtorcon()) |
|---|
| 61 | provider.check_onion_config() |
|---|
| 62 | return provider |
|---|
| 63 | |
|---|
| 64 | |
|---|
| 65 | def data_directory(private_dir): |
|---|
| 66 | return os.path.join(private_dir, "tor-statedir") |
|---|
| 67 | |
|---|
| 68 | # different ways we might approach this: |
|---|
| 69 | |
|---|
| 70 | # 1: get an ITorControlProtocol, make a |
|---|
| 71 | # txtorcon.EphemeralHiddenService(ports), yield ehs.add_to_tor(tcp), store |
|---|
| 72 | # ehs.hostname and ehs.private_key, yield ehs.remove_from_tor(tcp) |
|---|
| 73 | |
|---|
| 74 | def _try_to_connect(reactor, endpoint_desc, stdout, txtorcon): |
|---|
| 75 | # yields a TorState, or None |
|---|
| 76 | ep = clientFromString(reactor, endpoint_desc) |
|---|
| 77 | d = txtorcon.build_tor_connection(ep) |
|---|
| 78 | def _failed(f): |
|---|
| 79 | # depending upon what's listening at that endpoint, we might get |
|---|
| 80 | # various errors. If this list is too short, we might expose an |
|---|
| 81 | # exception to the user (causing "tahoe create-node" to fail messily) |
|---|
| 82 | # when we're supposed to just try the next potential port instead. |
|---|
| 83 | # But I don't want to catch everything, because that may hide actual |
|---|
| 84 | # coding errrors. |
|---|
| 85 | f.trap(ConnectionRefusedError, # nothing listening on TCP |
|---|
| 86 | ConnectError, # missing unix socket, or permission denied |
|---|
| 87 | #ValueError, |
|---|
| 88 | # connecting to e.g. an HTTP server causes an |
|---|
| 89 | # UnhandledException (around a ValueError) when the handshake |
|---|
| 90 | # fails to parse, but that's not something we can catch. The |
|---|
| 91 | # attempt hangs, so don't do that. |
|---|
| 92 | RuntimeError, # authentication failure |
|---|
| 93 | ) |
|---|
| 94 | if stdout: |
|---|
| 95 | stdout.write("Unable to reach Tor at '%s': %s\n" % |
|---|
| 96 | (endpoint_desc, f.value)) |
|---|
| 97 | return None |
|---|
| 98 | d.addErrback(_failed) |
|---|
| 99 | return d |
|---|
| 100 | |
|---|
| 101 | @inlineCallbacks |
|---|
| 102 | def _launch_tor(reactor, tor_executable, private_dir, txtorcon): |
|---|
| 103 | """ |
|---|
| 104 | Launches Tor, returns a corresponding ``(control endpoint string, |
|---|
| 105 | txtorcon.Tor instance)`` tuple. |
|---|
| 106 | """ |
|---|
| 107 | # TODO: handle default tor-executable |
|---|
| 108 | # TODO: it might be a good idea to find exactly which Tor we used, |
|---|
| 109 | # and record it's absolute path into tahoe.cfg . This would protect |
|---|
| 110 | # us against one Tor being on $PATH at create-node time, but then a |
|---|
| 111 | # different Tor being present at node startup. OTOH, maybe we don't |
|---|
| 112 | # need to worry about it. |
|---|
| 113 | |
|---|
| 114 | # unix-domain control socket |
|---|
| 115 | tor_control_endpoint_desc = "unix:" + os.path.join(private_dir, "tor.control") |
|---|
| 116 | |
|---|
| 117 | tor = yield txtorcon.launch( |
|---|
| 118 | reactor, |
|---|
| 119 | control_port=tor_control_endpoint_desc, |
|---|
| 120 | data_directory=data_directory(private_dir), |
|---|
| 121 | tor_binary=tor_executable, |
|---|
| 122 | socks_port=allocate_tcp_port(), |
|---|
| 123 | # can be useful when debugging; mirror Tor's output to ours |
|---|
| 124 | # stdout=sys.stdout, |
|---|
| 125 | # stderr=sys.stderr, |
|---|
| 126 | ) |
|---|
| 127 | |
|---|
| 128 | # How/when to shut down the new process? for normal usage, the child |
|---|
| 129 | # tor will exit when it notices its parent (us) quit. Unit tests will |
|---|
| 130 | # mock out txtorcon.launch_tor(), so there will never be a real Tor |
|---|
| 131 | # process. So I guess we don't need to track the process. |
|---|
| 132 | |
|---|
| 133 | # If we do want to do anything with it, we can call tpp.quit() |
|---|
| 134 | # (because it's a TorProcessProtocol) which returns a Deferred |
|---|
| 135 | # that fires when Tor has actually exited. |
|---|
| 136 | |
|---|
| 137 | returnValue((tor_control_endpoint_desc, tor)) |
|---|
| 138 | |
|---|
| 139 | |
|---|
| 140 | @inlineCallbacks |
|---|
| 141 | def _connect_to_tor(reactor, cli_config, txtorcon): |
|---|
| 142 | # we assume tor is already running |
|---|
| 143 | ports_to_try = ["unix:/var/run/tor/control", |
|---|
| 144 | "tcp:127.0.0.1:9051", |
|---|
| 145 | "tcp:127.0.0.1:9151", # TorBrowserBundle |
|---|
| 146 | ] |
|---|
| 147 | if cli_config["tor-control-port"]: |
|---|
| 148 | ports_to_try = [cli_config["tor-control-port"]] |
|---|
| 149 | for port in ports_to_try: |
|---|
| 150 | tor_state = yield _try_to_connect(reactor, port, cli_config.stdout, |
|---|
| 151 | txtorcon) |
|---|
| 152 | if tor_state: |
|---|
| 153 | tor_control_proto = tor_state.protocol |
|---|
| 154 | returnValue((port, tor_control_proto)) ; break # helps editor |
|---|
| 155 | else: |
|---|
| 156 | raise ValueError("unable to reach any default Tor control port") |
|---|
| 157 | |
|---|
| 158 | async def create_config(reactor: Any, cli_config: Options) -> ListenerConfig: |
|---|
| 159 | txtorcon = _import_txtorcon() |
|---|
| 160 | if not txtorcon: |
|---|
| 161 | raise ValueError("Cannot create onion without txtorcon. " |
|---|
| 162 | "Please 'pip install tahoe-lafs[tor]' to fix this.") |
|---|
| 163 | tahoe_config_tor = [] # written into tahoe.cfg:[tor] |
|---|
| 164 | private_dir = os.path.abspath(os.path.join(cli_config["basedir"], "private")) |
|---|
| 165 | # XXX We shouldn't carry stdout around by jamming it into the Options |
|---|
| 166 | # value. See https://tahoe-lafs.org/trac/tahoe-lafs/ticket/4048 |
|---|
| 167 | stdout = cli_config.stdout # type: ignore[attr-defined] |
|---|
| 168 | if cli_config["tor-launch"]: |
|---|
| 169 | tahoe_config_tor.append(("launch", "true")) |
|---|
| 170 | tor_executable = cli_config["tor-executable"] |
|---|
| 171 | if tor_executable: |
|---|
| 172 | tahoe_config_tor.append(("tor.executable", tor_executable)) |
|---|
| 173 | print("launching Tor (to allocate .onion address)..", file=stdout) |
|---|
| 174 | (_, tor) = await _launch_tor( |
|---|
| 175 | reactor, tor_executable, private_dir, txtorcon) |
|---|
| 176 | tor_control_proto = tor.protocol |
|---|
| 177 | print("Tor launched", file=stdout) |
|---|
| 178 | else: |
|---|
| 179 | print("connecting to Tor (to allocate .onion address)..", file=stdout) |
|---|
| 180 | (port, tor_control_proto) = await _connect_to_tor( |
|---|
| 181 | reactor, cli_config, txtorcon) |
|---|
| 182 | print("Tor connection established", file=stdout) |
|---|
| 183 | tahoe_config_tor.append(("control.port", port)) |
|---|
| 184 | |
|---|
| 185 | external_port = 3457 # TODO: pick this randomly? there's no contention. |
|---|
| 186 | |
|---|
| 187 | local_port = allocate_tcp_port() |
|---|
| 188 | ehs = txtorcon.EphemeralHiddenService( |
|---|
| 189 | "%d 127.0.0.1:%d" % (external_port, local_port) |
|---|
| 190 | ) |
|---|
| 191 | print("allocating .onion address (takes ~40s)..", file=stdout) |
|---|
| 192 | await ehs.add_to_tor(tor_control_proto) |
|---|
| 193 | print(".onion address allocated", file=stdout) |
|---|
| 194 | tor_port = "tcp:%d:interface=127.0.0.1" % local_port |
|---|
| 195 | tor_location = "tor:%s:%d" % (ehs.hostname, external_port) |
|---|
| 196 | privkey = ehs.private_key |
|---|
| 197 | await ehs.remove_from_tor(tor_control_proto) |
|---|
| 198 | |
|---|
| 199 | # in addition to the "how to launch/connect-to tor" keys above, we also |
|---|
| 200 | # record information about the onion service into tahoe.cfg. |
|---|
| 201 | # * "local_port" is a server endpont string, which should match |
|---|
| 202 | # "tor_port" (which will be added to tahoe.cfg [node] tub.port) |
|---|
| 203 | # * "external_port" is the random "public onion port" (integer), which |
|---|
| 204 | # (when combined with the .onion address) should match "tor_location" |
|---|
| 205 | # (which will be added to tub.location) |
|---|
| 206 | # * "private_key_file" points to the on-disk copy of the private key |
|---|
| 207 | # material (although we always write it to the same place) |
|---|
| 208 | |
|---|
| 209 | tahoe_config_tor.extend([ |
|---|
| 210 | ("onion", "true"), |
|---|
| 211 | ("onion.local_port", str(local_port)), |
|---|
| 212 | ("onion.external_port", str(external_port)), |
|---|
| 213 | ("onion.private_key_file", os.path.join("private", "tor_onion.privkey")), |
|---|
| 214 | ]) |
|---|
| 215 | privkeyfile = os.path.join(private_dir, "tor_onion.privkey") |
|---|
| 216 | with open(privkeyfile, "wb") as f: |
|---|
| 217 | if isinstance(privkey, str): |
|---|
| 218 | privkey = privkey.encode("ascii") |
|---|
| 219 | f.write(privkey) |
|---|
| 220 | |
|---|
| 221 | # tahoe_config_tor: this is a dictionary of keys/values to add to the |
|---|
| 222 | # "[tor]" section of tahoe.cfg, which tells the new node how to launch |
|---|
| 223 | # Tor in the right way. |
|---|
| 224 | |
|---|
| 225 | # tor_port: a server endpoint string, it will be added to tub.port= |
|---|
| 226 | |
|---|
| 227 | # tor_location: a foolscap connection hint, "tor:ONION:EXTERNAL_PORT" |
|---|
| 228 | |
|---|
| 229 | # We assume/require that the Node gives us the same data_directory= |
|---|
| 230 | # at both create-node and startup time. The data directory is not |
|---|
| 231 | # recorded in tahoe.cfg |
|---|
| 232 | |
|---|
| 233 | return ListenerConfig( |
|---|
| 234 | [tor_port], |
|---|
| 235 | [tor_location], |
|---|
| 236 | {"tor": tahoe_config_tor}, |
|---|
| 237 | ) |
|---|
| 238 | |
|---|
| 239 | |
|---|
| 240 | @implementer(IAddressFamily) |
|---|
| 241 | class _Provider(service.MultiService): |
|---|
| 242 | def __init__(self, config, reactor, tor, txtorcon): |
|---|
| 243 | service.MultiService.__init__(self) |
|---|
| 244 | self._config = config |
|---|
| 245 | self._tor_launched = None |
|---|
| 246 | self._onion_ehs = None |
|---|
| 247 | self._onion_tor_control_proto = None |
|---|
| 248 | self._tor = tor |
|---|
| 249 | self._txtorcon = txtorcon |
|---|
| 250 | self._reactor = reactor |
|---|
| 251 | |
|---|
| 252 | def _get_tor_config(self, *args, **kwargs): |
|---|
| 253 | return self._config.get_config("tor", *args, **kwargs) |
|---|
| 254 | |
|---|
| 255 | def get_listener(self): |
|---|
| 256 | local_port = int(self._get_tor_config("onion.local_port")) |
|---|
| 257 | ep = TCP4ServerEndpoint(self._reactor, local_port, interface="127.0.0.1") |
|---|
| 258 | return ep |
|---|
| 259 | |
|---|
| 260 | def get_client_endpoint(self): |
|---|
| 261 | """ |
|---|
| 262 | Get an ``IStreamClientEndpoint`` which will set up a connection using Tor. |
|---|
| 263 | |
|---|
| 264 | If Tor is not enabled or the dependencies are not available, return |
|---|
| 265 | ``None`` instead. |
|---|
| 266 | """ |
|---|
| 267 | enabled = self._get_tor_config("enabled", True, boolean=True) |
|---|
| 268 | if not enabled: |
|---|
| 269 | return None |
|---|
| 270 | if not self._tor: |
|---|
| 271 | return None |
|---|
| 272 | |
|---|
| 273 | if self._get_tor_config("launch", False, boolean=True): |
|---|
| 274 | if not self._txtorcon: |
|---|
| 275 | return None |
|---|
| 276 | return self._tor.control_endpoint_maker(self._make_control_endpoint, |
|---|
| 277 | takes_status=True) |
|---|
| 278 | |
|---|
| 279 | socks_endpoint_desc = self._get_tor_config("socks.port", None) |
|---|
| 280 | if socks_endpoint_desc: |
|---|
| 281 | socks_ep = clientFromString(self._reactor, socks_endpoint_desc) |
|---|
| 282 | return self._tor.socks_endpoint(socks_ep) |
|---|
| 283 | |
|---|
| 284 | controlport = self._get_tor_config("control.port", None) |
|---|
| 285 | if controlport: |
|---|
| 286 | ep = clientFromString(self._reactor, controlport) |
|---|
| 287 | return self._tor.control_endpoint(ep) |
|---|
| 288 | |
|---|
| 289 | return self._tor.default_socks() |
|---|
| 290 | |
|---|
| 291 | # Backwards compatibility alias |
|---|
| 292 | get_tor_handler = get_client_endpoint |
|---|
| 293 | |
|---|
| 294 | @inlineCallbacks |
|---|
| 295 | def _make_control_endpoint(self, reactor, update_status): |
|---|
| 296 | # this will only be called when tahoe.cfg has "[tor] launch = true" |
|---|
| 297 | update_status("launching Tor") |
|---|
| 298 | with self._tor.add_context(update_status, "launching Tor"): |
|---|
| 299 | (endpoint_desc, _) = yield self._get_launched_tor(reactor) |
|---|
| 300 | tor_control_endpoint = clientFromString(reactor, endpoint_desc) |
|---|
| 301 | returnValue(tor_control_endpoint) |
|---|
| 302 | |
|---|
| 303 | def _get_launched_tor(self, reactor): |
|---|
| 304 | # this fires with a tuple of (control_endpoint, txtorcon.Tor instance) |
|---|
| 305 | if not self._tor_launched: |
|---|
| 306 | self._tor_launched = OneShotObserverList() |
|---|
| 307 | private_dir = self._config.get_config_path("private") |
|---|
| 308 | tor_binary = self._get_tor_config("tor.executable", None) |
|---|
| 309 | d = _launch_tor(reactor, tor_binary, private_dir, self._txtorcon) |
|---|
| 310 | d.addBoth(self._tor_launched.fire) |
|---|
| 311 | return self._tor_launched.when_fired() |
|---|
| 312 | |
|---|
| 313 | def check_onion_config(self): |
|---|
| 314 | if self._get_tor_config("onion", False, boolean=True): |
|---|
| 315 | if not self._txtorcon: |
|---|
| 316 | raise ValueError("Cannot create onion without txtorcon. " |
|---|
| 317 | "Please 'pip install tahoe-lafs[tor]' to fix.") |
|---|
| 318 | |
|---|
| 319 | # to start an onion server, we either need a Tor control port, or |
|---|
| 320 | # we need to launch tor |
|---|
| 321 | launch = self._get_tor_config("launch", False, boolean=True) |
|---|
| 322 | controlport = self._get_tor_config("control.port", None) |
|---|
| 323 | if not launch and not controlport: |
|---|
| 324 | raise ValueError("[tor] onion = true, but we have neither " |
|---|
| 325 | "launch=true nor control.port=") |
|---|
| 326 | # check that all the expected onion-specific keys are present |
|---|
| 327 | def require(name): |
|---|
| 328 | if not self._get_tor_config("onion.%s" % name, None): |
|---|
| 329 | raise ValueError("[tor] onion = true," |
|---|
| 330 | " but onion.%s= is missing" % name) |
|---|
| 331 | require("local_port") |
|---|
| 332 | require("external_port") |
|---|
| 333 | require("private_key_file") |
|---|
| 334 | |
|---|
| 335 | def get_tor_instance(self, reactor: object): |
|---|
| 336 | """Return a ``Deferred`` that fires with a ``txtorcon.Tor`` instance.""" |
|---|
| 337 | # launch tor, if necessary |
|---|
| 338 | if self._get_tor_config("launch", False, boolean=True): |
|---|
| 339 | return self._get_launched_tor(reactor).addCallback(lambda t: t[1]) |
|---|
| 340 | else: |
|---|
| 341 | controlport = self._get_tor_config("control.port", None) |
|---|
| 342 | tcep = clientFromString(reactor, controlport) |
|---|
| 343 | return self._txtorcon.connect(reactor, tcep) |
|---|
| 344 | |
|---|
| 345 | @inlineCallbacks |
|---|
| 346 | def _start_onion(self, reactor): |
|---|
| 347 | tor_instance = yield self.get_tor_instance(reactor) |
|---|
| 348 | tor_control_proto = tor_instance.protocol |
|---|
| 349 | local_port = int(self._get_tor_config("onion.local_port")) |
|---|
| 350 | external_port = int(self._get_tor_config("onion.external_port")) |
|---|
| 351 | |
|---|
| 352 | fn = self._get_tor_config("onion.private_key_file") |
|---|
| 353 | privkeyfile = self._config.get_config_path(fn) |
|---|
| 354 | with open(privkeyfile, "rb") as f: |
|---|
| 355 | privkey = f.read() |
|---|
| 356 | ehs = self._txtorcon.EphemeralHiddenService( |
|---|
| 357 | "%d 127.0.0.1:%d" % (external_port, local_port), privkey) |
|---|
| 358 | yield ehs.add_to_tor(tor_control_proto) |
|---|
| 359 | self._onion_ehs = ehs |
|---|
| 360 | self._onion_tor_control_proto = tor_control_proto |
|---|
| 361 | |
|---|
| 362 | |
|---|
| 363 | def startService(self): |
|---|
| 364 | service.MultiService.startService(self) |
|---|
| 365 | # if we need to start an onion service, now is the time |
|---|
| 366 | if self._get_tor_config("onion", False, boolean=True): |
|---|
| 367 | return self._start_onion(self._reactor) # so tests can synchronize |
|---|
| 368 | |
|---|
| 369 | @inlineCallbacks |
|---|
| 370 | def stopService(self): |
|---|
| 371 | if self._onion_ehs and self._onion_tor_control_proto: |
|---|
| 372 | yield self._onion_ehs.remove_from_tor(self._onion_tor_control_proto) |
|---|
| 373 | # TODO: can we also stop tor? |
|---|
| 374 | yield service.MultiService.stopService(self) |
|---|