Skip to content

Commit

Permalink
fix: Add retry ability to tor-control and update Tor port on resume (#…
Browse files Browse the repository at this point in the history
…2360)

We were getting an uncaught ECONNREFUSED exception on iOS due to the Tor control port not getting updated after the app has been resumed on iOS, so this commit fixes that and refactors the tor-control code in order to add retry ability and fix misc issues with that code.
  • Loading branch information
Lucas Leblow authored Mar 13, 2024
1 parent 800bf37 commit 9517f08
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 81 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,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:

Expand Down
5 changes: 3 additions & 2 deletions packages/backend/src/backendManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,9 @@ export const runBackendMobile = async () => {
})
rn_bridge.channel.on('open', async (msg: OpenServices) => {
const connectionsManager = app.get<ConnectionsManagerService>(ConnectionsManagerService)
const torControlParams = app.get<TorControl>(TorControl)
torControlParams.torControlParams.auth.value = msg.authCookie
const torControl = app.get<TorControl>(TorControl)
torControl.torControlParams.port = msg.torControlPort
torControl.torControlParams.auth.value = msg.authCookie
await connectionsManager.openSocket()
})
}
Expand Down
101 changes: 64 additions & 37 deletions packages/backend/src/nest/tor/tor-control.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -25,74 +29,97 @@ export class TorControl {
}
}

private async connect(): Promise<void> {
return await new Promise((resolve, reject) => {
if (this.connection) {
reject(new Error('TOR: Connection already established'))
}

private async _connect(): Promise<void> {
return new Promise<void>((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<void> {
// 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<void>(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
}
}
84 changes: 42 additions & 42 deletions packages/mobile/ios/Quiet/AppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -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];
};

Expand All @@ -60,94 +60,94 @@ - (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];

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;
}

Expand All @@ -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];
}
Expand All @@ -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<NSNumber *> *codes, NSArray<NSData *> *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<NSString *, NSString *> *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:message, NSLocalizedDescriptionKey, nil];
BOOL success = (code == TORControlReplyCodeOK && [message isEqualToString:@"OK"]);

Expand All @@ -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;
Expand All @@ -216,7 +216,7 @@ - (void)applicationWillEnterForeground:(UIApplication *)application
[[self.bridge moduleForName:@"CommunicationModule"] appResume];
});
});

[self spinupBackend:false];
}

Expand Down

0 comments on commit 9517f08

Please sign in to comment.