Skip to content

Commit

Permalink
feat(suite-desktop-core): support external Tor
Browse files Browse the repository at this point in the history
  • Loading branch information
karliatto committed Dec 3, 2024
1 parent 09ed8cf commit 5dd716e
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 39 deletions.
5 changes: 4 additions & 1 deletion packages/suite-desktop-core/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<TorProcessStatus> {
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<void> {
this.isStopped = false;
await this.torController.waitUntilAlive();
}

public stop() {
// We should not stop External Tor Process but ignore it.
this.isStopped = true;
}
}
10 changes: 7 additions & 3 deletions packages/suite-desktop-core/src/libs/processes/TorProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,15 @@ export class TorProcess extends BaseProcess {
});
}

setTorConfig(torConfig: Pick<TorConnectionOptions, 'snowflakeBinaryPath'>) {
public setTorConfig(torConfig: Pick<TorConnectionOptions, 'snowflakeBinaryPath'>) {
this.snowflakeBinaryPath = torConfig.snowflakeBinaryPath;
}

async status(): Promise<TorProcessStatus> {
public getPort() {
return this.port;
}

public async status(): Promise<TorProcessStatus> {
const torControllerStatus = await this.torController.getStatus();

return {
Expand All @@ -45,7 +49,7 @@ export class TorProcess extends BaseProcess {
};
}

async start(): Promise<void> {
public async start(): Promise<void> {
const electronProcessId = process.pid;
const torConfiguration = await this.torController.getTorConfiguration(
electronProcessId,
Expand Down
3 changes: 3 additions & 0 deletions packages/suite-desktop-core/src/libs/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '',
});
}

Expand Down
146 changes: 111 additions & 35 deletions packages/suite-desktop-core/src/modules/tor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;

Expand Down Expand Up @@ -90,55 +120,93 @@ 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) {
return;
}

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',
message: error.message,
});
// 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 });
};

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 };
Expand Down Expand Up @@ -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(
Expand All @@ -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 };
Expand All @@ -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',
Expand All @@ -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);
});

Expand All @@ -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) => {
Expand All @@ -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 {
Expand All @@ -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 };
Expand Down

0 comments on commit 5dd716e

Please sign in to comment.