appium-ios-tuntap 0.2.5 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +20 -3
- package/binding.gyp +20 -4
- package/lib/platform/create-platform.js +3 -0
- package/lib/platform/require-admin.d.ts +17 -0
- package/lib/platform/require-admin.js +32 -0
- package/lib/platform/windows.d.ts +13 -0
- package/lib/platform/windows.js +195 -0
- package/lib/tunnel.d.ts +1 -1
- package/package.json +7 -3
- package/prebuilds/darwin-arm64/appium-ios-tuntap.node +0 -0
- package/prebuilds/darwin-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/scripts/fetch-wintun.mjs +70 -0
- package/src/native/handle.cc +59 -0
- package/src/native/handle.h +30 -0
- package/src/native/tun_backend.h +22 -2
- package/src/native/tun_backend_windows.cc +370 -0
- package/src/native/wintun_loader.cc +218 -0
- package/src/native/wintun_loader.h +93 -0
- package/vendor/wintun/LICENSE.txt +84 -0
- package/vendor/wintun/README.md +37 -0
- package/vendor/wintun/bin/amd64/wintun.dll +0 -0
- package/vendor/wintun/bin/arm/wintun.dll +0 -0
- package/vendor/wintun/bin/arm64/wintun.dll +0 -0
- package/vendor/wintun/bin/x86/wintun.dll +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [0.4.0](https://github.com/appium/appium-ios-tuntap/compare/v0.3.0...v0.4.0) (2026-05-30)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
* implement Windows (WinTun) JavaScript platform layer ([#44](https://github.com/appium/appium-ios-tuntap/issues/44)) ([da2a9bb](https://github.com/appium/appium-ios-tuntap/commit/da2a9bba1d7226f67ce0f00c533df72164aac858))
|
|
6
|
+
|
|
7
|
+
## [0.3.0](https://github.com/appium/appium-ios-tuntap/compare/v0.2.5...v0.3.0) (2026-05-22)
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* add Windows (WinTun) native backend ([#43](https://github.com/appium/appium-ios-tuntap/issues/43)) ([565b4c1](https://github.com/appium/appium-ios-tuntap/commit/565b4c1cfd2ddf4956ed32ade4f8208cd0d4f0f6)), closes [#ifdef](https://github.com/appium/appium-ios-tuntap/issues/ifdef)
|
|
12
|
+
|
|
1
13
|
## [0.2.5](https://github.com/appium/appium-ios-tuntap/compare/v0.2.4...v0.2.5) (2026-05-14)
|
|
2
14
|
|
|
3
15
|
### Code Refactoring
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# TunTap Bridge
|
|
2
2
|
|
|
3
|
-
A native TUN/TAP interface module for Node.js that works on
|
|
3
|
+
A native TUN/TAP interface module for Node.js that works on macOS, Linux, and Windows, with enhanced error handling, signal management, and thread safety.
|
|
4
4
|
|
|
5
5
|
## Description
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ This module provides a Node.js interface to TUN/TAP virtual network devices, all
|
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
|
-
- **Cross-platform**: Works on macOS (utun)
|
|
11
|
+
- **Cross-platform**: Works on macOS (utun), Linux (TUN/TAP), and Windows (WinTun)
|
|
12
12
|
- **TypeScript support**: Full TypeScript definitions included
|
|
13
13
|
- **Signal handling**: Graceful shutdown on SIGINT/SIGTERM
|
|
14
14
|
- **Thread safety**: Safe to use from multiple Node.js worker threads
|
|
@@ -88,6 +88,14 @@ On Linux, the module requires:
|
|
|
88
88
|
sudo pacman -S linux-headers
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
+
### Windows
|
|
92
|
+
|
|
93
|
+
On Windows the module uses [WinTun](https://www.wintun.net/) (the same userspace TUN driver shipped with WireGuard). Requirements:
|
|
94
|
+
|
|
95
|
+
1. **`wintun.dll`**: ships with the package. The official signed binaries for `amd64`, `arm64`, `x86`, and `arm` are bundled under `vendor/wintun/bin/<arch>/wintun.dll`; the addon discovers the right one automatically based on its own compile-time architecture. No download or copy step is required.
|
|
96
|
+
2. **Administrator privileges**: required to create the kernel adapter and configure addresses/routes via `netsh`. Launch your shell with **Run as administrator**.
|
|
97
|
+
3. **Build toolchain (only if compiling from source)**: Visual Studio Build Tools 2022 with the C++ workload, the Windows 10 SDK, and Python 3.x on `PATH`.
|
|
98
|
+
|
|
91
99
|
## Usage
|
|
92
100
|
|
|
93
101
|
### Basic Usage
|
|
@@ -207,7 +215,7 @@ socket.connect(port, host, async () => {
|
|
|
207
215
|
|
|
208
216
|
#### Properties
|
|
209
217
|
- `name: string` - The device name (e.g., 'utun0', 'tun0')
|
|
210
|
-
- `fd: number` - The file descriptor
|
|
218
|
+
- `fd: number` - The native file descriptor on POSIX (macOS/Linux). Returns `-1` on Windows; Wintun does not expose a numeric file descriptor.
|
|
211
219
|
|
|
212
220
|
### Error Types
|
|
213
221
|
|
|
@@ -294,3 +302,12 @@ This ensures the signal handler works as intended.
|
|
|
294
302
|
## License
|
|
295
303
|
|
|
296
304
|
Apache-2.0
|
|
305
|
+
|
|
306
|
+
### Third-party software
|
|
307
|
+
|
|
308
|
+
This package redistributes the official signed **WinTun** DLLs (version 0.14.1) from [wintun.net](https://www.wintun.net/) under the bundled-binary license shipped by the WinTun project. The unmodified binaries and the upstream license live under [vendor/wintun/](vendor/wintun/):
|
|
309
|
+
|
|
310
|
+
- `vendor/wintun/bin/{amd64,arm64,x86,arm}/wintun.dll`
|
|
311
|
+
- `vendor/wintun/LICENSE.txt` — the upstream WinTun license; required when redistributing the DLL
|
|
312
|
+
|
|
313
|
+
Maintainers can refresh the bundled binaries with `npm run refresh:wintun` after bumping `WINTUN_VERSION` in [scripts/fetch-wintun.mjs](scripts/fetch-wintun.mjs).
|
package/binding.gyp
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"-Wno-unused-parameter",
|
|
21
21
|
"-fPIC"
|
|
22
22
|
],
|
|
23
|
-
"cflags_cc": [
|
|
23
|
+
"cflags_cc": [
|
|
24
24
|
"-std=c++17",
|
|
25
25
|
"-Wno-vla-extension",
|
|
26
26
|
"-O3",
|
|
@@ -48,15 +48,15 @@
|
|
|
48
48
|
]
|
|
49
49
|
},
|
|
50
50
|
"msvs_settings": {
|
|
51
|
-
"VCCLCompilerTool": {
|
|
51
|
+
"VCCLCompilerTool": {
|
|
52
52
|
"ExceptionHandling": 1,
|
|
53
53
|
"AdditionalOptions": [
|
|
54
|
-
"/std:c++
|
|
54
|
+
"/std:c++20",
|
|
55
55
|
"/O2"
|
|
56
56
|
]
|
|
57
57
|
}
|
|
58
58
|
},
|
|
59
|
-
"defines": [
|
|
59
|
+
"defines": [
|
|
60
60
|
"NAPI_CPP_EXCEPTIONS",
|
|
61
61
|
"NAPI_VERSION=8"
|
|
62
62
|
],
|
|
@@ -89,6 +89,22 @@
|
|
|
89
89
|
"-framework", "CoreFoundation"
|
|
90
90
|
]
|
|
91
91
|
}
|
|
92
|
+
}],
|
|
93
|
+
["OS=='win'", {
|
|
94
|
+
"sources": [
|
|
95
|
+
"src/native/handle.cc",
|
|
96
|
+
"src/native/wintun_loader.cc",
|
|
97
|
+
"src/native/tun_backend_windows.cc"
|
|
98
|
+
],
|
|
99
|
+
"libraries": [
|
|
100
|
+
"iphlpapi.lib",
|
|
101
|
+
"ws2_32.lib"
|
|
102
|
+
],
|
|
103
|
+
"defines": [
|
|
104
|
+
"_WIN32_WINNT=0x0A00",
|
|
105
|
+
"WIN32_LEAN_AND_MEAN",
|
|
106
|
+
"NOMINMAX"
|
|
107
|
+
]
|
|
92
108
|
}]
|
|
93
109
|
]
|
|
94
110
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DarwinTunTapPlatform } from './darwin.js';
|
|
2
2
|
import { LinuxTunTapPlatform } from './linux.js';
|
|
3
3
|
import { UnsupportedTunTapPlatform } from './unsupported.js';
|
|
4
|
+
import { WindowsTunTapPlatform } from './windows.js';
|
|
4
5
|
/** @internal Built-in {@link TunTapPlatform} for a Node `process.platform` value. */
|
|
5
6
|
export function createTunTapPlatform(platform) {
|
|
6
7
|
switch (platform) {
|
|
@@ -8,6 +9,8 @@ export function createTunTapPlatform(platform) {
|
|
|
8
9
|
return new DarwinTunTapPlatform();
|
|
9
10
|
case 'linux':
|
|
10
11
|
return new LinuxTunTapPlatform();
|
|
12
|
+
case 'win32':
|
|
13
|
+
return new WindowsTunTapPlatform();
|
|
11
14
|
default:
|
|
12
15
|
return new UnsupportedTunTapPlatform(platform);
|
|
13
16
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Throws {@link TunTapPermissionError} unless the current process is an
|
|
3
|
+
* elevated (Administrator) Windows process. Mirrors `assertEffectiveRoot`
|
|
4
|
+
* from {@link ./require-root.ts} for POSIX.
|
|
5
|
+
*/
|
|
6
|
+
export declare function assertAdminOnWindows(): Promise<void>;
|
|
7
|
+
/**
|
|
8
|
+
* Returns true when the current process is running with Administrator
|
|
9
|
+
* privileges on Windows. Implementation runs `net session` (which always
|
|
10
|
+
* exists, regardless of locale) and inspects the exit code.
|
|
11
|
+
*
|
|
12
|
+
* The result is memoized for the lifetime of the process; admin status cannot
|
|
13
|
+
* change between calls without restarting the shell.
|
|
14
|
+
*/
|
|
15
|
+
export declare const isAdministrator: (() => Promise<boolean>) & {
|
|
16
|
+
cache: Map<unknown, Promise<boolean>>;
|
|
17
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { util } from '@appium/support';
|
|
2
|
+
import { TunTapPermissionError } from '../errors.js';
|
|
3
|
+
import { execFileAsync } from './exec.js';
|
|
4
|
+
/**
|
|
5
|
+
* Throws {@link TunTapPermissionError} unless the current process is an
|
|
6
|
+
* elevated (Administrator) Windows process. Mirrors `assertEffectiveRoot`
|
|
7
|
+
* from {@link ./require-root.ts} for POSIX.
|
|
8
|
+
*/
|
|
9
|
+
export async function assertAdminOnWindows() {
|
|
10
|
+
if (await isAdministrator()) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
throw new TunTapPermissionError('TUN interface configuration and routing require Administrator privileges on Windows. ' +
|
|
14
|
+
'Re-launch the shell with "Run as administrator".');
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Returns true when the current process is running with Administrator
|
|
18
|
+
* privileges on Windows. Implementation runs `net session` (which always
|
|
19
|
+
* exists, regardless of locale) and inspects the exit code.
|
|
20
|
+
*
|
|
21
|
+
* The result is memoized for the lifetime of the process; admin status cannot
|
|
22
|
+
* change between calls without restarting the shell.
|
|
23
|
+
*/
|
|
24
|
+
export const isAdministrator = util.memoize(async function isAdministratorUncached() {
|
|
25
|
+
try {
|
|
26
|
+
await execFileAsync('net', ['session'], { windowsHide: true });
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { TunTapInterfaceStats, TunTapPlatform } from './types.js';
|
|
2
|
+
/** Windows implementation backed by `netsh` for configuration/routing and
|
|
3
|
+
* PowerShell `Get-NetAdapterStatistics` for byte counters. */
|
|
4
|
+
export declare class WindowsTunTapPlatform implements TunTapPlatform {
|
|
5
|
+
/** @inheritdoc */
|
|
6
|
+
configure(interfaceName: string, address: string, mtu: number): Promise<void>;
|
|
7
|
+
/** @inheritdoc */
|
|
8
|
+
addRoute(interfaceName: string, destination: string): Promise<void>;
|
|
9
|
+
/** @inheritdoc */
|
|
10
|
+
removeRoute(interfaceName: string, destination: string): Promise<void>;
|
|
11
|
+
/** @inheritdoc */
|
|
12
|
+
getStats(interfaceName: string): Promise<TunTapInterfaceStats>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { TunTapError } from '../errors.js';
|
|
2
|
+
import { log } from '../logger.js';
|
|
3
|
+
import { assertAdminOnWindows } from './require-admin.js';
|
|
4
|
+
import { execFileAsync } from './exec.js';
|
|
5
|
+
/** Tightly-restricted character set for adapter names passed into PowerShell. */
|
|
6
|
+
const SAFE_NAME_RE = /^[A-Za-z0-9_\- ]+$/;
|
|
7
|
+
/** Phrases that indicate `netsh` could not find the requested route/address. */
|
|
8
|
+
const MISSING_TARGET_HINTS = [
|
|
9
|
+
'element not found',
|
|
10
|
+
'cannot find',
|
|
11
|
+
'no matching',
|
|
12
|
+
'does not exist',
|
|
13
|
+
'not found',
|
|
14
|
+
];
|
|
15
|
+
/** Windows implementation backed by `netsh` for configuration/routing and
|
|
16
|
+
* PowerShell `Get-NetAdapterStatistics` for byte counters. */
|
|
17
|
+
export class WindowsTunTapPlatform {
|
|
18
|
+
/** @inheritdoc */
|
|
19
|
+
async configure(interfaceName, address, mtu) {
|
|
20
|
+
await assertAdminOnWindows();
|
|
21
|
+
assertSafeAdapterName(interfaceName);
|
|
22
|
+
log.debug(`[win] configure: interface=${interfaceName} address=${address} mtu=${mtu}`);
|
|
23
|
+
await addIpv6Address(interfaceName, address);
|
|
24
|
+
await setIpv6Mtu(interfaceName, mtu);
|
|
25
|
+
}
|
|
26
|
+
/** @inheritdoc */
|
|
27
|
+
async addRoute(interfaceName, destination) {
|
|
28
|
+
await assertAdminOnWindows();
|
|
29
|
+
assertSafeAdapterName(interfaceName);
|
|
30
|
+
log.debug(`[win] addRoute: interface=${interfaceName} destination=${destination}`);
|
|
31
|
+
await addIpv6Route(interfaceName, destination);
|
|
32
|
+
// WinTun presents as an Ethernet adapter, so Windows requires Neighbor
|
|
33
|
+
// Discovery (NDP) before it will send packets through the interface.
|
|
34
|
+
// For /128 host routes we seed a static neighbor entry so NDP is bypassed
|
|
35
|
+
// and the first connection attempt is not silently dropped.
|
|
36
|
+
if (destination.endsWith('/128')) {
|
|
37
|
+
const address = destination.slice(0, -4);
|
|
38
|
+
await addStaticNeighbor(interfaceName, address);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** @inheritdoc */
|
|
42
|
+
async removeRoute(interfaceName, destination) {
|
|
43
|
+
await assertAdminOnWindows();
|
|
44
|
+
assertSafeAdapterName(interfaceName);
|
|
45
|
+
await deleteIpv6Route(interfaceName, destination);
|
|
46
|
+
}
|
|
47
|
+
/** @inheritdoc */
|
|
48
|
+
async getStats(interfaceName) {
|
|
49
|
+
assertSafeAdapterName(interfaceName);
|
|
50
|
+
const script = `Get-NetAdapterStatistics -Name '${interfaceName}' ` +
|
|
51
|
+
'| Select-Object ReceivedBytes,SentBytes,ReceivedUnicastPackets,' +
|
|
52
|
+
'SentUnicastPackets,ReceivedDiscardedPackets,OutboundDiscardedPackets ' +
|
|
53
|
+
'| ConvertTo-Json -Compress';
|
|
54
|
+
const { stdout } = await execFileAsync('powershell', [
|
|
55
|
+
'-NoProfile',
|
|
56
|
+
'-NonInteractive',
|
|
57
|
+
'-ExecutionPolicy',
|
|
58
|
+
'Bypass',
|
|
59
|
+
'-Command',
|
|
60
|
+
script,
|
|
61
|
+
]);
|
|
62
|
+
let parsed;
|
|
63
|
+
try {
|
|
64
|
+
parsed = JSON.parse(stdout);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
throw new TunTapError(`Failed to parse Get-NetAdapterStatistics output: ${stdout.trim()}`);
|
|
68
|
+
}
|
|
69
|
+
const num = (key) => {
|
|
70
|
+
const value = parsed[key];
|
|
71
|
+
const n = typeof value === 'number' ? value : parseInt(String(value ?? ''), 10);
|
|
72
|
+
return Number.isFinite(n) ? n : 0;
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
rxBytes: num('ReceivedBytes'),
|
|
76
|
+
rxPackets: num('ReceivedUnicastPackets'),
|
|
77
|
+
rxErrors: num('ReceivedDiscardedPackets'),
|
|
78
|
+
txBytes: num('SentBytes'),
|
|
79
|
+
txPackets: num('SentUnicastPackets'),
|
|
80
|
+
txErrors: num('OutboundDiscardedPackets'),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Validates an adapter name before embedding in a PowerShell expression. */
|
|
85
|
+
function assertSafeAdapterName(interfaceName) {
|
|
86
|
+
if (!SAFE_NAME_RE.test(interfaceName)) {
|
|
87
|
+
throw new TunTapError(`Refusing to use adapter name with unsupported characters: ${JSON.stringify(interfaceName)}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function isMissingTargetError(err) {
|
|
91
|
+
const message = String(err?.message ?? '').toLowerCase();
|
|
92
|
+
return MISSING_TARGET_HINTS.some((hint) => message.includes(hint));
|
|
93
|
+
}
|
|
94
|
+
async function addIpv6Address(interfaceName, address) {
|
|
95
|
+
try {
|
|
96
|
+
const r = await execFileAsync('netsh', [
|
|
97
|
+
'interface',
|
|
98
|
+
'ipv6',
|
|
99
|
+
'add',
|
|
100
|
+
'address',
|
|
101
|
+
`interface=${interfaceName}`,
|
|
102
|
+
`address=${address}/64`,
|
|
103
|
+
'store=active',
|
|
104
|
+
]);
|
|
105
|
+
log.debug(`[win] add address ok: ${r.stdout.trim() || '(no output)'}`);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
const message = err.message ?? '';
|
|
109
|
+
log.warn(`[win] add address err: ${message}`);
|
|
110
|
+
if (!/already exists|object already/i.test(message)) {
|
|
111
|
+
throw err;
|
|
112
|
+
}
|
|
113
|
+
log.warn(`Address ${address} may already be configured on ${interfaceName}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function setIpv6Mtu(interfaceName, mtu) {
|
|
117
|
+
try {
|
|
118
|
+
const r = await execFileAsync('netsh', [
|
|
119
|
+
'interface',
|
|
120
|
+
'ipv6',
|
|
121
|
+
'set',
|
|
122
|
+
'subinterface',
|
|
123
|
+
interfaceName,
|
|
124
|
+
`mtu=${mtu}`,
|
|
125
|
+
'store=active',
|
|
126
|
+
]);
|
|
127
|
+
log.debug(`[win] set mtu ok: ${r.stdout.trim() || '(no output)'}`);
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
log.warn(`[win] set mtu err: ${err.message ?? err}`);
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function addIpv6Route(interfaceName, destination) {
|
|
135
|
+
try {
|
|
136
|
+
const r = await execFileAsync('netsh', [
|
|
137
|
+
'interface',
|
|
138
|
+
'ipv6',
|
|
139
|
+
'add',
|
|
140
|
+
'route',
|
|
141
|
+
destination,
|
|
142
|
+
interfaceName,
|
|
143
|
+
'store=active',
|
|
144
|
+
]);
|
|
145
|
+
log.debug(`[win] add route ok: ${r.stdout.trim() || '(no output)'}`);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
const message = err.message ?? '';
|
|
149
|
+
log.warn(`[win] add route err: ${message}`);
|
|
150
|
+
if (/already exists|object already/i.test(message)) {
|
|
151
|
+
log.debug(`Route to ${destination} already exists`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function deleteIpv6Route(interfaceName, destination) {
|
|
158
|
+
try {
|
|
159
|
+
await execFileAsync('netsh', [
|
|
160
|
+
'interface',
|
|
161
|
+
'ipv6',
|
|
162
|
+
'delete',
|
|
163
|
+
'route',
|
|
164
|
+
destination,
|
|
165
|
+
interfaceName,
|
|
166
|
+
'store=active',
|
|
167
|
+
]);
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
if (isMissingTargetError(err)) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
throw err;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function addStaticNeighbor(interfaceName, address) {
|
|
177
|
+
log.debug(`[win] addStaticNeighbor: interface=${interfaceName} address=${address}`);
|
|
178
|
+
try {
|
|
179
|
+
const r = await execFileAsync('netsh', [
|
|
180
|
+
'interface',
|
|
181
|
+
'ipv6',
|
|
182
|
+
'add',
|
|
183
|
+
'neighbor',
|
|
184
|
+
interfaceName,
|
|
185
|
+
address,
|
|
186
|
+
'00-00-00-00-00-01',
|
|
187
|
+
'store=active',
|
|
188
|
+
]);
|
|
189
|
+
log.debug(`[win] add neighbor ok: ${r.stdout.trim() || '(no output)'}`);
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
const msg = err.message ?? String(err);
|
|
193
|
+
log.warn(`[win] add neighbor err: ${msg}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
package/lib/tunnel.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appium-ios-tuntap",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Native TUN/TAP interface module for Node.js",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -15,16 +15,19 @@
|
|
|
15
15
|
"format": "prettier -w ./src ./test",
|
|
16
16
|
"format:check": "prettier --check ./src ./test",
|
|
17
17
|
"install": "node-gyp-build",
|
|
18
|
+
"refresh:wintun": "node scripts/fetch-wintun.mjs",
|
|
18
19
|
"prepare": "npm run build",
|
|
19
20
|
"test": "npm run test:integration && npm run test:unit",
|
|
20
|
-
"test:unit": "mocha
|
|
21
|
-
"test:integration": "mocha
|
|
21
|
+
"test:unit": "mocha \"test/unit/**/*.spec.mjs\" --exit --timeout 2m",
|
|
22
|
+
"test:integration": "mocha \"test/integration/**/*.spec.mjs\" --exit --timeout 2m"
|
|
22
23
|
},
|
|
23
24
|
"files": [
|
|
24
25
|
"src/tuntap.cc",
|
|
25
26
|
"src/native",
|
|
26
27
|
"lib",
|
|
27
28
|
"prebuilds",
|
|
29
|
+
"scripts",
|
|
30
|
+
"vendor",
|
|
28
31
|
"binding.gyp",
|
|
29
32
|
"package.json",
|
|
30
33
|
"README.md",
|
|
@@ -56,6 +59,7 @@
|
|
|
56
59
|
"@semantic-release/changelog": "^6.0.3",
|
|
57
60
|
"@semantic-release/git": "^10.0.1",
|
|
58
61
|
"@types/node": "^25.0.1",
|
|
62
|
+
"commander": "^14.0.3",
|
|
59
63
|
"conventional-changelog-conventionalcommits": "^9.0.0",
|
|
60
64
|
"mocha": "^11.7.5",
|
|
61
65
|
"prebuildify": "^6.0.1",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Maintainer-only helper that refreshes the bundled WinTun binaries committed
|
|
2
|
+
// under vendor/wintun/. The npm `install` hook does NOT invoke this script —
|
|
3
|
+
// the package ships with the official signed DLLs already checked in. Run
|
|
4
|
+
// `npm run refresh:wintun -- --version <semver>` to pull a different release.
|
|
5
|
+
|
|
6
|
+
import {fs, logger, net, tempDir, zip} from '@appium/support';
|
|
7
|
+
import {Command} from 'commander';
|
|
8
|
+
import {join, dirname} from 'node:path';
|
|
9
|
+
import {fileURLToPath} from 'node:url';
|
|
10
|
+
|
|
11
|
+
const log = logger.getLogger('refresh-wintun');
|
|
12
|
+
|
|
13
|
+
const DEFAULT_WINTUN_VERSION = '0.14.1';
|
|
14
|
+
const BUNDLED_ARCHES = ['amd64', 'arm64', 'x86', 'arm'];
|
|
15
|
+
|
|
16
|
+
const rootDir = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
17
|
+
const vendorDir = join(rootDir, 'vendor', 'wintun');
|
|
18
|
+
|
|
19
|
+
async function deployDll(arch, extractDir) {
|
|
20
|
+
const destDir = join(vendorDir, 'bin', arch);
|
|
21
|
+
await fs.mkdir(destDir, {recursive: true});
|
|
22
|
+
const src = join(extractDir, 'wintun', 'bin', arch, 'wintun.dll');
|
|
23
|
+
const dest = join(destDir, 'wintun.dll');
|
|
24
|
+
await fs.copyFile(src, dest);
|
|
25
|
+
log.info(`wintun.dll (${arch}) -> ${dest}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function deployLicense(extractDir) {
|
|
29
|
+
const src = join(extractDir, 'wintun', 'LICENSE.txt');
|
|
30
|
+
const dest = join(vendorDir, 'LICENSE.txt');
|
|
31
|
+
await fs.copyFile(src, dest);
|
|
32
|
+
log.info(`LICENSE.txt -> ${dest}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function refreshWintun(version) {
|
|
36
|
+
const url = `https://www.wintun.net/builds/wintun-${version}.zip`;
|
|
37
|
+
const tmpDir = await tempDir.openDir();
|
|
38
|
+
try {
|
|
39
|
+
const zipPath = join(tmpDir, 'wintun.zip');
|
|
40
|
+
const extractDir = join(tmpDir, 'out');
|
|
41
|
+
await fs.mkdir(extractDir, {recursive: true});
|
|
42
|
+
|
|
43
|
+
log.info(`Downloading WinTun ${version}...`);
|
|
44
|
+
await net.downloadFile(url, zipPath);
|
|
45
|
+
await zip.extractAllTo(zipPath, extractDir);
|
|
46
|
+
|
|
47
|
+
await fs.mkdir(vendorDir, {recursive: true});
|
|
48
|
+
await Promise.all([
|
|
49
|
+
...BUNDLED_ARCHES.map((arch) => deployDll(arch, extractDir)),
|
|
50
|
+
deployLicense(extractDir),
|
|
51
|
+
]);
|
|
52
|
+
} finally {
|
|
53
|
+
await fs.rimraf(tmpDir);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const program = new Command();
|
|
58
|
+
program
|
|
59
|
+
.name('refresh-wintun')
|
|
60
|
+
.description('Refresh the bundled WinTun binaries under vendor/wintun/')
|
|
61
|
+
.option(
|
|
62
|
+
'-v, --version <semver>',
|
|
63
|
+
'WinTun release version to download',
|
|
64
|
+
DEFAULT_WINTUN_VERSION,
|
|
65
|
+
)
|
|
66
|
+
.action(async (options) => {
|
|
67
|
+
await refreshWintun(options.version);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await program.parseAsync(process.argv);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#ifdef _WIN32
|
|
2
|
+
|
|
3
|
+
#include "handle.h"
|
|
4
|
+
|
|
5
|
+
namespace {
|
|
6
|
+
|
|
7
|
+
bool IsRealHandle(HANDLE handle) {
|
|
8
|
+
return handle != nullptr && handle != INVALID_HANDLE_VALUE;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
} // namespace
|
|
12
|
+
|
|
13
|
+
Handle::Handle() : handle_(nullptr) {}
|
|
14
|
+
|
|
15
|
+
Handle::Handle(HANDLE handle) : handle_(handle) {}
|
|
16
|
+
|
|
17
|
+
Handle::~Handle() {
|
|
18
|
+
if (IsRealHandle(handle_)) {
|
|
19
|
+
::CloseHandle(handle_);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
Handle::Handle(Handle&& other) noexcept : handle_(other.handle_) {
|
|
24
|
+
other.handle_ = nullptr;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
Handle& Handle::operator=(Handle&& other) noexcept {
|
|
28
|
+
if (this != &other) {
|
|
29
|
+
if (IsRealHandle(handle_)) {
|
|
30
|
+
::CloseHandle(handle_);
|
|
31
|
+
}
|
|
32
|
+
handle_ = other.handle_;
|
|
33
|
+
other.handle_ = nullptr;
|
|
34
|
+
}
|
|
35
|
+
return *this;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
HANDLE Handle::get() const {
|
|
39
|
+
return handle_;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
HANDLE Handle::release() {
|
|
43
|
+
HANDLE temp = handle_;
|
|
44
|
+
handle_ = nullptr;
|
|
45
|
+
return temp;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
bool Handle::is_valid() const {
|
|
49
|
+
return IsRealHandle(handle_);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
void Handle::reset(HANDLE handle) {
|
|
53
|
+
if (IsRealHandle(handle_)) {
|
|
54
|
+
::CloseHandle(handle_);
|
|
55
|
+
}
|
|
56
|
+
handle_ = handle;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#endif
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#ifdef _WIN32
|
|
4
|
+
|
|
5
|
+
#include <windows.h>
|
|
6
|
+
|
|
7
|
+
// RAII wrapper for a Win32 `HANDLE`. Mirrors `FileDescriptor` so backends can
|
|
8
|
+
// rely on the same lifetime semantics regardless of OS.
|
|
9
|
+
class Handle {
|
|
10
|
+
public:
|
|
11
|
+
Handle();
|
|
12
|
+
explicit Handle(HANDLE handle);
|
|
13
|
+
~Handle();
|
|
14
|
+
|
|
15
|
+
Handle(const Handle&) = delete;
|
|
16
|
+
Handle& operator=(const Handle&) = delete;
|
|
17
|
+
|
|
18
|
+
Handle(Handle&& other) noexcept;
|
|
19
|
+
Handle& operator=(Handle&& other) noexcept;
|
|
20
|
+
|
|
21
|
+
HANDLE get() const;
|
|
22
|
+
HANDLE release();
|
|
23
|
+
bool is_valid() const;
|
|
24
|
+
void reset(HANDLE handle = nullptr);
|
|
25
|
+
|
|
26
|
+
private:
|
|
27
|
+
HANDLE handle_;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
#endif
|
package/src/native/tun_backend.h
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#pragma once
|
|
2
2
|
|
|
3
|
-
#if !defined(__linux__) && !defined(__APPLE__)
|
|
4
|
-
#error "appium-ios-tuntap native addon supports only Linux and
|
|
3
|
+
#if !defined(__linux__) && !defined(__APPLE__) && !defined(_WIN32)
|
|
4
|
+
#error "appium-ios-tuntap native addon supports only Linux, macOS, and Windows"
|
|
5
5
|
#endif
|
|
6
6
|
|
|
7
7
|
#include <cstddef>
|
|
@@ -27,9 +27,25 @@ enum class ReadPacketStatus {
|
|
|
27
27
|
Error,
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Backend abstraction that hides OS-specific TUN device handling from the
|
|
32
|
+
* N-API surface.
|
|
33
|
+
*
|
|
34
|
+
* Each backend owns:
|
|
35
|
+
* - its native handle (POSIX file descriptor or Win32 `HANDLE`)
|
|
36
|
+
* - the receive-loop primitive it needs (libuv `uv_poll_t` on POSIX, a
|
|
37
|
+
* dedicated worker thread plus a Win32 event on Windows)
|
|
38
|
+
*/
|
|
30
39
|
class TunPlatformBackend {
|
|
31
40
|
public:
|
|
41
|
+
// Invoked once per packet read by the receive loop. Always called on a
|
|
42
|
+
// background thread (libuv loop thread on POSIX, worker thread on Windows);
|
|
43
|
+
// the caller in `tuntap.cc` is responsible for marshalling onto the JS
|
|
44
|
+
// thread via `Napi::ThreadSafeFunction`.
|
|
32
45
|
using PacketCallback = std::function<void(std::vector<uint8_t>)>;
|
|
46
|
+
|
|
47
|
+
// Invoked at most once when the receive loop encounters a fatal error and
|
|
48
|
+
// stops. The receive loop must not deliver any further packets afterwards.
|
|
33
49
|
using ErrorCallback = std::function<void(const std::string&)>;
|
|
34
50
|
|
|
35
51
|
virtual ~TunPlatformBackend() = default;
|
|
@@ -47,6 +63,8 @@ public:
|
|
|
47
63
|
size_t length,
|
|
48
64
|
std::string& error) = 0;
|
|
49
65
|
|
|
66
|
+
// Begin asynchronous packet delivery. `loop` is supplied by Node-API and is
|
|
67
|
+
// used by POSIX backends for `uv_poll_init`; Windows ignores it.
|
|
50
68
|
virtual bool StartReceiveLoop(uv_loop_t* loop,
|
|
51
69
|
size_t buffer_size,
|
|
52
70
|
PacketCallback on_packet,
|
|
@@ -54,6 +72,8 @@ public:
|
|
|
54
72
|
std::string& error) = 0;
|
|
55
73
|
virtual void StopReceiveLoop() = 0;
|
|
56
74
|
|
|
75
|
+
// Returns the underlying POSIX file descriptor when one exists. Backends
|
|
76
|
+
// without a numeric fd (e.g. Wintun on Windows) return `-1`.
|
|
57
77
|
virtual int GetNativeFd() const { return -1; }
|
|
58
78
|
};
|
|
59
79
|
|