Skip to content

Commit 5312c99

Browse files
committed
Reuse persistent ADB tunnel logic in Frida Android integration
1 parent 643f0fc commit 5312c99

File tree

3 files changed

+78
-37
lines changed

3 files changed

+78
-37
lines changed

src/interceptors/android/adb-commands.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,4 +522,72 @@ export async function startActivity(
522522
});
523523
}
524524
}
525+
}
526+
527+
const adbTunnelIds: { [id: string]: NodeJS.Timeout } = {};
528+
529+
export function closeReverseTunnel(
530+
adbClient: Adb.DeviceClient,
531+
localPort: number | string,
532+
remotePort: number | string,
533+
) {
534+
const id = `${adbClient.serial}:${localPort}->${remotePort}`;
535+
const tunnelInterval = adbTunnelIds[id];
536+
if (!tunnelInterval) return;
537+
538+
// This ensures the interval maintaining the tunnel stops:
539+
clearInterval(tunnelInterval);
540+
delete adbTunnelIds[id];
541+
}
542+
543+
export async function createPersistentReverseTunnel(
544+
adbClient: Adb.DeviceClient,
545+
localPort: number,
546+
remotePort: number,
547+
options: {
548+
maxFailures: number,
549+
delay: number
550+
} = { maxFailures: 5, delay: 2000 } // 10 seconds total
551+
) {
552+
const id = `${adbClient.serial}:${localPort}->${remotePort}`;
553+
554+
await adbClient.reverse('tcp:' + localPort, 'tcp:' + remotePort);
555+
556+
// This tunnel can break in quite a few days, notably when connecting/disconnecting
557+
// from the VPN app with a wifi connection, or when ADB is restarted, when using flaky
558+
// cables, or switching ADB into root mode, etc etc. This is a problem!
559+
560+
// To handle this, we constantly reinforce the tunnel while HTTP Toolkit is running &
561+
// the device is connected, until it actually persistently fails.
562+
563+
// If tunnel is already being maintained elsewhere, no need to repeat (although we
564+
// do re-create it above, just in case there's any flakiness at this exact moment)
565+
if (adbTunnelIds[id]) return;
566+
567+
let tunnelConnectFailures = 0;
568+
569+
const tunnelCheckInterval = adbTunnelIds[id] = setInterval(async () => {
570+
if (adbTunnelIds[id] !== tunnelCheckInterval) {
571+
clearInterval(tunnelCheckInterval);
572+
return;
573+
}
574+
575+
try {
576+
// Repeated calls to do this do nothing if the tunnel is already in place
577+
await adbClient.reverse('tcp:' + remotePort, 'tcp:' + localPort);
578+
tunnelConnectFailures = 0;
579+
} catch (e) {
580+
tunnelConnectFailures += 1;
581+
console.log(`${id} ADB tunnel failed`, isErrorLike(e) ? e.message : e);
582+
583+
if (tunnelConnectFailures >= options.maxFailures) {
584+
// After 10 seconds disconnected, give up
585+
console.warn(`${id} tunnel disconnected`);
586+
587+
delete adbTunnelIds[id];
588+
clearInterval(tunnelCheckInterval);
589+
}
590+
}
591+
}, options.delay);
592+
tunnelCheckInterval.unref(); // Don't let this block shutdown
525593
}

src/interceptors/android/android-adb-interceptor.ts

Lines changed: 7 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ import {
1919
hasCertInstalled,
2020
bringToFront,
2121
setChromeFlags,
22-
startActivity
22+
startActivity,
23+
createPersistentReverseTunnel,
24+
closeReverseTunnel
2325
} from './adb-commands';
2426
import { streamLatestApk, clearAllApks } from './fetch-apk';
2527
import { parseCert, getCertificateFingerprint, getCertificateSubjectHash } from '../../certificates';
@@ -108,7 +110,8 @@ export class AndroidAdbInterceptor implements Interceptor {
108110
};
109111
const intentData = urlSafeBase64(JSON.stringify(setupParams));
110112

