appium-ios-tuntap 0.4.2 → 0.4.4
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/TunTap.d.ts +9 -1
- package/lib/TunTap.js +19 -2
- package/lib/tunnel/constants.d.ts +10 -0
- package/lib/tunnel/constants.js +10 -0
- package/lib/tunnel/manager.d.ts +6 -0
- package/lib/tunnel/manager.js +66 -10
- 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_tun_backend.h +4 -0
- package/src/native/posix_uv_poll_loop.cc +17 -0
- package/src/native/posix_uv_poll_loop.h +3 -0
- package/src/native/tun_backend.h +4 -0
- 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 +15 -1
- package/src/tuntap.cc +38 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [0.4.4](https://github.com/appium/appium-ios-tuntap/compare/v0.4.3...v0.4.4) (2026-06-12)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* Update tunnel backpressure handling ([#50](https://github.com/appium/appium-ios-tuntap/issues/50)) ([28b8bce](https://github.com/appium/appium-ios-tuntap/commit/28b8bce8edcf2a0446a653bbec759e6b4a199827))
|
|
6
|
+
|
|
7
|
+
## [0.4.3](https://github.com/appium/appium-ios-tuntap/compare/v0.4.2...v0.4.3) (2026-06-10)
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* Improve raw transfer performance ([#48](https://github.com/appium/appium-ios-tuntap/issues/48)) ([f5cf38f](https://github.com/appium/appium-ios-tuntap/commit/f5cf38f1156fec4dfbd8a71840d616a17f728106))
|
|
12
|
+
|
|
1
13
|
## [0.4.2](https://github.com/appium/appium-ios-tuntap/compare/v0.4.1...v0.4.2) (2026-06-01)
|
|
2
14
|
|
|
3
15
|
### Miscellaneous Chores
|
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/TunTap.d.ts
CHANGED
|
@@ -73,7 +73,15 @@ export declare class TunTap {
|
|
|
73
73
|
* @throws {TypeError} if `callback` is not a function
|
|
74
74
|
* @throws {RangeError} if `bufferSize` is out of range
|
|
75
75
|
*/
|
|
76
|
-
startPolling(callback: PacketCallback, bufferSize?: number): void;
|
|
76
|
+
startPolling(callback: PacketCallback, bufferSize?: number, queueDepth?: number): void;
|
|
77
|
+
/**
|
|
78
|
+
* Pause libuv-driven polling without tearing down the receive callback.
|
|
79
|
+
*/
|
|
80
|
+
pausePolling(): void;
|
|
81
|
+
/**
|
|
82
|
+
* Resume polling after {@link TunTap.pausePolling}.
|
|
83
|
+
*/
|
|
84
|
+
resumePolling(): void;
|
|
77
85
|
/**
|
|
78
86
|
* Configure IPv6 address and MTU on this interface using the platform backend (must run as root on Darwin/Linux).
|
|
79
87
|
*
|
package/lib/TunTap.js
CHANGED
|
@@ -184,7 +184,7 @@ export class TunTap {
|
|
|
184
184
|
* @throws {TypeError} if `callback` is not a function
|
|
185
185
|
* @throws {RangeError} if `bufferSize` is out of range
|
|
186
186
|
*/
|
|
187
|
-
startPolling(callback, bufferSize = MAX_BUFFER_SIZE) {
|
|
187
|
+
startPolling(callback, bufferSize = MAX_BUFFER_SIZE, queueDepth = 8) {
|
|
188
188
|
this.assertReady();
|
|
189
189
|
if (typeof callback !== 'function') {
|
|
190
190
|
throw new TypeError('Callback must be a function');
|
|
@@ -192,7 +192,24 @@ export class TunTap {
|
|
|
192
192
|
if (bufferSize <= 0 || bufferSize > MAX_BUFFER_SIZE) {
|
|
193
193
|
throw new RangeError(`Buffer size must be between 1 and ${MAX_BUFFER_SIZE} bytes`);
|
|
194
194
|
}
|
|
195
|
-
|
|
195
|
+
if (queueDepth <= 0 || queueDepth > 64) {
|
|
196
|
+
throw new RangeError('Queue depth must be between 1 and 64');
|
|
197
|
+
}
|
|
198
|
+
this.device.startPolling(callback, bufferSize, queueDepth);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Pause libuv-driven polling without tearing down the receive callback.
|
|
202
|
+
*/
|
|
203
|
+
pausePolling() {
|
|
204
|
+
this.assertReady();
|
|
205
|
+
this.device.pausePolling();
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Resume polling after {@link TunTap.pausePolling}.
|
|
209
|
+
*/
|
|
210
|
+
resumePolling() {
|
|
211
|
+
this.assertReady();
|
|
212
|
+
this.device.resumePolling();
|
|
196
213
|
}
|
|
197
214
|
/**
|
|
198
215
|
* Configure IPv6 address and MTU on this interface using the platform backend (must run as root on Darwin/Linux).
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
/** CDTunnel lockdown handshake MTU (IPv6 minimum). */
|
|
2
2
|
export declare const CD_TUNNEL_MTU = 1280;
|
|
3
|
+
/** Upper bound for native TUN poll read size (matches N-API addon). */
|
|
4
|
+
export declare const MAX_TUN_POLL_BUFFER = 65535;
|
|
5
|
+
/** Preferred poll read size when L4 packet tap is off. */
|
|
6
|
+
export declare const LARGE_TUN_POLL_BUFFER: number;
|
|
7
|
+
/** Default ThreadSafeFunction queue depth for TUN polling. */
|
|
8
|
+
export declare const DEFAULT_TUN_POLL_QUEUE_DEPTH = 8;
|
|
9
|
+
/** Deeper TSFN queue when packet tap is off (bulk transfer). */
|
|
10
|
+
export declare const FAST_TUN_POLL_QUEUE_DEPTH = 16;
|
|
3
11
|
export declare const CD_TUNNEL_MAGIC = "CDTunnel";
|
|
4
12
|
export declare const CD_TUNNEL_MAGIC_SIZE = 8;
|
|
5
13
|
export declare const CD_TUNNEL_HEADER_SIZE: number;
|
|
@@ -8,3 +16,5 @@ export declare const IPV6_HEADER_SIZE = 40;
|
|
|
8
16
|
export declare const IPV6_VERSION = 6;
|
|
9
17
|
export declare const IPPROTO_TCP = 6;
|
|
10
18
|
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
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
/** CDTunnel lockdown handshake MTU (IPv6 minimum). */
|
|
2
2
|
export const CD_TUNNEL_MTU = 1280;
|
|
3
|
+
/** Upper bound for native TUN poll read size (matches N-API addon). */
|
|
4
|
+
export const MAX_TUN_POLL_BUFFER = 65_535;
|
|
5
|
+
/** Preferred poll read size when L4 packet tap is off. */
|
|
6
|
+
export const LARGE_TUN_POLL_BUFFER = 64 * 1024;
|
|
7
|
+
/** Default ThreadSafeFunction queue depth for TUN polling. */
|
|
8
|
+
export const DEFAULT_TUN_POLL_QUEUE_DEPTH = 8;
|
|
9
|
+
/** Deeper TSFN queue when packet tap is off (bulk transfer). */
|
|
10
|
+
export const FAST_TUN_POLL_QUEUE_DEPTH = 16;
|
|
3
11
|
export const CD_TUNNEL_MAGIC = 'CDTunnel';
|
|
4
12
|
export const CD_TUNNEL_MAGIC_SIZE = 8;
|
|
5
13
|
export const CD_TUNNEL_HEADER_SIZE = CD_TUNNEL_MAGIC_SIZE + 2;
|
|
@@ -8,3 +16,5 @@ export const IPV6_HEADER_SIZE = 40;
|
|
|
8
16
|
export const IPV6_VERSION = 6;
|
|
9
17
|
export const IPPROTO_TCP = 6;
|
|
10
18
|
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;
|
package/lib/tunnel/manager.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export declare class TunnelManager extends EventEmitter<TunnelManagerEvents> {
|
|
|
14
14
|
private readonly packetConsumers;
|
|
15
15
|
private deviceConn;
|
|
16
16
|
private cleanupPromise;
|
|
17
|
+
private tunReadPausedForBackpressure;
|
|
18
|
+
private deviceIngressPausedForTun;
|
|
17
19
|
/**
|
|
18
20
|
* Register a listener for parsed tunnel packets (in addition to the `data` event).
|
|
19
21
|
*
|
|
@@ -58,9 +60,13 @@ export declare class TunnelManager extends EventEmitter<TunnelManagerEvents> {
|
|
|
58
60
|
private hasPacketTap;
|
|
59
61
|
private processBuffer;
|
|
60
62
|
private writeDeviceFrameToTun;
|
|
63
|
+
private shouldResumeDeviceIngress;
|
|
64
|
+
private pauseDeviceIngress;
|
|
65
|
+
private resumeDeviceIngress;
|
|
61
66
|
private tapL4Packet;
|
|
62
67
|
private dispatchPacketData;
|
|
63
68
|
private startTunReadLoop;
|
|
69
|
+
private writeTunPacketToDevice;
|
|
64
70
|
private _performStop;
|
|
65
71
|
}
|
|
66
72
|
/**
|
package/lib/tunnel/manager.js
CHANGED
|
@@ -2,7 +2,7 @@ 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, IPV6_HEADER_SIZE, IPV6_VERSION, IPPROTO_TCP, IPPROTO_UDP, } from './constants.js';
|
|
5
|
+
import { CD_TUNNEL_HANDSHAKE_TIMEOUT_MS, CD_TUNNEL_HEADER_SIZE, CD_TUNNEL_MAGIC, CD_TUNNEL_MAGIC_SIZE, CD_TUNNEL_MTU, DEFAULT_TUN_POLL_QUEUE_DEPTH, FAST_TUN_POLL_QUEUE_DEPTH, IPV6_HEADER_SIZE, LARGE_TUN_POLL_BUFFER, MAX_DEVICE_INGRESS_BUFFER, MAX_TUN_POLL_BUFFER, IPV6_VERSION, IPPROTO_TCP, IPPROTO_UDP, } from './constants.js';
|
|
6
6
|
import { appendBuffer } from './buffer-utils.js';
|
|
7
7
|
/**
|
|
8
8
|
* Bridges a CoreDevice tunnel `Socket` and a {@link TunTap} interface: IPv6 framing, TUN I/O, and packet fan-out.
|
|
@@ -16,6 +16,8 @@ export class TunnelManager extends EventEmitter {
|
|
|
16
16
|
packetConsumers = new Set();
|
|
17
17
|
deviceConn = null;
|
|
18
18
|
cleanupPromise = null;
|
|
19
|
+
tunReadPausedForBackpressure = false;
|
|
20
|
+
deviceIngressPausedForTun = false;
|
|
19
21
|
/**
|
|
20
22
|
* Register a listener for parsed tunnel packets (in addition to the `data` event).
|
|
21
23
|
*
|
|
@@ -135,7 +137,9 @@ export class TunnelManager extends EventEmitter {
|
|
|
135
137
|
}
|
|
136
138
|
try {
|
|
137
139
|
this.buffer = appendBuffer(this.buffer, data);
|
|
138
|
-
|
|
140
|
+
if (this.buffer.length > MAX_DEVICE_INGRESS_BUFFER) {
|
|
141
|
+
this.pauseDeviceIngress();
|
|
142
|
+
}
|
|
139
143
|
this.processBuffer();
|
|
140
144
|
}
|
|
141
145
|
catch (err) {
|
|
@@ -200,19 +204,47 @@ export class TunnelManager extends EventEmitter {
|
|
|
200
204
|
offset += frame.length;
|
|
201
205
|
}
|
|
202
206
|
if (offset > 0) {
|
|
203
|
-
|
|
207
|
+
if (offset >= this.buffer.length) {
|
|
208
|
+
this.buffer = Buffer.alloc(0);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
this.buffer = this.buffer.subarray(offset);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (this.deviceIngressPausedForTun && this.shouldResumeDeviceIngress()) {
|
|
215
|
+
this.resumeDeviceIngress();
|
|
204
216
|
}
|
|
205
217
|
}
|
|
206
218
|
writeDeviceFrameToTun(tun, packet, nextHeader) {
|
|
207
|
-
|
|
219
|
+
tun.write(packet);
|
|
208
220
|
if (!this.hasPacketTap()) {
|
|
209
|
-
log.debug(`Device → TUN: ${bytesWritten} bytes`);
|
|
210
221
|
return;
|
|
211
222
|
}
|
|
212
223
|
const { src, dst } = ipv6Endpoints(packet);
|
|
213
|
-
log.debug(`Device → TUN: ${
|
|
224
|
+
log.debug(`Device → TUN: ${packet.length} bytes, IPv6 src=${src}, dst=${dst}`);
|
|
214
225
|
this.tapL4Packet(packet, nextHeader, src, dst);
|
|
215
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();
|
|
247
|
+
}
|
|
216
248
|
tapL4Packet(packet, nextHeader, src, dst) {
|
|
217
249
|
let packetData = null;
|
|
218
250
|
if (nextHeader === IPPROTO_UDP) {
|
|
@@ -256,18 +288,42 @@ export class TunnelManager extends EventEmitter {
|
|
|
256
288
|
if (!this.tun) {
|
|
257
289
|
return;
|
|
258
290
|
}
|
|
291
|
+
const tapOn = this.hasPacketTap();
|
|
292
|
+
const pollBuffer = tapOn
|
|
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;
|
|
259
296
|
this.tun.startPolling((data) => {
|
|
260
297
|
if (this.cancelled || !data.length || deviceConn.destroyed) {
|
|
261
298
|
return;
|
|
262
299
|
}
|
|
263
|
-
if (
|
|
300
|
+
if (tapOn && data.length >= IPV6_HEADER_SIZE) {
|
|
264
301
|
log.debug(`TUN → Device: ${data.length} bytes, IPv6 src=${formatIPv6Address(data.subarray(8, 24))}, dst=${formatIPv6Address(data.subarray(24, 40))}`);
|
|
265
302
|
}
|
|
266
|
-
else if (
|
|
303
|
+
else if (tapOn) {
|
|
267
304
|
log.debug(`TUN → Device: ${data.length} bytes (too small for IPv6 header)`);
|
|
268
305
|
}
|
|
269
|
-
|
|
270
|
-
},
|
|
306
|
+
this.writeTunPacketToDevice(deviceConn, data);
|
|
307
|
+
}, pollBuffer, queueDepth);
|
|
308
|
+
}
|
|
309
|
+
writeTunPacketToDevice(deviceConn, data) {
|
|
310
|
+
if (deviceConn.destroyed) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const canWriteMore = deviceConn.write(data);
|
|
314
|
+
if (canWriteMore || this.tunReadPausedForBackpressure || !this.tun) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
this.tunReadPausedForBackpressure = true;
|
|
318
|
+
this.tun.pausePolling();
|
|
319
|
+
const onDrain = () => {
|
|
320
|
+
if (this.cancelled || deviceConn.destroyed) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
this.tunReadPausedForBackpressure = false;
|
|
324
|
+
this.tun?.resumePolling();
|
|
325
|
+
};
|
|
326
|
+
deviceConn.once('drain', onDrain);
|
|
271
327
|
}
|
|
272
328
|
async _performStop() {
|
|
273
329
|
const tunName = this.tun ? this.tun.name : 'unknown';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appium-ios-tuntap",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
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": "
|
|
22
|
-
"test:integration": "
|
|
21
|
+
"test:unit": "node --test --test-force-exit --test-timeout=120000 \"test/unit/**/*.spec.mjs\"",
|
|
22
|
+
"test:integration": "node --test --test-force-exit --test-timeout=120000 \"test/integration/**/*.spec.mjs\""
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"src/tuntap.cc",
|
|
@@ -61,7 +61,6 @@
|
|
|
61
61
|
"@types/node": "^25.0.1",
|
|
62
62
|
"commander": "^14.0.3",
|
|
63
63
|
"conventional-changelog-conventionalcommits": "^9.0.0",
|
|
64
|
-
"mocha": "^11.7.5",
|
|
65
64
|
"prebuildify": "^6.0.1",
|
|
66
65
|
"prettier": "^3.0.0",
|
|
67
66
|
"semantic-release": "^25.0.2"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -47,6 +47,10 @@ public:
|
|
|
47
47
|
|
|
48
48
|
void StopReceiveLoop() override { poll_loop_.Stop(); }
|
|
49
49
|
|
|
50
|
+
void PauseReceiveLoop() override { poll_loop_.Pause(); }
|
|
51
|
+
|
|
52
|
+
void ResumeReceiveLoop() override { poll_loop_.Resume(); }
|
|
53
|
+
|
|
50
54
|
int GetNativeFd() const override { return fd_.get(); }
|
|
51
55
|
|
|
52
56
|
protected:
|
|
@@ -59,6 +59,7 @@ void PosixUvPollLoop::Stop() {
|
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
paused_ = false;
|
|
62
63
|
uv_poll_stop(handle_);
|
|
63
64
|
handle_->data = nullptr;
|
|
64
65
|
uv_close(reinterpret_cast<uv_handle_t*>(handle_),
|
|
@@ -67,6 +68,22 @@ void PosixUvPollLoop::Stop() {
|
|
|
67
68
|
state_.reset();
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
void PosixUvPollLoop::Pause() {
|
|
72
|
+
if (!handle_ || paused_) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
paused_ = true;
|
|
76
|
+
uv_poll_stop(handle_);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
void PosixUvPollLoop::Resume() {
|
|
80
|
+
if (!handle_ || !paused_) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
paused_ = false;
|
|
84
|
+
uv_poll_start(handle_, UV_READABLE, &PosixUvPollLoop::OnPoll);
|
|
85
|
+
}
|
|
86
|
+
|
|
70
87
|
void PosixUvPollLoop::OnPoll(uv_poll_t* handle, int status, int events) {
|
|
71
88
|
auto* state = static_cast<State*>(handle->data);
|
|
72
89
|
if (!state) {
|
package/src/native/tun_backend.h
CHANGED
|
@@ -72,6 +72,10 @@ public:
|
|
|
72
72
|
std::string& error) = 0;
|
|
73
73
|
virtual void StopReceiveLoop() = 0;
|
|
74
74
|
|
|
75
|
+
// Temporarily stop delivering packets without tearing down the receive loop.
|
|
76
|
+
virtual void PauseReceiveLoop() {}
|
|
77
|
+
virtual void ResumeReceiveLoop() {}
|
|
78
|
+
|
|
75
79
|
// Returns the underlying POSIX file descriptor when one exists. Backends
|
|
76
80
|
// without a numeric fd (e.g. Wintun on Windows) return `-1`.
|
|
77
81
|
virtual int GetNativeFd() const { return -1; }
|
|
@@ -134,6 +134,9 @@ public:
|
|
|
134
134
|
|
|
135
135
|
ssize_t bytes_written = write(fd_.get(), write_frame_.data(), write_frame_.size());
|
|
136
136
|
if (bytes_written < 0) {
|
|
137
|
+
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
137
140
|
error = std::string("Write error: ") + strerror(errno);
|
|
138
141
|
return -1;
|
|
139
142
|
}
|
|
@@ -104,6 +104,9 @@ public:
|
|
|
104
104
|
}
|
|
105
105
|
ssize_t bytes_written = write(fd_.get(), data, length);
|
|
106
106
|
if (bytes_written < 0) {
|
|
107
|
+
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
107
110
|
error = std::string("Write error: ") + strerror(errno);
|
|
108
111
|
return -1;
|
|
109
112
|
}
|
|
@@ -238,9 +238,11 @@ public:
|
|
|
238
238
|
// Either never started or already cleaned up. Reset the event in case
|
|
239
239
|
// it was created without ever spawning a thread.
|
|
240
240
|
quit_event_.reset();
|
|
241
|
+
receive_paused_.store(false);
|
|
241
242
|
return;
|
|
242
243
|
}
|
|
243
244
|
worker_running_.store(false);
|
|
245
|
+
receive_paused_.store(false);
|
|
244
246
|
if (quit_event_.is_valid()) {
|
|
245
247
|
::SetEvent(quit_event_.get());
|
|
246
248
|
}
|
|
@@ -248,6 +250,10 @@ public:
|
|
|
248
250
|
quit_event_.reset();
|
|
249
251
|
}
|
|
250
252
|
|
|
253
|
+
void PauseReceiveLoop() override { receive_paused_.store(true); }
|
|
254
|
+
|
|
255
|
+
void ResumeReceiveLoop() override { receive_paused_.store(false); }
|
|
256
|
+
|
|
251
257
|
// WinTun exposes no POSIX file descriptor: its readable object is a Win32
|
|
252
258
|
// event `HANDLE`, not a numeric fd. Always -1 — the N-API layer treats -1
|
|
253
259
|
// as "no pollable fd" and drives delivery through `StartReceiveLoop`.
|
|
@@ -293,11 +299,18 @@ private:
|
|
|
293
299
|
HANDLE wait_handles[2] = {read_event_, quit_event_.get()};
|
|
294
300
|
|
|
295
301
|
while (worker_running_.load()) {
|
|
302
|
+
while (receive_paused_.load() && worker_running_.load()) {
|
|
303
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
304
|
+
}
|
|
305
|
+
if (!worker_running_.load()) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
296
309
|
// Drain everything available before going back to wait. WinTun's
|
|
297
310
|
// read-wait event is auto-reset on signal, so we must consume all
|
|
298
311
|
// queued packets before re-arming.
|
|
299
312
|
bool drained = false;
|
|
300
|
-
while (worker_running_.load()) {
|
|
313
|
+
while (worker_running_.load() && !receive_paused_.load()) {
|
|
301
314
|
DWORD packet_size = 0;
|
|
302
315
|
BYTE* packet = api.ReceivePacket(session_, &packet_size);
|
|
303
316
|
if (packet) {
|
|
@@ -358,6 +371,7 @@ private:
|
|
|
358
371
|
Handle quit_event_;
|
|
359
372
|
std::thread worker_;
|
|
360
373
|
std::atomic<bool> worker_running_{false};
|
|
374
|
+
std::atomic<bool> receive_paused_{false};
|
|
361
375
|
std::string interface_name_;
|
|
362
376
|
};
|
|
363
377
|
|
package/src/tuntap.cc
CHANGED
|
@@ -28,6 +28,8 @@ private:
|
|
|
28
28
|
Napi::Value GetName(const Napi::CallbackInfo& info);
|
|
29
29
|
Napi::Value GetFd(const Napi::CallbackInfo& info);
|
|
30
30
|
Napi::Value StartPolling(const Napi::CallbackInfo& info);
|
|
31
|
+
Napi::Value PausePolling(const Napi::CallbackInfo& info);
|
|
32
|
+
Napi::Value ResumePolling(const Napi::CallbackInfo& info);
|
|
31
33
|
|
|
32
34
|
std::unique_ptr<TunPlatformBackend> backend_;
|
|
33
35
|
std::string requested_name_;
|
|
@@ -56,6 +58,8 @@ Napi::Object TunDevice::Init(Napi::Env env, Napi::Object exports) {
|
|
|
56
58
|
InstanceMethod("getName", &TunDevice::GetName),
|
|
57
59
|
InstanceMethod("getFd", &TunDevice::GetFd),
|
|
58
60
|
InstanceMethod("startPolling", &TunDevice::StartPolling),
|
|
61
|
+
InstanceMethod("pausePolling", &TunDevice::PausePolling),
|
|
62
|
+
InstanceMethod("resumePolling", &TunDevice::ResumePolling),
|
|
59
63
|
});
|
|
60
64
|
|
|
61
65
|
constructor = Napi::Persistent(func);
|
|
@@ -209,6 +213,15 @@ Napi::Value TunDevice::StartPolling(const Napi::CallbackInfo& info) {
|
|
|
209
213
|
buffer_size = size;
|
|
210
214
|
}
|
|
211
215
|
|
|
216
|
+
size_t queue_depth = 8;
|
|
217
|
+
if (info.Length() > 2 && info[2].IsNumber()) {
|
|
218
|
+
queue_depth = info[2].As<Napi::Number>().Uint32Value();
|
|
219
|
+
if (queue_depth == 0 || queue_depth > 64) {
|
|
220
|
+
Napi::RangeError::New(env, "Queue depth must be between 1 and 64").ThrowAsJavaScriptException();
|
|
221
|
+
return env.Null();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
212
225
|
// Queue depth > 1 lets the poll thread post the next packet while JS is still
|
|
213
226
|
// handling the previous callback (still serialized on the main thread).
|
|
214
227
|
tsfn_ = Napi::ThreadSafeFunction::New(
|
|
@@ -216,7 +229,7 @@ Napi::Value TunDevice::StartPolling(const Napi::CallbackInfo& info) {
|
|
|
216
229
|
info[0].As<Napi::Function>(),
|
|
217
230
|
"TunDeviceDataCallback",
|
|
218
231
|
0,
|
|
219
|
-
|
|
232
|
+
queue_depth);
|
|
220
233
|
|
|
221
234
|
uv_loop_t* loop = nullptr;
|
|
222
235
|
napi_status napi_st = napi_get_uv_event_loop(env, &loop);
|
|
@@ -265,6 +278,30 @@ Napi::Value TunDevice::StartPolling(const Napi::CallbackInfo& info) {
|
|
|
265
278
|
return env.Undefined();
|
|
266
279
|
}
|
|
267
280
|
|
|
281
|
+
Napi::Value TunDevice::PausePolling(const Napi::CallbackInfo& info) {
|
|
282
|
+
Napi::Env env = info.Env();
|
|
283
|
+
std::lock_guard<std::mutex> lock(device_mutex_);
|
|
284
|
+
|
|
285
|
+
if (!polling_ || !backend_ || !backend_->IsOpen()) {
|
|
286
|
+
return env.Undefined();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
backend_->PauseReceiveLoop();
|
|
290
|
+
return env.Undefined();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
Napi::Value TunDevice::ResumePolling(const Napi::CallbackInfo& info) {
|
|
294
|
+
Napi::Env env = info.Env();
|
|
295
|
+
std::lock_guard<std::mutex> lock(device_mutex_);
|
|
296
|
+
|
|
297
|
+
if (!polling_ || !backend_ || !backend_->IsOpen()) {
|
|
298
|
+
return env.Undefined();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
backend_->ResumeReceiveLoop();
|
|
302
|
+
return env.Undefined();
|
|
303
|
+
}
|
|
304
|
+
|
|
268
305
|
Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
269
306
|
return TunDevice::Init(env, exports);
|
|
270
307
|
}
|