appium-ios-tuntap 0.4.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +3 -3
- package/lib/platform/windows.js +9 -8
- package/lib/tunnel/constants.d.ts +12 -2
- package/lib/tunnel/constants.js +12 -2
- package/lib/tunnel/debug-log.d.ts +12 -0
- package/lib/tunnel/debug-log.js +44 -0
- package/lib/tunnel/device-to-tun-pump.d.ts +36 -0
- package/lib/tunnel/device-to-tun-pump.js +229 -0
- package/lib/tunnel/manager.d.ts +8 -3
- package/lib/tunnel/manager.js +161 -79
- package/lib/tunnel/tun-to-device-pump.d.ts +30 -0
- package/lib/tunnel/tun-to-device-pump.js +166 -0
- package/package.json +3 -4
- package/prebuilds/darwin-arm64/appium-ios-tuntap.node +0 -0
- package/prebuilds/darwin-x64/appium-ios-tuntap.node +0 -0
- package/prebuilds/linux-arm64/appium-ios-tuntap.node +0 -0
- package/prebuilds/linux-x64/appium-ios-tuntap.node +0 -0
- package/prebuilds/win32-arm64/appium-ios-tuntap.node +0 -0
- package/prebuilds/win32-x64/appium-ios-tuntap.node +0 -0
- package/src/native/posix_uv_poll_loop.cc +2 -2
- package/src/native/tun_backend.h +1 -1
- package/src/native/tun_backend_darwin.cc +3 -0
- package/src/native/tun_backend_linux.cc +3 -0
- package/src/native/tun_backend_windows.cc +2 -2
- package/src/tuntap.cc +77 -16
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [0.5.0](https://github.com/appium/appium-ios-tuntap/compare/v0.4.4...v0.5.0) (2026-06-13)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
* Switch to a different tunnelling architecture ([#51](https://github.com/appium/appium-ios-tuntap/issues/51)) ([7267755](https://github.com/appium/appium-ios-tuntap/commit/726775526d50d1976192bf3ebfbae6cb81883631))
|
|
6
|
+
|
|
7
|
+
## [0.4.4](https://github.com/appium/appium-ios-tuntap/compare/v0.4.3...v0.4.4) (2026-06-12)
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* Update tunnel backpressure handling ([#50](https://github.com/appium/appium-ios-tuntap/issues/50)) ([28b8bce](https://github.com/appium/appium-ios-tuntap/commit/28b8bce8edcf2a0446a653bbec759e6b4a199827))
|
|
12
|
+
|
|
1
13
|
## [0.4.3](https://github.com/appium/appium-ios-tuntap/compare/v0.4.2...v0.4.3) (2026-06-10)
|
|
2
14
|
|
|
3
15
|
### Bug Fixes
|
package/README.md
CHANGED
|
@@ -272,13 +272,13 @@ Most tests for this module require **root privileges** (sudo) to create and mana
|
|
|
272
272
|
From the project root, run:
|
|
273
273
|
|
|
274
274
|
```sh
|
|
275
|
-
sudo
|
|
275
|
+
sudo npm run test:unit
|
|
276
276
|
```
|
|
277
277
|
|
|
278
|
-
Or, to run all tests
|
|
278
|
+
Or, to run all tests:
|
|
279
279
|
|
|
280
280
|
```sh
|
|
281
|
-
sudo
|
|
281
|
+
sudo npm test
|
|
282
282
|
```
|
|
283
283
|
|
|
284
284
|
If you are **not** running as root, you will see a message that tests are skipped.
|
package/lib/platform/windows.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { TunTapError } from '../errors.js';
|
|
2
2
|
import { log } from '../logger.js';
|
|
3
|
+
import { tunDebug } from '../tunnel/debug-log.js';
|
|
3
4
|
import { assertAdminOnWindows } from './require-admin.js';
|
|
4
5
|
import { execFileAsync } from './exec.js';
|
|
5
6
|
/** Tightly-restricted character set for adapter names passed into PowerShell. */
|
|
@@ -19,7 +20,7 @@ export class WindowsTunTapPlatform {
|
|
|
19
20
|
async configure(interfaceName, address, mtu) {
|
|
20
21
|
await assertAdminOnWindows();
|
|
21
22
|
assertSafeAdapterName(interfaceName);
|
|
22
|
-
|
|
23
|
+
tunDebug(`[win] configure: interface=${interfaceName} address=${address} mtu=${mtu}`);
|
|
23
24
|
await addIpv6Address(interfaceName, address);
|
|
24
25
|
await setIpv6Mtu(interfaceName, mtu);
|
|
25
26
|
}
|
|
@@ -27,7 +28,7 @@ export class WindowsTunTapPlatform {
|
|
|
27
28
|
async addRoute(interfaceName, destination) {
|
|
28
29
|
await assertAdminOnWindows();
|
|
29
30
|
assertSafeAdapterName(interfaceName);
|
|
30
|
-
|
|
31
|
+
tunDebug(`[win] addRoute: interface=${interfaceName} destination=${destination}`);
|
|
31
32
|
await addIpv6Route(interfaceName, destination);
|
|
32
33
|
// WinTun presents as an Ethernet adapter, so Windows requires Neighbor
|
|
33
34
|
// Discovery (NDP) before it will send packets through the interface.
|
|
@@ -102,7 +103,7 @@ async function addIpv6Address(interfaceName, address) {
|
|
|
102
103
|
`address=${address}/64`,
|
|
103
104
|
'store=active',
|
|
104
105
|
]);
|
|
105
|
-
|
|
106
|
+
tunDebug(`[win] add address ok: ${r.stdout.trim() || '(no output)'}`);
|
|
106
107
|
}
|
|
107
108
|
catch (err) {
|
|
108
109
|
const message = err.message ?? '';
|
|
@@ -124,7 +125,7 @@ async function setIpv6Mtu(interfaceName, mtu) {
|
|
|
124
125
|
`mtu=${mtu}`,
|
|
125
126
|
'store=active',
|
|
126
127
|
]);
|
|
127
|
-
|
|
128
|
+
tunDebug(`[win] set mtu ok: ${r.stdout.trim() || '(no output)'}`);
|
|
128
129
|
}
|
|
129
130
|
catch (err) {
|
|
130
131
|
log.warn(`[win] set mtu err: ${err.message ?? err}`);
|
|
@@ -142,13 +143,13 @@ async function addIpv6Route(interfaceName, destination) {
|
|
|
142
143
|
interfaceName,
|
|
143
144
|
'store=active',
|
|
144
145
|
]);
|
|
145
|
-
|
|
146
|
+
tunDebug(`[win] add route ok: ${r.stdout.trim() || '(no output)'}`);
|
|
146
147
|
}
|
|
147
148
|
catch (err) {
|
|
148
149
|
const message = err.message ?? '';
|
|
149
150
|
log.warn(`[win] add route err: ${message}`);
|
|
150
151
|
if (/already exists|object already/i.test(message)) {
|
|
151
|
-
|
|
152
|
+
tunDebug(`Route to ${destination} already exists`);
|
|
152
153
|
return;
|
|
153
154
|
}
|
|
154
155
|
throw err;
|
|
@@ -174,7 +175,7 @@ async function deleteIpv6Route(interfaceName, destination) {
|
|
|
174
175
|
}
|
|
175
176
|
}
|
|
176
177
|
async function addStaticNeighbor(interfaceName, address) {
|
|
177
|
-
|
|
178
|
+
tunDebug(`[win] addStaticNeighbor: interface=${interfaceName} address=${address}`);
|
|
178
179
|
try {
|
|
179
180
|
const r = await execFileAsync('netsh', [
|
|
180
181
|
'interface',
|
|
@@ -186,7 +187,7 @@ async function addStaticNeighbor(interfaceName, address) {
|
|
|
186
187
|
'00-00-00-00-00-01',
|
|
187
188
|
'store=active',
|
|
188
189
|
]);
|
|
189
|
-
|
|
190
|
+
tunDebug(`[win] add neighbor ok: ${r.stdout.trim() || '(no output)'}`);
|
|
190
191
|
}
|
|
191
192
|
catch (err) {
|
|
192
193
|
const msg = err.message ?? String(err);
|
|
@@ -6,12 +6,22 @@ export declare const MAX_TUN_POLL_BUFFER = 65535;
|
|
|
6
6
|
export declare const LARGE_TUN_POLL_BUFFER: number;
|
|
7
7
|
/** Default ThreadSafeFunction queue depth for TUN polling. */
|
|
8
8
|
export declare const DEFAULT_TUN_POLL_QUEUE_DEPTH = 8;
|
|
9
|
-
/**
|
|
10
|
-
export declare const
|
|
9
|
+
/** One in-flight TUN read for sequential pump forwarding. */
|
|
10
|
+
export declare const SEQUENTIAL_TUN_POLL_QUEUE_DEPTH = 1;
|
|
11
|
+
/** Max utun packets buffered on the JS side before TLS write. */
|
|
12
|
+
export declare const MAX_TUN_INGRESS_QUEUE = 256;
|
|
13
|
+
/** Max ThreadSafeFunction queue depth (native addon limit). */
|
|
14
|
+
export declare const MAX_TUN_POLL_TSFN_QUEUE_DEPTH = 64;
|
|
15
|
+
/** ThreadSafeFunction queue depth passed to {@link TunTap.startPolling}. */
|
|
16
|
+
export declare const TUN_POLL_TSFN_QUEUE_DEPTH = 64;
|
|
17
|
+
/** Yield device→TUN loop periodically so utun poll callbacks can run. */
|
|
18
|
+
export declare const DEVICE_PUMP_YIELD_EVERY_FRAMES = 64;
|
|
11
19
|
export declare const CD_TUNNEL_MAGIC = "CDTunnel";
|
|
12
20
|
export declare const CD_TUNNEL_MAGIC_SIZE = 8;
|
|
13
21
|
export declare const CD_TUNNEL_HEADER_SIZE: number;
|
|
14
22
|
export declare const CD_TUNNEL_HANDSHAKE_TIMEOUT_MS = 30000;
|
|
23
|
+
/** Pause device ingress when the reassembly buffer exceeds this size. */
|
|
24
|
+
export declare const MAX_DEVICE_INGRESS_BUFFER: number;
|
|
15
25
|
export declare const IPV6_HEADER_SIZE = 40;
|
|
16
26
|
export declare const IPV6_VERSION = 6;
|
|
17
27
|
export declare const IPPROTO_TCP = 6;
|
package/lib/tunnel/constants.js
CHANGED
|
@@ -6,12 +6,22 @@ export const MAX_TUN_POLL_BUFFER = 65_535;
|
|
|
6
6
|
export const LARGE_TUN_POLL_BUFFER = 64 * 1024;
|
|
7
7
|
/** Default ThreadSafeFunction queue depth for TUN polling. */
|
|
8
8
|
export const DEFAULT_TUN_POLL_QUEUE_DEPTH = 8;
|
|
9
|
-
/**
|
|
10
|
-
export const
|
|
9
|
+
/** One in-flight TUN read for sequential pump forwarding. */
|
|
10
|
+
export const SEQUENTIAL_TUN_POLL_QUEUE_DEPTH = 1;
|
|
11
|
+
/** Max utun packets buffered on the JS side before TLS write. */
|
|
12
|
+
export const MAX_TUN_INGRESS_QUEUE = 256;
|
|
13
|
+
/** Max ThreadSafeFunction queue depth (native addon limit). */
|
|
14
|
+
export const MAX_TUN_POLL_TSFN_QUEUE_DEPTH = 64;
|
|
15
|
+
/** ThreadSafeFunction queue depth passed to {@link TunTap.startPolling}. */
|
|
16
|
+
export const TUN_POLL_TSFN_QUEUE_DEPTH = MAX_TUN_POLL_TSFN_QUEUE_DEPTH;
|
|
17
|
+
/** Yield device→TUN loop periodically so utun poll callbacks can run. */
|
|
18
|
+
export const DEVICE_PUMP_YIELD_EVERY_FRAMES = 64;
|
|
11
19
|
export const CD_TUNNEL_MAGIC = 'CDTunnel';
|
|
12
20
|
export const CD_TUNNEL_MAGIC_SIZE = 8;
|
|
13
21
|
export const CD_TUNNEL_HEADER_SIZE = CD_TUNNEL_MAGIC_SIZE + 2;
|
|
14
22
|
export const CD_TUNNEL_HANDSHAKE_TIMEOUT_MS = 30_000;
|
|
23
|
+
/** Pause device ingress when the reassembly buffer exceeds this size. */
|
|
24
|
+
export const MAX_DEVICE_INGRESS_BUFFER = 8 * 1024 * 1024;
|
|
15
25
|
export const IPV6_HEADER_SIZE = 40;
|
|
16
26
|
export const IPV6_VERSION = 6;
|
|
17
27
|
export const IPPROTO_TCP = 6;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { log } from '../logger.js';
|
|
2
|
+
/** Tunnel debug logging — set APPIUM_TUNTAP_DEBUG=1 on the tunnel process. */
|
|
3
|
+
export declare const APPIUM_TUNTAP_DEBUG: boolean;
|
|
4
|
+
/** {@link log.debug} when {@link APPIUM_TUNTAP_DEBUG} is enabled. */
|
|
5
|
+
export declare function tunDebug(...args: Parameters<typeof log.debug>): void;
|
|
6
|
+
/** Log a numbered tunnel forward diagnostic when {@link APPIUM_TUNTAP_DEBUG} is enabled. */
|
|
7
|
+
export declare function fwdDebug(event: string, detail?: Record<string, string | number | boolean>): void;
|
|
8
|
+
/** Summarize reassembly buffer state for debug logs. */
|
|
9
|
+
export declare function fwdBufferState(buffer: Buffer): {
|
|
10
|
+
buf: number;
|
|
11
|
+
tailKind: string;
|
|
12
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { log } from '../logger.js';
|
|
2
|
+
/** Tunnel debug logging — set APPIUM_TUNTAP_DEBUG=1 on the tunnel process. */
|
|
3
|
+
export const APPIUM_TUNTAP_DEBUG = process.env.APPIUM_TUNTAP_DEBUG === '1' || process.env.APPIUM_TUNTAP_DEBUG === 'true';
|
|
4
|
+
let seq = 0;
|
|
5
|
+
/** {@link log.debug} when {@link APPIUM_TUNTAP_DEBUG} is enabled. */
|
|
6
|
+
export function tunDebug(...args) {
|
|
7
|
+
if (!APPIUM_TUNTAP_DEBUG) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
log.debug(...args);
|
|
11
|
+
}
|
|
12
|
+
/** Log a numbered tunnel forward diagnostic when {@link APPIUM_TUNTAP_DEBUG} is enabled. */
|
|
13
|
+
export function fwdDebug(event, detail) {
|
|
14
|
+
if (!APPIUM_TUNTAP_DEBUG) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
seq += 1;
|
|
18
|
+
const parts = [`#${seq}`, event];
|
|
19
|
+
if (detail) {
|
|
20
|
+
for (const [key, value] of Object.entries(detail)) {
|
|
21
|
+
parts.push(`${key}=${value}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
log.info(`[fwd] ${parts.join(' ')}`);
|
|
25
|
+
}
|
|
26
|
+
/** Summarize reassembly buffer state for debug logs. */
|
|
27
|
+
export function fwdBufferState(buffer) {
|
|
28
|
+
if (buffer.length === 0) {
|
|
29
|
+
return { buf: 0, tailKind: 'empty' };
|
|
30
|
+
}
|
|
31
|
+
if (buffer.length < 40) {
|
|
32
|
+
return { buf: buffer.length, tailKind: 'short' };
|
|
33
|
+
}
|
|
34
|
+
const version = (buffer[0] >> 4) & 0x0f;
|
|
35
|
+
if (version !== 6) {
|
|
36
|
+
return { buf: buffer.length, tailKind: 'resync-needed' };
|
|
37
|
+
}
|
|
38
|
+
const payloadLength = buffer.readUInt16BE(4);
|
|
39
|
+
const frameLength = 40 + payloadLength;
|
|
40
|
+
if (buffer.length >= frameLength) {
|
|
41
|
+
return { buf: buffer.length, tailKind: 'has-complete-frame' };
|
|
42
|
+
}
|
|
43
|
+
return { buf: buffer.length, tailKind: 'incomplete-tail' };
|
|
44
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Socket } from 'node:net';
|
|
2
|
+
import type { TunTap } from '../TunTap.js';
|
|
3
|
+
export type DeviceToTunProgressHook = () => void;
|
|
4
|
+
/**
|
|
5
|
+
* Device→TUN forwarding: read one exact IPv6 frame from the socket, write to TUN,
|
|
6
|
+
* repeat. Yields only when TUN write blocks (via notifyTunWritable from the TUN→device pump).
|
|
7
|
+
*/
|
|
8
|
+
export declare class DeviceToTunPump {
|
|
9
|
+
private readonly onFrameWritten?;
|
|
10
|
+
private cancelled;
|
|
11
|
+
private running;
|
|
12
|
+
private buffer;
|
|
13
|
+
private frameWaiter;
|
|
14
|
+
private frameReject;
|
|
15
|
+
private tunWritableWaiter;
|
|
16
|
+
private loopPromise;
|
|
17
|
+
private fwdFrames;
|
|
18
|
+
private deviceIngressPaused;
|
|
19
|
+
private deviceConn;
|
|
20
|
+
private pendingDeviceChunks;
|
|
21
|
+
private deviceDrainScheduled;
|
|
22
|
+
constructor(onFrameWritten?: DeviceToTunProgressHook | undefined);
|
|
23
|
+
start(deviceConn: Socket, tun: TunTap): void;
|
|
24
|
+
notifyTunWritable(): void;
|
|
25
|
+
stop(): Promise<void>;
|
|
26
|
+
private enqueueDeviceData;
|
|
27
|
+
private onDeviceData;
|
|
28
|
+
private maybeResumeDeviceIngress;
|
|
29
|
+
private tryDeliverFrame;
|
|
30
|
+
private readOneFrame;
|
|
31
|
+
private pauseDeviceIngress;
|
|
32
|
+
private resumeDeviceIngress;
|
|
33
|
+
private waitTunWritable;
|
|
34
|
+
private writeFrameToTun;
|
|
35
|
+
private runLoop;
|
|
36
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { appendBuffer } from './buffer-utils.js';
|
|
3
|
+
import { fwdDebug } from './debug-log.js';
|
|
4
|
+
import { IPV6_HEADER_SIZE, IPV6_VERSION, MAX_DEVICE_INGRESS_BUFFER, DEVICE_PUMP_YIELD_EVERY_FRAMES, } from './constants.js';
|
|
5
|
+
/**
|
|
6
|
+
* Device→TUN forwarding: read one exact IPv6 frame from the socket, write to TUN,
|
|
7
|
+
* repeat. Yields only when TUN write blocks (via notifyTunWritable from the TUN→device pump).
|
|
8
|
+
*/
|
|
9
|
+
export class DeviceToTunPump {
|
|
10
|
+
onFrameWritten;
|
|
11
|
+
cancelled = false;
|
|
12
|
+
running = false;
|
|
13
|
+
buffer = Buffer.alloc(0);
|
|
14
|
+
frameWaiter = null;
|
|
15
|
+
frameReject = null;
|
|
16
|
+
tunWritableWaiter = null;
|
|
17
|
+
loopPromise = null;
|
|
18
|
+
fwdFrames = 0;
|
|
19
|
+
deviceIngressPaused = false;
|
|
20
|
+
deviceConn = null;
|
|
21
|
+
pendingDeviceChunks = [];
|
|
22
|
+
deviceDrainScheduled = false;
|
|
23
|
+
constructor(onFrameWritten) {
|
|
24
|
+
this.onFrameWritten = onFrameWritten;
|
|
25
|
+
}
|
|
26
|
+
start(deviceConn, tun) {
|
|
27
|
+
if (this.running) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
this.running = true;
|
|
31
|
+
this.cancelled = false;
|
|
32
|
+
this.deviceConn = deviceConn;
|
|
33
|
+
deviceConn.on('data', (chunk) => this.enqueueDeviceData(chunk));
|
|
34
|
+
fwdDebug('device-pump-start', {});
|
|
35
|
+
this.loopPromise = this.runLoop(deviceConn, tun);
|
|
36
|
+
}
|
|
37
|
+
notifyTunWritable() {
|
|
38
|
+
const waiter = this.tunWritableWaiter;
|
|
39
|
+
if (waiter) {
|
|
40
|
+
this.tunWritableWaiter = null;
|
|
41
|
+
waiter();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async stop() {
|
|
45
|
+
this.cancelled = true;
|
|
46
|
+
if (this.frameReject) {
|
|
47
|
+
this.frameReject(new Error(DEVICE_PUMP_CANCELLED));
|
|
48
|
+
}
|
|
49
|
+
if (this.tunWritableWaiter) {
|
|
50
|
+
this.tunWritableWaiter();
|
|
51
|
+
}
|
|
52
|
+
this.frameWaiter = null;
|
|
53
|
+
this.frameReject = null;
|
|
54
|
+
this.tunWritableWaiter = null;
|
|
55
|
+
if (this.loopPromise) {
|
|
56
|
+
await this.loopPromise.catch(() => undefined);
|
|
57
|
+
this.loopPromise = null;
|
|
58
|
+
}
|
|
59
|
+
this.buffer = Buffer.alloc(0);
|
|
60
|
+
this.deviceConn = null;
|
|
61
|
+
this.running = false;
|
|
62
|
+
}
|
|
63
|
+
enqueueDeviceData(chunk) {
|
|
64
|
+
if (this.cancelled || chunk.length === 0) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
this.pendingDeviceChunks.push(chunk);
|
|
68
|
+
if (this.deviceDrainScheduled) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
this.deviceDrainScheduled = true;
|
|
72
|
+
setImmediate(() => {
|
|
73
|
+
this.deviceDrainScheduled = false;
|
|
74
|
+
const chunks = this.pendingDeviceChunks;
|
|
75
|
+
this.pendingDeviceChunks = [];
|
|
76
|
+
for (const pending of chunks) {
|
|
77
|
+
this.onDeviceData(pending);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
onDeviceData(chunk) {
|
|
82
|
+
if (this.cancelled || chunk.length === 0) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
this.buffer = appendBuffer(this.buffer, chunk);
|
|
86
|
+
if (this.deviceConn &&
|
|
87
|
+
this.buffer.length > MAX_DEVICE_INGRESS_BUFFER &&
|
|
88
|
+
!this.deviceIngressPaused) {
|
|
89
|
+
this.pauseDeviceIngress(this.deviceConn, 'max-buffer');
|
|
90
|
+
}
|
|
91
|
+
this.tryDeliverFrame();
|
|
92
|
+
}
|
|
93
|
+
maybeResumeDeviceIngress() {
|
|
94
|
+
if (!this.deviceConn ||
|
|
95
|
+
!this.deviceIngressPaused ||
|
|
96
|
+
this.buffer.length > MAX_DEVICE_INGRESS_BUFFER) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
this.resumeDeviceIngress(this.deviceConn);
|
|
100
|
+
}
|
|
101
|
+
tryDeliverFrame() {
|
|
102
|
+
const waiter = this.frameWaiter;
|
|
103
|
+
if (!waiter) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const taken = takeOneIpv6Frame(this.buffer);
|
|
107
|
+
if (taken.kind === 'incomplete' || taken.kind === 'resync') {
|
|
108
|
+
if (taken.kind === 'resync') {
|
|
109
|
+
this.buffer = this.buffer.subarray(1);
|
|
110
|
+
this.tryDeliverFrame();
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
this.buffer = shrinkBuffer(this.buffer, taken.length);
|
|
115
|
+
this.frameWaiter = null;
|
|
116
|
+
this.frameReject = null;
|
|
117
|
+
waiter(taken.packet);
|
|
118
|
+
this.maybeResumeDeviceIngress();
|
|
119
|
+
}
|
|
120
|
+
readOneFrame() {
|
|
121
|
+
if (this.cancelled) {
|
|
122
|
+
return Promise.reject(new Error(DEVICE_PUMP_CANCELLED));
|
|
123
|
+
}
|
|
124
|
+
const taken = takeOneIpv6Frame(this.buffer);
|
|
125
|
+
if (taken.kind === 'frame') {
|
|
126
|
+
this.buffer = shrinkBuffer(this.buffer, taken.length);
|
|
127
|
+
return Promise.resolve(taken.packet);
|
|
128
|
+
}
|
|
129
|
+
if (taken.kind === 'resync') {
|
|
130
|
+
this.buffer = this.buffer.subarray(1);
|
|
131
|
+
return this.readOneFrame();
|
|
132
|
+
}
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
this.frameWaiter = resolve;
|
|
135
|
+
this.frameReject = reject;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
pauseDeviceIngress(deviceConn, reason) {
|
|
139
|
+
if (this.deviceIngressPaused || deviceConn.destroyed) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
this.deviceIngressPaused = true;
|
|
143
|
+
deviceConn.pause();
|
|
144
|
+
fwdDebug('ingress-pause', { reason, buf: this.buffer.length });
|
|
145
|
+
}
|
|
146
|
+
resumeDeviceIngress(deviceConn) {
|
|
147
|
+
if (!this.deviceIngressPaused || deviceConn.destroyed) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
this.deviceIngressPaused = false;
|
|
151
|
+
deviceConn.resume();
|
|
152
|
+
fwdDebug('ingress-resume', { buf: this.buffer.length });
|
|
153
|
+
}
|
|
154
|
+
waitTunWritable() {
|
|
155
|
+
if (this.cancelled) {
|
|
156
|
+
return Promise.reject(new Error(DEVICE_PUMP_CANCELLED));
|
|
157
|
+
}
|
|
158
|
+
return new Promise((resolve) => {
|
|
159
|
+
this.tunWritableWaiter = resolve;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
async writeFrameToTun(deviceConn, tun, packet) {
|
|
163
|
+
while (!this.cancelled) {
|
|
164
|
+
const bytesWritten = tun.write(packet);
|
|
165
|
+
if (bytesWritten > 0) {
|
|
166
|
+
this.maybeResumeDeviceIngress();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// Block on tun.write() without pausing the TLS stream — keep reading into the
|
|
170
|
+
// reassembly buffer while waiting for TUN→device progress to free utun capacity.
|
|
171
|
+
fwdDebug('tun-write-blocked', { frameLen: packet.length, buf: this.buffer.length });
|
|
172
|
+
await this.waitTunWritable();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async runLoop(deviceConn, tun) {
|
|
176
|
+
try {
|
|
177
|
+
while (!this.cancelled && !deviceConn.destroyed) {
|
|
178
|
+
const packet = await this.readOneFrame();
|
|
179
|
+
if (this.cancelled) {
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
await this.writeFrameToTun(deviceConn, tun, packet);
|
|
183
|
+
this.fwdFrames += 1;
|
|
184
|
+
if (this.fwdFrames === 1 || this.fwdFrames % 200 === 0) {
|
|
185
|
+
fwdDebug('device-pump-write', { len: packet.length, frames: this.fwdFrames });
|
|
186
|
+
}
|
|
187
|
+
this.onFrameWritten?.();
|
|
188
|
+
if (this.fwdFrames % DEVICE_PUMP_YIELD_EVERY_FRAMES === 0) {
|
|
189
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
if (!isPumpCancelled(err)) {
|
|
195
|
+
throw err;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
fwdDebug('device-pump-stop', { frames: this.fwdFrames });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const DEVICE_PUMP_CANCELLED = 'device pump cancelled';
|
|
202
|
+
function isPumpCancelled(err) {
|
|
203
|
+
return err instanceof Error && err.message === DEVICE_PUMP_CANCELLED;
|
|
204
|
+
}
|
|
205
|
+
function takeOneIpv6Frame(buffer) {
|
|
206
|
+
if (buffer.length < IPV6_HEADER_SIZE) {
|
|
207
|
+
return { kind: 'incomplete' };
|
|
208
|
+
}
|
|
209
|
+
const version = (buffer[0] >> 4) & 0x0f;
|
|
210
|
+
if (version !== IPV6_VERSION) {
|
|
211
|
+
return { kind: 'resync' };
|
|
212
|
+
}
|
|
213
|
+
const payloadLength = buffer.readUInt16BE(4);
|
|
214
|
+
const length = IPV6_HEADER_SIZE + payloadLength;
|
|
215
|
+
if (buffer.length < length) {
|
|
216
|
+
return { kind: 'incomplete' };
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
kind: 'frame',
|
|
220
|
+
packet: buffer.subarray(0, length),
|
|
221
|
+
length,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function shrinkBuffer(buffer, consumed) {
|
|
225
|
+
if (consumed >= buffer.length) {
|
|
226
|
+
return Buffer.alloc(0);
|
|
227
|
+
}
|
|
228
|
+
return buffer.subarray(consumed);
|
|
229
|
+
}
|
package/lib/tunnel/manager.d.ts
CHANGED
|
@@ -14,7 +14,10 @@ export declare class TunnelManager extends EventEmitter<TunnelManagerEvents> {
|
|
|
14
14
|
private readonly packetConsumers;
|
|
15
15
|
private deviceConn;
|
|
16
16
|
private cleanupPromise;
|
|
17
|
-
private
|
|
17
|
+
private tunToDevicePump;
|
|
18
|
+
private deviceToTunPump;
|
|
19
|
+
private deviceIngressPaused;
|
|
20
|
+
private fwdDeviceDataChunks;
|
|
18
21
|
/**
|
|
19
22
|
* Register a listener for parsed tunnel packets (in addition to the `data` event).
|
|
20
23
|
*
|
|
@@ -57,12 +60,14 @@ export declare class TunnelManager extends EventEmitter<TunnelManagerEvents> {
|
|
|
57
60
|
*/
|
|
58
61
|
stop(): Promise<void>;
|
|
59
62
|
private hasPacketTap;
|
|
63
|
+
private pauseDeviceIngress;
|
|
64
|
+
private resumeDeviceIngress;
|
|
60
65
|
private processBuffer;
|
|
61
66
|
private writeDeviceFrameToTun;
|
|
62
67
|
private tapL4Packet;
|
|
63
68
|
private dispatchPacketData;
|
|
64
|
-
private
|
|
65
|
-
private
|
|
69
|
+
private startDeviceToTunPump;
|
|
70
|
+
private startTunToDevicePump;
|
|
66
71
|
private _performStop;
|
|
67
72
|
}
|
|
68
73
|
/**
|