From f7c150fde53c68ac819caa343c2c63d4ef4da98a Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 8 Oct 2026 22:43:55 +0700 Subject: [PATCH] fix(dashmate): add dedicated onion bind for Core 24 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Render bind=127.0.0.1=onion beside the network bind when Tor is enabled, allowing Core to choose its chain-specific onion port. Explain why the listener is required and belongs in the network section; pin that placement for every network and the lack of a preceding section on mainnet. Preserve the original focused Tor migration tests and cover upgrades before and after enabling Tor in one separate case. Core 23.1.8 and Core 24 rc.3 resolve portless onion binds using OnionServiceTargetPort; no migration or Tor sidecar changes are needed. Test would have caught this in CI: ✖ before fix, ✔ after. Against the original template: 6 passing / 4 failing; restored fix: 10 passing. Combined Tor and migration suites: 23 passing. Full Dashmate unit suite: 865 passing; targeted lint and whitespace checks pass. --- .../dashmate/templates/core/dash.conf.dot | 8 +++- .../migrateConfigFileFactory.spec.js | 34 ++++++++++++++ .../test/unit/templates/torTemplates.spec.js | 44 ++++++++++++++++--- 3 files changed, 77 insertions(+), 9 deletions(-) diff --git a/packages/dashmate/templates/core/dash.conf.dot b/packages/dashmate/templates/core/dash.conf.dot index 957a589bd93..4f2cb5763b2 100644 --- a/packages/dashmate/templates/core/dash.conf.dot +++ b/packages/dashmate/templates/core/dash.conf.dot @@ -61,8 +61,9 @@ externalip={{=it.externalIp}} # Tor. The sidecar shares this container's network namespace, so its SOCKS and # control ports are on loopback. Only onion destinations use the proxy: clearnet # peers (including quorum traffic) stay direct, so `proxy=` is deliberately not -# set. Core registers an onion service through the control port and binds the -# default onion target (127.0.0.1:) for its inbound side. +# set. An explicit bind disables Core's automatic onion target, and listenonion=1 +# requires a dedicated non-wildcard onion bind. Since bind is network-only, the +# onion bind is rendered beside the network bind in the # network block below. {{? it.core.tor.enabled }} onion=127.0.0.1:9050 listenonion=1 @@ -148,5 +149,8 @@ addnode={{=seed.host}}:{{=seed.port}}{{~}} # network port={{=it.core.p2p.port}} bind=0.0.0.0 +{{? it.core.tor.enabled }} +bind=127.0.0.1=onion +{{?}} rpcbind=0.0.0.0 rpcport={{=it.core.rpc.port}} diff --git a/packages/dashmate/test/unit/config/configFile/migrateConfigFileFactory.spec.js b/packages/dashmate/test/unit/config/configFile/migrateConfigFileFactory.spec.js index 57f051f0565..2069b45f429 100644 --- a/packages/dashmate/test/unit/config/configFile/migrateConfigFileFactory.spec.js +++ b/packages/dashmate/test/unit/config/configFile/migrateConfigFileFactory.spec.js @@ -342,6 +342,40 @@ describe('migrateConfigFileFactory', () => { }); }); + it('should render a migrated legacy config before and after enabling Tor', async () => { + const { version } = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT_DIR, 'package.json'), 'utf8')); + const baseConfig = container.resolve('defaultConfigs').get('base'); + const renderServiceTemplates = container.resolve('renderServiceTemplates'); + + for (const fromVersion of ['4.1.0', '4.1.1', '4.2.0-dev.1']) { + const configFileData = createConfigFile().toObject(); + configFileData.configFormatVersion = fromVersion; + const devnetConfig = new Config('devnet', baseConfig.getStoredOptions()); + devnetConfig.set('network', 'devnet'); + configFileData.configs.devnet = devnetConfig.getStoredOptions(); + for (const options of Object.values(configFileData.configs)) { + delete options.core.tor; + } + + const migrated = migrateConfigFile(configFileData, fromVersion, version); + + for (const [name, options] of Object.entries(migrated.configs)) { + const config = new Config(name, options); + const disabledConf = renderServiceTemplates(config)['core/dash.conf']; + expect(disabledConf).to.match(/^listenonion=0$/m); + expect(disabledConf.match(/^bind=.*$/gm)).to.deep.equal(['bind=0.0.0.0']); + + config.set('core.tor.enabled', true); + const enabledConf = renderServiceTemplates(config)['core/dash.conf']; + expect(enabledConf).to.match(/^listenonion=1$/m); + expect(enabledConf.match(/^bind=.*$/gm)).to.deep.equal([ + 'bind=0.0.0.0', + 'bind=127.0.0.1=onion', + ]); + } + } + }); + it('should keep an operator image that predates the 4.0.0 re-pin', async () => { // Every config older than 4.0.0 crosses the unconditional re-pin in that // migration, so it is the first place operator intent can be respected. It diff --git a/packages/dashmate/test/unit/templates/torTemplates.spec.js b/packages/dashmate/test/unit/templates/torTemplates.spec.js index 9598d0a564e..fcbc4ff4958 100644 --- a/packages/dashmate/test/unit/templates/torTemplates.spec.js +++ b/packages/dashmate/test/unit/templates/torTemplates.spec.js @@ -13,15 +13,45 @@ describe('Tor templates', () => { renderServiceTemplates = renderServiceTemplatesFactory(renderTemplateFactory()); }); - it('should leave onion listening off and set no proxy when Tor is disabled', () => { - config.set('core.tor.enabled', false); + [ + ['mainnet', null], + ['testnet', '[test]'], + ['devnet', '[devnet]'], + ['local', '[regtest]'], + ].forEach(([network, section]) => { + it(`should provide the dedicated onion bind required by Core 24 on ${network}`, () => { + config.set('network', network); + config.set('core.tor.enabled', true); + // The onion target is independent of an operator's clearnet P2P port. + config.set('core.p2p.port', 21000); - const dashConf = renderServiceTemplates(config)['core/dash.conf']; + const dashConf = renderServiceTemplates(config)['core/dash.conf']; + // -bind is network-only, so non-mainnet binds must be in their network section. + const networkConf = section ? dashConf.split(section)[1] : dashConf; - expect(dashConf).to.match(/^listenonion=0$/m); - expect(dashConf).to.not.match(/^onion=/m); - expect(dashConf).to.not.match(/^torcontrol=/m); - expect(dashConf).to.not.match(/^proxy=/m); + expect(dashConf).to.match(/^listenonion=1$/m); + expect(networkConf.match(/^bind=.*$/gm)).to.deep.equal([ + 'bind=0.0.0.0', + 'bind=127.0.0.1=onion', + ]); + if (network === 'mainnet') { + const beforeOnionBind = dashConf.slice(0, dashConf.indexOf('bind=127.0.0.1=onion')); + expect(beforeOnionBind).to.not.match(/^\s*\[.*\]\s*$/m); + } + }); + + it(`should leave onion listening off and set no proxy when Tor is disabled on ${network}`, () => { + config.set('network', network); + config.set('core.tor.enabled', false); + + const dashConf = renderServiceTemplates(config)['core/dash.conf']; + + expect(dashConf).to.match(/^listenonion=0$/m); + expect(dashConf.match(/^bind=.*$/gm)).to.deep.equal(['bind=0.0.0.0']); + expect(dashConf).to.not.match(/^onion=/m); + expect(dashConf).to.not.match(/^torcontrol=/m); + expect(dashConf).to.not.match(/^proxy=/m); + }); }); it('should point Core at the sidecar on loopback by default', () => {