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/TunTap.js ADDED
@@ -0,0 +1,347 @@
1
+ import { createRequire } from 'module';
2
+ import { exec } from 'child_process';
3
+ import { promisify } from 'util';
4
+ const require = createRequire(import.meta.url);
5
+ const nativeTuntap = require('../build/Release/tuntap.node');
6
+ const execPromise = promisify(exec);
7
+ // Custom error types
8
+ export class TunTapError extends Error {
9
+ code;
10
+ constructor(message, code) {
11
+ super(message);
12
+ this.code = code;
13
+ this.name = 'TunTapError';
14
+ }
15
+ }
16
+ export class TunTapPermissionError extends TunTapError {
17
+ constructor(message) {
18
+ super(message, 'EPERM');
19
+ this.name = 'TunTapPermissionError';
20
+ }
21
+ }
22
+ export class TunTapDeviceError extends TunTapError {
23
+ constructor(message) {
24
+ super(message, 'ENODEV');
25
+ this.name = 'TunTapDeviceError';
26
+ }
27
+ }
28
+ /**
29
+ * TUN/TAP device for IP tunneling
30
+ */
31
+ export class TunTap {
32
+ device;
33
+ isOpen;
34
+ isClosed;
35
+ cleanupHandlers = [];
36
+ constructor(name = '') {
37
+ this.device = new nativeTuntap.TunDevice(name);
38
+ this.isOpen = false;
39
+ this.isClosed = false;
40
+ // Register cleanup on process exit
41
+ const cleanup = () => {
42
+ if (this.isOpen && !this.isClosed) {
43
+ try {
44
+ this.close();
45
+ }
46
+ catch (err) {
47
+ console.error('Error closing TUN device during cleanup:', err);
48
+ }
49
+ }
50
+ };
51
+ process.once('exit', cleanup);
52
+ process.once('SIGINT', cleanup);
53
+ process.once('SIGTERM', cleanup);
54
+ this.cleanupHandlers.push(() => {
55
+ process.removeListener('exit', cleanup);
56
+ process.removeListener('SIGINT', cleanup);
57
+ process.removeListener('SIGTERM', cleanup);
58
+ });
59
+ }
60
+ open() {
61
+ if (this.isClosed) {
62
+ throw new TunTapError('Device has been closed and cannot be reopened');
63
+ }
64
+ if (!this.isOpen) {
65
+ try {
66
+ this.isOpen = this.device.open();
67
+ if (!this.isOpen) {
68
+ throw new TunTapDeviceError('Failed to open TUN device');
69
+ }
70
+ }
71
+ catch (err) {
72
+ // Re-throw with more specific error types
73
+ if (err.message?.includes('Permission denied') || err.message?.includes('sudo')) {
74
+ throw new TunTapPermissionError(err.message);
75
+ }
76
+ else if (err.message?.includes('not available') || err.message?.includes('does not exist')) {
77
+ throw new TunTapDeviceError(err.message);
78
+ }
79
+ throw err;
80
+ }
81
+ }
82
+ return this.isOpen;
83
+ }
84
+ close() {
85
+ if (!this.isClosed) {
86
+ try {
87
+ if (this.isOpen) {
88
+ this.device.close();
89
+ this.isOpen = false;
90
+ }
91
+ this.isClosed = true;
92
+ // Run cleanup handlers
93
+ this.cleanupHandlers.forEach(handler => handler());
94
+ this.cleanupHandlers = [];
95
+ }
96
+ catch (err) {
97
+ throw new TunTapError(`Failed to close device: ${err.message}`);
98
+ }
99
+ }
100
+ return true;
101
+ }
102
+ read(maxSize = 4096) {
103
+ if (!this.isOpen) {
104
+ throw new TunTapError('Device not open');
105
+ }
106
+ if (this.isClosed) {
107
+ throw new TunTapError('Device has been closed');
108
+ }
109
+ if (maxSize <= 0 || maxSize > 65536) {
110
+ throw new RangeError('Read size must be between 1 and 65536 bytes');
111
+ }
112
+ try {
113
+ return this.device.read(maxSize);
114
+ }
115
+ catch (err) {
116
+ throw new TunTapError(`Read failed: ${err.message}`);
117
+ }
118
+ }
119
+ write(data) {
120
+ if (!this.isOpen) {
121
+ throw new TunTapError('Device not open');
122
+ }
123
+ if (this.isClosed) {
124
+ throw new TunTapError('Device has been closed');
125
+ }
126
+ if (!Buffer.isBuffer(data)) {
127
+ throw new TypeError('Data must be a Buffer');
128
+ }
129
+ if (data.length === 0) {
130
+ return 0;
131
+ }
132
+ if (data.length > 65536) {
133
+ throw new RangeError('Write data too large (max 65536 bytes)');
134
+ }
135
+ try {
136
+ const result = this.device.write(data);
137
+ if (result < 0) {
138
+ throw new TunTapError('Write operation failed');
139
+ }
140
+ return result;
141
+ }
142
+ catch (err) {
143
+ throw new TunTapError(`Write failed: ${err.message}`);
144
+ }
145
+ }
146
+ get name() {
147
+ return this.device.getName();
148
+ }
149
+ get fd() {
150
+ return this.device.getFd();
151
+ }
152
+ async configure(address, mtu = 1500) {
153
+ if (!this.isOpen) {
154
+ throw new TunTapError('Device not open');
155
+ }
156
+ if (this.isClosed) {
157
+ throw new TunTapError('Device has been closed');
158
+ }
159
+ // Validate IPv6 address format
160
+ const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;
161
+ if (!ipv6Regex.test(address)) {
162
+ throw new TypeError('Invalid IPv6 address format');
163
+ }
164
+ // Validate MTU
165
+ if (mtu < 1280 || mtu > 65535) {
166
+ throw new RangeError('MTU must be between 1280 and 65535');
167
+ }
168
+ const platform = process.platform;
169
+ try {
170
+ if (platform === 'darwin') {
171
+ // macOS configuration
172
+ await execPromise(`sudo ifconfig ${this.name} inet6 ${address} prefixlen 64 up`);
173
+ await execPromise(`sudo ifconfig ${this.name} mtu ${mtu}`);
174
+ }
175
+ else if (platform === 'linux') {
176
+ // Linux configuration
177
+ try {
178
+ // Check if ip command is available
179
+ await execPromise('which ip');
180
+ }
181
+ catch (err) {
182
+ throw new TunTapError('The "ip" command is not available. Please install the iproute2 package (e.g., sudo apt install iproute2)');
183
+ }
184
+ try {
185
+ await execPromise(`sudo ip -6 addr add ${address}/64 dev ${this.name}`);
186
+ await execPromise(`sudo ip link set dev ${this.name} up mtu ${mtu}`);
187
+ }
188
+ catch (err) {
189
+ if (err.message.includes('Permission denied')) {
190
+ throw new TunTapPermissionError(`Permission denied when configuring network interface. Make sure you have sudo privileges or run the application with sudo.`);
191
+ }
192
+ else if (err.message.includes('File exists')) {
193
+ // Address already configured, which might be okay
194
+ console.warn(`Address ${address} may already be configured on ${this.name}`);
195
+ }
196
+ else {
197
+ throw err;
198
+ }
199
+ }
200
+ }
201
+ else {
202
+ throw new TunTapError(`Unsupported platform: ${platform}`);
203
+ }
204
+ }
205
+ catch (err) {
206
+ if (err instanceof TunTapError) {
207
+ throw err;
208
+ }
209
+ throw new TunTapError(`Failed to configure TUN interface: ${err.message}`);
210
+ }
211
+ }
212
+ async addRoute(destination) {
213
+ if (!this.isOpen) {
214
+ throw new TunTapError('Device not open');
215
+ }
216
+ if (this.isClosed) {
217
+ throw new TunTapError('Device has been closed');
218
+ }
219
+ // Basic validation of destination format
220
+ if (!destination || typeof destination !== 'string') {
221
+ throw new TypeError('Destination must be a non-empty string');
222
+ }
223
+ const platform = process.platform;
224
+ try {
225
+ if (platform === 'darwin') {
226
+ // macOS route
227
+ await execPromise(`sudo route -n add -inet6 ${destination} -interface ${this.name}`);
228
+ }
229
+ else if (platform === 'linux') {
230
+ // Linux route
231
+ try {
232
+ await execPromise(`sudo ip -6 route add ${destination} dev ${this.name}`);
233
+ }
234
+ catch (err) {
235
+ if (err.message.includes('Permission denied')) {
236
+ throw new TunTapPermissionError(`Permission denied when adding route. Make sure you have sudo privileges or run the application with sudo.`);
237
+ }
238
+ else if (err.message.includes('File exists')) {
239
+ // Route already exists, which is fine
240
+ console.log(`Route to ${destination} already exists`);
241
+ }
242
+ else {
243
+ throw err;
244
+ }
245
+ }
246
+ }
247
+ else {
248
+ throw new TunTapError(`Unsupported platform: ${platform}`);
249
+ }
250
+ }
251
+ catch (err) {
252
+ // Only throw if it's not the "route already exists" case we handled above
253
+ if (err instanceof TunTapError) {
254
+ throw err;
255
+ }
256
+ if (!err.message.includes('Route to') && !err.message.includes('already exists')) {
257
+ throw new TunTapError(`Failed to add route: ${err.message}`);
258
+ }
259
+ }
260
+ }
261
+ async removeRoute(destination) {
262
+ if (!this.isOpen) {
263
+ throw new TunTapError('Device not open');
264
+ }
265
+ const platform = process.platform;
266
+ try {
267
+ if (platform === 'darwin') {
268
+ // macOS route removal
269
+ await execPromise(`sudo route -n delete -inet6 ${destination}`);
270
+ }
271
+ else if (platform === 'linux') {
272
+ // Linux route removal
273
+ await execPromise(`sudo ip -6 route del ${destination} dev ${this.name}`);
274
+ }
275
+ else {
276
+ throw new TunTapError(`Unsupported platform: ${platform}`);
277
+ }
278
+ }
279
+ catch (err) {
280
+ // Ignore errors if route doesn't exist
281
+ if (!err.message.includes('not in table') && !err.message.includes('No such process')) {
282
+ throw new TunTapError(`Failed to remove route: ${err.message}`);
283
+ }
284
+ }
285
+ }
286
+ /**
287
+ * Get interface statistics
288
+ */
289
+ async getStats() {
290
+ if (!this.isOpen) {
291
+ throw new TunTapError('Device not open');
292
+ }
293
+ const platform = process.platform;
294
+ try {
295
+ if (platform === 'darwin') {
296
+ const { stdout } = await execPromise(`netstat -I ${this.name} -b`);
297
+ // Parse macOS netstat output
298
+ const lines = stdout.trim().split('\n');
299
+ if (lines.length < 2) {
300
+ throw new Error('Unexpected netstat output');
301
+ }
302
+ const stats = lines[1].split(/\s+/);
303
+ return {
304
+ rxPackets: parseInt(stats[4], 10) || 0,
305
+ rxErrors: parseInt(stats[5], 10) || 0,
306
+ rxBytes: parseInt(stats[6], 10) || 0,
307
+ txPackets: parseInt(stats[7], 10) || 0,
308
+ txErrors: parseInt(stats[8], 10) || 0,
309
+ txBytes: parseInt(stats[9], 10) || 0
310
+ };
311
+ }
312
+ else if (platform === 'linux') {
313
+ const { stdout } = await execPromise(`ip -s link show ${this.name}`);
314
+ // Parse Linux ip command output
315
+ const lines = stdout.trim().split('\n');
316
+ // Find RX and TX statistics
317
+ let rxIndex = -1;
318
+ let txIndex = -1;
319
+ for (let i = 0; i < lines.length; i++) {
320
+ if (lines[i].includes('RX:'))
321
+ rxIndex = i + 1;
322
+ if (lines[i].includes('TX:'))
323
+ txIndex = i + 1;
324
+ }
325
+ if (rxIndex === -1 || txIndex === -1) {
326
+ throw new Error('Could not parse interface statistics');
327
+ }
328
+ const rxStats = lines[rxIndex].trim().split(/\s+/);
329
+ const txStats = lines[txIndex].trim().split(/\s+/);
330
+ return {
331
+ rxBytes: parseInt(rxStats[0], 10) || 0,
332
+ rxPackets: parseInt(rxStats[1], 10) || 0,
333
+ rxErrors: parseInt(rxStats[2], 10) || 0,
334
+ txBytes: parseInt(txStats[0], 10) || 0,
335
+ txPackets: parseInt(txStats[1], 10) || 0,
336
+ txErrors: parseInt(txStats[2], 10) || 0
337
+ };
338
+ }
339
+ else {
340
+ throw new TunTapError(`Unsupported platform: ${platform}`);
341
+ }
342
+ }
343
+ catch (err) {
344
+ throw new TunTapError(`Failed to get interface statistics: ${err.message}`);
345
+ }
346
+ }
347
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { TunTap } from './TunTap.js';
2
+ export * from './tunnel.js';
package/lib/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { TunTap } from './TunTap.js';
2
+ export * from './tunnel.js';
@@ -0,0 +1 @@
1
+ export declare function log(...args: any[]): void;
package/lib/logger.js ADDED
@@ -0,0 +1,9 @@
1
+ // logger.ts
2
+ // Check if the '--debug' flag is present among the command line arguments.
3
+ const debugEnabled = process.argv.includes('--debug');
4
+ // A simple log function that prints to console only if debug is enabled.
5
+ export function log(...args) {
6
+ if (debugEnabled) {
7
+ console.log(...args);
8
+ }
9
+ }
@@ -0,0 +1,60 @@
1
+ import { TunTap } from './TunTap.js';
2
+ import { EventEmitter } from 'events';
3
+ import { Socket } from 'net';
4
+ import { Buffer } from 'buffer';
5
+ interface TunnelClientParameters {
6
+ address: string;
7
+ mtu: number;
8
+ }
9
+ interface TunnelInfo {
10
+ clientParameters: TunnelClientParameters;
11
+ serverAddress: string;
12
+ serverRSDPort?: number;
13
+ }
14
+ export interface PacketData {
15
+ protocol: 'TCP' | 'UDP';
16
+ src: string;
17
+ dst: string;
18
+ sourcePort: number;
19
+ destPort: number;
20
+ payload: Buffer;
21
+ }
22
+ export interface PacketConsumer {
23
+ onPacket(packet: PacketData): void;
24
+ }
25
+ export interface TunnelConnection {
26
+ Address: string;
27
+ RsdPort?: number;
28
+ tunnelManager: TunnelManager;
29
+ closer: () => Promise<void>;
30
+ addPacketConsumer(consumer: PacketConsumer): void;
31
+ removePacketConsumer(consumer: PacketConsumer): void;
32
+ getPacketStream(): AsyncIterable<PacketData>;
33
+ }
34
+ export declare class TunnelManager extends EventEmitter {
35
+ private tun;
36
+ private cancelled;
37
+ private readInterval;
38
+ private buffer;
39
+ private packetConsumers;
40
+ private packetQueue;
41
+ private deviceConn;
42
+ private cleanupPromise;
43
+ constructor();
44
+ addPacketConsumer(consumer: PacketConsumer): void;
45
+ removePacketConsumer(consumer: PacketConsumer): void;
46
+ getPacketStream(): AsyncIterable<PacketData>;
47
+ setupInterface(tunnelInfo: TunnelInfo): Promise<{
48
+ name: string;
49
+ mtu: number;
50
+ interface: TunTap;
51
+ }>;
52
+ startForwarding(deviceConn: Socket): void;
53
+ private processBuffer;
54
+ private startTunReadLoop;
55
+ stop(): Promise<void>;
56
+ private _performStop;
57
+ }
58
+ export declare function exchangeCoreTunnelParameters(socket: Socket): Promise<TunnelInfo>;
59
+ export declare function connectToTunnelLockdown(secureServiceSocket: Socket): Promise<TunnelConnection>;
60
+ export {};