Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Add retry ability to tor-control and misc fixes #2360

Merged
merged 2 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

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?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we should handle that

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noting that this is how it was in the previous version of this code, so I haven't changed anything

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
Loading