diff --git a/packages/suite-desktop-core/src/index.d.ts b/packages/suite-desktop-core/src/index.d.ts index 3c39c93cfb37..64d3b1b6a4ab 100644 --- a/packages/suite-desktop-core/src/index.d.ts +++ b/packages/suite-desktop-core/src/index.d.ts @@ -100,8 +100,11 @@ declare type UpdateSettings = { declare type TorSettings = { running: boolean; // Tor should be enabled host: string; // Hostname of the tor process through which traffic is routed - port: number; // Port of the tor process through which traffic is routed + port: number; // Port of the Tor process through which traffic is routed + controlPort: number; // Port of the Tor Control Port + torDataDir: string; // Path of tor data directory snowflakeBinaryPath: string; // Path in user system to the snowflake binary + useExternalTor: boolean; // Tor should use external daemon instead of the one built-in suite. }; declare type BridgeSettings = { diff --git a/packages/suite-desktop-core/src/libs/processes/TorExternalProcess.ts b/packages/suite-desktop-core/src/libs/processes/TorExternalProcess.ts new file mode 100644 index 000000000000..d65b68737be8 --- /dev/null +++ b/packages/suite-desktop-core/src/libs/processes/TorExternalProcess.ts @@ -0,0 +1,46 @@ +import { Status } from './BaseProcess'; + +import { TOR_CONTROLLER_STATUS, TorControllerExternal } from '@trezor/request-manager'; + +export type TorProcessStatus = Status & { isBootstrapping?: boolean }; + +const DEFAULT_TOR_EXTERNAL_HOST = '127.0.0.1'; +const DEFAULT_TOR_EXTERNAL_PORT = 9050; + +export class TorExternalProcess { + isStopped = true; + torController: TorControllerExternal; + port = DEFAULT_TOR_EXTERNAL_PORT; + host = DEFAULT_TOR_EXTERNAL_HOST; + constructor() { + this.torController = new TorControllerExternal({ host: this.host, port: this.port }); + } + + public setTorConfig(_torConfig: { useExternalTor: boolean; snowflakeBinaryPath: string }) { + // Do nothing + } + + public getPort() { + return this.port; + } + + public async status(): Promise { + const torControllerStatus = await this.torController.getStatus(); + + return { + service: torControllerStatus === TOR_CONTROLLER_STATUS.ExternalTorRunning, + process: torControllerStatus === TOR_CONTROLLER_STATUS.ExternalTorRunning, + isBootstrapping: torControllerStatus === TOR_CONTROLLER_STATUS.Bootstrapping, // For Tor external we fake bootstrap process. + }; + } + + public async start(): Promise { + this.isStopped = false; + await this.torController.waitUntilAlive(); + } + + public stop() { + // We should not stop External Tor Process but ignore it. + this.isStopped = true; + } +} diff --git a/packages/suite-desktop-core/src/libs/processes/TorProcess.ts b/packages/suite-desktop-core/src/libs/processes/TorProcess.ts index 6ac1d5b56225..97b7183f5a81 100644 --- a/packages/suite-desktop-core/src/libs/processes/TorProcess.ts +++ b/packages/suite-desktop-core/src/libs/processes/TorProcess.ts @@ -31,11 +31,15 @@ export class TorProcess extends BaseProcess { }); } - setTorConfig(torConfig: Pick) { + public setTorConfig(torConfig: Pick) { this.snowflakeBinaryPath = torConfig.snowflakeBinaryPath; } - async status(): Promise { + public getPort() { + return this.port; + } + + public async status(): Promise { const torControllerStatus = await this.torController.getStatus(); return { @@ -45,7 +49,7 @@ export class TorProcess extends BaseProcess { }; } - async start(): Promise { + public async start(): Promise { const electronProcessId = process.pid; const torConfiguration = await this.torController.getTorConfiguration( electronProcessId, diff --git a/packages/suite-desktop-core/src/libs/store.ts b/packages/suite-desktop-core/src/libs/store.ts index f8a5cd9e02d0..67700c2b90f0 100644 --- a/packages/suite-desktop-core/src/libs/store.ts +++ b/packages/suite-desktop-core/src/libs/store.ts @@ -63,8 +63,11 @@ export class Store { return this.store.get('torSettings', { running: false, port: 9050, + controlPort: 9051, host: '127.0.0.1', snowflakeBinaryPath: '', + useExternalTor: false, + torDataDir: '', }); } diff --git a/packages/suite-desktop-core/src/modules/tor.ts b/packages/suite-desktop-core/src/modules/tor.ts index 5c3ca1b5c3cc..016ed250a30f 100644 --- a/packages/suite-desktop-core/src/modules/tor.ts +++ b/packages/suite-desktop-core/src/modules/tor.ts @@ -12,28 +12,52 @@ import { getFreePort } from '@trezor/node-utils'; import { validateIpcMessage } from '@trezor/ipc-proxy'; import { TorProcess, TorProcessStatus } from '../libs/processes/TorProcess'; +import { TorExternalProcess } from '../libs/processes/TorExternalProcess'; import { app, ipcMain } from '../typed-electron'; import type { Dependencies } from './index'; const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) => { const { logger } = global; - const host = '127.0.0.1'; - const port = await getFreePort(); - const controlPort = await getFreePort(); - const torDataDir = path.join(app.getPath('userData'), 'tor'); const initialSettings = store.getTorSettings(); - store.setTorSettings({ ...initialSettings, host, port }); - - const tor = new TorProcess({ - host, - port, - controlPort, - torDataDir, - snowflakeBinaryPath: initialSettings.snowflakeBinaryPath, + store.setTorSettings({ + ...initialSettings, + port: await getFreePort(), + controlPort: await getFreePort(), + torDataDir: path.join(app.getPath('userData'), 'tor'), }); + const settings = store.getTorSettings(); + + const processes = [ + { + type: 'tor', + process: new TorProcess({ + host: settings.host, + port: settings.port, + controlPort: settings.controlPort, + torDataDir: settings.torDataDir, + snowflakeBinaryPath: settings.snowflakeBinaryPath, + }), + }, + { + type: 'tor-external', + process: new TorExternalProcess(), + }, + ]; + + const getTarget = () => { + const { useExternalTor } = store.getTorSettings(); + const currentTarget = useExternalTor ? 'tor-external' : 'tor'; + + return processes.find(process => process.type === currentTarget)!.process; + }; + + const updateTorPort = (port: number) => { + store.setTorSettings({ ...store.getTorSettings(), port }); + }; + const setProxy = (rule: string) => { logger.info('tor', `Setting proxy rules to "${rule}"`); // Including network session of electron auto-updater in the Tor proxy. @@ -44,9 +68,15 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) }); }; - const getProxySettings = (shouldEnableTor: boolean) => - shouldEnableTor ? { proxy: `socks://${host}:${port}` } : { proxy: '' }; + const getProxySettings = (shouldEnableTor: boolean) => { + const { useExternalTor, port, host } = store.getTorSettings(); + return shouldEnableTor + ? { + proxy: `socks://${host}:${useExternalTor ? 9050 : port}`, + } + : { proxy: '' }; + }; const handleTorProcessStatus = (status: TorProcessStatus) => { let type: TorStatus; @@ -90,8 +120,29 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) } }; + const createFakeBootstrapProcess = () => { + let progress = 0; + const duration = 3_000; + // update progress every 300ms. + const interval = 300; + + const increment = (100 / duration) * interval; + const intervalId = setInterval(() => { + progress += increment; + if (progress >= 100) { + progress = 100; + clearInterval(intervalId); + } + handleBootstrapEvent({ + type: 'progress', + progress: `${progress}`, + summary: 'Using External Tor fake progress', + }); + }, interval); + }; + const setupTor = async (shouldEnableTor: boolean) => { - const isTorRunning = (await tor.status()).process; + const isTorRunning = (await getTarget().status()).process; const { snowflakeBinaryPath } = store.getTorSettings(); if (shouldEnableTor === isTorRunning) { @@ -99,11 +150,22 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) } if (shouldEnableTor === true) { - setProxy(`socks5://${host}:${port}`); - tor.torController.on('bootstrap/event', handleBootstrapEvent); + const { host } = store.getTorSettings(); + const port = getTarget().getPort(); + const proxyRule = `socks5://${host}:${port}`; + setProxy(proxyRule); + getTarget().torController.on('bootstrap/event', handleBootstrapEvent); + try { - tor.setTorConfig({ snowflakeBinaryPath }); - await tor.start(); + const { useExternalTor } = store.getTorSettings(); + getTarget().setTorConfig({ snowflakeBinaryPath, useExternalTor }); + updateTorPort(port); + if (useExternalTor) { + await getTarget().start(); + createFakeBootstrapProcess(); + } else { + await getTarget().start(); + } } catch (error) { mainWindowProxy.getInstance()?.webContents.send('tor/bootstrap', { type: 'error', @@ -111,18 +173,19 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) }); // When there is error does not mean that the process is stop, // so we make sure to stop it so we are able to restart it. - tor.stop(); + getTarget().stop(); + throw error; } finally { - tor.torController.removeAllListeners(); + getTarget().torController.removeAllListeners(); } } else { mainWindowProxy.getInstance()?.webContents.send('tor/status', { type: TorStatus.Disabling, }); setProxy(''); - tor.torController.stop(); - await tor.stop(); + getTarget().torController.stop(); + await getTarget().stop(); } store.setTorSettings({ ...store.getTorSettings(), running: shouldEnableTor }); @@ -130,15 +193,20 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) ipcMain.handle( 'tor/change-settings', - (ipcEvent, { snowflakeBinaryPath }: { snowflakeBinaryPath: string }) => { + ( + ipcEvent, + { + snowflakeBinaryPath, + useExternalTor, + }: { snowflakeBinaryPath: string; useExternalTor: boolean }, + ) => { validateIpcMessage(ipcEvent); try { store.setTorSettings({ - running: store.getTorSettings().running, - host, - port, + ...store.getTorSettings(), snowflakeBinaryPath, + useExternalTor, }); return { success: true }; @@ -175,6 +243,7 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) // correctly set in module trezor-connect-ipc. const proxySettings = getProxySettings(shouldEnableTor); + // Proxy is also set in packages/suite-desktop-core/src/modules/trezor-connect.ts await TrezorConnect.setProxy(proxySettings); logger.info( @@ -201,7 +270,8 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) } // Once Tor is toggled it renderer should know the new status. - const status = await tor.status(); + const status = await getTarget().status(); + handleTorProcessStatus(status); return { success: true }; @@ -211,11 +281,16 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) let lastCircuitResetTime = 0; const socksTimeout = 30000; // this value reflects --SocksTimeout flag set by TorController config mainThreadEmitter.on('module/reset-tor-circuits', event => { + if (store.getTorSettings().useExternalTor) { + logger.debug('tor', `Ignore circuit reset. Running External Tor without Control Port.`); + + return; + } const lastResetDiff = Date.now() - lastCircuitResetTime; if (lastResetDiff > socksTimeout) { logger.debug('tor', `Close active circuits. Triggered by identity ${event.identity}`); lastCircuitResetTime = Date.now(); - tor.torController.closeActiveCircuits(); + getTarget().torController.closeActiveCircuits(); } else { logger.debug( 'tor', @@ -225,8 +300,9 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) }); ipcMain.on('tor/get-status', async () => { - logger.debug('tor', `Getting status (${store.getTorSettings().running ? 'ON' : 'OFF'})`); - const status = await tor.status(); + const { running } = store.getTorSettings(); + logger.debug('tor', `Getting status (${running ? 'ON' : 'OFF'})`); + const status = await getTarget().status(); handleTorProcessStatus(status); }); @@ -235,7 +311,7 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) store.setTorSettings({ ...store.getTorSettings(), running: true }); } - return tor; + return getTarget; }; type TorModule = (dependencies: Dependencies) => { @@ -245,13 +321,13 @@ type TorModule = (dependencies: Dependencies) => { export const init: TorModule = dependencies => { let loaded = false; - let tor: TorProcess | undefined; + let getTarget: any; const onLoad = async () => { if (loaded) return { shouldRunTor: false }; loaded = true; - tor = await load(dependencies); + getTarget = await load(dependencies); const torSettings = dependencies.store.getTorSettings(); return { @@ -262,7 +338,7 @@ export const init: TorModule = dependencies => { const onQuit = async () => { const { logger } = global; logger.info('tor', 'Stopping (app quit)'); - await tor?.stop(); + await getTarget()?.stop(); }; return { onLoad, onQuit };