From 7592b2673cdd89685b2e46c03c693c9f18dbc6a6 Mon Sep 17 00:00:00 2001 From: 3np <3np@example.com> Date: Wed, 24 Sep 2025 08:10:29 +0000 Subject: [PATCH 1/4] tor: allow authenticating to control port using hashedpassword auth - add configuration for tor_control_password for onion services --- docs/PAYJOIN.md | 5 +++++ docs/onion-message-channels.md | 5 +++++ docs/tor.md | 27 ++++++++++++++++++++++++++- scripts/snicker/snicker-server.py | 7 ++++++- src/jmbase/twisted_utils.py | 7 ++++++- src/jmclient/client_protocol.py | 1 + src/jmclient/configure.py | 10 ++++++++++ src/jmdaemon/daemon_protocol.py | 2 ++ src/jmdaemon/onionmc.py | 4 +++- 9 files changed, 64 insertions(+), 4 deletions(-) diff --git a/docs/PAYJOIN.md b/docs/PAYJOIN.md index 2186eb254..6b6f8d42d 100644 --- a/docs/PAYJOIN.md +++ b/docs/PAYJOIN.md @@ -216,6 +216,11 @@ tor_control_host = localhost # note: port needs to be provided (but is ignored for UNIX socket) tor_control_port = 9051 +# by default, the tor control connection used cookie auth, which assumes +# the tor node to be running on a shared filesystem. +# to authenticate to the tor control port using HashedControlPassword instead: +#tor_control_password=xxx + # the host/port actually serving the hidden service # (note the *virtual port*, that the client uses, # is hardcoded to 80): diff --git a/docs/onion-message-channels.md b/docs/onion-message-channels.md index c21483d9f..03e8c0e93 100644 --- a/docs/onion-message-channels.md +++ b/docs/onion-message-channels.md @@ -46,6 +46,11 @@ tor_control_host = localhost # note: port needs to be provided (but is ignored for UNIX socket) tor_control_port = 9051 +# by default, the tor control connection used cookie auth, which assumes +# the tor node to be running on a shared filesystem. +# to authenticate to the tor control port using HashedControlPassword instead: +#tor_control_password=xxx + # the host/port actually serving the hidden service # (note the *virtual port*, that the client uses, # is hardcoded to as per below 'directory node configuration'. diff --git a/docs/tor.md b/docs/tor.md index 01efa805d..ec48600de 100644 --- a/docs/tor.md +++ b/docs/tor.md @@ -38,7 +38,7 @@ To use the new onion messaging system (see [here](onion-message-channels.md)) as -#### Configuring Tor to setup an onion service +#### Configuring Tor to setup an onion service (local Tor node) (These steps were prepared using Ubuntu; you may have to adjust for your distro). @@ -83,3 +83,28 @@ sudo service tor start ``` Once this is done, you should be able to start the yieldgenerator successfully. + +#### Configuring Tor to setup an onion service (remote Tor node) + +Cookie authentication assumes the Joinmarket and Tor processes have shared filesystem access to where Tor stores its control cookie. + +If this is not possible or desired, for example if Tor is running on another machine, you can instead use a password. On the tor node, hash the password: + +``` +tor --hash-password !CHANGEME! + +sudo vim /etc/tor/torrc +``` + +, and add it to the torrc: + +``` +ControlPort 9051 +HashedControlPassword RESULT_OF_hash-password +``` + +Then add the same password to your joinmarket configuration: + +``` +tor_control_password=!CHANGEME! +``` diff --git a/scripts/snicker/snicker-server.py b/scripts/snicker/snicker-server.py index a132c69c9..15c2b4673 100755 --- a/scripts/snicker/snicker-server.py +++ b/scripts/snicker/snicker-server.py @@ -302,11 +302,16 @@ def start_tor(self): if not self.local_port: control_host = jm_single().config.get("PAYJOIN", "tor_control_host") control_port = int(jm_single().config.get("PAYJOIN", "tor_control_port")) + control_pass = jm_single().config.get("PAYJOIN", "tor_control_password") if str(control_host).startswith('unix:'): control_endpoint = UNIXClientEndpoint(reactor, control_host[5:]) else: control_endpoint = TCP4ClientEndpoint(reactor, control_host, control_port) - d = txtorcon.connect(reactor, control_endpoint) + + if control_pass is None or len(str(control_pass)) == 0: + d = txtorcon.connect(reactor, control_endpoint) + else: + d = txtorcon.connect(reactor, control_endpoint, password_function=lambda : control_pass) d.addCallback(self.create_onion_ep) d.addErrback(self.setup_failed) else: diff --git a/src/jmbase/twisted_utils.py b/src/jmbase/twisted_utils.py index ef50b26ad..71f73e249 100644 --- a/src/jmbase/twisted_utils.py +++ b/src/jmbase/twisted_utils.py @@ -137,6 +137,7 @@ def __init__(self, proto_factory_or_resource, info_callback, tor_control_port, serving_host, serving_port, virtual_port=None, shutdown_callback=None, + tor_control_password=None, hidden_service_dir=""): if isinstance(proto_factory_or_resource, Resource): # TODO bad naming, in this case it doesn't start @@ -158,6 +159,7 @@ def __init__(self, proto_factory_or_resource, info_callback, self.virtual_port = virtual_port self.tor_control_host = tor_control_host self.tor_control_port = tor_control_port + self.tor_control_password = tor_control_password # note that defaults only exist in jmclient # config object, so no default here: self.serving_host = serving_host @@ -183,7 +185,10 @@ def start_tor(self): else: control_endpoint = TCP4ClientEndpoint(reactor, self.tor_control_host, self.tor_control_port) - d = txtorcon.connect(reactor, control_endpoint) + if self.tor_control_password is None: + d = txtorcon.connect(reactor, control_endpoint) + else: + d = txtorcon.connect(reactor, control_endpoint, password_function=lambda : self.tor_control_password) d.addCallback(self.create_onion_ep) d.addErrback(self.setup_failed) # TODO: add errbacks to the next two calls in diff --git a/src/jmclient/client_protocol.py b/src/jmclient/client_protocol.py index 180932d5b..e8c2e7064 100644 --- a/src/jmclient/client_protocol.py +++ b/src/jmclient/client_protocol.py @@ -87,6 +87,7 @@ def connectionMade(self): netconfig = {"port": 80, "tor_control_host": jcg("PAYJOIN", "tor_control_host"), "tor_control_port": jcg("PAYJOIN", "tor_control_port"), + "tor_control_password": jcg("PAYJOIN", "tor_control_password"), "onion_serving_host": jcg("PAYJOIN", "onion_serving_host"), "onion_serving_port": jcg("PAYJOIN", "onion_serving_port")} d = self.callRemote(commands.BIP78ReceiverInit, diff --git a/src/jmclient/configure.py b/src/jmclient/configure.py index bb5559049..097876fbb 100644 --- a/src/jmclient/configure.py +++ b/src/jmclient/configure.py @@ -158,6 +158,10 @@ def jm_single() -> AttributeDict: # tor_control_host = unix:/var/run/tor/control # note: port needs to be provided (but is ignored for UNIX socket) tor_control_port = 9051 +# by default, the tor control connection used cookie auth, which assumes +# the tor node to be running on a shared filesystem. +# to authenticate to the tor control port using HashedControlPassword instead: +#tor_control_password=xxx # the host/port actually serving the hidden service # (note the *virtual port*, that the client uses, @@ -450,6 +454,11 @@ def jm_single() -> AttributeDict: # note: port needs to be provided (but is ignored for UNIX socket) tor_control_port = 9051 +# by default, the tor control connection used cookie auth, which assumes +# the tor node to be running on a shared filesystem. +# to authenticate to the tor control port using HashedControlPassword instead: +#tor_control_password=xxx + # the host/port actually serving the hidden service # (note the *virtual port*, that the client uses, # is hardcoded to 80): @@ -533,6 +542,7 @@ def get_mchannels(mode: str = "TAKER") -> list: onion_fields = [("type", str), ("directory_nodes", str), ("regtest_count", str), ("socks5_host", str), ("socks5_port", int), ("tor_control_host", str), ("tor_control_port", int), + ("tor_control_password", str), ("onion_serving_host", str), ("onion_serving_port", int), ("hidden_service_dir", str)] diff --git a/src/jmdaemon/daemon_protocol.py b/src/jmdaemon/daemon_protocol.py index 9fdd641d6..b1354515b 100644 --- a/src/jmdaemon/daemon_protocol.py +++ b/src/jmdaemon/daemon_protocol.py @@ -281,6 +281,7 @@ def on_BIP78_RECEIVER_INIT(self, netconfig): self.serving_port = int(netconfig["port"]) self.tor_control_host = netconfig["tor_control_host"] self.tor_control_port = int(netconfig["tor_control_port"]) + self.tor_control_password = netconfig["tor_control_password"] self.onion_serving_host=netconfig["onion_serving_host"] self.onion_serving_port=int(netconfig["onion_serving_port"]) self.bip78_rr = BIP78ReceiverResource(self.info_callback, @@ -294,6 +295,7 @@ def on_BIP78_RECEIVER_INIT(self, netconfig): self.tor_control_port, self.onion_serving_host, self.onion_serving_port, + tor_control_password=self.tor_control_password, shutdown_callback=self.shutdown_callback) # this call will start bringing up the HS; when it's finished, # it will fire the `onion_hostname_callback`, or if it fails, diff --git a/src/jmdaemon/onionmc.py b/src/jmdaemon/onionmc.py index ca5110d8e..ede40d5a5 100644 --- a/src/jmdaemon/onionmc.py +++ b/src/jmdaemon/onionmc.py @@ -641,7 +641,8 @@ def __init__(self, self.serverport = self.hostid self.tor_control_host = configdata["tor_control_host"] self.tor_control_port = configdata["tor_control_port"] - self.onion_serving_host=configdata["onion_serving_host"] + self.tor_control_password = configdata["tor_control_password"] + self.onion_serving_host = configdata["onion_serving_host"] self.onion_serving = configdata["serving"] if self.onion_serving: self.onion_serving_port = configdata["onion_serving_port"] @@ -691,6 +692,7 @@ def __init__(self, self.onion_serving_port, virtual_port=ONION_VIRTUAL_PORT, shutdown_callback=self.shutdown_callback, + tor_control_password=self.tor_control_password, hidden_service_dir=self.hidden_service_dir) # this call will start bringing up the HS; when it's finished, # it will fire the `onion_hostname_callback`, or if it fails, From f08e75940c0079e325b6352e8684908ee974b5f5 Mon Sep 17 00:00:00 2001 From: 3nprob <74199244+3nprob@users.noreply.github.com> Date: Tue, 28 Oct 2025 20:00:39 +0000 Subject: [PATCH 2/4] chore: Leading space for config comment Co-authored-by: roshii <6266997+roshii@users.noreply.github.com> --- docs/PAYJOIN.md | 2 +- docs/onion-message-channels.md | 2 +- src/jmclient/configure.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/PAYJOIN.md b/docs/PAYJOIN.md index 6b6f8d42d..e6981b810 100644 --- a/docs/PAYJOIN.md +++ b/docs/PAYJOIN.md @@ -219,7 +219,7 @@ tor_control_port = 9051 # by default, the tor control connection used cookie auth, which assumes # the tor node to be running on a shared filesystem. # to authenticate to the tor control port using HashedControlPassword instead: -#tor_control_password=xxx +# tor_control_password=xxx # the host/port actually serving the hidden service # (note the *virtual port*, that the client uses, diff --git a/docs/onion-message-channels.md b/docs/onion-message-channels.md index 03e8c0e93..f3bd9081e 100644 --- a/docs/onion-message-channels.md +++ b/docs/onion-message-channels.md @@ -49,7 +49,7 @@ tor_control_port = 9051 # by default, the tor control connection used cookie auth, which assumes # the tor node to be running on a shared filesystem. # to authenticate to the tor control port using HashedControlPassword instead: -#tor_control_password=xxx +# tor_control_password=xxx # the host/port actually serving the hidden service # (note the *virtual port*, that the client uses, diff --git a/src/jmclient/configure.py b/src/jmclient/configure.py index 097876fbb..85a4aa6e5 100644 --- a/src/jmclient/configure.py +++ b/src/jmclient/configure.py @@ -161,7 +161,7 @@ def jm_single() -> AttributeDict: # by default, the tor control connection used cookie auth, which assumes # the tor node to be running on a shared filesystem. # to authenticate to the tor control port using HashedControlPassword instead: -#tor_control_password=xxx +# tor_control_password=xxx # the host/port actually serving the hidden service # (note the *virtual port*, that the client uses, @@ -457,7 +457,7 @@ def jm_single() -> AttributeDict: # by default, the tor control connection used cookie auth, which assumes # the tor node to be running on a shared filesystem. # to authenticate to the tor control port using HashedControlPassword instead: -#tor_control_password=xxx +# tor_control_password=xxx # the host/port actually serving the hidden service # (note the *virtual port*, that the client uses, From 5dec884470774ea3f65f8ed64d6d19a5703042b9 Mon Sep 17 00:00:00 2001 From: 3nprob <74199244+3nprob@users.noreply.github.com> Date: Tue, 28 Oct 2025 20:06:27 +0000 Subject: [PATCH 3/4] use .get for accessing optional tor_control_password config entry Co-authored-by: roshii <6266997+roshii@users.noreply.github.com> --- src/jmdaemon/daemon_protocol.py | 2 +- src/jmdaemon/onionmc.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/jmdaemon/daemon_protocol.py b/src/jmdaemon/daemon_protocol.py index b1354515b..5d5ee44f7 100644 --- a/src/jmdaemon/daemon_protocol.py +++ b/src/jmdaemon/daemon_protocol.py @@ -281,7 +281,7 @@ def on_BIP78_RECEIVER_INIT(self, netconfig): self.serving_port = int(netconfig["port"]) self.tor_control_host = netconfig["tor_control_host"] self.tor_control_port = int(netconfig["tor_control_port"]) - self.tor_control_password = netconfig["tor_control_password"] + self.tor_control_password = netconfig.get("tor_control_password") self.onion_serving_host=netconfig["onion_serving_host"] self.onion_serving_port=int(netconfig["onion_serving_port"]) self.bip78_rr = BIP78ReceiverResource(self.info_callback, diff --git a/src/jmdaemon/onionmc.py b/src/jmdaemon/onionmc.py index ede40d5a5..c5df4136f 100644 --- a/src/jmdaemon/onionmc.py +++ b/src/jmdaemon/onionmc.py @@ -641,7 +641,7 @@ def __init__(self, self.serverport = self.hostid self.tor_control_host = configdata["tor_control_host"] self.tor_control_port = configdata["tor_control_port"] - self.tor_control_password = configdata["tor_control_password"] + self.tor_control_password = configdata.get("tor_control_password") self.onion_serving_host = configdata["onion_serving_host"] self.onion_serving = configdata["serving"] if self.onion_serving: From 408124adee79bf9f1b18e9d1a4c0fa0b0702b411 Mon Sep 17 00:00:00 2001 From: 3np <3np@example.com> Date: Sat, 1 Nov 2025 02:18:24 +0000 Subject: [PATCH 4/4] test: add tor_control_password to test config --- test/regtest_joinmarket.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/test/regtest_joinmarket.cfg b/test/regtest_joinmarket.cfg index ab0742aac..9bcd738dc 100644 --- a/test/regtest_joinmarket.cfg +++ b/test/regtest_joinmarket.cfg @@ -50,6 +50,7 @@ tor_control_host = localhost # or, to use a UNIX socket # control_host = unix:/var/run/tor/control tor_control_port = 9051 +tor_control_password = mytorcontrolpassword # the host/port actually serving the hidden service # (note the *virtual port*, that the client uses, # is hardcoded to 80):