Loading lib/chutney/TorNet.py +9 −3 Original line number Diff line number Diff line Loading @@ -375,7 +375,13 @@ class Node(object): @property def hs_hostname(self) -> Option[str]: """Generated hostname for this hidden service""" """Generated hostname for this hidden service. Should be available (non-None) if the node is configured as a hidden service (`Node.is_hs`), after the Node's builder's `preConfig` has been called (which the chutney `Network` does as part of this node's `config_phase`). """ return self._builder.get_hs_hostname() ###### Loading Loading @@ -493,8 +499,8 @@ class NodeBuilder(ABC): def get_hs_hostname(self) -> Option[str]: """Return the hidden service hostname, if any. Returns `Option(None)` if there is no hidden service for this node, or if the hostname isn't available yet. Should be available (non-None) if the node is configured as a hidden service (`Node.is_hs`), after `preConfig` has been called. """ ... Loading lib/chutney/arti/builder.py +24 −9 Original line number Diff line number Diff line Loading @@ -39,7 +39,7 @@ class LocalArtiNodeBuilder(TorNet.NodeBuilder): def _info_log_path(self) -> Path: return self._node.dir.joinpath("info.log") def _gen_config_str(self, net: TorNet.Network) -> str: def _gen_config(self, net: TorNet.Network) -> dict[str, object]: if self._node._config.exit: raise ChutneyInternalError("Arti exit unimplemented") if self._node._config.authority: Loading Loading @@ -117,15 +117,32 @@ class LocalArtiNodeBuilder(TorNet.NodeBuilder): bridges = check_type(config.setdefault("bridges", {}), dict) bridges["enabled"] = bool(self._node._config.bridgeclient) return tomli_w.dumps(config) return config @override def checkConfig(self, net: TorNet.Network) -> None: self._gen_config_str(net) # Just check that it doesn't fail to generate. self._gen_config(net) def _early_config_path(self) -> Path: return self._node.dir.joinpath("early_config.toml") def _generate_early_config(self, net: TorNet.Network) -> None: early_config: dict[str, object] = self._gen_config(net) early_config_str: str = tomli_w.dumps(early_config) with self._early_config_path().open("w") as f: f.write( "# Early version of config that doesn't depend on other node configs.\n" ) f.write( "# Used e.g. when running arti during configuration to generate keys.\n" ) f.write(early_config_str) @override def preConfig(self, net: TorNet.Network) -> None: pass mkdir_p(self._node.dir) self._generate_early_config(net) @override def get_fingerprint(self) -> Option[str]: Loading @@ -137,10 +154,7 @@ class LocalArtiNodeBuilder(TorNet.NodeBuilder): @override def config(self, net: TorNet.Network) -> None: config_str = self._gen_config_str(net) mkdir_p(self._node.dir) with self._node.torrc_path.open("w") as f: f.write(config_str) self._node.torrc_path.write_text(tomli_w.dumps(self._gen_config(net))) @override def postConfig(self, net: TorNet.Network) -> None: Loading Loading @@ -178,10 +192,11 @@ class LocalArtiNodeBuilder(TorNet.NodeBuilder): # stdout along with the actual output we want. # <https://gitlab.torproject.org/tpo/core/arti/-/issues/2024> "--log-level=", f"--config={str(self._node.torrc_path)}", f"--config={str(self._early_config_path())}", "hss", f"--nickname={_HS_NICKNAME}", "onion-address", "--generate=if-needed", ], capture_output=True, text=True, Loading lib/chutney/tor/builder.py +59 −5 Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ import logging import re import shutil import subprocess import time from pathlib import Path from typeguard import check_type Loading Loading @@ -233,12 +234,65 @@ class LocalNodeBuilder(TorNet.NodeBuilder): self._genRouterKey() if self._node._config.hs: self._makeHiddenServiceDir() self._generate_hs_state() if net.family_ids: for fid in self._node._config.families: shutil.copy( TorNet.get_familykey_path(fid), Path(self._node.dir, "keys") ) def _generate_hs_state(self) -> None: # Force generation of the hidden service key and address. p = launch_process( [ str(self._node._config.tor), "-f", # Read config from stdin; we don't need or want most of the # generated torrc. e.g. we don't want this output in the # logfiles. "-", # Don't try to connect to the network "-DisableNetwork", "1", # Do generate our hidden service state "-HiddenServiceDir", str(self._node.dir.joinpath(self._node._config.hs_directory)), # Dummy ports; tor won't try to bind to them since the # network is disabled. "-HiddenServicePort", "1 1", ], stdin=subprocess.DEVNULL, ) start_time = time.time() last_warn_time = start_time while True: if p.poll() is not None: # Process exited unexpectedly. logger.error( "tor exited: %s", p.stdout.read() if p.stdout else "<None>" ) raise ChutneyError( f"tor exited unexpectedly with returncode={p.returncode}" ) if self._hs_hostname_path().exists(): logger.debug("Generated hostname %s", self.get_hs_hostname().unwrap()) # Done. Terminate and wait for process to exit. p.terminate() rv = p.wait() logger.debug("tor exited with returncode %d, after terminating", rv) break logger.debug(f"waiting for tor {p.pid} to generate hidden service state") now = time.time() if (now - last_warn_time) > 5: total_seconds = round(now - start_time, 1) logger.warning( f"{self._node.nick} has been waiting {total_seconds} seconds for" f" tor process {p.pid} to generate {self._hs_hostname_path()}" ) last_warn_time = now time.sleep(0.01) @override def config(self, net: TorNet.Network) -> None: self._createTorrcFile() Loading Loading @@ -442,15 +496,15 @@ class LocalNodeBuilder(TorNet.NodeBuilder): ) return res def _hs_hostname_path(self) -> Path: """The path to the hostname file, if this node has a hidden service""" return self._node.dir.joinpath(self._node._config.hs_directory, "hostname") @override def get_hs_hostname(self) -> Option[str]: if not self._node._config.hs: return Option(None) # a file containing a single line with the hs' .onion address, generated # on first run. hs_hostname_file = self._node.dir.joinpath( self._node._config.hs_directory, "hostname" ) hs_hostname_file = self._hs_hostname_path() try: hostname_file_contents = hs_hostname_file.read_text() except FileNotFoundError: Loading lib/chutney/tor/torrc.py +1 −1 Original line number Diff line number Diff line Loading @@ -120,7 +120,7 @@ def format(n: TorNet.Node) -> str: if n._config.hs: res += textwrap.dedent( f""" HiddenServiceDir {n.dir}/hidden_service HiddenServiceDir {n.dir.joinpath(n._config.hs_directory)} # Redirect requests to the port used by chutney verify HiddenServicePort {n.hs_virtport.unwrap()} 127.0.0.1:{n.hs_targetport.unwrap()} Loading Loading
lib/chutney/TorNet.py +9 −3 Original line number Diff line number Diff line Loading @@ -375,7 +375,13 @@ class Node(object): @property def hs_hostname(self) -> Option[str]: """Generated hostname for this hidden service""" """Generated hostname for this hidden service. Should be available (non-None) if the node is configured as a hidden service (`Node.is_hs`), after the Node's builder's `preConfig` has been called (which the chutney `Network` does as part of this node's `config_phase`). """ return self._builder.get_hs_hostname() ###### Loading Loading @@ -493,8 +499,8 @@ class NodeBuilder(ABC): def get_hs_hostname(self) -> Option[str]: """Return the hidden service hostname, if any. Returns `Option(None)` if there is no hidden service for this node, or if the hostname isn't available yet. Should be available (non-None) if the node is configured as a hidden service (`Node.is_hs`), after `preConfig` has been called. """ ... Loading
lib/chutney/arti/builder.py +24 −9 Original line number Diff line number Diff line Loading @@ -39,7 +39,7 @@ class LocalArtiNodeBuilder(TorNet.NodeBuilder): def _info_log_path(self) -> Path: return self._node.dir.joinpath("info.log") def _gen_config_str(self, net: TorNet.Network) -> str: def _gen_config(self, net: TorNet.Network) -> dict[str, object]: if self._node._config.exit: raise ChutneyInternalError("Arti exit unimplemented") if self._node._config.authority: Loading Loading @@ -117,15 +117,32 @@ class LocalArtiNodeBuilder(TorNet.NodeBuilder): bridges = check_type(config.setdefault("bridges", {}), dict) bridges["enabled"] = bool(self._node._config.bridgeclient) return tomli_w.dumps(config) return config @override def checkConfig(self, net: TorNet.Network) -> None: self._gen_config_str(net) # Just check that it doesn't fail to generate. self._gen_config(net) def _early_config_path(self) -> Path: return self._node.dir.joinpath("early_config.toml") def _generate_early_config(self, net: TorNet.Network) -> None: early_config: dict[str, object] = self._gen_config(net) early_config_str: str = tomli_w.dumps(early_config) with self._early_config_path().open("w") as f: f.write( "# Early version of config that doesn't depend on other node configs.\n" ) f.write( "# Used e.g. when running arti during configuration to generate keys.\n" ) f.write(early_config_str) @override def preConfig(self, net: TorNet.Network) -> None: pass mkdir_p(self._node.dir) self._generate_early_config(net) @override def get_fingerprint(self) -> Option[str]: Loading @@ -137,10 +154,7 @@ class LocalArtiNodeBuilder(TorNet.NodeBuilder): @override def config(self, net: TorNet.Network) -> None: config_str = self._gen_config_str(net) mkdir_p(self._node.dir) with self._node.torrc_path.open("w") as f: f.write(config_str) self._node.torrc_path.write_text(tomli_w.dumps(self._gen_config(net))) @override def postConfig(self, net: TorNet.Network) -> None: Loading Loading @@ -178,10 +192,11 @@ class LocalArtiNodeBuilder(TorNet.NodeBuilder): # stdout along with the actual output we want. # <https://gitlab.torproject.org/tpo/core/arti/-/issues/2024> "--log-level=", f"--config={str(self._node.torrc_path)}", f"--config={str(self._early_config_path())}", "hss", f"--nickname={_HS_NICKNAME}", "onion-address", "--generate=if-needed", ], capture_output=True, text=True, Loading
lib/chutney/tor/builder.py +59 −5 Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ import logging import re import shutil import subprocess import time from pathlib import Path from typeguard import check_type Loading Loading @@ -233,12 +234,65 @@ class LocalNodeBuilder(TorNet.NodeBuilder): self._genRouterKey() if self._node._config.hs: self._makeHiddenServiceDir() self._generate_hs_state() if net.family_ids: for fid in self._node._config.families: shutil.copy( TorNet.get_familykey_path(fid), Path(self._node.dir, "keys") ) def _generate_hs_state(self) -> None: # Force generation of the hidden service key and address. p = launch_process( [ str(self._node._config.tor), "-f", # Read config from stdin; we don't need or want most of the # generated torrc. e.g. we don't want this output in the # logfiles. "-", # Don't try to connect to the network "-DisableNetwork", "1", # Do generate our hidden service state "-HiddenServiceDir", str(self._node.dir.joinpath(self._node._config.hs_directory)), # Dummy ports; tor won't try to bind to them since the # network is disabled. "-HiddenServicePort", "1 1", ], stdin=subprocess.DEVNULL, ) start_time = time.time() last_warn_time = start_time while True: if p.poll() is not None: # Process exited unexpectedly. logger.error( "tor exited: %s", p.stdout.read() if p.stdout else "<None>" ) raise ChutneyError( f"tor exited unexpectedly with returncode={p.returncode}" ) if self._hs_hostname_path().exists(): logger.debug("Generated hostname %s", self.get_hs_hostname().unwrap()) # Done. Terminate and wait for process to exit. p.terminate() rv = p.wait() logger.debug("tor exited with returncode %d, after terminating", rv) break logger.debug(f"waiting for tor {p.pid} to generate hidden service state") now = time.time() if (now - last_warn_time) > 5: total_seconds = round(now - start_time, 1) logger.warning( f"{self._node.nick} has been waiting {total_seconds} seconds for" f" tor process {p.pid} to generate {self._hs_hostname_path()}" ) last_warn_time = now time.sleep(0.01) @override def config(self, net: TorNet.Network) -> None: self._createTorrcFile() Loading Loading @@ -442,15 +496,15 @@ class LocalNodeBuilder(TorNet.NodeBuilder): ) return res def _hs_hostname_path(self) -> Path: """The path to the hostname file, if this node has a hidden service""" return self._node.dir.joinpath(self._node._config.hs_directory, "hostname") @override def get_hs_hostname(self) -> Option[str]: if not self._node._config.hs: return Option(None) # a file containing a single line with the hs' .onion address, generated # on first run. hs_hostname_file = self._node.dir.joinpath( self._node._config.hs_directory, "hostname" ) hs_hostname_file = self._hs_hostname_path() try: hostname_file_contents = hs_hostname_file.read_text() except FileNotFoundError: Loading
lib/chutney/tor/torrc.py +1 −1 Original line number Diff line number Diff line Loading @@ -120,7 +120,7 @@ def format(n: TorNet.Node) -> str: if n._config.hs: res += textwrap.dedent( f""" HiddenServiceDir {n.dir}/hidden_service HiddenServiceDir {n.dir.joinpath(n._config.hs_directory)} # Redirect requests to the port used by chutney verify HiddenServicePort {n.hs_virtport.unwrap()} 127.0.0.1:{n.hs_targetport.unwrap()} Loading