Commit 60d0051f authored by Jim Newsome's avatar Jim Newsome
Browse files

Merge branch 'make-hs-addr-available-sooner' into 'main'

Generate hidden service addresses during preConfig

See merge request tpo/core/chutney!91
parents 0246b2db 0c6a088d
Loading
Loading
Loading
Loading
Loading
+9 −3
Original line number Diff line number Diff line
@@ -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()

    ######
@@ -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.
        """
        ...

+24 −9
Original line number Diff line number Diff line
@@ -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:
@@ -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]:
@@ -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:
@@ -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,
+59 −5
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@ import logging
import re
import shutil
import subprocess
import time

from pathlib import Path
from typeguard import check_type
@@ -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()
@@ -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:
+1 −1
Original line number Diff line number Diff line
@@ -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()}