appium-ios-tuntap 0.4.4 → 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 +6 -0
- package/lib/platform/windows.js +9 -8
- package/lib/tunnel/constants.d.ts +12 -4
- package/lib/tunnel/constants.js +12 -4
- 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 -7
- package/lib/tunnel/manager.js +159 -103
- 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 -3
- 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_windows.cc +2 -2
- package/src/tuntap.cc +77 -16
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
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
|
+
|
|
1
7
|
## [0.4.4](https://github.com/appium/appium-ios-tuntap/compare/v0.4.3...v0.4.4) (2026-06-12)
|
|
2
8
|
|
|
3
9
|
### Bug Fixes
|
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,15 +6,23 @@ 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;
|
|
18
28
|
export declare const IPPROTO_UDP = 17;
|
|
19
|
-
/** Pause device ingress when the reassembly buffer exceeds this size. */
|
|
20
|
-
export declare const MAX_DEVICE_INGRESS_BUFFER: number;
|
package/lib/tunnel/constants.js
CHANGED
|
@@ -6,15 +6,23 @@ 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;
|
|
18
28
|
export const IPPROTO_UDP = 17;
|
|
19
|
-
/** Pause device ingress when the reassembly buffer exceeds this size. */
|
|
20
|
-
export const MAX_DEVICE_INGRESS_BUFFER = 8 * 1024 * 1024;
|
|
@@ -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,8 +14,10 @@ export declare class TunnelManager extends EventEmitter<TunnelManagerEvents> {
|
|
|
14
14
|
private readonly packetConsumers;
|
|
15
15
|
private deviceConn;
|
|
16
16
|
private cleanupPromise;
|
|
17
|
-
private
|
|
18
|
-
private
|
|
17
|
+
private tunToDevicePump;
|
|
18
|
+
private deviceToTunPump;
|
|
19
|
+
private deviceIngressPaused;
|
|
20
|
+
private fwdDeviceDataChunks;
|
|
19
21
|
/**
|
|
20
22
|
* Register a listener for parsed tunnel packets (in addition to the `data` event).
|
|
21
23
|
*
|
|
@@ -58,15 +60,14 @@ export declare class TunnelManager extends EventEmitter<TunnelManagerEvents> {
|
|
|
58
60
|
*/
|
|
59
61
|
stop(): Promise<void>;
|
|
60
62
|
private hasPacketTap;
|
|
61
|
-
private processBuffer;
|
|
62
|
-
private writeDeviceFrameToTun;
|
|
63
|
-
private shouldResumeDeviceIngress;
|
|
64
63
|
private pauseDeviceIngress;
|
|
65
64
|
private resumeDeviceIngress;
|
|
65
|
+
private processBuffer;
|
|
66
|
+
private writeDeviceFrameToTun;
|
|
66
67
|
private tapL4Packet;
|
|
67
68
|
private dispatchPacketData;
|
|
68
|
-
private
|
|
69
|
-
private
|
|
69
|
+
private startDeviceToTunPump;
|
|
70
|
+
private startTunToDevicePump;
|
|
70
71
|
private _performStop;
|
|
71
72
|
}
|
|
72
73
|
/**
|
package/lib/tunnel/manager.js
CHANGED
|
@@ -2,8 +2,11 @@ import { log } from '../logger.js';
|
|
|
2
2
|
import { TunTap } from '../TunTap.js';
|
|
3
3
|
import { EventEmitter } from 'node:events';
|
|
4
4
|
import { Buffer } from 'node:buffer';
|
|
5
|
-
import { CD_TUNNEL_HANDSHAKE_TIMEOUT_MS, CD_TUNNEL_HEADER_SIZE, CD_TUNNEL_MAGIC, CD_TUNNEL_MAGIC_SIZE, CD_TUNNEL_MTU,
|
|
5
|
+
import { CD_TUNNEL_HANDSHAKE_TIMEOUT_MS, CD_TUNNEL_HEADER_SIZE, CD_TUNNEL_MAGIC, CD_TUNNEL_MAGIC_SIZE, CD_TUNNEL_MTU, IPV6_HEADER_SIZE, MAX_DEVICE_INGRESS_BUFFER, IPV6_VERSION, IPPROTO_TCP, IPPROTO_UDP, } from './constants.js';
|
|
6
6
|
import { appendBuffer } from './buffer-utils.js';
|
|
7
|
+
import { fwdBufferState, fwdDebug, tunDebug } from './debug-log.js';
|
|
8
|
+
import { TunToDevicePump } from './tun-to-device-pump.js';
|
|
9
|
+
import { DeviceToTunPump } from './device-to-tun-pump.js';
|
|
7
10
|
/**
|
|
8
11
|
* Bridges a CoreDevice tunnel `Socket` and a {@link TunTap} interface: IPv6 framing, TUN I/O, and packet fan-out.
|
|
9
12
|
* Emits {@link TunnelManagerEvents} (currently `data` with {@link PacketData}) for TCP/UDP packets, same as registered consumers.
|
|
@@ -16,8 +19,10 @@ export class TunnelManager extends EventEmitter {
|
|
|
16
19
|
packetConsumers = new Set();
|
|
17
20
|
deviceConn = null;
|
|
18
21
|
cleanupPromise = null;
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
tunToDevicePump = null;
|
|
23
|
+
deviceToTunPump = null;
|
|
24
|
+
deviceIngressPaused = false;
|
|
25
|
+
fwdDeviceDataChunks = 0;
|
|
21
26
|
/**
|
|
22
27
|
* Register a listener for parsed tunnel packets (in addition to the `data` event).
|
|
23
28
|
*
|
|
@@ -82,20 +87,20 @@ export class TunnelManager extends EventEmitter {
|
|
|
82
87
|
* @returns interface name, MTU, and the live {@link TunTap} instance
|
|
83
88
|
*/
|
|
84
89
|
async setupInterface(tunnelInfo) {
|
|
85
|
-
|
|
90
|
+
tunDebug(`Setting up tunnel with parameters:`, tunnelInfo);
|
|
86
91
|
try {
|
|
87
92
|
this.tun = new TunTap();
|
|
88
93
|
// Open the TUN device
|
|
89
94
|
if (!this.tun.open()) {
|
|
90
95
|
throw new Error('Failed to open TUN device');
|
|
91
96
|
}
|
|
92
|
-
|
|
97
|
+
tunDebug(`Opened TUN device: ${this.tun.name}`);
|
|
93
98
|
this.mtu = tunnelInfo.clientParameters.mtu;
|
|
94
99
|
// Configure the TUN device with IPv6 address and MTU
|
|
95
100
|
await this.tun.configure(tunnelInfo.clientParameters.address, tunnelInfo.clientParameters.mtu);
|
|
96
101
|
// Add route for the server address
|
|
97
102
|
await this.tun.addRoute(`${tunnelInfo.serverAddress}/128`);
|
|
98
|
-
|
|
103
|
+
tunDebug(`Configured TUN interface ${this.tun.name} with address ${tunnelInfo.clientParameters.address} and MTU ${tunnelInfo.clientParameters.mtu}`);
|
|
99
104
|
return {
|
|
100
105
|
name: this.tun.name,
|
|
101
106
|
mtu: tunnelInfo.clientParameters.mtu,
|
|
@@ -127,32 +132,47 @@ export class TunnelManager extends EventEmitter {
|
|
|
127
132
|
return;
|
|
128
133
|
}
|
|
129
134
|
this.deviceConn = deviceConn;
|
|
130
|
-
|
|
135
|
+
tunDebug(`Starting bidirectional data forwarding for ${this.tun.name}`);
|
|
131
136
|
deviceConn.setNoDelay(true);
|
|
132
137
|
deviceConn.setKeepAlive(true, 1000);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
try {
|
|
139
|
-
this.buffer = appendBuffer(this.buffer, data);
|
|
140
|
-
if (this.buffer.length > MAX_DEVICE_INGRESS_BUFFER) {
|
|
141
|
-
this.pauseDeviceIngress();
|
|
138
|
+
if (this.hasPacketTap()) {
|
|
139
|
+
deviceConn.on('data', (data) => {
|
|
140
|
+
if (this.cancelled) {
|
|
141
|
+
return;
|
|
142
142
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
143
|
+
try {
|
|
144
|
+
this.fwdDeviceDataChunks += 1;
|
|
145
|
+
this.buffer = appendBuffer(this.buffer, data);
|
|
146
|
+
if (this.buffer.length > MAX_DEVICE_INGRESS_BUFFER) {
|
|
147
|
+
this.pauseDeviceIngress('max-buffer');
|
|
148
|
+
}
|
|
149
|
+
if (this.fwdDeviceDataChunks === 1 || this.fwdDeviceDataChunks % 200 === 0) {
|
|
150
|
+
fwdDebug('device-data', {
|
|
151
|
+
chunk: data.length,
|
|
152
|
+
...fwdBufferState(this.buffer),
|
|
153
|
+
ingressPaused: this.deviceIngressPaused,
|
|
154
|
+
chunks: this.fwdDeviceDataChunks,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
this.processBuffer('data');
|
|
148
158
|
}
|
|
149
|
-
|
|
159
|
+
catch (err) {
|
|
160
|
+
if (!this.cancelled) {
|
|
161
|
+
log.error('Error processing device data:', err.message);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
this.startDeviceToTunPump(deviceConn);
|
|
168
|
+
}
|
|
169
|
+
deviceConn.on('drain', () => {
|
|
170
|
+
this.deviceToTunPump?.notifyTunWritable();
|
|
150
171
|
});
|
|
151
|
-
|
|
152
|
-
this.startTunReadLoop(deviceConn);
|
|
172
|
+
this.startTunToDevicePump(deviceConn);
|
|
153
173
|
// Listen for device connection close
|
|
154
174
|
deviceConn.on('close', async () => {
|
|
155
|
-
|
|
175
|
+
tunDebug('Device connection closed, stopping tunnel');
|
|
156
176
|
try {
|
|
157
177
|
await this.stop();
|
|
158
178
|
}
|
|
@@ -180,8 +200,32 @@ export class TunnelManager extends EventEmitter {
|
|
|
180
200
|
hasPacketTap() {
|
|
181
201
|
return this.packetConsumers.size > 0 || this.listenerCount('data') > 0;
|
|
182
202
|
}
|
|
183
|
-
|
|
203
|
+
pauseDeviceIngress(reason) {
|
|
204
|
+
if (this.deviceIngressPaused || !this.deviceConn || this.deviceConn.destroyed) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
this.deviceIngressPaused = true;
|
|
208
|
+
this.deviceConn.pause();
|
|
209
|
+
fwdDebug('ingress-pause', {
|
|
210
|
+
reason,
|
|
211
|
+
...fwdBufferState(this.buffer),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
resumeDeviceIngress() {
|
|
215
|
+
if (!this.deviceIngressPaused || !this.deviceConn || this.deviceConn.destroyed) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
this.deviceIngressPaused = false;
|
|
219
|
+
this.deviceConn.resume();
|
|
220
|
+
fwdDebug('ingress-resume', {
|
|
221
|
+
...fwdBufferState(this.buffer),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
processBuffer(trigger = 'unknown') {
|
|
184
225
|
let offset = 0;
|
|
226
|
+
let tunWriteBlocked = false;
|
|
227
|
+
let framesWritten = 0;
|
|
228
|
+
const bufBefore = this.buffer.length;
|
|
185
229
|
while (offset + IPV6_HEADER_SIZE <= this.buffer.length) {
|
|
186
230
|
const frame = nextIpv6Frame(this.buffer, offset);
|
|
187
231
|
if (frame.kind === 'incomplete') {
|
|
@@ -196,11 +240,22 @@ export class TunnelManager extends EventEmitter {
|
|
|
196
240
|
break;
|
|
197
241
|
}
|
|
198
242
|
try {
|
|
199
|
-
this.writeDeviceFrameToTun(this.tun, frame.packet, frame.nextHeader);
|
|
243
|
+
const bytesWritten = this.writeDeviceFrameToTun(this.tun, frame.packet, frame.nextHeader);
|
|
244
|
+
if (bytesWritten === 'blocked') {
|
|
245
|
+
tunWriteBlocked = true;
|
|
246
|
+
fwdDebug('tun-write-blocked', {
|
|
247
|
+
trigger,
|
|
248
|
+
frameLen: frame.length,
|
|
249
|
+
...fwdBufferState(this.buffer),
|
|
250
|
+
ingressPaused: this.deviceIngressPaused,
|
|
251
|
+
});
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
200
254
|
}
|
|
201
255
|
catch (err) {
|
|
202
256
|
log.error(`Error writing to TUN: ${err.message}`);
|
|
203
257
|
}
|
|
258
|
+
framesWritten += 1;
|
|
204
259
|
offset += frame.length;
|
|
205
260
|
}
|
|
206
261
|
if (offset > 0) {
|
|
@@ -211,62 +266,63 @@ export class TunnelManager extends EventEmitter {
|
|
|
211
266
|
this.buffer = this.buffer.subarray(offset);
|
|
212
267
|
}
|
|
213
268
|
}
|
|
214
|
-
if (this.
|
|
215
|
-
|
|
269
|
+
if (this.deviceIngressPaused) {
|
|
270
|
+
const mayResume = !tunWriteBlocked &&
|
|
271
|
+
(this.buffer.length === 0 || this.buffer.length <= MAX_DEVICE_INGRESS_BUFFER / 2);
|
|
272
|
+
if (mayResume) {
|
|
273
|
+
this.resumeDeviceIngress();
|
|
274
|
+
}
|
|
275
|
+
else if (tunWriteBlocked || framesWritten > 0 || bufBefore > 0) {
|
|
276
|
+
fwdDebug('process-buffer', {
|
|
277
|
+
trigger,
|
|
278
|
+
bufBefore,
|
|
279
|
+
bufAfter: this.buffer.length,
|
|
280
|
+
offset,
|
|
281
|
+
framesWritten,
|
|
282
|
+
tunWriteBlocked,
|
|
283
|
+
mayResume,
|
|
284
|
+
ingressPaused: this.deviceIngressPaused,
|
|
285
|
+
...fwdBufferState(this.buffer),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
216
288
|
}
|
|
217
289
|
}
|
|
218
290
|
writeDeviceFrameToTun(tun, packet, nextHeader) {
|
|
219
|
-
tun.write(packet);
|
|
291
|
+
const bytesWritten = tun.write(packet);
|
|
292
|
+
if (bytesWritten <= 0) {
|
|
293
|
+
this.pauseDeviceIngress('tun-write-blocked');
|
|
294
|
+
return 'blocked';
|
|
295
|
+
}
|
|
220
296
|
if (!this.hasPacketTap()) {
|
|
221
|
-
return;
|
|
297
|
+
return bytesWritten;
|
|
222
298
|
}
|
|
223
299
|
const { src, dst } = ipv6Endpoints(packet);
|
|
224
|
-
|
|
300
|
+
tunDebug(`Device → TUN: ${bytesWritten} bytes, IPv6 src=${src}, dst=${dst}`);
|
|
225
301
|
this.tapL4Packet(packet, nextHeader, src, dst);
|
|
226
|
-
|
|
227
|
-
shouldResumeDeviceIngress() {
|
|
228
|
-
if (this.buffer.length === 0) {
|
|
229
|
-
return true;
|
|
230
|
-
}
|
|
231
|
-
// All complete frames were written; waiting on more socket bytes to finish the tail frame.
|
|
232
|
-
return nextIpv6Frame(this.buffer, 0).kind === 'incomplete';
|
|
233
|
-
}
|
|
234
|
-
pauseDeviceIngress() {
|
|
235
|
-
if (this.deviceIngressPausedForTun || !this.deviceConn || this.deviceConn.destroyed) {
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
this.deviceIngressPausedForTun = true;
|
|
239
|
-
this.deviceConn.pause();
|
|
240
|
-
}
|
|
241
|
-
resumeDeviceIngress() {
|
|
242
|
-
if (!this.deviceIngressPausedForTun || !this.deviceConn || this.deviceConn.destroyed) {
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
this.deviceIngressPausedForTun = false;
|
|
246
|
-
this.deviceConn.resume();
|
|
302
|
+
return bytesWritten;
|
|
247
303
|
}
|
|
248
304
|
tapL4Packet(packet, nextHeader, src, dst) {
|
|
249
305
|
let packetData = null;
|
|
250
306
|
if (nextHeader === IPPROTO_UDP) {
|
|
251
307
|
packetData = parseUdpPacketData(packet, src, dst);
|
|
252
308
|
if (!packetData) {
|
|
253
|
-
|
|
309
|
+
tunDebug('UDP payload too short, not emitting event.');
|
|
254
310
|
}
|
|
255
311
|
else {
|
|
256
|
-
|
|
312
|
+
tunDebug(`UDP packet detected: payload length=${packetData.payload.length}`);
|
|
257
313
|
}
|
|
258
314
|
}
|
|
259
315
|
else if (nextHeader === IPPROTO_TCP) {
|
|
260
316
|
packetData = parseTcpPacketData(packet, src, dst);
|
|
261
317
|
if (!packetData) {
|
|
262
|
-
|
|
318
|
+
tunDebug('TCP packet too short or malformed, skipping.');
|
|
263
319
|
}
|
|
264
320
|
else {
|
|
265
|
-
|
|
321
|
+
tunDebug(`TCP packet detected: payload length=${packetData.payload.length}`);
|
|
266
322
|
}
|
|
267
323
|
}
|
|
268
324
|
else {
|
|
269
|
-
|
|
325
|
+
tunDebug('Packet is not UDP or TCP (nextHeader !== 17 and !== 6)');
|
|
270
326
|
}
|
|
271
327
|
if (packetData) {
|
|
272
328
|
this.dispatchPacketData(packetData);
|
|
@@ -282,54 +338,54 @@ export class TunnelManager extends EventEmitter {
|
|
|
282
338
|
log.error('Error in packet consumer:', err);
|
|
283
339
|
}
|
|
284
340
|
}
|
|
285
|
-
|
|
341
|
+
tunDebug(`Emitted data event for ${packetData.protocol} packet`);
|
|
286
342
|
}
|
|
287
|
-
|
|
343
|
+
startDeviceToTunPump(deviceConn) {
|
|
288
344
|
if (!this.tun) {
|
|
289
345
|
return;
|
|
290
346
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
? this.mtu
|
|
294
|
-
: Math.min(MAX_TUN_POLL_BUFFER, Math.max(this.mtu, LARGE_TUN_POLL_BUFFER));
|
|
295
|
-
const queueDepth = tapOn ? DEFAULT_TUN_POLL_QUEUE_DEPTH : FAST_TUN_POLL_QUEUE_DEPTH;
|
|
296
|
-
this.tun.startPolling((data) => {
|
|
297
|
-
if (this.cancelled || !data.length || deviceConn.destroyed) {
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
if (tapOn && data.length >= IPV6_HEADER_SIZE) {
|
|
301
|
-
log.debug(`TUN → Device: ${data.length} bytes, IPv6 src=${formatIPv6Address(data.subarray(8, 24))}, dst=${formatIPv6Address(data.subarray(24, 40))}`);
|
|
302
|
-
}
|
|
303
|
-
else if (tapOn) {
|
|
304
|
-
log.debug(`TUN → Device: ${data.length} bytes (too small for IPv6 header)`);
|
|
305
|
-
}
|
|
306
|
-
this.writeTunPacketToDevice(deviceConn, data);
|
|
307
|
-
}, pollBuffer, queueDepth);
|
|
347
|
+
this.deviceToTunPump = new DeviceToTunPump();
|
|
348
|
+
this.deviceToTunPump.start(deviceConn, this.tun);
|
|
308
349
|
}
|
|
309
|
-
|
|
310
|
-
if (
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
const canWriteMore = deviceConn.write(data);
|
|
314
|
-
if (canWriteMore || this.tunReadPausedForBackpressure || !this.tun) {
|
|
350
|
+
startTunToDevicePump(deviceConn) {
|
|
351
|
+
if (!this.tun) {
|
|
315
352
|
return;
|
|
316
353
|
}
|
|
317
|
-
|
|
318
|
-
this.tun.
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
354
|
+
const tapOn = this.hasPacketTap();
|
|
355
|
+
this.tunToDevicePump = new TunToDevicePump(this.tun, this.mtu, tapOn
|
|
356
|
+
? (data) => {
|
|
357
|
+
if (data.length >= IPV6_HEADER_SIZE) {
|
|
358
|
+
tunDebug(`TUN → Device: ${data.length} bytes, IPv6 src=${formatIPv6Address(data.subarray(8, 24))}, dst=${formatIPv6Address(data.subarray(24, 40))}`);
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
tunDebug(`TUN → Device: ${data.length} bytes (too small for IPv6 header)`);
|
|
362
|
+
}
|
|
322
363
|
}
|
|
323
|
-
|
|
324
|
-
this.
|
|
325
|
-
|
|
326
|
-
|
|
364
|
+
: undefined, () => {
|
|
365
|
+
this.deviceToTunPump?.notifyTunWritable();
|
|
366
|
+
if (this.deviceIngressPaused) {
|
|
367
|
+
fwdDebug('forward-progress', {
|
|
368
|
+
ingressPaused: this.deviceIngressPaused,
|
|
369
|
+
...fwdBufferState(this.buffer),
|
|
370
|
+
});
|
|
371
|
+
this.processBuffer('forward-progress');
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
this.tunToDevicePump.start(deviceConn);
|
|
327
375
|
}
|
|
328
376
|
async _performStop() {
|
|
329
377
|
const tunName = this.tun ? this.tun.name : 'unknown';
|
|
330
|
-
|
|
378
|
+
tunDebug(`Stopping tunnel manager for ${tunName}`);
|
|
331
379
|
// Signal cancellation
|
|
332
380
|
this.cancelled = true;
|
|
381
|
+
if (this.tunToDevicePump) {
|
|
382
|
+
await this.tunToDevicePump.stop();
|
|
383
|
+
this.tunToDevicePump = null;
|
|
384
|
+
}
|
|
385
|
+
if (this.deviceToTunPump) {
|
|
386
|
+
await this.deviceToTunPump.stop();
|
|
387
|
+
this.deviceToTunPump = null;
|
|
388
|
+
}
|
|
333
389
|
// Close device connection if exists
|
|
334
390
|
if (this.deviceConn && !this.deviceConn.destroyed) {
|
|
335
391
|
this.deviceConn.destroy();
|
|
@@ -351,7 +407,7 @@ export class TunnelManager extends EventEmitter {
|
|
|
351
407
|
}
|
|
352
408
|
this.tun = null;
|
|
353
409
|
}
|
|
354
|
-
|
|
410
|
+
tunDebug(`Tunnel for ${tunName} closed successfully`);
|
|
355
411
|
}
|
|
356
412
|
}
|
|
357
413
|
/**
|
|
@@ -366,7 +422,7 @@ export async function exchangeCoreTunnelParameters(socket) {
|
|
|
366
422
|
mtu: CD_TUNNEL_MTU,
|
|
367
423
|
});
|
|
368
424
|
const message = encodeCdTunnelMessage(requestJson);
|
|
369
|
-
|
|
425
|
+
tunDebug(`Sending CDTunnel packet: magic=${CD_TUNNEL_MAGIC}, length=${message.length - CD_TUNNEL_HEADER_SIZE}, body=${requestJson}`);
|
|
370
426
|
socket.write(message);
|
|
371
427
|
return readCdTunnelResponse(socket, CD_TUNNEL_HANDSHAKE_TIMEOUT_MS);
|
|
372
428
|
}
|
|
@@ -381,15 +437,15 @@ export async function connectToTunnelLockdown(secureServiceSocket) {
|
|
|
381
437
|
try {
|
|
382
438
|
// Exchange tunnel parameters with the device
|
|
383
439
|
const tunnelInfo = await exchangeCoreTunnelParameters(secureServiceSocket);
|
|
384
|
-
|
|
440
|
+
tunDebug('Tunnel parameters exchanged:', tunnelInfo);
|
|
385
441
|
// Setup tunnel interface
|
|
386
442
|
const tunInterfaceInfo = await tunnelManager.setupInterface(tunnelInfo);
|
|
387
|
-
|
|
443
|
+
tunDebug('Tunnel interface set up:', tunInterfaceInfo.name);
|
|
388
444
|
// Start bidirectional forwarding
|
|
389
445
|
tunnelManager.startForwarding(secureServiceSocket);
|
|
390
446
|
// Create close function
|
|
391
447
|
const closeFunc = async () => {
|
|
392
|
-
|
|
448
|
+
tunDebug('Closing tunnel connection');
|
|
393
449
|
await tunnelManager.stop();
|
|
394
450
|
if (!secureServiceSocket.destroyed) {
|
|
395
451
|
secureServiceSocket.end();
|
|
@@ -458,11 +514,11 @@ function readCdTunnelResponse(socket, timeoutMs) {
|
|
|
458
514
|
action();
|
|
459
515
|
};
|
|
460
516
|
const onData = (chunk) => {
|
|
461
|
-
|
|
517
|
+
tunDebug('Received data chunk:', chunk.length, 'bytes');
|
|
462
518
|
buffer = appendBuffer(buffer, chunk);
|
|
463
519
|
if (buffer.length >= CD_TUNNEL_HEADER_SIZE) {
|
|
464
520
|
const payloadLength = buffer.readUInt16BE(CD_TUNNEL_MAGIC_SIZE);
|
|
465
|
-
|
|
521
|
+
tunDebug('Expected total packet length:', CD_TUNNEL_HEADER_SIZE + payloadLength, 'current buffer:', buffer.length);
|
|
466
522
|
}
|
|
467
523
|
const result = tryParseCdTunnelResponse(buffer);
|
|
468
524
|
if (result.kind === 'incomplete') {
|
|
@@ -472,7 +528,7 @@ function readCdTunnelResponse(socket, timeoutMs) {
|
|
|
472
528
|
finish(() => reject(result.error));
|
|
473
529
|
return;
|
|
474
530
|
}
|
|
475
|
-
|
|
531
|
+
tunDebug('Parsed CDTunnel response:', result.value);
|
|
476
532
|
finish(() => resolve(result.value));
|
|
477
533
|
};
|
|
478
534
|
const onError = (err) => {
|
|
@@ -480,9 +536,9 @@ function readCdTunnelResponse(socket, timeoutMs) {
|
|
|
480
536
|
finish(() => reject(err));
|
|
481
537
|
};
|
|
482
538
|
const onEnd = () => {
|
|
483
|
-
|
|
539
|
+
tunDebug('Connection ended');
|
|
484
540
|
if (buffer.length > 0) {
|
|
485
|
-
|
|
541
|
+
tunDebug('Buffer at end:', buffer.toString('hex'));
|
|
486
542
|
}
|
|
487
543
|
finish(() => reject(new Error('Connection closed before receiving complete response')));
|
|
488
544
|
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Socket } from 'node:net';
|
|
2
|
+
import type { TunTap } from '../TunTap.js';
|
|
3
|
+
export type TunToDeviceProgressHook = () => void;
|
|
4
|
+
/**
|
|
5
|
+
* TUN→device forwarding: utun poll fills an ingress queue; a writer loop drains
|
|
6
|
+
* the queue to TLS with backpressure (await drain).
|
|
7
|
+
*/
|
|
8
|
+
export declare class TunToDevicePump {
|
|
9
|
+
private readonly tun;
|
|
10
|
+
private readonly mtu;
|
|
11
|
+
private readonly onPacketRead?;
|
|
12
|
+
private readonly onForwardProgress?;
|
|
13
|
+
private cancelled;
|
|
14
|
+
private running;
|
|
15
|
+
private tunIngressQueue;
|
|
16
|
+
private ingressWaiter;
|
|
17
|
+
private ingressReject;
|
|
18
|
+
private drainAbort;
|
|
19
|
+
private loopPromise;
|
|
20
|
+
private fwdPumpPackets;
|
|
21
|
+
constructor(tun: TunTap, mtu: number, onPacketRead?: ((data: Buffer) => void) | undefined, onForwardProgress?: TunToDeviceProgressHook | undefined);
|
|
22
|
+
start(deviceConn: Socket): void;
|
|
23
|
+
stop(): Promise<void>;
|
|
24
|
+
private onTunPollData;
|
|
25
|
+
private signalIngress;
|
|
26
|
+
private maybeResumeTunPolling;
|
|
27
|
+
private waitForIngress;
|
|
28
|
+
private writeAndDrain;
|
|
29
|
+
private runLoop;
|
|
30
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { once } from 'node:events';
|
|
2
|
+
import { MAX_TUN_INGRESS_QUEUE, TUN_POLL_TSFN_QUEUE_DEPTH } from './constants.js';
|
|
3
|
+
import { fwdDebug } from './debug-log.js';
|
|
4
|
+
/**
|
|
5
|
+
* TUN→device forwarding: utun poll fills an ingress queue; a writer loop drains
|
|
6
|
+
* the queue to TLS with backpressure (await drain).
|
|
7
|
+
*/
|
|
8
|
+
export class TunToDevicePump {
|
|
9
|
+
tun;
|
|
10
|
+
mtu;
|
|
11
|
+
onPacketRead;
|
|
12
|
+
onForwardProgress;
|
|
13
|
+
cancelled = false;
|
|
14
|
+
running = false;
|
|
15
|
+
tunIngressQueue = [];
|
|
16
|
+
ingressWaiter = null;
|
|
17
|
+
ingressReject = null;
|
|
18
|
+
drainAbort = null;
|
|
19
|
+
loopPromise = null;
|
|
20
|
+
fwdPumpPackets = 0;
|
|
21
|
+
constructor(tun, mtu, onPacketRead, onForwardProgress) {
|
|
22
|
+
this.tun = tun;
|
|
23
|
+
this.mtu = mtu;
|
|
24
|
+
this.onPacketRead = onPacketRead;
|
|
25
|
+
this.onForwardProgress = onForwardProgress;
|
|
26
|
+
}
|
|
27
|
+
start(deviceConn) {
|
|
28
|
+
if (this.running) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
this.running = true;
|
|
32
|
+
this.cancelled = false;
|
|
33
|
+
this.tun.startPolling((data) => this.onTunPollData(data), this.mtu, TUN_POLL_TSFN_QUEUE_DEPTH);
|
|
34
|
+
this.tun.resumePolling();
|
|
35
|
+
fwdDebug('pump-start', { mtu: this.mtu, queueDepth: TUN_POLL_TSFN_QUEUE_DEPTH });
|
|
36
|
+
this.loopPromise = this.runLoop(deviceConn);
|
|
37
|
+
}
|
|
38
|
+
async stop() {
|
|
39
|
+
this.cancelled = true;
|
|
40
|
+
this.drainAbort?.abort();
|
|
41
|
+
this.drainAbort = null;
|
|
42
|
+
this.tun.pausePolling();
|
|
43
|
+
if (this.ingressReject) {
|
|
44
|
+
this.ingressReject(new Error(TUN_PUMP_CANCELLED));
|
|
45
|
+
}
|
|
46
|
+
else if (this.ingressWaiter) {
|
|
47
|
+
this.ingressWaiter();
|
|
48
|
+
}
|
|
49
|
+
this.ingressWaiter = null;
|
|
50
|
+
this.ingressReject = null;
|
|
51
|
+
this.tunIngressQueue = [];
|
|
52
|
+
if (this.loopPromise) {
|
|
53
|
+
await this.loopPromise.catch(() => undefined);
|
|
54
|
+
this.loopPromise = null;
|
|
55
|
+
}
|
|
56
|
+
this.running = false;
|
|
57
|
+
}
|
|
58
|
+
onTunPollData(data) {
|
|
59
|
+
if (this.cancelled || !data.length) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
this.onPacketRead?.(data);
|
|
63
|
+
this.tunIngressQueue.push(data);
|
|
64
|
+
if (this.tunIngressQueue.length >= MAX_TUN_INGRESS_QUEUE) {
|
|
65
|
+
this.tun.pausePolling();
|
|
66
|
+
fwdDebug('tun-ingress-pause', { queued: this.tunIngressQueue.length });
|
|
67
|
+
}
|
|
68
|
+
this.signalIngress();
|
|
69
|
+
}
|
|
70
|
+
signalIngress() {
|
|
71
|
+
const waiter = this.ingressWaiter;
|
|
72
|
+
if (!waiter) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
this.ingressWaiter = null;
|
|
76
|
+
this.ingressReject = null;
|
|
77
|
+
waiter();
|
|
78
|
+
}
|
|
79
|
+
maybeResumeTunPolling() {
|
|
80
|
+
if (this.tunIngressQueue.length < MAX_TUN_INGRESS_QUEUE) {
|
|
81
|
+
this.tun.resumePolling();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
waitForIngress() {
|
|
85
|
+
if (this.cancelled) {
|
|
86
|
+
return Promise.reject(new Error(TUN_PUMP_CANCELLED));
|
|
87
|
+
}
|
|
88
|
+
if (this.tunIngressQueue.length > 0) {
|
|
89
|
+
return Promise.resolve();
|
|
90
|
+
}
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
this.ingressWaiter = resolve;
|
|
93
|
+
this.ingressReject = reject;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
async writeAndDrain(deviceConn, data) {
|
|
97
|
+
if (deviceConn.destroyed) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const canWriteMore = deviceConn.write(data);
|
|
101
|
+
if (this.fwdPumpPackets === 1 || this.fwdPumpPackets % 200 === 0 || !canWriteMore) {
|
|
102
|
+
fwdDebug('pump-write', {
|
|
103
|
+
len: data.length,
|
|
104
|
+
canWriteMore,
|
|
105
|
+
writableLength: deviceConn.writableLength,
|
|
106
|
+
packets: this.fwdPumpPackets,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (!canWriteMore || deviceConn.writableNeedDrain) {
|
|
110
|
+
this.onForwardProgress?.();
|
|
111
|
+
fwdDebug('pump-drain-wait', {
|
|
112
|
+
writableLength: deviceConn.writableLength,
|
|
113
|
+
len: data.length,
|
|
114
|
+
tunQueued: this.tunIngressQueue.length,
|
|
115
|
+
});
|
|
116
|
+
this.drainAbort = new AbortController();
|
|
117
|
+
try {
|
|
118
|
+
await once(deviceConn, 'drain', { signal: this.drainAbort.signal });
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
this.drainAbort = null;
|
|
125
|
+
}
|
|
126
|
+
fwdDebug('pump-drain-done', {
|
|
127
|
+
writableLength: deviceConn.writableLength,
|
|
128
|
+
tunQueued: this.tunIngressQueue.length,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
if (this.cancelled) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
this.onForwardProgress?.();
|
|
135
|
+
}
|
|
136
|
+
async runLoop(deviceConn) {
|
|
137
|
+
try {
|
|
138
|
+
while (!this.cancelled && !deviceConn.destroyed) {
|
|
139
|
+
await this.waitForIngress();
|
|
140
|
+
while (this.tunIngressQueue.length > 0 && !this.cancelled && !deviceConn.destroyed) {
|
|
141
|
+
const packet = this.tunIngressQueue.shift();
|
|
142
|
+
if (!packet) {
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
this.maybeResumeTunPolling();
|
|
146
|
+
this.fwdPumpPackets += 1;
|
|
147
|
+
if (this.fwdPumpPackets === 1 || this.fwdPumpPackets % 200 === 0) {
|
|
148
|
+
fwdDebug('pump-read', { len: packet.length, packets: this.fwdPumpPackets });
|
|
149
|
+
}
|
|
150
|
+
this.onForwardProgress?.();
|
|
151
|
+
await this.writeAndDrain(deviceConn, packet);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
if (!isPumpCancelled(err)) {
|
|
157
|
+
throw err;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
fwdDebug('pump-stop', { packets: this.fwdPumpPackets });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const TUN_PUMP_CANCELLED = 'pump cancelled';
|
|
164
|
+
function isPumpCancelled(err) {
|
|
165
|
+
return err instanceof Error && err.message === TUN_PUMP_CANCELLED;
|
|
166
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appium-ios-tuntap",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Native TUN/TAP interface module for Node.js",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"refresh:wintun": "node scripts/fetch-wintun.mjs",
|
|
19
19
|
"prepare": "npm run build",
|
|
20
20
|
"test": "npm run test:integration && npm run test:unit",
|
|
21
|
-
"test:unit": "node --test --test-force-exit --test-timeout=
|
|
22
|
-
"test:integration": "node --test --test-force-exit --test-timeout=
|
|
21
|
+
"test:unit": "node --test --test-force-exit --test-timeout=5000 \"test/unit/**/*.spec.mjs\"",
|
|
22
|
+
"test:integration": "node --test --test-force-exit --test-timeout=15000 \"test/integration/**/*.spec.mjs\""
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"src/tuntap.cc",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -121,8 +121,8 @@ void PosixUvPollLoop::OnPoll(uv_poll_t* handle, int status, int events) {
|
|
|
121
121
|
|
|
122
122
|
switch (rs) {
|
|
123
123
|
case ReadPacketStatus::Data:
|
|
124
|
-
if (state->on_packet) {
|
|
125
|
-
|
|
124
|
+
if (state->on_packet && !state->on_packet(std::move(packet))) {
|
|
125
|
+
return;
|
|
126
126
|
}
|
|
127
127
|
break;
|
|
128
128
|
case ReadPacketStatus::NoData:
|
package/src/native/tun_backend.h
CHANGED
|
@@ -42,7 +42,7 @@ public:
|
|
|
42
42
|
// background thread (libuv loop thread on POSIX, worker thread on Windows);
|
|
43
43
|
// the caller in `tuntap.cc` is responsible for marshalling onto the JS
|
|
44
44
|
// thread via `Napi::ThreadSafeFunction`.
|
|
45
|
-
using PacketCallback = std::function<
|
|
45
|
+
using PacketCallback = std::function<bool(std::vector<uint8_t>)>;
|
|
46
46
|
|
|
47
47
|
// Invoked at most once when the receive loop encounters a fatal error and
|
|
48
48
|
// stops. The receive loop must not deliver any further packets afterwards.
|
|
@@ -319,8 +319,8 @@ private:
|
|
|
319
319
|
: static_cast<size_t>(packet_size);
|
|
320
320
|
std::vector<uint8_t> data(packet, packet + copy_len);
|
|
321
321
|
api.ReleaseReceivePacket(session_, packet);
|
|
322
|
-
if (on_packet) {
|
|
323
|
-
|
|
322
|
+
if (on_packet && !on_packet(std::move(data))) {
|
|
323
|
+
break;
|
|
324
324
|
}
|
|
325
325
|
continue;
|
|
326
326
|
}
|
package/src/tuntap.cc
CHANGED
|
@@ -6,10 +6,74 @@
|
|
|
6
6
|
#include <mutex>
|
|
7
7
|
#include <string>
|
|
8
8
|
#include <utility>
|
|
9
|
-
#include <
|
|
9
|
+
#include <deque>
|
|
10
10
|
|
|
11
11
|
#include "native/tun_backend.h"
|
|
12
12
|
|
|
13
|
+
struct TunPollDispatch {
|
|
14
|
+
Napi::ThreadSafeFunction tsfn;
|
|
15
|
+
std::mutex mutex;
|
|
16
|
+
std::deque<std::vector<uint8_t>> pending;
|
|
17
|
+
static constexpr size_t MAX_PENDING = 512;
|
|
18
|
+
|
|
19
|
+
struct PacketJob {
|
|
20
|
+
TunPollDispatch* dispatch;
|
|
21
|
+
std::vector<uint8_t>* packet;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
static void CallJs(Napi::Env env,
|
|
25
|
+
Napi::Function jsCallback,
|
|
26
|
+
TunPollDispatch* self,
|
|
27
|
+
std::vector<uint8_t>* packet) {
|
|
28
|
+
if (env == nullptr || jsCallback.IsEmpty() || packet == nullptr) {
|
|
29
|
+
delete packet;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
auto* backing = packet;
|
|
33
|
+
Napi::Buffer<uint8_t> buf = Napi::Buffer<uint8_t>::New(
|
|
34
|
+
env,
|
|
35
|
+
backing->data(),
|
|
36
|
+
backing->size(),
|
|
37
|
+
[](Napi::Env, uint8_t*, std::vector<uint8_t>* vec) { delete vec; },
|
|
38
|
+
backing);
|
|
39
|
+
jsCallback.Call({buf});
|
|
40
|
+
if (self != nullptr) {
|
|
41
|
+
self->FlushPending();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
void FlushPending() {
|
|
46
|
+
std::lock_guard<std::mutex> lock(mutex);
|
|
47
|
+
while (!pending.empty()) {
|
|
48
|
+
auto* packet = new std::vector<uint8_t>(std::move(pending.front()));
|
|
49
|
+
auto* job = new PacketJob{this, packet};
|
|
50
|
+
napi_status status = tsfn.NonBlockingCall(
|
|
51
|
+
job,
|
|
52
|
+
[](Napi::Env env, Napi::Function jsCallback, PacketJob* job) {
|
|
53
|
+
CallJs(env, jsCallback, job->dispatch, job->packet);
|
|
54
|
+
delete job;
|
|
55
|
+
});
|
|
56
|
+
if (status != napi_ok) {
|
|
57
|
+
delete packet;
|
|
58
|
+
delete job;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
pending.pop_front();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
bool PostPacket(std::vector<uint8_t> packet) {
|
|
66
|
+
{
|
|
67
|
+
std::lock_guard<std::mutex> lock(mutex);
|
|
68
|
+
pending.push_back(std::move(packet));
|
|
69
|
+
}
|
|
70
|
+
FlushPending();
|
|
71
|
+
std::lock_guard<std::mutex> lock(mutex);
|
|
72
|
+
// Stop reading more from utun until JS drains the backlog.
|
|
73
|
+
return pending.size() < MAX_PENDING;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
13
77
|
class TunDevice : public Napi::ObjectWrap<TunDevice> {
|
|
14
78
|
public:
|
|
15
79
|
static Napi::Object Init(Napi::Env env, Napi::Object exports);
|
|
@@ -38,6 +102,7 @@ private:
|
|
|
38
102
|
std::mutex device_mutex_;
|
|
39
103
|
|
|
40
104
|
Napi::ThreadSafeFunction tsfn_;
|
|
105
|
+
TunPollDispatch* poll_dispatch_ = nullptr;
|
|
41
106
|
std::atomic<bool> polling_;
|
|
42
107
|
static constexpr size_t MAX_POLL_BUFFER = 65535;
|
|
43
108
|
|
|
@@ -240,21 +305,11 @@ Napi::Value TunDevice::StartPolling(const Napi::CallbackInfo& info) {
|
|
|
240
305
|
}
|
|
241
306
|
|
|
242
307
|
Napi::ThreadSafeFunction tsfn = tsfn_;
|
|
243
|
-
auto
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
auto* backing = new std::vector<uint8_t>(std::move(packet));
|
|
250
|
-
Napi::Buffer<uint8_t> buf = Napi::Buffer<uint8_t>::New(
|
|
251
|
-
env,
|
|
252
|
-
backing->data(),
|
|
253
|
-
backing->size(),
|
|
254
|
-
[](Napi::Env, uint8_t*, std::vector<uint8_t>* vec) { delete vec; },
|
|
255
|
-
backing);
|
|
256
|
-
jsCallback.Call({buf});
|
|
257
|
-
});
|
|
308
|
+
auto* dispatch = new TunPollDispatch();
|
|
309
|
+
dispatch->tsfn = tsfn;
|
|
310
|
+
poll_dispatch_ = dispatch;
|
|
311
|
+
auto packet_cb = [dispatch](std::vector<uint8_t> packet) mutable -> bool {
|
|
312
|
+
return dispatch->PostPacket(std::move(packet));
|
|
258
313
|
};
|
|
259
314
|
// Terminal errors from the receive loop (poll error, device closed, read
|
|
260
315
|
// error) call back here so the JS-side polling_ flag and TSFN are released
|
|
@@ -326,8 +381,14 @@ void TunDevice::StopPollingLocked() {
|
|
|
326
381
|
}
|
|
327
382
|
|
|
328
383
|
void TunDevice::ReleaseTsfnLocked() {
|
|
384
|
+
// Release TSFN first — it blocks until queued callbacks finish. Those callbacks
|
|
385
|
+
// may still dereference poll_dispatch_, so it must outlive the TSFN drain.
|
|
329
386
|
if (tsfn_) {
|
|
330
387
|
tsfn_.Release();
|
|
331
388
|
tsfn_ = nullptr;
|
|
332
389
|
}
|
|
390
|
+
if (poll_dispatch_ != nullptr) {
|
|
391
|
+
delete poll_dispatch_;
|
|
392
|
+
poll_dispatch_ = nullptr;
|
|
393
|
+
}
|
|
333
394
|
}
|