appium-ios-tuntap 0.4.0 → 0.4.2
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/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/tunnel/buffer-utils.d.ts +4 -0
- package/lib/tunnel/buffer-utils.js +15 -0
- package/lib/tunnel/constants.d.ts +10 -0
- package/lib/tunnel/constants.js +10 -0
- package/lib/tunnel/index.d.ts +2 -0
- package/lib/tunnel/index.js +1 -0
- package/lib/{tunnel.d.ts → tunnel/manager.d.ts} +8 -58
- package/lib/{tunnel.js → tunnel/manager.js} +235 -228
- package/lib/tunnel/types.d.ts +71 -0
- package/lib/tunnel/types.js +1 -0
- package/package.json +2 -2
- 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 +21 -18
- package/src/native/tun_backend_darwin.cc +13 -8
- package/src/tuntap.cc +12 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [0.4.2](https://github.com/appium/appium-ios-tuntap/compare/v0.4.1...v0.4.2) (2026-06-01)
|
|
2
|
+
|
|
3
|
+
### Miscellaneous Chores
|
|
4
|
+
|
|
5
|
+
* Tune further tunnel perf ([#46](https://github.com/appium/appium-ios-tuntap/issues/46)) ([d422937](https://github.com/appium/appium-ios-tuntap/commit/d422937a47b16bf1c984030772d1de6e555609e6))
|
|
6
|
+
|
|
7
|
+
## [0.4.1](https://github.com/appium/appium-ios-tuntap/compare/v0.4.0...v0.4.1) (2026-05-31)
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* Improve tunnel performance ([#45](https://github.com/appium/appium-ios-tuntap/issues/45)) ([576d353](https://github.com/appium/appium-ios-tuntap/commit/576d3535137bb0cf79634ab820b3675db6cbbb86))
|
|
12
|
+
|
|
1
13
|
## [0.4.0](https://github.com/appium/appium-ios-tuntap/compare/v0.3.0...v0.4.0) (2026-05-30)
|
|
2
14
|
|
|
3
15
|
### Features
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append socket/TUN chunks without repeated Buffer.concat growth copies.
|
|
3
|
+
*/
|
|
4
|
+
export function appendBuffer(existing, chunk) {
|
|
5
|
+
if (chunk.length === 0) {
|
|
6
|
+
return existing;
|
|
7
|
+
}
|
|
8
|
+
if (existing.length === 0) {
|
|
9
|
+
return Buffer.from(chunk);
|
|
10
|
+
}
|
|
11
|
+
const combined = Buffer.allocUnsafe(existing.length + chunk.length);
|
|
12
|
+
existing.copy(combined, 0);
|
|
13
|
+
chunk.copy(combined, existing.length);
|
|
14
|
+
return combined;
|
|
15
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** CDTunnel lockdown handshake MTU (IPv6 minimum). */
|
|
2
|
+
export declare const CD_TUNNEL_MTU = 1280;
|
|
3
|
+
export declare const CD_TUNNEL_MAGIC = "CDTunnel";
|
|
4
|
+
export declare const CD_TUNNEL_MAGIC_SIZE = 8;
|
|
5
|
+
export declare const CD_TUNNEL_HEADER_SIZE: number;
|
|
6
|
+
export declare const CD_TUNNEL_HANDSHAKE_TIMEOUT_MS = 30000;
|
|
7
|
+
export declare const IPV6_HEADER_SIZE = 40;
|
|
8
|
+
export declare const IPV6_VERSION = 6;
|
|
9
|
+
export declare const IPPROTO_TCP = 6;
|
|
10
|
+
export declare const IPPROTO_UDP = 17;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** CDTunnel lockdown handshake MTU (IPv6 minimum). */
|
|
2
|
+
export const CD_TUNNEL_MTU = 1280;
|
|
3
|
+
export const CD_TUNNEL_MAGIC = 'CDTunnel';
|
|
4
|
+
export const CD_TUNNEL_MAGIC_SIZE = 8;
|
|
5
|
+
export const CD_TUNNEL_HEADER_SIZE = CD_TUNNEL_MAGIC_SIZE + 2;
|
|
6
|
+
export const CD_TUNNEL_HANDSHAKE_TIMEOUT_MS = 30_000;
|
|
7
|
+
export const IPV6_HEADER_SIZE = 40;
|
|
8
|
+
export const IPV6_VERSION = 6;
|
|
9
|
+
export const IPPROTO_TCP = 6;
|
|
10
|
+
export const IPPROTO_UDP = 17;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TunnelManager, connectToTunnelLockdown, exchangeCoreTunnelParameters } from './manager.js';
|
|
@@ -1,57 +1,7 @@
|
|
|
1
|
-
import { TunTap } from '
|
|
1
|
+
import { TunTap } from '../TunTap.js';
|
|
2
2
|
import { EventEmitter } from 'node:events';
|
|
3
3
|
import type { Socket } from 'node:net';
|
|
4
|
-
import {
|
|
5
|
-
export interface PacketData {
|
|
6
|
-
protocol: 'TCP' | 'UDP';
|
|
7
|
-
src: string;
|
|
8
|
-
dst: string;
|
|
9
|
-
sourcePort: number;
|
|
10
|
-
destPort: number;
|
|
11
|
-
payload: Buffer;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Event names and listener argument tuples for {@link TunnelManager}
|
|
15
|
-
* (matches Node’s `EventEmitter` event map shape).
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* tunnelManager.on('data', (packet) => {
|
|
19
|
-
* // `packet` is PacketData
|
|
20
|
-
* });
|
|
21
|
-
*/
|
|
22
|
-
export interface PacketConsumer {
|
|
23
|
-
/**
|
|
24
|
-
* Invoked for each parsed TCP/UDP payload extracted from the tunnel stream.
|
|
25
|
-
*
|
|
26
|
-
* @param packet — decoded addresses, ports, and payload
|
|
27
|
-
*/
|
|
28
|
-
onPacket(packet: PacketData): void;
|
|
29
|
-
}
|
|
30
|
-
export interface TunnelManagerEvents {
|
|
31
|
-
data: [packet: PacketData];
|
|
32
|
-
}
|
|
33
|
-
export interface TunnelConnection {
|
|
34
|
-
Address: string;
|
|
35
|
-
RsdPort?: number;
|
|
36
|
-
tunnelManager: TunnelManager;
|
|
37
|
-
/** Tear down the tunnel, close the TUN device, and end the socket when appropriate. */
|
|
38
|
-
closer: () => Promise<void>;
|
|
39
|
-
/** @param consumer — receives packets for the lifetime of the registration */
|
|
40
|
-
addPacketConsumer(consumer: PacketConsumer): void;
|
|
41
|
-
/** @param consumer — must be the same reference passed to {@link TunnelConnection.addPacketConsumer} */
|
|
42
|
-
removePacketConsumer(consumer: PacketConsumer): void;
|
|
43
|
-
/** @returns async iterator of packets until the tunnel is stopped */
|
|
44
|
-
getPacketStream(): AsyncIterable<PacketData>;
|
|
45
|
-
}
|
|
46
|
-
interface TunnelClientParameters {
|
|
47
|
-
address: string;
|
|
48
|
-
mtu: number;
|
|
49
|
-
}
|
|
50
|
-
interface TunnelInfo {
|
|
51
|
-
clientParameters: TunnelClientParameters;
|
|
52
|
-
serverAddress: string;
|
|
53
|
-
serverRSDPort?: number;
|
|
54
|
-
}
|
|
4
|
+
import type { PacketConsumer, PacketData, TunnelConnection, TunnelInfo, TunnelManagerEvents } from './types.js';
|
|
55
5
|
/**
|
|
56
6
|
* Bridges a CoreDevice tunnel `Socket` and a {@link TunTap} interface: IPv6 framing, TUN I/O, and packet fan-out.
|
|
57
7
|
* Emits {@link TunnelManagerEvents} (currently `data` with {@link PacketData}) for TCP/UDP packets, same as registered consumers.
|
|
@@ -59,14 +9,11 @@ interface TunnelInfo {
|
|
|
59
9
|
export declare class TunnelManager extends EventEmitter<TunnelManagerEvents> {
|
|
60
10
|
private tun;
|
|
61
11
|
private cancelled;
|
|
62
|
-
private
|
|
12
|
+
private mtu;
|
|
63
13
|
private buffer;
|
|
64
|
-
private packetConsumers;
|
|
65
|
-
private packetQueue;
|
|
14
|
+
private readonly packetConsumers;
|
|
66
15
|
private deviceConn;
|
|
67
16
|
private cleanupPromise;
|
|
68
|
-
/** Creates a manager with no TUN device until {@link TunnelManager.setupInterface} succeeds. */
|
|
69
|
-
constructor();
|
|
70
17
|
/**
|
|
71
18
|
* Register a listener for parsed tunnel packets (in addition to the `data` event).
|
|
72
19
|
*
|
|
@@ -108,7 +55,11 @@ export declare class TunnelManager extends EventEmitter<TunnelManagerEvents> {
|
|
|
108
55
|
* @returns the same promise if already stopping/stopped
|
|
109
56
|
*/
|
|
110
57
|
stop(): Promise<void>;
|
|
58
|
+
private hasPacketTap;
|
|
111
59
|
private processBuffer;
|
|
60
|
+
private writeDeviceFrameToTun;
|
|
61
|
+
private tapL4Packet;
|
|
62
|
+
private dispatchPacketData;
|
|
112
63
|
private startTunReadLoop;
|
|
113
64
|
private _performStop;
|
|
114
65
|
}
|
|
@@ -126,4 +77,3 @@ export declare function exchangeCoreTunnelParameters(socket: Socket): Promise<Tu
|
|
|
126
77
|
* @returns connection handle with {@link TunnelConnection.closer} and packet APIs
|
|
127
78
|
*/
|
|
128
79
|
export declare function connectToTunnelLockdown(secureServiceSocket: Socket): Promise<TunnelConnection>;
|
|
129
|
-
export {};
|
|
@@ -1,32 +1,21 @@
|
|
|
1
|
-
import { log } from '
|
|
2
|
-
import { TunTap } from '
|
|
1
|
+
import { log } from '../logger.js';
|
|
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';
|
|
6
|
+
import { appendBuffer } from './buffer-utils.js';
|
|
5
7
|
/**
|
|
6
8
|
* Bridges a CoreDevice tunnel `Socket` and a {@link TunTap} interface: IPv6 framing, TUN I/O, and packet fan-out.
|
|
7
9
|
* Emits {@link TunnelManagerEvents} (currently `data` with {@link PacketData}) for TCP/UDP packets, same as registered consumers.
|
|
8
10
|
*/
|
|
9
11
|
export class TunnelManager extends EventEmitter {
|
|
10
|
-
tun;
|
|
11
|
-
cancelled;
|
|
12
|
-
|
|
13
|
-
buffer;
|
|
14
|
-
packetConsumers;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
cleanupPromise;
|
|
18
|
-
/** Creates a manager with no TUN device until {@link TunnelManager.setupInterface} succeeds. */
|
|
19
|
-
constructor() {
|
|
20
|
-
super();
|
|
21
|
-
this.tun = null;
|
|
22
|
-
this.cancelled = false;
|
|
23
|
-
this.readInterval = null;
|
|
24
|
-
this.buffer = Buffer.alloc(0);
|
|
25
|
-
this.packetConsumers = new Set();
|
|
26
|
-
this.packetQueue = [];
|
|
27
|
-
this.deviceConn = null;
|
|
28
|
-
this.cleanupPromise = null;
|
|
29
|
-
}
|
|
12
|
+
tun = null;
|
|
13
|
+
cancelled = false;
|
|
14
|
+
mtu = CD_TUNNEL_MTU;
|
|
15
|
+
buffer = Buffer.alloc(0);
|
|
16
|
+
packetConsumers = new Set();
|
|
17
|
+
deviceConn = null;
|
|
18
|
+
cleanupPromise = null;
|
|
30
19
|
/**
|
|
31
20
|
* Register a listener for parsed tunnel packets (in addition to the `data` event).
|
|
32
21
|
*
|
|
@@ -99,6 +88,7 @@ export class TunnelManager extends EventEmitter {
|
|
|
99
88
|
throw new Error('Failed to open TUN device');
|
|
100
89
|
}
|
|
101
90
|
log.debug(`Opened TUN device: ${this.tun.name}`);
|
|
91
|
+
this.mtu = tunnelInfo.clientParameters.mtu;
|
|
102
92
|
// Configure the TUN device with IPv6 address and MTU
|
|
103
93
|
await this.tun.configure(tunnelInfo.clientParameters.address, tunnelInfo.clientParameters.mtu);
|
|
104
94
|
// Add route for the server address
|
|
@@ -136,14 +126,15 @@ export class TunnelManager extends EventEmitter {
|
|
|
136
126
|
}
|
|
137
127
|
this.deviceConn = deviceConn;
|
|
138
128
|
log.debug(`Starting bidirectional data forwarding for ${this.tun.name}`);
|
|
129
|
+
deviceConn.setNoDelay(true);
|
|
130
|
+
deviceConn.setKeepAlive(true, 1000);
|
|
139
131
|
// Handle data from the device connection
|
|
140
132
|
deviceConn.on('data', (data) => {
|
|
141
133
|
if (this.cancelled) {
|
|
142
134
|
return;
|
|
143
135
|
}
|
|
144
136
|
try {
|
|
145
|
-
|
|
146
|
-
this.buffer = Buffer.concat([this.buffer, data]);
|
|
137
|
+
this.buffer = appendBuffer(this.buffer, data);
|
|
147
138
|
// Process IPv6 packets
|
|
148
139
|
this.processBuffer();
|
|
149
140
|
}
|
|
@@ -182,162 +173,107 @@ export class TunnelManager extends EventEmitter {
|
|
|
182
173
|
this.cleanupPromise = this._performStop();
|
|
183
174
|
return this.cleanupPromise;
|
|
184
175
|
}
|
|
176
|
+
hasPacketTap() {
|
|
177
|
+
return this.packetConsumers.size > 0 || this.listenerCount('data') > 0;
|
|
178
|
+
}
|
|
185
179
|
processBuffer() {
|
|
186
180
|
let offset = 0;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (version !== 6) {
|
|
181
|
+
while (offset + IPV6_HEADER_SIZE <= this.buffer.length) {
|
|
182
|
+
const frame = nextIpv6Frame(this.buffer, offset);
|
|
183
|
+
if (frame.kind === 'incomplete') {
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
if (frame.kind === 'resync') {
|
|
194
187
|
offset++;
|
|
195
188
|
continue;
|
|
196
189
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (offset + 40 + payloadLength > this.buffer.length) {
|
|
201
|
-
break; // Wait for more data
|
|
190
|
+
if (!this.tun) {
|
|
191
|
+
log.error('TUN device is null during packet processing');
|
|
192
|
+
break;
|
|
202
193
|
}
|
|
203
|
-
// Extract the complete IPv6 packet
|
|
204
|
-
const packet = this.buffer.slice(offset, offset + 40 + payloadLength);
|
|
205
|
-
// Extract source and destination IPv6 addresses
|
|
206
|
-
const src = formatIPv6Address(packet.slice(8, 24));
|
|
207
|
-
const dst = formatIPv6Address(packet.slice(24, 40));
|
|
208
|
-
// Get the IPv6 next header value
|
|
209
|
-
const nextHeader = header[6];
|
|
210
|
-
log.debug(`Processing packet: nextHeader=${nextHeader}, totalLength=${40 + payloadLength}`);
|
|
211
194
|
try {
|
|
212
|
-
|
|
213
|
-
log.error('TUN device is null during packet processing');
|
|
214
|
-
break;
|
|
215
|
-
}
|
|
216
|
-
const bytesWritten = this.tun.write(packet);
|
|
217
|
-
log.debug(`Device → TUN: ${bytesWritten} bytes, IPv6 src=${src}, dst=${dst}`);
|
|
218
|
-
// Handle UDP packets (nextHeader === 17)
|
|
219
|
-
if (nextHeader === 17) {
|
|
220
|
-
const payload = packet.slice(40);
|
|
221
|
-
log.debug(`UDP packet detected: payload length=${payload.length}`);
|
|
222
|
-
if (payload.length < 8) {
|
|
223
|
-
log.debug('UDP payload too short, not emitting event.');
|
|
224
|
-
}
|
|
225
|
-
else {
|
|
226
|
-
const sourcePort = payload.readUInt16BE(0);
|
|
227
|
-
const destPort = payload.readUInt16BE(2);
|
|
228
|
-
const udpPayload = payload.slice(8);
|
|
229
|
-
const packetData = {
|
|
230
|
-
protocol: 'UDP',
|
|
231
|
-
src,
|
|
232
|
-
dst,
|
|
233
|
-
sourcePort,
|
|
234
|
-
destPort,
|
|
235
|
-
payload: udpPayload,
|
|
236
|
-
};
|
|
237
|
-
this.emit('data', packetData);
|
|
238
|
-
this.packetConsumers.forEach((consumer) => {
|
|
239
|
-
try {
|
|
240
|
-
consumer.onPacket(packetData);
|
|
241
|
-
}
|
|
242
|
-
catch (err) {
|
|
243
|
-
log.error('Error in packet consumer:', err);
|
|
244
|
-
}
|
|
245
|
-
});
|
|
246
|
-
log.debug('Emitted data event for UDP packet');
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
// Handle TCP packets (nextHeader === 6)
|
|
250
|
-
else if (nextHeader === 6) {
|
|
251
|
-
const tcpHeaderStart = 40;
|
|
252
|
-
if (packet.length < tcpHeaderStart + 20) {
|
|
253
|
-
log.debug('TCP packet too short for minimum header, skipping.');
|
|
254
|
-
}
|
|
255
|
-
else {
|
|
256
|
-
const sourcePort = packet.readUInt16BE(tcpHeaderStart);
|
|
257
|
-
const destPort = packet.readUInt16BE(tcpHeaderStart + 2);
|
|
258
|
-
const dataOffsetByte = packet.readUInt8(tcpHeaderStart + 12);
|
|
259
|
-
const tcpHeaderLength = (dataOffsetByte >> 4) * 4;
|
|
260
|
-
if (packet.length < tcpHeaderStart + tcpHeaderLength) {
|
|
261
|
-
log.debug('TCP header length exceeds packet length, skipping.');
|
|
262
|
-
}
|
|
263
|
-
else {
|
|
264
|
-
const tcpPayload = packet.slice(tcpHeaderStart + tcpHeaderLength);
|
|
265
|
-
log.debug(`TCP packet detected: headerLength=${tcpHeaderLength}, payload length=${tcpPayload.length}`);
|
|
266
|
-
const packetData = {
|
|
267
|
-
protocol: 'TCP',
|
|
268
|
-
src,
|
|
269
|
-
dst,
|
|
270
|
-
sourcePort,
|
|
271
|
-
destPort,
|
|
272
|
-
payload: tcpPayload,
|
|
273
|
-
};
|
|
274
|
-
this.emit('data', packetData);
|
|
275
|
-
this.packetConsumers.forEach((consumer) => {
|
|
276
|
-
try {
|
|
277
|
-
consumer.onPacket(packetData);
|
|
278
|
-
}
|
|
279
|
-
catch (err) {
|
|
280
|
-
log.error('Error in packet consumer:', err);
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
log.debug('Emitted data event for TCP packet');
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
else {
|
|
288
|
-
log.debug('Packet is not UDP or TCP (nextHeader !== 17 and !== 6)');
|
|
289
|
-
}
|
|
195
|
+
this.writeDeviceFrameToTun(this.tun, frame.packet, frame.nextHeader);
|
|
290
196
|
}
|
|
291
197
|
catch (err) {
|
|
292
198
|
log.error(`Error writing to TUN: ${err.message}`);
|
|
293
199
|
}
|
|
294
|
-
|
|
295
|
-
offset += 40 + payloadLength;
|
|
200
|
+
offset += frame.length;
|
|
296
201
|
}
|
|
297
|
-
// Keep any remaining partial data
|
|
298
202
|
if (offset > 0) {
|
|
299
|
-
this.buffer = this.buffer.
|
|
203
|
+
this.buffer = this.buffer.subarray(offset);
|
|
300
204
|
}
|
|
301
205
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
206
|
+
writeDeviceFrameToTun(tun, packet, nextHeader) {
|
|
207
|
+
const bytesWritten = tun.write(packet);
|
|
208
|
+
if (!this.hasPacketTap()) {
|
|
209
|
+
log.debug(`Device → TUN: ${bytesWritten} bytes`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const { src, dst } = ipv6Endpoints(packet);
|
|
213
|
+
log.debug(`Device → TUN: ${bytesWritten} bytes, IPv6 src=${src}, dst=${dst}`);
|
|
214
|
+
this.tapL4Packet(packet, nextHeader, src, dst);
|
|
215
|
+
}
|
|
216
|
+
tapL4Packet(packet, nextHeader, src, dst) {
|
|
217
|
+
let packetData = null;
|
|
218
|
+
if (nextHeader === IPPROTO_UDP) {
|
|
219
|
+
packetData = parseUdpPacketData(packet, src, dst);
|
|
220
|
+
if (!packetData) {
|
|
221
|
+
log.debug('UDP payload too short, not emitting event.');
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
log.debug(`UDP packet detected: payload length=${packetData.payload.length}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
else if (nextHeader === IPPROTO_TCP) {
|
|
228
|
+
packetData = parseTcpPacketData(packet, src, dst);
|
|
229
|
+
if (!packetData) {
|
|
230
|
+
log.debug('TCP packet too short or malformed, skipping.');
|
|
306
231
|
}
|
|
232
|
+
else {
|
|
233
|
+
log.debug(`TCP packet detected: payload length=${packetData.payload.length}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
log.debug('Packet is not UDP or TCP (nextHeader !== 17 and !== 6)');
|
|
238
|
+
}
|
|
239
|
+
if (packetData) {
|
|
240
|
+
this.dispatchPacketData(packetData);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
dispatchPacketData(packetData) {
|
|
244
|
+
this.emit('data', packetData);
|
|
245
|
+
for (const consumer of this.packetConsumers) {
|
|
307
246
|
try {
|
|
308
|
-
|
|
309
|
-
const data = this.tun.read(16384); // A large buffer for MTU
|
|
310
|
-
// If we got data, send it to the device
|
|
311
|
-
if (data && data.length > 0) {
|
|
312
|
-
if (data.length >= 40) {
|
|
313
|
-
// Minimum IPv6 header size
|
|
314
|
-
log.debug(`TUN → Device: ${data.length} bytes, IPv6 src=${formatIPv6Address(data.slice(8, 24))}, dst=${formatIPv6Address(data.slice(24, 40))}`);
|
|
315
|
-
}
|
|
316
|
-
else {
|
|
317
|
-
log.debug(`TUN → Device: ${data.length} bytes (too small for IPv6 header)`);
|
|
318
|
-
}
|
|
319
|
-
if (!deviceConn.destroyed) {
|
|
320
|
-
deviceConn.write(data);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
247
|
+
consumer.onPacket(packetData);
|
|
323
248
|
}
|
|
324
249
|
catch (err) {
|
|
325
|
-
|
|
326
|
-
log.error('Error reading from TUN:', err.message);
|
|
327
|
-
}
|
|
250
|
+
log.error('Error in packet consumer:', err);
|
|
328
251
|
}
|
|
329
|
-
}
|
|
252
|
+
}
|
|
253
|
+
log.debug(`Emitted data event for ${packetData.protocol} packet`);
|
|
254
|
+
}
|
|
255
|
+
startTunReadLoop(deviceConn) {
|
|
256
|
+
if (!this.tun) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
this.tun.startPolling((data) => {
|
|
260
|
+
if (this.cancelled || !data.length || deviceConn.destroyed) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (this.hasPacketTap() && data.length >= IPV6_HEADER_SIZE) {
|
|
264
|
+
log.debug(`TUN → Device: ${data.length} bytes, IPv6 src=${formatIPv6Address(data.subarray(8, 24))}, dst=${formatIPv6Address(data.subarray(24, 40))}`);
|
|
265
|
+
}
|
|
266
|
+
else if (this.hasPacketTap()) {
|
|
267
|
+
log.debug(`TUN → Device: ${data.length} bytes (too small for IPv6 header)`);
|
|
268
|
+
}
|
|
269
|
+
deviceConn.write(data);
|
|
270
|
+
}, this.mtu);
|
|
330
271
|
}
|
|
331
272
|
async _performStop() {
|
|
332
273
|
const tunName = this.tun ? this.tun.name : 'unknown';
|
|
333
274
|
log.debug(`Stopping tunnel manager for ${tunName}`);
|
|
334
275
|
// Signal cancellation
|
|
335
276
|
this.cancelled = true;
|
|
336
|
-
// Clear read interval
|
|
337
|
-
if (this.readInterval) {
|
|
338
|
-
clearInterval(this.readInterval);
|
|
339
|
-
this.readInterval = null;
|
|
340
|
-
}
|
|
341
277
|
// Close device connection if exists
|
|
342
278
|
if (this.deviceConn && !this.deviceConn.destroyed) {
|
|
343
279
|
this.deviceConn.destroy();
|
|
@@ -369,81 +305,14 @@ export class TunnelManager extends EventEmitter {
|
|
|
369
305
|
* @returns parsed tunnel parameters from the device response
|
|
370
306
|
*/
|
|
371
307
|
export async function exchangeCoreTunnelParameters(socket) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
mtu: 16000,
|
|
376
|
-
};
|
|
377
|
-
const requestJSON = JSON.stringify(request);
|
|
378
|
-
const jsonBuffer = Buffer.from(requestJSON);
|
|
379
|
-
const magic = Buffer.from('CDTunnel');
|
|
380
|
-
const length = Buffer.alloc(2);
|
|
381
|
-
length.writeUInt16BE(jsonBuffer.length);
|
|
382
|
-
const message = Buffer.concat([magic, length, jsonBuffer]);
|
|
383
|
-
log.debug(`Sending CDTunnel packet: magic=${magic.toString()}, length=${jsonBuffer.length}, body=${requestJSON}`);
|
|
384
|
-
socket.write(message);
|
|
385
|
-
// For receiving the response
|
|
386
|
-
let buffer = Buffer.alloc(0);
|
|
387
|
-
function cleanup() {
|
|
388
|
-
socket.removeListener('data', handleData);
|
|
389
|
-
socket.removeListener('error', handleError);
|
|
390
|
-
socket.removeListener('end', handleEnd);
|
|
391
|
-
if (timeoutHandle) {
|
|
392
|
-
clearTimeout(timeoutHandle);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
function handleData(data) {
|
|
396
|
-
log.debug('Received data chunk:', data.length, 'bytes');
|
|
397
|
-
buffer = Buffer.concat([buffer, data]);
|
|
398
|
-
if (buffer.length < 10) {
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
const receivedMagic = buffer.slice(0, 8).toString();
|
|
402
|
-
if (receivedMagic !== 'CDTunnel') {
|
|
403
|
-
log.error('Invalid magic header:', receivedMagic);
|
|
404
|
-
cleanup();
|
|
405
|
-
return reject(new Error('Invalid packet format'));
|
|
406
|
-
}
|
|
407
|
-
const payloadLength = buffer.readUInt16BE(8);
|
|
408
|
-
const totalLength = 8 + 2 + payloadLength;
|
|
409
|
-
log.debug('Expected total packet length:', totalLength, 'current buffer:', buffer.length);
|
|
410
|
-
if (buffer.length >= totalLength) {
|
|
411
|
-
const payload = buffer.slice(10, totalLength);
|
|
412
|
-
try {
|
|
413
|
-
const response = JSON.parse(payload.toString());
|
|
414
|
-
log.debug('Parsed CDTunnel response:', response);
|
|
415
|
-
cleanup();
|
|
416
|
-
resolve(response);
|
|
417
|
-
}
|
|
418
|
-
catch (err) {
|
|
419
|
-
log.error('Failed to parse JSON:', err);
|
|
420
|
-
cleanup();
|
|
421
|
-
reject(new Error('Invalid JSON response'));
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
function handleError(err) {
|
|
426
|
-
log.error('Socket error:', err);
|
|
427
|
-
cleanup();
|
|
428
|
-
reject(err);
|
|
429
|
-
}
|
|
430
|
-
function handleEnd() {
|
|
431
|
-
log.debug('Connection ended');
|
|
432
|
-
if (buffer.length > 0) {
|
|
433
|
-
log.debug('Buffer at end:', buffer.toString('hex'));
|
|
434
|
-
}
|
|
435
|
-
cleanup();
|
|
436
|
-
reject(new Error('Connection closed before receiving complete response'));
|
|
437
|
-
}
|
|
438
|
-
// Set a timeout for the handshake
|
|
439
|
-
const timeoutHandle = setTimeout(() => {
|
|
440
|
-
cleanup();
|
|
441
|
-
reject(new Error('Tunnel handshake timeout'));
|
|
442
|
-
}, 30000); // 30 second timeout
|
|
443
|
-
socket.on('data', handleData);
|
|
444
|
-
socket.on('error', handleError);
|
|
445
|
-
socket.on('end', handleEnd);
|
|
308
|
+
const requestJson = JSON.stringify({
|
|
309
|
+
type: 'clientHandshakeRequest',
|
|
310
|
+
mtu: CD_TUNNEL_MTU,
|
|
446
311
|
});
|
|
312
|
+
const message = encodeCdTunnelMessage(requestJson);
|
|
313
|
+
log.debug(`Sending CDTunnel packet: magic=${CD_TUNNEL_MAGIC}, length=${message.length - CD_TUNNEL_HEADER_SIZE}, body=${requestJson}`);
|
|
314
|
+
socket.write(message);
|
|
315
|
+
return readCdTunnelResponse(socket, CD_TUNNEL_HANDSHAKE_TIMEOUT_MS);
|
|
447
316
|
}
|
|
448
317
|
/**
|
|
449
318
|
* End-to-end setup: handshake, TUN configuration, route, and forwarding on `secureServiceSocket`.
|
|
@@ -489,6 +358,144 @@ export async function connectToTunnelLockdown(secureServiceSocket) {
|
|
|
489
358
|
throw err;
|
|
490
359
|
}
|
|
491
360
|
}
|
|
361
|
+
function encodeCdTunnelMessage(json) {
|
|
362
|
+
const body = Buffer.from(json);
|
|
363
|
+
const header = Buffer.alloc(CD_TUNNEL_HEADER_SIZE);
|
|
364
|
+
header.write(CD_TUNNEL_MAGIC, 0, CD_TUNNEL_MAGIC_SIZE, 'ascii');
|
|
365
|
+
header.writeUInt16BE(body.length, CD_TUNNEL_MAGIC_SIZE);
|
|
366
|
+
return Buffer.concat([header, body]);
|
|
367
|
+
}
|
|
368
|
+
function tryParseCdTunnelResponse(buffer) {
|
|
369
|
+
if (buffer.length < CD_TUNNEL_HEADER_SIZE) {
|
|
370
|
+
return { kind: 'incomplete' };
|
|
371
|
+
}
|
|
372
|
+
const magic = buffer.subarray(0, CD_TUNNEL_MAGIC_SIZE).toString();
|
|
373
|
+
if (magic !== CD_TUNNEL_MAGIC) {
|
|
374
|
+
log.error('Invalid magic header:', magic);
|
|
375
|
+
return { kind: 'error', error: new Error('Invalid packet format') };
|
|
376
|
+
}
|
|
377
|
+
const payloadLength = buffer.readUInt16BE(CD_TUNNEL_MAGIC_SIZE);
|
|
378
|
+
const totalLength = CD_TUNNEL_HEADER_SIZE + payloadLength;
|
|
379
|
+
if (buffer.length < totalLength) {
|
|
380
|
+
return { kind: 'incomplete' };
|
|
381
|
+
}
|
|
382
|
+
try {
|
|
383
|
+
const value = JSON.parse(buffer.subarray(CD_TUNNEL_HEADER_SIZE, totalLength).toString());
|
|
384
|
+
return { kind: 'ok', value };
|
|
385
|
+
}
|
|
386
|
+
catch (err) {
|
|
387
|
+
log.error('Failed to parse JSON:', err);
|
|
388
|
+
return { kind: 'error', error: new Error('Invalid JSON response') };
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
function readCdTunnelResponse(socket, timeoutMs) {
|
|
392
|
+
return new Promise((resolve, reject) => {
|
|
393
|
+
let buffer = Buffer.alloc(0);
|
|
394
|
+
const cleanup = () => {
|
|
395
|
+
socket.removeListener('data', onData);
|
|
396
|
+
socket.removeListener('error', onError);
|
|
397
|
+
socket.removeListener('end', onEnd);
|
|
398
|
+
clearTimeout(timeoutHandle);
|
|
399
|
+
};
|
|
400
|
+
const finish = (action) => {
|
|
401
|
+
cleanup();
|
|
402
|
+
action();
|
|
403
|
+
};
|
|
404
|
+
const onData = (chunk) => {
|
|
405
|
+
log.debug('Received data chunk:', chunk.length, 'bytes');
|
|
406
|
+
buffer = appendBuffer(buffer, chunk);
|
|
407
|
+
if (buffer.length >= CD_TUNNEL_HEADER_SIZE) {
|
|
408
|
+
const payloadLength = buffer.readUInt16BE(CD_TUNNEL_MAGIC_SIZE);
|
|
409
|
+
log.debug('Expected total packet length:', CD_TUNNEL_HEADER_SIZE + payloadLength, 'current buffer:', buffer.length);
|
|
410
|
+
}
|
|
411
|
+
const result = tryParseCdTunnelResponse(buffer);
|
|
412
|
+
if (result.kind === 'incomplete') {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (result.kind === 'error') {
|
|
416
|
+
finish(() => reject(result.error));
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
log.debug('Parsed CDTunnel response:', result.value);
|
|
420
|
+
finish(() => resolve(result.value));
|
|
421
|
+
};
|
|
422
|
+
const onError = (err) => {
|
|
423
|
+
log.error('Socket error:', err);
|
|
424
|
+
finish(() => reject(err));
|
|
425
|
+
};
|
|
426
|
+
const onEnd = () => {
|
|
427
|
+
log.debug('Connection ended');
|
|
428
|
+
if (buffer.length > 0) {
|
|
429
|
+
log.debug('Buffer at end:', buffer.toString('hex'));
|
|
430
|
+
}
|
|
431
|
+
finish(() => reject(new Error('Connection closed before receiving complete response')));
|
|
432
|
+
};
|
|
433
|
+
const timeoutHandle = setTimeout(() => {
|
|
434
|
+
finish(() => reject(new Error('Tunnel handshake timeout')));
|
|
435
|
+
}, timeoutMs);
|
|
436
|
+
socket.on('data', onData);
|
|
437
|
+
socket.on('error', onError);
|
|
438
|
+
socket.on('end', onEnd);
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
function nextIpv6Frame(buffer, offset) {
|
|
442
|
+
if (offset + IPV6_HEADER_SIZE > buffer.length) {
|
|
443
|
+
return { kind: 'incomplete' };
|
|
444
|
+
}
|
|
445
|
+
const header = buffer.subarray(offset, offset + IPV6_HEADER_SIZE);
|
|
446
|
+
if (((header[0] >> 4) & 0x0f) !== IPV6_VERSION) {
|
|
447
|
+
return { kind: 'resync' };
|
|
448
|
+
}
|
|
449
|
+
const payloadLength = header.readUInt16BE(4);
|
|
450
|
+
const length = IPV6_HEADER_SIZE + payloadLength;
|
|
451
|
+
if (offset + length > buffer.length) {
|
|
452
|
+
return { kind: 'incomplete' };
|
|
453
|
+
}
|
|
454
|
+
return {
|
|
455
|
+
kind: 'frame',
|
|
456
|
+
packet: buffer.subarray(offset, offset + length),
|
|
457
|
+
nextHeader: header[6],
|
|
458
|
+
length,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
function ipv6Endpoints(packet) {
|
|
462
|
+
return {
|
|
463
|
+
src: formatIPv6Address(packet.subarray(8, 24)),
|
|
464
|
+
dst: formatIPv6Address(packet.subarray(24, 40)),
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
function parseUdpPacketData(packet, src, dst) {
|
|
468
|
+
const payload = packet.subarray(IPV6_HEADER_SIZE);
|
|
469
|
+
if (payload.length < 8) {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
return {
|
|
473
|
+
protocol: 'UDP',
|
|
474
|
+
src,
|
|
475
|
+
dst,
|
|
476
|
+
sourcePort: payload.readUInt16BE(0),
|
|
477
|
+
destPort: payload.readUInt16BE(2),
|
|
478
|
+
payload: payload.subarray(8),
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function parseTcpPacketData(packet, src, dst) {
|
|
482
|
+
const tcpStart = IPV6_HEADER_SIZE;
|
|
483
|
+
if (packet.length < tcpStart + 20) {
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
const tcpHeaderLength = (packet.readUInt8(tcpStart + 12) >> 4) * 4;
|
|
487
|
+
if (packet.length < tcpStart + tcpHeaderLength) {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
return {
|
|
491
|
+
protocol: 'TCP',
|
|
492
|
+
src,
|
|
493
|
+
dst,
|
|
494
|
+
sourcePort: packet.readUInt16BE(tcpStart),
|
|
495
|
+
destPort: packet.readUInt16BE(tcpStart + 2),
|
|
496
|
+
payload: packet.subarray(tcpStart + tcpHeaderLength),
|
|
497
|
+
};
|
|
498
|
+
}
|
|
492
499
|
function formatIPv6Address(buffer) {
|
|
493
500
|
if (!buffer || buffer.length !== 16) {
|
|
494
501
|
return 'invalid-address';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Buffer } from 'node:buffer';
|
|
2
|
+
import type { TunnelManager } from './manager.js';
|
|
3
|
+
export interface PacketData {
|
|
4
|
+
protocol: 'TCP' | 'UDP';
|
|
5
|
+
src: string;
|
|
6
|
+
dst: string;
|
|
7
|
+
sourcePort: number;
|
|
8
|
+
destPort: number;
|
|
9
|
+
payload: Buffer;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Event names and listener argument tuples for {@link TunnelManager}
|
|
13
|
+
* (matches Node’s `EventEmitter` event map shape).
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* tunnelManager.on('data', (packet) => {
|
|
17
|
+
* // `packet` is PacketData
|
|
18
|
+
* });
|
|
19
|
+
*/
|
|
20
|
+
export interface PacketConsumer {
|
|
21
|
+
/**
|
|
22
|
+
* Invoked for each parsed TCP/UDP payload extracted from the tunnel stream.
|
|
23
|
+
*
|
|
24
|
+
* @param packet — decoded addresses, ports, and payload
|
|
25
|
+
*/
|
|
26
|
+
onPacket(packet: PacketData): void;
|
|
27
|
+
}
|
|
28
|
+
export interface TunnelManagerEvents {
|
|
29
|
+
data: [packet: PacketData];
|
|
30
|
+
}
|
|
31
|
+
export interface TunnelConnection {
|
|
32
|
+
Address: string;
|
|
33
|
+
RsdPort?: number;
|
|
34
|
+
tunnelManager: TunnelManager;
|
|
35
|
+
/** Tear down the tunnel, close the TUN device, and end the socket when appropriate. */
|
|
36
|
+
closer: () => Promise<void>;
|
|
37
|
+
/** @param consumer — receives packets for the lifetime of the registration */
|
|
38
|
+
addPacketConsumer(consumer: PacketConsumer): void;
|
|
39
|
+
/** @param consumer — must be the same reference passed to {@link TunnelConnection.addPacketConsumer} */
|
|
40
|
+
removePacketConsumer(consumer: PacketConsumer): void;
|
|
41
|
+
/** @returns async iterator of packets until the tunnel is stopped */
|
|
42
|
+
getPacketStream(): AsyncIterable<PacketData>;
|
|
43
|
+
}
|
|
44
|
+
export interface TunnelClientParameters {
|
|
45
|
+
address: string;
|
|
46
|
+
mtu: number;
|
|
47
|
+
}
|
|
48
|
+
export interface TunnelInfo {
|
|
49
|
+
clientParameters: TunnelClientParameters;
|
|
50
|
+
serverAddress: string;
|
|
51
|
+
serverRSDPort?: number;
|
|
52
|
+
}
|
|
53
|
+
export type Ipv6Frame = {
|
|
54
|
+
kind: 'frame';
|
|
55
|
+
packet: Buffer;
|
|
56
|
+
nextHeader: number;
|
|
57
|
+
length: number;
|
|
58
|
+
} | {
|
|
59
|
+
kind: 'incomplete';
|
|
60
|
+
} | {
|
|
61
|
+
kind: 'resync';
|
|
62
|
+
};
|
|
63
|
+
export type CdTunnelParseResult = {
|
|
64
|
+
kind: 'incomplete';
|
|
65
|
+
} | {
|
|
66
|
+
kind: 'ok';
|
|
67
|
+
value: TunnelInfo;
|
|
68
|
+
} | {
|
|
69
|
+
kind: 'error';
|
|
70
|
+
error: Error;
|
|
71
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appium-ios-tuntap",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Native TUN/TAP interface module for Node.js",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"license": "Apache-2.0",
|
|
50
50
|
"gypfile": true,
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@appium/support": "^7.
|
|
52
|
+
"@appium/support": "^7.2.0",
|
|
53
53
|
"node-addon-api": "^8.5.0",
|
|
54
54
|
"node-gyp-build": "^4.8.4",
|
|
55
55
|
"typescript": "^6.0.2"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -96,24 +96,27 @@ void PosixUvPollLoop::OnPoll(uv_poll_t* handle, int status, int events) {
|
|
|
96
96
|
return;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
99
|
+
// Drain all readable packets before returning to libuv (match WinTun worker).
|
|
100
|
+
while (true) {
|
|
101
|
+
std::vector<uint8_t> packet;
|
|
102
|
+
std::string error;
|
|
103
|
+
ReadPacketStatus rs = state->read_fn(state->buffer_size, packet, error);
|
|
104
|
+
|
|
105
|
+
switch (rs) {
|
|
106
|
+
case ReadPacketStatus::Data:
|
|
107
|
+
if (state->on_packet) {
|
|
108
|
+
state->on_packet(std::move(packet));
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
case ReadPacketStatus::NoData:
|
|
112
|
+
return;
|
|
113
|
+
case ReadPacketStatus::Closed:
|
|
114
|
+
handle_terminal("Device closed");
|
|
115
|
+
return;
|
|
116
|
+
case ReadPacketStatus::Error:
|
|
117
|
+
handle_terminal(error);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
117
120
|
}
|
|
118
121
|
}
|
|
119
122
|
|
|
@@ -91,8 +91,11 @@ public:
|
|
|
91
91
|
return ReadPacketStatus::Error;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
const size_t read_cap = max_payload_size + kUtunHeaderSize;
|
|
95
|
+
if (read_frame_.size() < read_cap) {
|
|
96
|
+
read_frame_.resize(read_cap);
|
|
97
|
+
}
|
|
98
|
+
ssize_t bytes_read = read(fd_.get(), read_frame_.data(), read_cap);
|
|
96
99
|
if (bytes_read < 0) {
|
|
97
100
|
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
|
98
101
|
out.clear();
|
|
@@ -111,9 +114,8 @@ public:
|
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
const auto payload_len = static_cast<size_t>(bytes_read - kUtunHeaderSize);
|
|
114
|
-
// Collapse the utun 4-byte address-family prefix in-place.
|
|
115
|
-
memmove(out.data(), out.data() + kUtunHeaderSize, payload_len);
|
|
116
117
|
out.resize(payload_len);
|
|
118
|
+
memcpy(out.data(), read_frame_.data() + kUtunHeaderSize, payload_len);
|
|
117
119
|
return ReadPacketStatus::Data;
|
|
118
120
|
}
|
|
119
121
|
|
|
@@ -125,12 +127,12 @@ public:
|
|
|
125
127
|
return -1;
|
|
126
128
|
}
|
|
127
129
|
|
|
128
|
-
|
|
130
|
+
write_frame_.resize(length + kUtunHeaderSize);
|
|
129
131
|
uint32_t family = htonl(AF_INET6);
|
|
130
|
-
memcpy(
|
|
131
|
-
memcpy(
|
|
132
|
+
memcpy(write_frame_.data(), &family, kUtunHeaderSize);
|
|
133
|
+
memcpy(write_frame_.data() + kUtunHeaderSize, data, length);
|
|
132
134
|
|
|
133
|
-
ssize_t bytes_written = write(fd_.get(),
|
|
135
|
+
ssize_t bytes_written = write(fd_.get(), write_frame_.data(), write_frame_.size());
|
|
134
136
|
if (bytes_written < 0) {
|
|
135
137
|
error = std::string("Write error: ") + strerror(errno);
|
|
136
138
|
return -1;
|
|
@@ -167,6 +169,9 @@ private:
|
|
|
167
169
|
error = "Could not find an available utun device";
|
|
168
170
|
return false;
|
|
169
171
|
}
|
|
172
|
+
|
|
173
|
+
std::vector<uint8_t> read_frame_;
|
|
174
|
+
std::vector<uint8_t> write_frame_;
|
|
170
175
|
};
|
|
171
176
|
|
|
172
177
|
} // namespace
|
package/src/tuntap.cc
CHANGED
|
@@ -209,12 +209,14 @@ Napi::Value TunDevice::StartPolling(const Napi::CallbackInfo& info) {
|
|
|
209
209
|
buffer_size = size;
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
// Queue depth > 1 lets the poll thread post the next packet while JS is still
|
|
213
|
+
// handling the previous callback (still serialized on the main thread).
|
|
212
214
|
tsfn_ = Napi::ThreadSafeFunction::New(
|
|
213
215
|
env,
|
|
214
216
|
info[0].As<Napi::Function>(),
|
|
215
217
|
"TunDeviceDataCallback",
|
|
216
218
|
0,
|
|
217
|
-
|
|
219
|
+
8);
|
|
218
220
|
|
|
219
221
|
uv_loop_t* loop = nullptr;
|
|
220
222
|
napi_status napi_st = napi_get_uv_event_loop(env, &loop);
|
|
@@ -227,11 +229,18 @@ Napi::Value TunDevice::StartPolling(const Napi::CallbackInfo& info) {
|
|
|
227
229
|
Napi::ThreadSafeFunction tsfn = tsfn_;
|
|
228
230
|
auto packet_cb = [tsfn](std::vector<uint8_t> packet) mutable {
|
|
229
231
|
tsfn.BlockingCall(
|
|
230
|
-
[packet = std::move(packet)](Napi::Env env, Napi::Function jsCallback) {
|
|
232
|
+
[packet = std::move(packet)](Napi::Env env, Napi::Function jsCallback) mutable {
|
|
231
233
|
if (env == nullptr || jsCallback.IsEmpty()) {
|
|
232
234
|
return;
|
|
233
235
|
}
|
|
234
|
-
|
|
236
|
+
auto* backing = new std::vector<uint8_t>(std::move(packet));
|
|
237
|
+
Napi::Buffer<uint8_t> buf = Napi::Buffer<uint8_t>::New(
|
|
238
|
+
env,
|
|
239
|
+
backing->data(),
|
|
240
|
+
backing->size(),
|
|
241
|
+
[](Napi::Env, uint8_t*, std::vector<uint8_t>* vec) { delete vec; },
|
|
242
|
+
backing);
|
|
243
|
+
jsCallback.Call({buf});
|
|
235
244
|
});
|
|
236
245
|
};
|
|
237
246
|
// Terminal errors from the receive loop (poll error, device closed, read
|