appium-ios-tuntap 0.0.1

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/lib/tunnel.js ADDED
@@ -0,0 +1,486 @@
1
+ import { log } from './logger.js';
2
+ import { TunTap } from './TunTap.js';
3
+ import { EventEmitter } from 'events';
4
+ import { Buffer } from 'buffer';
5
+ // Global registry for active tunnel managers
6
+ const activeTunnelManagers = new Set();
7
+ // Setup process signal handlers
8
+ let signalHandlersSetup = false;
9
+ function setupSignalHandlers() {
10
+ if (signalHandlersSetup)
11
+ return;
12
+ signalHandlersSetup = true;
13
+ const gracefulShutdown = async (signal) => {
14
+ log(`Received ${signal}, initiating graceful shutdown...`);
15
+ // Copy the set to avoid modification during iteration
16
+ const managers = Array.from(activeTunnelManagers);
17
+ // Stop all tunnel managers
18
+ await Promise.all(managers.map(manager => {
19
+ try {
20
+ return manager.stop();
21
+ }
22
+ catch (err) {
23
+ console.error('Error stopping tunnel manager:', err);
24
+ }
25
+ }));
26
+ log('All tunnel managers stopped, exiting...');
27
+ process.exit(0);
28
+ };
29
+ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
30
+ process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
31
+ // Handle uncaught exceptions
32
+ process.on('uncaughtException', (err) => {
33
+ console.error('Uncaught exception:', err);
34
+ gracefulShutdown('uncaughtException').then(() => process.exit(1));
35
+ });
36
+ // Handle unhandled promise rejections
37
+ process.on('unhandledRejection', (reason, promise) => {
38
+ console.error('Unhandled rejection at:', promise, 'reason:', reason);
39
+ });
40
+ }
41
+ export class TunnelManager extends EventEmitter {
42
+ tun;
43
+ cancelled;
44
+ readInterval;
45
+ buffer;
46
+ packetConsumers;
47
+ packetQueue;
48
+ deviceConn;
49
+ cleanupPromise;
50
+ constructor() {
51
+ super();
52
+ this.tun = null;
53
+ this.cancelled = false;
54
+ this.readInterval = null;
55
+ this.buffer = Buffer.alloc(0);
56
+ this.packetConsumers = new Set();
57
+ this.packetQueue = [];
58
+ this.deviceConn = null;
59
+ this.cleanupPromise = null;
60
+ // Setup signal handlers on first tunnel manager creation
61
+ setupSignalHandlers();
62
+ // Register this manager
63
+ activeTunnelManagers.add(this);
64
+ }
65
+ addPacketConsumer(consumer) {
66
+ this.packetConsumers.add(consumer);
67
+ }
68
+ removePacketConsumer(consumer) {
69
+ this.packetConsumers.delete(consumer);
70
+ }
71
+ async *getPacketStream() {
72
+ const queue = [];
73
+ let resolver = null;
74
+ const consumer = {
75
+ onPacket: (packet) => {
76
+ if (resolver) {
77
+ resolver({ value: packet, done: false });
78
+ resolver = null;
79
+ }
80
+ else {
81
+ queue.push(packet);
82
+ }
83
+ }
84
+ };
85
+ this.addPacketConsumer(consumer);
86
+ try {
87
+ while (!this.cancelled) {
88
+ if (queue.length > 0) {
89
+ yield queue.shift();
90
+ }
91
+ else {
92
+ yield await new Promise((resolve) => {
93
+ resolver = (result) => {
94
+ if (!result.done) {
95
+ resolve(result.value);
96
+ }
97
+ };
98
+ });
99
+ }
100
+ }
101
+ }
102
+ finally {
103
+ this.removePacketConsumer(consumer);
104
+ }
105
+ }
106
+ async setupInterface(tunnelInfo) {
107
+ log(`Setting up tunnel with parameters:`, tunnelInfo);
108
+ try {
109
+ this.tun = new TunTap();
110
+ // Open the TUN device
111
+ if (!this.tun.open()) {
112
+ throw new Error("Failed to open TUN device");
113
+ }
114
+ log(`Opened TUN device: ${this.tun.name}`);
115
+ // Configure the TUN device with IPv6 address and MTU
116
+ await this.tun.configure(tunnelInfo.clientParameters.address, tunnelInfo.clientParameters.mtu);
117
+ // Add route for the server address
118
+ await this.tun.addRoute(`${tunnelInfo.serverAddress}/128`);
119
+ log(`Configured TUN interface ${this.tun.name} with address ${tunnelInfo.clientParameters.address} and MTU ${tunnelInfo.clientParameters.mtu}`);
120
+ return {
121
+ name: this.tun.name,
122
+ mtu: tunnelInfo.clientParameters.mtu,
123
+ interface: this.tun
124
+ };
125
+ }
126
+ catch (err) {
127
+ console.error(`Error setting up TUN interface: ${err.message}`);
128
+ if (this.tun) {
129
+ try {
130
+ this.tun.close();
131
+ }
132
+ catch (closeErr) {
133
+ console.error('Error closing TUN device:', closeErr);
134
+ }
135
+ this.tun = null;
136
+ }
137
+ throw err;
138
+ }
139
+ }
140
+ startForwarding(deviceConn) {
141
+ if (!this.tun) {
142
+ console.error("TUN device is not set up");
143
+ return;
144
+ }
145
+ this.deviceConn = deviceConn;
146
+ log(`Starting bidirectional data forwarding for ${this.tun.name}`);
147
+ // Handle data from the device connection
148
+ deviceConn.on('data', (data) => {
149
+ if (this.cancelled)
150
+ return;
151
+ try {
152
+ // Add data to buffer
153
+ this.buffer = Buffer.concat([this.buffer, data]);
154
+ // Process IPv6 packets
155
+ this.processBuffer();
156
+ }
157
+ catch (err) {
158
+ if (!this.cancelled) {
159
+ console.error('Error processing device data:', err.message);
160
+ }
161
+ }
162
+ });
163
+ // Set up TUN read loop
164
+ this.startTunReadLoop(deviceConn);
165
+ // Listen for device connection close
166
+ deviceConn.on('close', () => {
167
+ log('Device connection closed, stopping tunnel');
168
+ this.stop().catch(err => console.error('Error stopping tunnel:', err));
169
+ });
170
+ deviceConn.on('error', (err) => {
171
+ console.error('Device connection error:', err.message);
172
+ });
173
+ }
174
+ processBuffer() {
175
+ let offset = 0;
176
+ // Process as many complete packets as available
177
+ while (offset + 40 <= this.buffer.length) {
178
+ // Extract IPv6 header (fixed 40 bytes)
179
+ const header = this.buffer.slice(offset, offset + 40);
180
+ // Ensure this is an IPv6 packet (version 6)
181
+ const version = (header[0] >> 4) & 0x0F;
182
+ if (version !== 6) {
183
+ offset++;
184
+ continue;
185
+ }
186
+ // Get payload length from the IPv6 header
187
+ const payloadLength = header.readUInt16BE(4);
188
+ // Ensure we have the full packet (IPv6 header + payload)
189
+ if (offset + 40 + payloadLength > this.buffer.length) {
190
+ break; // Wait for more data
191
+ }
192
+ // Extract the complete IPv6 packet
193
+ const packet = this.buffer.slice(offset, offset + 40 + payloadLength);
194
+ // Extract source and destination IPv6 addresses
195
+ const src = formatIPv6Address(packet.slice(8, 24));
196
+ const dst = formatIPv6Address(packet.slice(24, 40));
197
+ // Get the IPv6 next header value
198
+ const nextHeader = header[6];
199
+ log(`Processing packet: nextHeader=${nextHeader}, totalLength=${40 + payloadLength}`);
200
+ try {
201
+ if (!this.tun) {
202
+ console.error('TUN device is null during packet processing');
203
+ break;
204
+ }
205
+ const bytesWritten = this.tun.write(packet);
206
+ log(`Device → TUN: ${bytesWritten} bytes, IPv6 src=${src}, dst=${dst}`);
207
+ // Handle UDP packets (nextHeader === 17)
208
+ if (nextHeader === 17) {
209
+ const payload = packet.slice(40);
210
+ log(`UDP packet detected: payload length=${payload.length}`);
211
+ if (payload.length < 8) {
212
+ log("UDP payload too short, not emitting event.");
213
+ }
214
+ else {
215
+ const sourcePort = payload.readUInt16BE(0);
216
+ const destPort = payload.readUInt16BE(2);
217
+ const udpPayload = payload.slice(8);
218
+ const packetData = {
219
+ protocol: 'UDP',
220
+ src,
221
+ dst,
222
+ sourcePort,
223
+ destPort,
224
+ payload: udpPayload
225
+ };
226
+ this.emit('data', packetData);
227
+ this.packetConsumers.forEach(consumer => {
228
+ try {
229
+ consumer.onPacket(packetData);
230
+ }
231
+ catch (err) {
232
+ console.error('Error in packet consumer:', err);
233
+ }
234
+ });
235
+ log('Emitted data event for UDP packet');
236
+ }
237
+ }
238
+ // Handle TCP packets (nextHeader === 6)
239
+ else if (nextHeader === 6) {
240
+ const tcpHeaderStart = 40;
241
+ if (packet.length < tcpHeaderStart + 20) {
242
+ log("TCP packet too short for minimum header, skipping.");
243
+ }
244
+ else {
245
+ const sourcePort = packet.readUInt16BE(tcpHeaderStart);
246
+ const destPort = packet.readUInt16BE(tcpHeaderStart + 2);
247
+ const dataOffsetByte = packet.readUInt8(tcpHeaderStart + 12);
248
+ const tcpHeaderLength = (dataOffsetByte >> 4) * 4;
249
+ if (packet.length < tcpHeaderStart + tcpHeaderLength) {
250
+ log("TCP header length exceeds packet length, skipping.");
251
+ }
252
+ else {
253
+ const tcpPayload = packet.slice(tcpHeaderStart + tcpHeaderLength);
254
+ log(`TCP packet detected: headerLength=${tcpHeaderLength}, payload length=${tcpPayload.length}`);
255
+ const packetData = {
256
+ protocol: 'TCP',
257
+ src,
258
+ dst,
259
+ sourcePort,
260
+ destPort,
261
+ payload: tcpPayload
262
+ };
263
+ this.emit('data', packetData);
264
+ this.packetConsumers.forEach(consumer => {
265
+ try {
266
+ consumer.onPacket(packetData);
267
+ }
268
+ catch (err) {
269
+ console.error('Error in packet consumer:', err);
270
+ }
271
+ });
272
+ log('Emitted data event for TCP packet');
273
+ }
274
+ }
275
+ }
276
+ else {
277
+ log("Packet is not UDP or TCP (nextHeader !== 17 and !== 6)");
278
+ }
279
+ }
280
+ catch (err) {
281
+ console.error(`Error writing to TUN: ${err.message}`);
282
+ }
283
+ // Move to the next packet
284
+ offset += 40 + payloadLength;
285
+ }
286
+ // Keep any remaining partial data
287
+ if (offset > 0) {
288
+ this.buffer = this.buffer.slice(offset);
289
+ }
290
+ }
291
+ startTunReadLoop(deviceConn) {
292
+ this.readInterval = setInterval(() => {
293
+ if (this.cancelled || !this.tun)
294
+ return;
295
+ try {
296
+ // Read from TUN
297
+ const data = this.tun.read(16384); // A large buffer for MTU
298
+ // If we got data, send it to the device
299
+ if (data && data.length > 0) {
300
+ if (data.length >= 40) { // Minimum IPv6 header size
301
+ log(`TUN → Device: ${data.length} bytes, IPv6 src=${formatIPv6Address(data.slice(8, 24))}, dst=${formatIPv6Address(data.slice(24, 40))}`);
302
+ }
303
+ else {
304
+ log(`TUN → Device: ${data.length} bytes (too small for IPv6 header)`);
305
+ }
306
+ if (!deviceConn.destroyed) {
307
+ deviceConn.write(data);
308
+ }
309
+ }
310
+ }
311
+ catch (err) {
312
+ if (!this.cancelled) {
313
+ console.error('Error reading from TUN:', err.message);
314
+ }
315
+ }
316
+ }, 5); // Poll every 5ms
317
+ }
318
+ async stop() {
319
+ // Prevent multiple concurrent stops
320
+ if (this.cleanupPromise) {
321
+ return this.cleanupPromise;
322
+ }
323
+ this.cleanupPromise = this._performStop();
324
+ return this.cleanupPromise;
325
+ }
326
+ async _performStop() {
327
+ const tunName = this.tun ? this.tun.name : 'unknown';
328
+ log(`Stopping tunnel manager for ${tunName}`);
329
+ // Signal cancellation
330
+ this.cancelled = true;
331
+ // Clear read interval
332
+ if (this.readInterval) {
333
+ clearInterval(this.readInterval);
334
+ this.readInterval = null;
335
+ }
336
+ // Close device connection if exists
337
+ if (this.deviceConn && !this.deviceConn.destroyed) {
338
+ this.deviceConn.destroy();
339
+ this.deviceConn = null;
340
+ }
341
+ // Clear buffer
342
+ this.buffer = Buffer.alloc(0);
343
+ // Clear packet consumers
344
+ this.packetConsumers.clear();
345
+ // Remove all listeners
346
+ this.removeAllListeners();
347
+ // Close TUN device
348
+ if (this.tun) {
349
+ try {
350
+ this.tun.close();
351
+ }
352
+ catch (err) {
353
+ console.error('Error closing TUN device:', err);
354
+ }
355
+ this.tun = null;
356
+ }
357
+ // Unregister from active managers
358
+ activeTunnelManagers.delete(this);
359
+ log(`Tunnel for ${tunName} closed successfully`);
360
+ }
361
+ }
362
+ function formatIPv6Address(buffer) {
363
+ if (!buffer || buffer.length !== 16) {
364
+ return 'invalid-address';
365
+ }
366
+ const parts = [];
367
+ for (let i = 0; i < 16; i += 2) {
368
+ parts.push(buffer.readUInt16BE(i).toString(16));
369
+ }
370
+ return parts.join(':');
371
+ }
372
+ export async function exchangeCoreTunnelParameters(socket) {
373
+ return new Promise((resolve, reject) => {
374
+ const request = {
375
+ type: "clientHandshakeRequest",
376
+ mtu: 16000
377
+ };
378
+ const requestJSON = JSON.stringify(request);
379
+ const jsonBuffer = Buffer.from(requestJSON);
380
+ const magic = Buffer.from('CDTunnel');
381
+ const length = Buffer.alloc(2);
382
+ length.writeUInt16BE(jsonBuffer.length);
383
+ const message = Buffer.concat([magic, length, jsonBuffer]);
384
+ log(`Sending CDTunnel packet: magic=${magic.toString()}, length=${jsonBuffer.length}, body=${requestJSON}`);
385
+ socket.write(message);
386
+ // For receiving the response
387
+ let buffer = Buffer.alloc(0);
388
+ let timeoutHandle;
389
+ function cleanup() {
390
+ socket.removeListener('data', handleData);
391
+ socket.removeListener('error', handleError);
392
+ socket.removeListener('end', handleEnd);
393
+ if (timeoutHandle) {
394
+ clearTimeout(timeoutHandle);
395
+ }
396
+ }
397
+ function handleData(data) {
398
+ log("Received data chunk:", data.length, "bytes");
399
+ buffer = Buffer.concat([buffer, data]);
400
+ if (buffer.length < 10)
401
+ return;
402
+ const receivedMagic = buffer.slice(0, 8).toString();
403
+ if (receivedMagic !== 'CDTunnel') {
404
+ console.error("Invalid magic header:", receivedMagic);
405
+ cleanup();
406
+ return reject(new Error("Invalid packet format"));
407
+ }
408
+ const payloadLength = buffer.readUInt16BE(8);
409
+ const totalLength = 8 + 2 + payloadLength;
410
+ log("Expected total packet length:", totalLength, "current buffer:", buffer.length);
411
+ if (buffer.length >= totalLength) {
412
+ const payload = buffer.slice(10, totalLength);
413
+ try {
414
+ const response = JSON.parse(payload.toString());
415
+ log("Parsed CDTunnel response:", response);
416
+ cleanup();
417
+ resolve(response);
418
+ }
419
+ catch (err) {
420
+ console.error("Failed to parse JSON:", err);
421
+ cleanup();
422
+ reject(new Error("Invalid JSON response"));
423
+ }
424
+ }
425
+ }
426
+ function handleError(err) {
427
+ console.error("Socket error:", err);
428
+ cleanup();
429
+ reject(err);
430
+ }
431
+ function handleEnd() {
432
+ log("Connection ended");
433
+ if (buffer.length > 0) {
434
+ log("Buffer at end:", buffer.toString('hex'));
435
+ }
436
+ cleanup();
437
+ reject(new Error("Connection closed before receiving complete response"));
438
+ }
439
+ // Set a timeout for the handshake
440
+ timeoutHandle = setTimeout(() => {
441
+ cleanup();
442
+ reject(new Error("Tunnel handshake timeout"));
443
+ }, 30000); // 30 second timeout
444
+ socket.on('data', handleData);
445
+ socket.on('error', handleError);
446
+ socket.on('end', handleEnd);
447
+ });
448
+ }
449
+ export async function connectToTunnelLockdown(secureServiceSocket) {
450
+ const tunnelManager = new TunnelManager();
451
+ try {
452
+ // Exchange tunnel parameters with the device
453
+ const tunnelInfo = await exchangeCoreTunnelParameters(secureServiceSocket);
454
+ log("Tunnel parameters exchanged:", tunnelInfo);
455
+ // Setup tunnel interface
456
+ const tunInterfaceInfo = await tunnelManager.setupInterface(tunnelInfo);
457
+ log("Tunnel interface set up:", tunInterfaceInfo.name);
458
+ // Start bidirectional forwarding
459
+ tunnelManager.startForwarding(secureServiceSocket);
460
+ // Create close function
461
+ const closeFunc = async () => {
462
+ log("Closing tunnel connection");
463
+ await tunnelManager.stop();
464
+ if (!secureServiceSocket.destroyed) {
465
+ secureServiceSocket.end();
466
+ }
467
+ };
468
+ return {
469
+ Address: tunnelInfo.serverAddress,
470
+ RsdPort: tunnelInfo.serverRSDPort,
471
+ tunnelManager: tunnelManager,
472
+ closer: closeFunc,
473
+ addPacketConsumer: (consumer) => tunnelManager.addPacketConsumer(consumer),
474
+ removePacketConsumer: (consumer) => tunnelManager.removePacketConsumer(consumer),
475
+ getPacketStream: () => tunnelManager.getPacketStream()
476
+ };
477
+ }
478
+ catch (err) {
479
+ console.error("Failed to connect to tunnel:", err);
480
+ await tunnelManager.stop();
481
+ if (!secureServiceSocket.destroyed) {
482
+ secureServiceSocket.end();
483
+ }
484
+ throw err;
485
+ }
486
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "appium-ios-tuntap",
3
+ "version": "0.0.1",
4
+ "description": "Native TUN/TAP interface module for Node.js",
5
+ "main": "lib/index.js",
6
+ "types": "lib/index.d.ts",
7
+ "type": "module",
8
+ "scripts": {
9
+ "clean": "rm -rf build node_modules lib",
10
+ "build": "npx tsc",
11
+ "build:addon": "node-gyp rebuild",
12
+ "prepare": "npm run build:addon && npm run build",
13
+ "test": "sudo node test/test-tuntap.js"
14
+ },
15
+ "files": [
16
+ "src/tuntap.cc",
17
+ "lib",
18
+ "build",
19
+ "binding.gyp",
20
+ "package.json",
21
+ "test",
22
+ "README.md",
23
+ "CHANGELOG.md"
24
+ ],
25
+ "author": {
26
+ "name": "Sri Harsha",
27
+ "url": "https://github.com/harsha509"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/appium/appium-ios-tuntap.git"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/appium/appium-ios-tuntap/issues"
35
+ },
36
+ "license": "Apache-2.0",
37
+ "gypfile": true,
38
+ "dependencies": {
39
+ "node-addon-api": "^8.3.1",
40
+ "typescript": "^5.8.3"
41
+ },
42
+ "devDependencies": {
43
+ "@semantic-release/changelog": "^6.0.3",
44
+ "@semantic-release/git": "^10.0.1",
45
+ "@types/node": "^22.15.30",
46
+ "conventional-changelog-conventionalcommits": "^9.0.0",
47
+ "semantic-release": "^24.0.0"
48
+ }
49
+ }