From 87cfaa20cd05a4da54f8636475f7c72802a64a66 Mon Sep 17 00:00:00 2001 From: Lucas Leblow Date: Tue, 12 Mar 2024 19:49:16 -0700 Subject: [PATCH 1/2] fix: Add retry ability to tor-control and misc fixes We were getting an uncaught ECONNREFUSED exception on iOS (perhaps due to a race condition), so this commit retries until we connect. It also fixes some existing code that appeared to be broken in other ways. --- CHANGELOG.md | 1 + .../src/nest/tor/tor-control.service.ts | 101 +++++++++++------- 2 files changed, 65 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86dd07be82..43d9620c7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ * Prevent channel creation with names that start with special character, then a hyphen * Choose random ports for Tor services (iOS) * Use consistent identicons for messages and profile +* Add retry ability to tor-control and misc tor-control fixes # Other: diff --git a/packages/backend/src/nest/tor/tor-control.service.ts b/packages/backend/src/nest/tor/tor-control.service.ts index 5fa1426bea..b83040c7a4 100644 --- a/packages/backend/src/nest/tor/tor-control.service.ts +++ b/packages/backend/src/nest/tor/tor-control.service.ts @@ -8,12 +8,16 @@ import Logger from '../common/logger' @Injectable() export class TorControl { connection: net.Socket | null + isSending: boolean authString: string private readonly logger = Logger(TorControl.name) + constructor( @Inject(TOR_CONTROL_PARAMS) public torControlParams: TorControlParams, @Inject(CONFIG_OPTIONS) public configOptions: ConfigOptions - ) {} + ) { + this.isSending = false + } private updateAuthString() { if (this.torControlParams.auth.type === TorControlAuthType.PASSWORD) { @@ -25,74 +29,97 @@ export class TorControl { } } - private async connect(): Promise { - return await new Promise((resolve, reject) => { - if (this.connection) { - reject(new Error('TOR: Connection already established')) - } - + private async _connect(): Promise { + return new Promise((resolve, reject) => { this.connection = net.connect({ port: this.torControlParams.port, family: 4, }) this.connection.once('error', err => { - reject(new Error(`TOR: Connection via tor control failed: ${err.message}`)) + this.disconnect() + reject(new Error(`Connection via Tor control failed: ${err.message}`)) }) + this.connection.once('data', (data: any) => { if (/250 OK/.test(data.toString())) { resolve() } else { - reject(new Error(`TOR: Control port error: ${data.toString() as string}`)) + this.disconnect() + reject(new Error(`Tor Control port error: ${data.toString() as string}`)) } }) + this.updateAuthString() this.connection.write(this.authString) }) } + private async connect(): Promise { + // TODO: We may want to limit the number of connection attempts. + // eslint-disable-next-line no-constant-condition + while (true) { + try { + this.logger(`Connecting to Tor, host: ${this.torControlParams.host} port: ${this.torControlParams.port}`) + await this._connect() + this.logger('Tor connected') + return + } catch (e) { + this.logger(e) + this.logger('Retrying...') + await new Promise(r => setTimeout(r, 500)) + } + } + } + private disconnect() { try { this.connection?.end() } catch (e) { - this.logger.error('Cant disconnect', e.message) + this.logger.error('Disconnect failed:', e.message) } this.connection = null } - // eslint-disable-next-line @typescript-eslint/ban-types - private async _sendCommand(command: string, resolve: Function, reject: Function) { - await this.connect() + public _sendCommand(command: string): Promise<{ code: number; messages: string[] }> { + return new Promise((resolve, reject) => { + const connectionTimeout = setTimeout(() => { + reject('Timeout while sending command to Tor') + }, 5000) + + this.connection?.on('data', async data => { + const dataArray = data.toString().split(/\r?\n/) - const connectionTimeout = setTimeout(() => { - reject('TOR: Send command timeout') - }, 5000) - this.connection?.on('data', async data => { - this.disconnect() - const dataArray = data.toString().split(/\r?\n/) - if (dataArray[0].startsWith('250')) { - resolve({ code: 250, messages: dataArray }) - } else { + if (dataArray[0].startsWith('250')) { + resolve({ code: 250, messages: dataArray }) + } else { + clearTimeout(connectionTimeout) + reject(`${dataArray[0]}`) + } clearTimeout(connectionTimeout) - reject(`${dataArray[0]}`) - } - clearTimeout(connectionTimeout) + }) + + this.connection?.write(command + '\r\n') }) - this.connection?.write(command + '\r\n') } public async sendCommand(command: string): Promise<{ code: number; messages: string[] }> { - await this.waitForDisconnect() - return await new Promise((resolve, reject) => { - void this._sendCommand(command, resolve, reject) - }) - } + // Only send one command at a time. + if (this.isSending) { + this.logger('Tor connection already established, waiting...') + } - private async waitForDisconnect() { - await new Promise(resolve => { - if (!this.connection) { - resolve() - } - }) + // Wait for existing command to finish. + while (this.isSending) { + await new Promise(r => setTimeout(r, 750)) + } + + this.isSending = true + await this.connect() + // FIXME: Errors are not caught here. Is this what we want? + const res = await this._sendCommand(command) + this.disconnect() + this.isSending = false + return res } } From a6b969de6e8da63499c9eece08b7623deba07f4f Mon Sep 17 00:00:00 2001 From: Lucas Leblow Date: Wed, 13 Mar 2024 12:16:21 -0700 Subject: [PATCH 2/2] Fix typo in AppDelegate.m and update Tor control port on resume --- packages/backend/src/backendManager.ts | 5 +- packages/mobile/ios/Quiet/AppDelegate.m | 84 ++++++++++++------------- 2 files changed, 45 insertions(+), 44 deletions(-) diff --git a/packages/backend/src/backendManager.ts b/packages/backend/src/backendManager.ts index abe94f7647..5862e47218 100644 --- a/packages/backend/src/backendManager.ts +++ b/packages/backend/src/backendManager.ts @@ -114,8 +114,9 @@ export const runBackendMobile = async () => { }) rn_bridge.channel.on('open', async (msg: OpenServices) => { const connectionsManager = app.get(ConnectionsManagerService) - const torControlParams = app.get(TorControl) - torControlParams.torControlParams.auth.value = msg.authCookie + const torControl = app.get(TorControl) + torControl.torControlParams.port = msg.torControlPort + torControl.torControlParams.auth.value = msg.authCookie await connectionsManager.openSocket() }) } diff --git a/packages/mobile/ios/Quiet/AppDelegate.m b/packages/mobile/ios/Quiet/AppDelegate.m index 564f23e6e7..733bdcc4b2 100644 --- a/packages/mobile/ios/Quiet/AppDelegate.m +++ b/packages/mobile/ios/Quiet/AppDelegate.m @@ -31,12 +31,12 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( // You can add your custom initial props in the dictionary below. // They will be passed down to the ViewController used by React Native. self.initialProps = @{}; - + // Call only once per nodejs thread [self createDataDirectory]; - + [self spinupBackend:true]; - + return [super application:application didFinishLaunchingWithOptions:launchOptions]; }; @@ -60,65 +60,65 @@ - (void) initWebsocketConnection { } - (void) spinupBackend:(BOOL)init { - + // (1/6) Find ports to use in tor and backend configuration - + Utils *utils = [Utils new]; - + if (self.socketIOSecret == nil) { self.socketIOSecret = [utils generateSecretWithLength:(20)]; } - + FindFreePort *findFreePort = [FindFreePort new]; - + self.dataPort = [findFreePort getFirstStartingFromPort:11000]; - + uint16_t socksPort = [findFreePort getFirstStartingFromPort:arc4random_uniform(65535 - 1024) + 1024]; uint16_t controlPort = [findFreePort getFirstStartingFromPort:arc4random_uniform(65535 - 1024) + 1024]; uint16_t httpTunnelPort = [findFreePort getFirstStartingFromPort:arc4random_uniform(65535 - 1024) + 1024]; - - + + // (2/6) Spawn tor with proper configuration - + self.tor = [TorHandler new]; - + self.torConfiguration = [self.tor getTorConfiguration:socksPort controlPort:controlPort httpTunnelPort:httpTunnelPort]; - + [self.tor removeOldAuthCookieWithConfiguration:self.torConfiguration]; - + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [self.tor spawnWithConfiguration:self.torConfiguration]; }); - - + + // (4/6) Connect to tor control port natively (so we can use it to shutdown tor when app goes idle) - + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ NSData *authCookieData = [self getAuthCookieData]; - + self.torController = [[TORController alloc] initWithSocketHost:@"127.0.0.1" port:controlPort]; - + NSError *error = nil; BOOL connected = [self.torController connect:&error]; - + NSLog(@"Tor control port error %@", error); - + [self.torController authenticateWithData:authCookieData completion:^(BOOL success, NSError * _Nullable error) { NSString *res = success ? @"YES" : @"NO"; NSLog(@"Tor control port auth success %@", res); NSLog(@"Tor control port auth error %@", error); }]; }); - - + + // (5/6) Update data port information and broadcast it to frontend if (init) { [self initWebsocketConnection]; } - - - // (6/6) Launch backend or reviwe services - + + + // (6/6) Launch backend or rewire services + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ NSString *authCookie = [self getAuthCookie]; @@ -126,28 +126,28 @@ - (void) spinupBackend:(BOOL)init { if (init) { [self launchBackend:controlPort :httpTunnelPort :authCookie]; } else { - [self reviweServices:controlPort :httpTunnelPort : authCookie]; + [self rewireServices:controlPort :httpTunnelPort : authCookie]; } }); } - (NSString *) getAuthCookie { NSString *authCookie = [self.tor getAuthCookieWithConfiguration:self.torConfiguration]; - + while (authCookie == nil) { authCookie = [self.tor getAuthCookieWithConfiguration:self.torConfiguration]; }; - + return authCookie; } - (NSData *) getAuthCookieData { NSData *authCookie = [self.tor getAuthCookieDataWithConfiguration:self.torConfiguration]; - + while (authCookie == nil) { authCookie = [self.tor getAuthCookieDataWithConfiguration:self.torConfiguration]; }; - + return authCookie; } @@ -156,13 +156,13 @@ - (void) launchBackend:(uint16_t)controlPort:(uint16_t)httpTunnelPort:(NSString [self.nodeJsMobile callStartNodeProject:[NSString stringWithFormat:@"bundle.cjs --dataPort %hu --dataPath %@ --controlPort %hu --httpTunnelPort %hu --authCookie %@ --platform %@ --socketIOSecret %@", self.dataPort, self.dataPath, controlPort, httpTunnelPort, authCookie, platform, self.socketIOSecret]]; } -- (void) reviweServices:(uint16_t)controlPort:(uint16_t)httpTunnelPort:(NSString *)authCookie { +- (void) rewireServices:(uint16_t)controlPort:(uint16_t)httpTunnelPort:(NSString *)authCookie { NSString * dataPortPayload = [NSString stringWithFormat:@"%@:%hu", @"socketIOPort", self.dataPort]; NSString * socketIOSecretPayload = [NSString stringWithFormat:@"%@:%@", @"socketIOSecret", self.socketIOSecret]; NSString * controlPortPayload = [NSString stringWithFormat:@"%@:%hu", @"torControlPort", controlPort]; NSString * httpTunnelPortPayload = [NSString stringWithFormat:@"%@:%hu", @"httpTunnelPort", httpTunnelPort]; NSString * authCookiePayload = [NSString stringWithFormat:@"%@:%@", @"authCookie", authCookie]; - + NSString * payload = [NSString stringWithFormat:@"%@|%@|%@|%@|%@", dataPortPayload, socketIOSecretPayload, controlPortPayload, httpTunnelPortPayload, authCookiePayload]; [self.nodeJsMobile sendMessageToNode:@"open":payload]; } @@ -171,16 +171,16 @@ - (void) stopTor { NSLog(@"Sending SIGNAL SHUTDOWN on Tor control port %d", (int)[self.torController isConnected]); [self.torController sendCommand:@"SIGNAL SHUTDOWN" arguments:nil data:nil observer:^BOOL(NSArray *codes, NSArray *lines, BOOL *stop) { NSUInteger code = codes.firstObject.unsignedIntegerValue; - + NSLog(@"Tor control port response code %lu", (unsigned long)code); - + if (code != TORControlReplyCodeOK && code != TORControlReplyCodeBadAuthentication) return NO; NSString *message = lines.firstObject ? [[NSString alloc] initWithData:(NSData * _Nonnull)lines.firstObject encoding:NSUTF8StringEncoding] : @""; - + NSLog(@"Tor control port response message %@", message); - + NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:message, NSLocalizedDescriptionKey, nil]; BOOL success = (code == TORControlReplyCodeOK && [message isEqualToString:@"OK"]); @@ -192,10 +192,10 @@ - (void) stopTor { - (void)applicationDidEnterBackground:(UIApplication *)application { [self stopTor]; - + NSString * message = [NSString stringWithFormat:@""]; [self.nodeJsMobile sendMessageToNode:@"close":message]; - + // Flush persistor before app goes idle dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSTimeInterval delayInSeconds = 0; @@ -216,7 +216,7 @@ - (void)applicationWillEnterForeground:(UIApplication *)application [[self.bridge moduleForName:@"CommunicationModule"] appResume]; }); }); - + [self spinupBackend:false]; }