111-
await deviceClient.reverse('tcp:' + proxyPort, 'tcp:' + proxyPort).catch(() => {});
113+
await createPersistentReverseTunnel(deviceClient, proxyPort, proxyPort)
114+
.catch(() => {}); // If we can't tunnel that's OK - we'll use wifi/etc instead
112115

113116
// Use ADB to launch the app with the proxy details
114117
await startActivity(deviceClient, {
@@ -118,41 +121,8 @@ export class AndroidAdbInterceptor implements Interceptor {
118121
});
119122

120123
this.deviceProxyMapping[proxyPort] = this.deviceProxyMapping[proxyPort] || [];
121-
122124
if (!this.deviceProxyMapping[proxyPort].includes(options.deviceId)) {
123125
this.deviceProxyMapping[proxyPort].push(options.deviceId);
124-
125-
let tunnelConnectFailures = 0;
126-
127-
// The reverse tunnel can break when connecting/disconnecting from the VPN. This is a problem! It can
128-
// also break in other cases, e.g. when ADB is restarted for some reason. To handle this, we constantly
129-
// reinforce the tunnel while HTTP Toolkit is running & the device is connected.
130-
const tunnelCheckInterval = setInterval(async () => {
131-
if (this.deviceProxyMapping[proxyPort].includes(options.deviceId)) {
132-
try {
133-
await deviceClient.reverse('tcp:' + proxyPort, 'tcp:' + proxyPort)
134-
tunnelConnectFailures = 0;
135-
} catch (e) {
136-
tunnelConnectFailures += 1;
137-
console.log(`${options.deviceId} ADB tunnel failed`,
138-
isErrorLike(e) ? e.message : e
139-
);
140-
141-
if (tunnelConnectFailures >= 5) {
142-
// After 10 seconds disconnected, give up
143-
console.log(`${options.deviceId} disconnected, dropping the ADB tunnel`);
144-
this.deviceProxyMapping[proxyPort] = this.deviceProxyMapping[proxyPort]
145-
.filter(id => id !== options.deviceId);
146-
clearInterval(tunnelCheckInterval);
147-
}
148-
}
149-
} else {
150-
// Deactivation at shutdown will clear the proxy data, and so clear this interval
151-
// will automatically shut down.
152-
clearInterval(tunnelCheckInterval);
153-
}
154-
}, 2000);
155-
tunnelCheckInterval.unref(); // Don't let this block shutdown
156126
}
157127
}
158128

@@ -174,6 +144,8 @@ export class AndroidAdbInterceptor implements Interceptor {
174144
wait: true,
175145
action: 'tech.httptoolkit.android.DEACTIVATE'
176146
});
147+
148+
closeReverseTunnel(deviceClient, port, port);
177149
})
178150
);
179151
}

src/interceptors/frida/frida-android-integration.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Client as AdbClient, DeviceClient } from '@devicefarmer/adbkit';
22
import * as FridaJs from 'frida-js';
33

4-
import { getConnectedDevices, getRootCommand, isProbablyRooted } from '../android/adb-commands';
4+
import { createPersistentReverseTunnel, getConnectedDevices, getRootCommand, isProbablyRooted } from '../android/adb-commands';
55
import { waitUntil } from '../../util/promise';
66
import { buildAndroidFridaScript } from './frida-scripts';
77
import {
@@ -169,7 +169,8 @@ export async function interceptAndroidFridaTarget(
169169
) {
170170
const deviceClient = adbClient.getDevice(hostId);
171171

172-
await deviceClient.reverse('tcp:' + proxyPort, 'tcp:' + proxyPort);
172+
await createPersistentReverseTunnel(deviceClient, proxyPort, proxyPort)
173+
.catch(() => {}); // If we can't tunnel that's OK - we'll use wifi/etc instead
173174

174175
// Try alt port first (preferred and more likely to work - it's ours)
175176
const fridaStream = await deviceClient.openTcp(FRIDA_ALTERNATE_PORT)

0 commit comments

Comments
 (0)